ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据库表设计:横表竖表怎么选?从维度思维到实战避坑

数据库表设计:横表竖表怎么选?从维度思维到实战避坑 做数据库设计的人几乎都会遇到同一个问题同样的业务数据别人数据库里是干干净净的十几列自己只能堆出又长又窄的“属性天梯”。这里说的就是横表和竖表。横表是一个对象一行数据竖表是一个属性一行数据。两种设计没有天然的优劣但选错的代价很高——后期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_idattr_keyattr_value1nickname小明1age281levelvip2nickname小红2age30一个用户有多少属性这个表里就有多少行。这里的attr_key是属性名attr_value是属性值user_id把同一对象的多个属性串在一起。学名叫做 EAVEntity-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程序直接能用。如果你使用的是 PostgreSQLJSONB 配合 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在内存里分组不要写循环一条一条查不然N1问题会让你直接崩溃。5.5 竖表变成“垃圾场”之后怎么办如果竖表已经出现大量乱用属性、类型混乱的情况也不要急着推翻重做。先写一个扫描脚本统计每个attr_key的行数、类型分布、空值比例。把高频且有筛选价值的属性提升为横表字段把低频属性继续留在竖表把完全报废的属性数据归档清理。这种渐进式重构比一步到位安全得多也能在重构过程中不断修正业务认知。6. 选型之外的落地建议最后分享几个我在实际项目中反复确认过的体会希望帮你避开那些教材里不会写出来的坑。第一别因为“以后可能扩展”就给每张表都配竖表扩展。没有具体需求驱动的灵活设计最后都会成为统计报表的噩梦。竖表这张牌要攥在手里等到属性真的变得非常不可控了再打出去。第二如果团队里数据分析师很多横表天然更受欢迎。数据分析师不懂你的 EAV 模型他们只想要一张一行一个对象的宽表。如果你非要给他们竖表他们会在月底对口径的时候怀疑人生。反过来说如果你的核心下游是业务配置更新而不太做分析竖表的优势就很明显。第三主数据用横表可变数据用竖表或JSON这是大部分项目最稳妥的起点。不要为了追求某种设计的正统性把所有数据都硬塞进同一种表里。数据库设计没有银弹只有当前是否适合业务的取舍。第四无论选哪种方案都建议在表注释和文档里写清楚“哪些列是稳定字段、哪些是动态字段、取值规则是什么”。我用竖表第二年的时候最痛苦的不是写SQL而是新同事问“这个 attr_key 有哪些合法值哪个代表什么含义”而我翻代码也说不清楚。后来补了元信息表才解决。横表和竖表的讨论本质上是稳定性与灵活性之间的博弈。问题是不会变的变的只是你在哪个维度上思考它。希望这篇内容能让你在下次建表的时候不再把这两种方案当作二选一的单选题而是当成一组可以灵活组合的工具。
返回列表