做数据库设计的人,几乎都会遇到同一个问题:同样的业务数据,别人数据库里是干干净净的十几列,自己只能堆出又长又窄的“属性天梯”。这里说的就是横表和竖表。横表是一个对象一行数据,竖表是一个属性一行数据。两种设计没有天然的优劣,但选错的代价很高——后期SQL写得难受、扩展时频繁改表、统计口径对不上,这些都和最初建表时的维度思维有关。这篇内容我会用真实业务案例,把两种方案的细节、底层逻辑、适用场景和踩坑经验一次讲透,适合后端开发、数据分析师和架构师参考。
1. 先搞懂横表和竖表到底是什么
1.1 横表:一行一个对象,一列一个属性
横表是我们最常见、最直觉的建表方式。用户表就长这样:
CREATE TABLE user_info ( user_id INT PRIMARY KEY, username VARCHAR(50), age INT, gender CHAR(1), email VARCHAR(100), created_at DATETIME );每一行代表一个完整用户,每一列代表用户的一个固定属性。查询的时候直接SELECT * FROM user_info WHERE user_id = 123,结果就是一整行,没有任何多余动作。ORM映射也方便,一个实体类对应一张表,字段一一对应。
这种设计的核心思想是:把业务对象作为建模的第一单位,每个对象的状态在一行内完整表达。它的前提是属性集合相对固定,业务方不会隔三差五提出“再加个字段”的需求。
横表最大的优点就是查询效率高、可读性强、索引设计直接。缺点也很明显:一旦属性变化,就需要执行ALTER TABLE。如果表里数据量已经上了千万级,加列虽然现在有很多数据库做了平滑优化,但仍然容易引发锁等待、主从延迟、磁盘空间增长等问题。
1.2 竖表:一个属性一行,对象靠标识串起来
竖表的形态刚好反过来,它把实体的属性“打散”存放:
CREATE TABLE user_ext ( user_id INT NOT NULL, attr_key VARCHAR(50) NOT NULL, attr_value VARCHAR(255), PRIMARY KEY (user_id, attr_key) );数据会长这样:
| user_id | attr_key | attr_value |
|---|---|---|
| 1 | nickname | 小明 |
| 1 | age | 28 |
| 1 | level | vip |
| 2 | nickname | 小红 |
| 2 | age | 30 |
一个用户有多少属性,这个表里就有多少行。这里的attr_key是属性名,attr_value是属性值,user_id把同一对象的多个属性串在一起。学名叫做 EAV(Entity-Attribute-Value)模型,很多低代码平台、表单引擎、配置中心底层都是这么干的。
这种表的核心价值是:新增属性不需要改表结构,直接插入一条新的attr_key记录即可。想给用户加一个“性别”,不用ALTER TABLE,只要INSERT一行就行。对属性不固定、扩展频繁的业务,这简直是救命设计。
但代价也马上来了:想查一个用户的所有属性,需要查出多行再在代码里拼成对象;想把“年龄大于35岁的vip用户”筛出来,SQL会变得绕,性能也容易失控。这就是竖表最让人头疼的地方——写入灵活,查询别扭。
1.3 两类设计的初心对比
| 维度 | 横表 | 竖表 |
|---|---|---|
| 建模对象 | 业务实体 | 属性集合 |
| 一行含义 | 一个完整对象 | 对象的一个属性 |
| 新增属性 | 改表结构 | 插入数据行 |
| 查询读取 | 直接、高效 | 需要行转列或多次关联 |
| 扩展能力 | 弱 | 强 |
| 可读性 | 高 | 低 |
| 适合场景 | 核心业务数据 | 扩展信息、配置、标签 |
理解到这里还不够,更重要的是搞清楚:为什么会产生这两种思路?它们各自解决的是什么层面的问题?这就涉及维度思维了。
2. 两种设计背后的维度思维差异
2.1 横表:面向稳定对象建模
横表的思维出发点是“先确定对象,再确定属性”。拿电商订单来说,订单就是一个稳定对象,它的属性无非是订单号、用户、金额、状态、时间。这些属性从业务诞生第一天就存在,几乎不会变。用横表建模,数据库结构就是业务概念的镜像,看表即知业务,沟通成本极低。
这种思维方式适合绝大多数核心交易数据,因为业务对象稳定,且属性关系强。横表的强类型还能借助数据库约束保证数据质量——比如age INT你就不可能写入“二十八”,created_at DATETIME就不可能写入“昨天”。数据规则在入口就被卡死,源头质量有保障。
2.2 竖表:面向动态集合建模
竖表的思维出发点恰恰相反,它认为“对象不是固定的,属性才是可枚举的组合”。所以它把属性的定义权从数据库结构下沉到了数据层——属性叫什么、值是什么,都由业务运行期决定。
这在配置中心里特别好用。比如你对一套优惠策略要配很多参数:满减金额、适用人群、限购数量、开始时间、结束时间、渠道限制,每种优惠类型可用参数都不一样。如果你为每个优惠类型建一张横表,几十张表会让人崩溃。用竖表所有类型共用一套config_key、config_value就能覆盖,新增优惠类型只是多插入几条数据,系统完全不需要发布新版本。
2.3 为什么大多数项目最开始都选了横表
答案很简单:交付快、理解成本低、工具生态好。横表让开发人员可以顺着业务语言直接翻译成表结构,不需要额外设计“元数据”体系。大部分ORM框架、后台管理系统、报表工具都默认按横表方式工作,一行就是一个实体,前端表格直接能映射用。
还有一点容易被忽略:横表的查询性能更好预测。因为每一行的宽度固定,索引可以直接建立在用户想用的列上,优化器能做出稳定准确的执行计划。这些问题在项目初期人少、活急、需求快速迭代的阶段都是实打实的优势。
2.4 竖表真正发威的场景
竖表并不是用来替代横表的,它解决的是横表“改结构难”和“列稀疏浪费”的问题。三类场景最典型。
第一类是用户自定义字段。比如客户管理系统允许不同客户配置不同的联系人字段,A客户需要记录“座机号”,B客户只需要“手机号”。如果使用横表,几十个自定义字段最终会积压成一张列很多但稀疏率极高的表。用竖表,每个客户只拥有自己需要的属性。
第二类是标签类业务。用户兴趣标签、内容分类标签、商品卖点标签,数量和内容都是运营随时定义的。竖表天然适合存储“对象 + 标签 + 值”的三元组。
第三类是配置类和规则类数据。属性名和属性值的组合变化快、种类多,用一个通用配置表承载所有配置项,比反复加字段要优雅得多。
3. 同一需求两种设计:完整实战对比
为了看清楚差距,我以一个在线课程平台的课程信息为例。课程固有属性包括课程标题、价格、讲师、时长,这些稳定存在。而课程还有一些非固定属性,比如“是否含1对1辅导”“答疑次数”“配套资料包数量”,不同课程各不相同。
先把公共字段用横表建好,再把可变字段分别用横表和竖表去试。
3.1 方案A:全部横表
CREATE TABLE course_full ( course_id INT PRIMARY KEY, title VARCHAR(100), price DECIMAL(10,2), teacher_id INT, duration INT, has_1v1 TINYINT DEFAULT 0, qa_count INT DEFAULT 0, material_cnt INT DEFAULT 0, ... );建表很痛快,查询也很痛快:
SELECT course_id, title, price FROM course_full WHERE has_1v1 = 1 AND qa_count >= 5;条件直接落在列上,索引可以加在has_1v1和qa_count上。但风险在于:一旦运营说“我们还要加一个‘是否含结业证书’字段”,你就要给大数据表做ALTER TABLE,并且这个新字段会让已经存在的每一行课程都补上一个默认值;如果只给部分课程用,其他课程的这列就是空着的,造成存储浪费。
3.2 方案B:横表主体 + 竖表扩展
CREATE TABLE course_base ( course_id INT PRIMARY KEY, title VARCHAR(100), price DECIMAL(10,2), teacher_id INT, duration INT ); CREATE TABLE course_attr ( course_id INT NOT NULL, attr_key VARCHAR(50) NOT NULL, attr_value VARCHAR(255), PRIMARY KEY (course_id, attr_key) );新增属性时完全不需要改表:
INSERT INTO course_attr (course_id, attr_key, attr_value) VALUES (1, 'has_1v1', '1') ON DUPLICATE KEY UPDATE attr_value = '1'; INSERT INTO course_attr (course_id, attr_key, attr_value) VALUES (1, 'qa_count', '5') ON DUPLICATE KEY UPDATE attr_value = '5';但不好的事情来了。如果想找出“含1对1辅导并且答疑次数不少于5次的课程”,SQL就变得曲折:
SELECT c.course_id, c.title FROM course_base c JOIN course_attr a1 ON a1.course_id = c.course_id AND a1.attr_key = 'has_1v1' AND a1.attr_value = '1' JOIN course_attr a2 ON a2.course_id = c.course_id AND a2.attr_key = 'qa_count' AND CAST(a2.attr_value AS SIGNED) >= 5;每增加一个筛选条件,就要多 JOIN 一次。SQL复杂不说,多个条件之间的关联关系会迫使优化器做多次索引查找,性能自然不如横表。这也是竖表“写入一时爽,查询火葬场”说法的来源。
3.3 竖表查询的行转列解法
在无法访问横表场景下,竖表要显示成一行的标准做法就是行转列。以用户扩展表为例:
SELECT user_id, MAX(CASE WHEN attr_key = 'nickname' THEN attr_value END) AS nickname, MAX(CASE WHEN attr_key = 'age' THEN attr_value END) AS age, MAX(CASE WHEN attr_key = 'level' THEN attr_value END) AS level FROM user_ext GROUP BY user_id;执行后,多行数据就被聚合成一行。这里的MAX本质是“取分组内唯一匹配值”,因为同一user_id下同名attr_key只有一条,所以MAX也只是把这唯一值带出来。这种写法能解燃眉之急,但要枚举出所有属性名,如果属性是动态的,SQL没法写完,就得靠程序动态拼SQL,或者用GROUP_CONCAT把整个属性集合压成一个长串再解析。
说实话,竖表在“单对象整体读取”场景下还可以接受,但在“按属性筛选汇总”场景下,复杂度会指数级上升。所以我的经验是:竖表只用来承载“不需要参与复杂筛选”的属性,凡是需要查询筛选的属性,都要尽量做到横表或者冗余到横表里去。
4. 选型决策:什么时候用横表,什么时候用竖表
4.1 五维度判断法
我总结了五个判断维度,指导我几乎所有表结构设计决策。
**属性稳定性。**如果业务属性未来半年内看不到新增或调整的可能,横表是首选。反之,属性每月都在变化,竖表就值得考虑。这是最核心的判断依据。
**查询复杂度。**建表之前先预想后续会怎么查:有没有固定条件的查询?有没有对多个属性组合过滤的需求?如果全部都是“按实体ID取全部属性”这一类简单读取,竖表的缺陷会被掩盖住;如果常有“属性A且属性B且属性C”的过滤,竖表会让你痛不欲生。
**数据密集度。**如果对象本身属性就多,而且大部分属性都有值,横表空间效率不差。如果对象属性很多但每个对象实际上只用到了其中五六个,横表会浪费大量空值存储,用竖表反而节省。
**工程化友好度。**团队使用的ORM、前端组件、报表工具对竖表的支持程度如何?如果基本全是横表思维的工具,竖表就需要你额外写很多桥接代码。小团队可以承受这种额外成本,大团队要慎重。
**维护成本与团队习惯。**横表的结构变更需要评审、脚本、灰度,流程成本高。竖表的数据变更只需要插入数据,但要防止key命名混乱、类型难统一。哪个方向上的坑,团队更愿意踩,往往决定了最终选择。
4.2 折中方案:混合建模才是大部分项目的最优解
实际业务里,很少有一张表能百分之百纯横表或纯竖表走到底。我见过的大多数健壮模型,都采用混合架构:核心稳定字段用横表保证性能,动态扩展字段用竖表补充灵活性。
课程业务可以这样做:
course_base:存储课程标题、价格、讲师、时长等稳定字段。course_attr:存储动态属性,比如是否含1对1辅导、答疑次数。- 把需要频繁查询或统计的动态属性,通过异步任务物化回
course_base的冗余列。
这样既保留了快速查询能力,又避免了每次新业务需求都去改核心表结构。冗余列更新的一致性可以由程序保证,如果怕不一致,还可以用视图去统一读取。
4.3 现代数据库给了第三种选项:JSON字段
很多开发者忽视了 JSON 字段这个中间地带。MySQL 5.7+、PostgreSQL、SQLite 都支持 JSON 或 JSONB。对于“什么时候加什么字段不确定,但整体读取时不希望拆成很多行”的需求,JSON 字段是比竖表更顺手的选择。
ALTER TABLE course ADD ext_info JSON; UPDATE course SET ext_info = JSON_SET( ext_info, '$.has_1v1', '1', '$.qa_count', '5' ) WHERE course_id = 1;读取时用JSON_EXTRACT(ext_info, '$.has_1v1')或者 PostgreSQL 的->>运算符就能拿到值。JSON字段的优势是单行内实现动态属性,读取时不用行转列;劣势是筛选效率不如真正的横表列,而且部分数据库对 JSON 内字段建立索引比较麻烦。
但比起裸竖表,JSON 字段在工程化上有优势,因为对象取出来后就是一个 Map,程序直接能用。如果你使用的是 PostgreSQL,JSONB 配合 GIN 索引,动态属性的查询能力会比 MySQL 的 JSON 更强,很多场景下足以替代竖表。
4.4 不要被“大表宽列”绑架
还有不少团队犯一个反向错误:明知道属性是动态的,还坚持把所有可能的属性都设计成横表字段,建出来的表动辄一两百列。这种表看起来仍然是横表,但已经失去了横表的优点——查询时经常要SELECT出几十个用不到的冗余列,索引设计也很困惑,每行还有大量空值。
这种情况不如老老实实把“临时的、可选的、低筛选价值的属性”挪进竖表或JSON,让主表保持简洁。数据库表设计不是追求“一表全收”,而是让结构跟得上业务变化。
5. 常见问题与实战避坑
5.1 竖表行转列到底怎么写更稳定
行转列的思路已经在前面例子里交代过,但有几个细节容易踩坑。
第一,attr_value如果同时存数字和字符串,MAX(CASE WHEN ...)出来的结果会是字符串类型。后续要跟数字比较时,要记得用CAST(attr_value AS SIGNED)之类的转换,否则会出现“9 > 100”这种离谱的比较结果。
第二,需要行转列的属性如果数量多,动态拼 SQL 时要防止 SQL 注入。属性名不要直接拼接,要做白名单校验,或者至少用参数化方式传递attr_key。
第三,行转列后如果多个属性缺失,部分列会是NULL,前端展示时需要做默认值处理。这里建议在查询层就有一层 API 返回0、空字符串等语义安全的默认值,别把NULL直接抛给前端。
5.2 竖表数据准入规范必须提前定
竖表由于太灵活,最容易出现的问题是“同一个属性,有的行存字符串,有的行存数字,还有的行存了JSON”。时间一长,这一列就废了。
我的做法是维护一个attr_key元信息表,里面记录每个属性名的取值类型、是否必填、取值范围。写入时先校验,再落库。同时建议给attr_value设定一个尽量合理的长度上限,避免有人塞入超大文本拖垮表空间。
5.3 横表加字段的大表处理
横表不可避免会遇到加字段的场景。如果表已经很大,还记得先看一眼数据库版本支持不支持区块秒加字段(比如MySQL 8.0的INSTANT ADD COLUMN),支持的话加列压力很小。如果版本较老,避免直接在业务高峰期执行ALTER TABLE,可以走以下方案:
- 创建一张影子表,包含旧字段和新字段。
- 后台分批把旧表数据搬移过去。
- 业务切换表名,在维护窗口内完成切换。
这个过程虽然麻烦,但能显著减少锁表影响。还有一种低成本办法:新字段先不加在表上,而是放到旁边独立的横表中,通过JOIN关联读取。业务上线后再评估是否合并回主表。
5.4 竖表查询性能优化三板斧
竖表查询慢的核心原因:一是需要行转列,二是无法对attr_value直接高效过滤。应对手段就三条。
第一,必要属性上冗余。把频繁筛选的属性同步写进主表,这是最常见也最有效的方法。
第二,联合索引要合理。竖表的查询通常形如“某对象的某几个属性”,联合主键(entity_id, attr_key)是底线。如果经常按属性名去查对象,可以额外建(attr_key, attr_value)索引,但这种索引只适合“属性值唯一性高”的场景,否则极易退化。
第三,批量读取时避免逐条取。读取N个对象的所有扩展属性时,用WHERE entity_id IN (...) AND attr_key IN (...)一次性取回,程序按entity_id在内存里分组,不要写循环一条一条查,不然N+1问题会让你直接崩溃。
5.5 竖表变成“垃圾场”之后怎么办
如果竖表已经出现大量乱用属性、类型混乱的情况,也不要急着推翻重做。先写一个扫描脚本,统计每个attr_key的行数、类型分布、空值比例。把高频且有筛选价值的属性提升为横表字段,把低频属性继续留在竖表,把完全报废的属性数据归档清理。这种渐进式重构比一步到位安全得多,也能在重构过程中不断修正业务认知。
6. 选型之外的落地建议
最后分享几个我在实际项目中反复确认过的体会,希望帮你避开那些教材里不会写出来的坑。
第一,别因为“以后可能扩展”就给每张表都配竖表扩展。没有具体需求驱动的灵活设计,最后都会成为统计报表的噩梦。竖表这张牌要攥在手里,等到属性真的变得非常不可控了再打出去。
第二,如果团队里数据分析师很多,横表天然更受欢迎。数据分析师不懂你的 EAV 模型,他们只想要一张一行一个对象的宽表。如果你非要给他们竖表,他们会在月底对口径的时候怀疑人生。反过来说,如果你的核心下游是业务配置更新而不太做分析,竖表的优势就很明显。
第三,主数据用横表,可变数据用竖表或JSON,这是大部分项目最稳妥的起点。不要为了追求某种设计的正统性,把所有数据都硬塞进同一种表里。数据库设计没有银弹,只有当前是否适合业务的取舍。
第四,无论选哪种方案,都建议在表注释和文档里写清楚“哪些列是稳定字段、哪些是动态字段、取值规则是什么”。我用竖表第二年的时候,最痛苦的不是写SQL,而是新同事问“这个 attr_key 有哪些合法值,哪个代表什么含义”而我翻代码也说不清楚。后来补了元信息表才解决。
横表和竖表的讨论本质上是稳定性与灵活性之间的博弈。问题是不会变的,变的只是你在哪个维度上思考它。希望这篇内容能让你在下次建表的时候,不再把这两种方案当作二选一的单选题,而是当成一组可以灵活组合的工具。