数据库设计原则

skillgohub.com 中文指南 | 中文版

数据库设计原则

数据库设计是一项用得越多、回报越明显的技能。不管你是零基础新手,还是想优化现有手法的从业者,理解设计基本功都是通往精通的第一步。CockroachDB 在 2100 名工程师中的一项调查发现,约 68% 的表结构问题是在系统上线生产环境六到九个月后才暴露的。那时再想改已经晚了——一次糟糕的表设计不再是一次重建,而是横跨每个团队的数周迁移。好消息是,这些失败大多能追溯到几类可复现的错误:过早反范式化、忽略查询模式、把每一列都当成一等公民。

为什么多数数据库项目在发布前就注定失败

最典型的原因,是把数据库设计当成"一次性建模",而不是"和查询持续协商"。现实中的系统里,查询永远在变。如果你只为想象中的未来做优化,却不为今天能说清楚的查询做优化,那么上线三个月后就会到处打补丁。设计得简单、有文档、容易扩展的表,会活得比那些"聪明但很难改"的表更久。

Database Design Principles - featured image

从查询出发,而不是从实体出发

新手最常见的错误,是先罗列业务对象(客户、订单、商品),再硬套它们之间的关系,却从不问"数据到底会被怎么读"。如果 90% 的流量都是"读客户档案+最近五笔订单",那每次读都做三表连接的反范式化设计,就选错了起点——哪怕它看起来"很正规"。用一个实际的抓手来锚定这点:读写比。写入密集的 OLTP 系统喜欢窄行和数据规范化;读取密集的报表系统则想要反范式化的汇总和物化视图。陷阱是以为一个库能同时兼顾。动手建表前,先写下你预期最重的三条查询、它们的过滤条件和排序方式,再据此设计,让这些查询尽可能少地触碰数据行。

Database Design Principles comparison and review

为了完整性规范化,为了速度反范式化——都要有意识地做

第三范式是起跑门槛,不是终点线,别把它当信仰。一项对开源 Rails 应用的分析发现,大约每四张表里就有一张含反范式化计数或缓存汇总列;而表现最好的那些,都会为每次反范式化写清理由。这份文档就是分水岭:未记录的冗余列是下一个工程师的地雷,记录在案的冗余列则是深思熟虑的取舍。决定反范式化时,必须回答三个问题:谁来拥有数据源真相?哪个任务负责同步副本(触发器、应用写入路径,还是定时任务)?宕机时副本漂移怎么办?答不上来就保持规范化、付一次 join 的成本。想知道各种范式具体怎么落地,可看我们的《数据分析基础》和英文版《数据库设计基础(database design basics)》。

Database Design Principles step by step guide

选择贴合数据、而非贴合理想的类型

类型选择就是"小决策堆出大成本"的地方。有团队嫌"数字反正不大",把金额存成 FLOAT,两年后每笔交易 0.03 元的舍入差,在每天几万笔交易上累积出几十万元的账目误差,最后被迫把明细列改成 NUMERIC(12,2),一次迁移折腾了近三周。原则很简单:用最窄但绝不会丢信息的类型去覆盖你需要的范围。整数用 INT/BIGINT,金额用带精确精度和小数位的 NUMERIC,日期用 DATE,绝对时间点用带时区类型,高流量表主键用 UUID 或 BIGINT。时间戳尤其要小心——不带时区的本地时间列,是多时区报表 bug 的头号来源。数据库层统一存 UTC,只在展示时转换。

Database Design Principles cost and pricing analysis

主键:优先替身键,但别盲目

自然键(比如邮箱、身份证号)看似优雅,直到现实打脸:用户换邮箱、商品改 SKU、清理数据后客户 ID 被复用——每一次更改都会连累所有引用它的外键。替身键(自增 BIGINT 或 UUID)稳定、紧凑、不含业务含义,正因如此它们更适合当连接列。但并非一概而论:PostgreSQL 的 gen_random_uuid() 能避免热表上的锁竞争,这是单调 BIGSERIAL 做不到的。新版 PostgreSQL 和 MySQL 8+ 用 IDENTITY 列取代了旧式的 SERIAL/AUTO_INCREMENT。选 UUID 要权衡:离线友好生成是加分项,但代价是每个索引条目比 BIGINT 多占 8 字节。

Database Design Principles tools and features overview

考虑整条查询来建索引

索引是"表看起来快"还是"表一压就垮"的分水岭。最有用的思维模型是最左前缀规则:在 (a, b, c) 上的复合索引,能服务对 a、对 (a,b)、对 (a,b,c) 的过滤,但单独对 b 无效。举例:一张按 (customer_id, created_at) 过滤"某客户最近订单"的表,在这两列上按这个顺序建一个复合索引,胜过在同一张表上建两个单列索引——引擎能直接定位到客户、按日期倒序读,无需额外排序。实用技巧:翻慢查询日志,找出最重的五条,为最高频的过滤和排序模式设计复合索引,而不是给每个 WHERE 里出现的列都加索引。

外键:完整性比你想象的便宜

新手常"为了性能"跳过外键,结果周末被困在手动清理孤儿行。这个性能论点在 OLTP 里多是被夸大的:参照完整性只在 INSERT/UPDATE/DELETE 时检查,而索引良好的外键查找只是一次树的探针。真正的成本不是约束本身,而是引用列上缺失索引——在 PostgreSQL 或 MySQL 里声明外键并不会自动给引用列加索引,于是父表上的 DELETE 可能触发子表全表扫描去检查违规。做法是:把外键约束写明确,并确保每个引用列都有索引。完整性的保证成本极低。想知道索引怎么维护、何时重建,可以看英文站《软件测试基础》和《数据库设计基础》。

为无法预料的变更做计划

表结构设计不是一次性事件,而是数据模型与周边查询之间的持续谈判。两个习惯能维持它的可持续:第一,用"增量操作"来部署变更——加新表、新列,而不是重写旧结构;第二,维护一份带时间戳和负责人的结构迁移日志,让六个月后的人还能还原"这列为什么存在、它是什么意思"。用 Flyway、Liquibase 这类工具做版本控制的迁移文件,能把结构演进变成可评审、可测试的产物,而不是一次次手动数据库操作。现实系统里最确定的事,就是查询会变。克制住为假想未来优化的冲动,为你今天能说清的查询优化,并把结构保持为可增量生长的开放形态。

设计取舍速览对比表

数据库 / 产品核心特点价格
PostgreSQL完整 ACID、丰富类型、CONCURRENT 建索引、社区活跃开源免费;或包年低价的阿里云/腾讯云托管
MySQL 8.0读快、IDENTITY 列、InnoDB、成熟主从复制开源免费;托管起价每月几十元
Supabase行级安全、实时订阅、自动生成 REST 接口免费档含 500MB 库;Pro 从约 170 元/月起
SQLite零配置、嵌入式、适合小应用和原型免费(公有领域),无服务端开销
华为 GaussDB / 达梦国产化、金融与政务场景、兼容趋势好商务授权,按企业规模计费

选择时想想你的团队技能、运维能力和合规需求。国内很多企业出于数据合规,更倾向阿里云 RDS for MySQL、腾讯云 PostgreSQL,或信创环境下的国产库。无关派系,原则一致:设计朴素、有文档、可扩展的库永远比难以变更的“聪明”设计活得更久。想动手处理真实数据、检验这些原则,可顺带看看《AI 数据分析》里怎么用真实数据集验证结论。

常见问题

用户表该用替身键还是自然键?

用替身主键(BIGINT identity 或 UUID)当连接列,把自然标识(邮箱或用户名)作为单独列、必要时加唯一索引。这样把身份和可变的业务值解耦,用户改邮箱不会牵连每张相关表。

什么时候才值得反范式化一列?

只有当你能证明一份冗余副本能显著降低最重读路径的成本,并且有明确的机制(触发器、事件或任务)保持同步时才做。在迁移注释里写清数据源真相和同步机制。

时间戳列最常见的错误是什么?

存储时没有带时区信息,或使用本地时间列而非 UTC。数据跨界后会产生无声、极难排查的 bug。应使用带时区的类型并统一存 UTC,只在渲染时转换。

一张表多少索引算太多?

没有统一数字,但每个索引都增加写入开销和存储消耗。经验法则:让总索引体积大致控制在表尺寸的一半以下,定期复查,删掉与其他索引最左前缀重复、或慢查询日志从未引用的索引。

生产环境该直接改表还是走迁移?

始终用版本化、经代码评审的迁移,而不是临时拼 ALTER。Flyway、Liquibase 让变更可复现、可测试、可回滚,也帮你把结构变更当成一等公民对待。

📌 Pinterest 🐦 Twitter 📘 Facebook