大厂 MySQL 建表军规:那些评审会上被打回来的坑

Posted on 六 18 7月 2026 in Tech

Abstract 大厂 MySQL 建表军规:那些评审会上被打回来的坑
Authors Walter Fan
Category learning note
Version v1.0
Updated 2026-07-18
License CC-BY-NC-ND 4.0

大厂 MySQL 建表军规:那些评审会上被打回来的坑

写业务代码这么多年,我发现一个规律:新人栽跟头,很少栽在算法上,多半栽在建表上。

一张表当时建得随手,id 用自增,时间用 TIMESTAMP,几个字段图省事全设成 NULL,再顺手加个 JSON 列存"扩展属性"。功能跑得好好的,评审也过了。等两年后数据涨到几千万行、要做分库分表、要跨机房迁移——才发现当初那几个"随手",每一个都是一颗定时炸弹。

我最近整理了一份大厂内部的 MySQL 设计评审清单,越看越觉得:这里面几乎每一条禁令,背后都躺着一次真实的线上事故。 规则本身好背,难的是理解它在防什么坑。这篇就把这些规则摊开,一条条讲清楚"为什么"。

  • 不是什么:这不是"MySQL 教程",不教你 SQL 语法。
  • 是什么:这是一份"上生产前该被谁拦下来"的评审清单,重点在每条规则背后的代价

急的读者可以直接跳到文末的自查清单。想搞懂原理的,跟我一条条过。

说明:下面的规则综合自业界常见的数据库设计规范(阿里、腾讯等都有类似版本),并做了脱敏和通用化处理。具体阈值(比如分页上限、字段宽度)各家不同,重要的是思路,不是数字本身。


一、建表:地基没打好,后面全是返工

主键:别用自增,用应用生成的 UUID

这条最反直觉,很多人第一反应是"自增主键不香吗,又快又简单"。

单机时代确实香。但一旦你要分库分表、跨机房迁移、多活,自增主键就变成灾难——两个库各自从 1 开始自增,合并时主键直接撞车。所以军规是:

不用自增主键。没有天然主键时,在应用层生成 UUID,存进一个字符串列。

配套的几条,每条都有它要防的具体问题:

  • 每张表必须有主键。原因有二:一是 InnoDB 的数据本身就是按主键组织的(聚簇索引),没有显式主键 MySQL 会偷偷给你造一个隐藏的,你既控制不了也用不上;二是主从复制在 ROW 模式下删/改一行时,要靠主键定位"是哪一行",没主键就可能定位错行,导致主从数据悄悄分叉。
  • 避免复合主键。复合主键意味着所有二级索引都要带上这几个主键列,索引更胖、写入更慢;ORM 映射、外部引用、数据迁移也都要处理"多列身份",复杂度陡增。能用单列 UUID 就用单列。
  • 不要更新主键值。主键是聚簇索引的组织依据,改主键等于把整行数据从物理位置 A 挪到 B,代价极大;而且所有引用它的地方都得跟着改,极易漏改。主键是身份证号,不是可改的属性——真要"换身份",删了重新插一行。
  • 主键别用二进制类型。跨环境的数据比对工具要拿主键做"行对齐",binary 类型在不同工具/编码下表示不一致,比对时会冒出一堆根本不存在的"假差异",让你没法判断两边到底同步没有。用定长字符串(比如 VARCHAR(32) 存 compact UUID)更稳妥。

NULL:能不用就不用

能设 NOT NULL 的列,尽量 NOT NULLNULL 的坑在于它的三值逻辑——NULL = NULL 结果不是 true 而是 NULLCOUNT(列) 会跳过 NULL,一不小心统计就错。

实操上:

  • 字符串列:如果"空"就等于"没有",用 NOT NULL DEFAULT ''
  • 整数列:如果 0 是合法的默认/缺省值,用 NOT NULL DEFAULT 0

当然,如果"没填"和"填了空"在业务上真的是两回事(比如"用户没设置过"vs"用户清空了"),那 NULL 有它的意义,这时候就该在设计文档里写清楚为什么留 NULL规则是默认值,例外要有理由。

时间:用 DATETIME,别用 TIMESTAMP

这条很多人不知道原因。TIMESTAMP 底层是 32 位整数,存的是从 1970 年起的秒数——它在 2038 年 1 月 19 日会溢出(著名的 "Year 2038 问题")。你的表如果活得够久,或者要存未来的时间(比如 2040 年到期的合约),TIMESTAMP 直接爆掉。

DATETIME(需要毫秒就 DATETIME(3)),不用 TIMESTAMP

另外,每张表都要有创建时间和修改时间两个列。哪怕是"只插不改"的日志表,也保留这俩字段——出问题排查时,时间戳是你唯一能抓住的线索。

字段宽度:按真实业务量身定,别拍脑袋 VARCHAR(255)

VARCHAR(255) 是很多人的默认习惯,但它未必合适。关键在索引

InnoDB 的索引键有长度上限(经典的 767 字节,新版本 DYNAMIC 行格式下 3072 字节)。而 utf8mb4一个字符最多占 4 字节,所以 VARCHAR(255) 在 utf8mb4 下是 255 × 4 = 1020 字节——一旦要给它建索引,很容易撞上长度限制。

军规的做法是:要建索引的字符串列,控制在 255 字节以内;utf8mb4 下大约 VARCHAR(60) 就到这个量级了。 需要更长的,先算清楚字节宽度,再走一道审批。

字符集:在库级别设一次,表和列都别单独设

不要在表或列上单独指定 CHARSETCOLLATION,让它们继承数据库的设置。原因有两个,都很隐蔽:

  • JOIN/比较时的"隐式转换":如果 A 表某列是 collation X,B 表某列是 collation Y,两列做关联或比较时 MySQL 要做隐式转换,轻则索引失效(本来能走索引的查询突然全表扫描),重则直接报 Illegal mix of collations 错误。
  • 升级时的"collation 漂移":不同 MySQL 版本的默认 collation 可能变(比如 8.0 默认从 utf8mb4_general_ci 改成了 utf8mb4_0900_ai_ci)。你在各处零散指定的 collation 和库默认值不一致时,升级后同样的字符串在不同列比较结果就可能不一样——这种 bug 隐蔽到让你怀疑人生。

补一句:如果实在必须在建表语句里写 CHARSET,那就必须同时写 COLLATION,别只写一半,否则升级时另一半会用新版本的默认值,埋雷。

那库级别该设成什么?推荐 utf8mb4,别用 utf8

这是个坑了无数人的历史遗留问题:MySQL 里的 utf8 并不是"真正的 UTF-8"——它是 utf8mb3 的别名,每个字符最多只占 3 字节,只能存 Unicode 基本平面(BMP)的字符。结果就是:

  • 存不了 emoji(😀 这类字符是 4 字节的),插入时要么报错要么变成一堆问号。
  • 存不了部分中日韩生僻字和补充平面字符——用户名字里有个生僻字,直接入库失败。

我见过不止一次线上事故就是这么来的:表建的时候用了 utf8,平时中英文都正常,直到有天用户昵称带了个 emoji,写入直接炸。而 utf8mb4 用 1-4 字节完整覆盖了 UTF-8,是 utf8 的超集,多出来的开销可以忽略。官方也早已在 8.0 把 utf8mb3 标记为弃用,默认字符集就是 utf8mb4

collation(校对规则,决定字符串怎么排序和比较)配套推荐:

  • utf8mb4_0900_ai_ci:MySQL 8.0+ 的默认值,基于新版 Unicode 排序算法,性能和正确性都更好。ai = accent insensitive(不区分重音),ci = case insensitive(不区分大小写)。新库首选它。
  • utf8mb4_unicode_ci:如果要兼容 5.7,用这个,排序比老的 general_ci 更符合语言习惯。
  • utf8mb4_bin:需要严格区分大小写(比如密码、token、区分大小写的业务编码)时用它,按二进制逐字节比较。

一句话:库级别统一 utf8mb4 + 一个明确的 utf8mb4_*_ci(有大小写敏感需求的用 _bin),表和列全部继承,不再零散指定。

一串"禁令":这些东西大厂基本都不让用

这部分我合并起来讲,因为逻辑是相通的——它们都在"某种运维/迁移/兼容场景"下会出事

禁用项 为什么禁(具体原因)
外键(Foreign Key) ① 分库分表后,被引用的表可能在另一个库,外键根本跨不过去;② 高并发写入时,外键检查会给父表加锁,容易引发锁等待甚至死锁,拖垮性能;③ 约束藏在 DB 里,应用层看不见,出错时排查困难。结论:把引用完整性放到应用层显式校验。
触发器(Trigger) ① 它是"隐形的连锁写入"——你只 UPDATE 了一行,背后却触发了一堆你没写的操作,出问题极难定位;② 复制/同步链路只搬明面的数据变更,下游未必会触发同样的 trigger,导致两边数据分叉;③ 业务逻辑散落在 DB 里,版本管理和 code review 都覆盖不到。
视图(View) ① 把复杂查询藏在 DB 层,优化器对多层嵌套视图的执行计划常常很糟,性能不可控;② 视图变更不走应用的发布流程,容易成为"没人负责"的黑盒;③ 迁移和同步工具对视图支持参差。要复用查询逻辑,放到应用/DAO 层。
全文索引 ① MySQL 的全文检索能力弱、分词和相关性排序都不专业;② 它的维护成本高、锁行为特殊,容易拖累写入。真要做搜索,用专门的搜索引擎(Elasticsearch 之类),各司其职。
生成列(Generated Column) 它的值是源库算出来的。复制/同步时,下游到底是"照搬这一列"还是"自己重算",不同工具行为不一,很容易两边算出不同结果——跨环境比对直接对不上。改用应用维护的普通列,所见即所存。
JSON 列 ① 早期版本改 JSON 里一个字段,binlog 要写整个文档,体积暴涨、复制变慢;② 无法像普通列那样高效建索引和查询;③ 跨平台同步对 JSON 支持不完整。把字段拆成明确的标量列,可索引、可比对、可复制。
大字段 BLOB/TEXT ① 单行过大,InnoDB 要把它拆到溢出页,读写都要多次 IO,还把 buffer pool 挤爆;② 大行让 binlog 膨胀、复制延迟飙升;③ 尤其 MEDIUMTEXT/LONGTEXT/MEDIUMBLOB/LONGBLOB 更甚。真要存大对象,放对象存储,DB 里只留一个引用(URL/key)。

这里的共同哲学是:MySQL 只放"当前的、基础的、结构化的"数据。 会无限增长的历史/审计数据,往能追加写、易扩展的存储去放(DynamoDB、对象存储等)。

索引命名与设计:给查询建,不是给列建

  • 命名要有约定:唯一索引 u_ 前缀,单列索引 i_ 前缀,复合索引 c_ 前缀。名字一眼看出类型,评审和排查都省事。
  • 别给每个列都单独建索引。索引不是越多越好——写入时每个索引都要维护,太多索引拖慢写入。要围绕真实的查询条件建复合索引
  • 避免前缀索引INDEX(col(10)))。前缀索引选择性差、还可能让优化器判断失误,能用全列索引就用全列。
  • 唯一索引要慎用。如果唯一性能在应用层安全地保证,就别用数据库唯一索引;只有并发场景下必须靠 DB 兜底时才用,而且要在文档里写明这个例外。

大表改结构:能不动就不动

对已经很大的表做 DDL(加列、改类型、加索引),可能锁表几分钟甚至更久,直接把线上服务干趴。军规是尽量避免大表的 schema 变更;真要改,走专门的在线 DDL 工具和迁移评审,别在业务高峰随手 ALTER


二、查询:慢 SQL 是怎样炼成的

建表是地基,查询是日常。这部分的规则,本质都在防一件事:别让一条 SQL 拖垮整个库。

三条最该刻在脑门上的

  1. 不许 SELECT *。只查你要的列。SELECT * 会把大字段、无用列全捞出来,浪费 IO 和内存,还让索引"覆盖查询"的优化失效。
  2. 会增长的表,绝不允许全表扫描。只有"故意做小"的表(比如配置元数据)才允许扫。上线前用 EXPLAIN / EXPLAIN ANALYZE 看执行计划,出现 type: ALL(全表扫描)就要警惕。
  3. 一定要加 limit。哪怕你"确定"结果不多。一页别超过 2000 行(具体阈值各家不同)。没有 limit 的查询,是把"表突然变大"的风险敞开着。

关于 JOIN 和子查询

  • SQL 越简单越好,JOIN 越少越好。复杂 JOIN 在数据量大时执行计划极不稳定。必要的关联,要作为"关键查询"单独评审,附上执行计划证据。
  • 避免 WHERE id IN (SELECT ...) 这类子查询。老版本 MySQL 对这种子查询优化很差,能改成经过评审的 JOIN 就改。

关于排序和分组

ORDER BYGROUP BYDISTINCT 不是不能用,而是除非结果契约真的需要,否则别用。它们容易触发 filesort(文件排序)和临时表,是慢查询的常客。用之前先想清楚:这个排序/去重,是业务必须的,还是我顺手加的?

复合索引:记住"最左前缀"

用了复合索引 c_a_b_c(列 a、b、c)的查询,必须带上它的最左列。只查 WHERE b = ? 是用不上这个索引的。这是复合索引最容易被忽略、也最容易白建的坑。

批量操作:分批,别一口吞

一次性 DELETE/UPDATE/INSERT 几十万行,会产生超长事务、锁大量行、撑爆 binlog。军规是分批处理,每批有上限(比如后台任务每批 1000 行,普通请求列表参数不超过 200 个),循环跑多批,而不是把范围塞进一条语句。

配套还有几条:

  • 避免长事务。事务开着不提交,会一直持有锁、撑大 undo log。把事务范围压到最小。
  • 禁止 INSERT INTO A SELECT * FROM B——一条语句锁两张表还全字段拷贝,风险极高。
  • 别用 INSERT ... ON DUPLICATE KEY UPDATEREPLACE INTO。这俩在并发下容易死锁;老老实实分开写"先查、再决定 update 还是 insert"的逻辑。

读写分离:用对场景

如果有读库(read replica),能容忍一点延迟的读,路由到读库分担压力;但"写完必须立刻读到"的场景,必须走主库——读库有复制延迟,你刚写的数据可能还没同步过去。这个判断错了,会出"我明明保存了怎么查不到"的诡异 bug。


三、实战对比:三张表,看规则到底在防什么

光讲规则容易左耳进右耳出。咱们建三张真实感的表,每张都先给一版"新手很可能写出来的",再给一版"评审能过的",然后掰开揉碎看差在哪、为什么。

例一:用户表 —— 主键、时间、NULL 的经典三连坑

先看一版随手写的 user 表——功能上完全能跑,评审时却会被红笔画满:

-- ❌ 反例:能跑,但埋了一堆雷
CREATE TABLE user (
  id          INT AUTO_INCREMENT PRIMARY KEY,   -- 自增主键
  email       VARCHAR(255),                      -- 没 NOT NULL,还要建唯一索引
  nickname    VARCHAR(255),
  profile     JSON,                              -- JSON 存扩展属性
  created     TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- TIMESTAMP
  UNIQUE KEY uk_email (email(20))                -- 前缀索引 + uk_ 命名
);

再看评审能过的版本:

-- ✅ 正例
CREATE TABLE user (
  id          VARCHAR(32)  NOT NULL,             -- 应用生成的 compact UUID
  email       VARCHAR(60)  NOT NULL DEFAULT '',  -- NOT NULL,宽度按真实域收敛
  nickname    VARCHAR(60)  NOT NULL DEFAULT '',
  status      TINYINT      NOT NULL DEFAULT 0,   -- 扩展属性拆成明确的标量列
  gmt_create  DATETIME(3)  NOT NULL,
  gmt_modify  DATETIME(3)  NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY u_email (email)                     -- 全列索引 + u_ 命名
);

逐条看差在哪:

反例的写法 埋了什么雷 正例怎么改
id INT AUTO_INCREMENT 分库分表/跨机房时两个库主键撞车;INT 最多 21 亿,涨爆过 应用生成 VARCHAR(32) UUID
email VARCHAR(255) 可 NULL NULL 三值逻辑坑统计;255×4=1020 字节,建索引撞长度上限 NOT NULL DEFAULT '' + VARCHAR(60)
profile JSON 体积/性能不可控,很多同步平台不支持 拆成 status 等明确标量列
created TIMESTAMP 2038 年溢出;且只有创建时间没有修改时间 gmt_create + gmt_modifyDATETIME(3)
uk_email (email(20)) 前缀索引选择性差;uk_ 不符命名约定;只取前 20 字符可能误判唯一 全列 u_email

一张十来行的建表语句,藏了 5 类规则违规。这就是为什么评审清单看着啰嗦——每一条都对应一个能让你半夜爬起来的坑。

例二:订单表 —— 复合索引"最左前缀"白建的钱

假设订单表最高频的查询是"查某个用户某种状态的订单,按时间倒序":

SELECT id, amount, status, gmt_create
FROM orders
WHERE user_id = ? AND status = ?
ORDER BY gmt_create DESC;

新手常见操作:给每个 WHERE 里出现的列都单独建一个索引,图个"都覆盖到":

-- ❌ 反例:三个单列索引,写入慢,查询还用不满
KEY i_user (user_id),
KEY i_status (status),
KEY i_time (gmt_create)

问题在于:MySQL 一次查询通常只能用上一个索引。上面这条 SQL 最多用到 i_user 过滤出该用户的所有订单,然后 status 过滤和 gmt_create 排序还得回表 + filesort。三个索引养着,写入时三份都要维护,钱花了效果差。

正确做法:按查询顺序建一个复合索引:

-- ✅ 正例:一个复合索引,精准命中
KEY c_user_status_time (user_id, status, gmt_create)

这样 user_id = ? AND status = ? 走索引前两列精准定位,gmt_create 已经在索引里有序,排序都省了。这就是复合索引的威力,也是"最左前缀"的意义——

建了 c_user_status_time,查询必须带上最左列 user_id才能用上它。 只写 WHERE status = ?(跳过了 user_id)?这个索引对它毫无用处,等于白建。

EXPLAIN 一看便知:正例的 typerefkeyc_user_status_timeExtra 没有 Using filesort;反例大概率是 Using where; Using filesort上线前跑一次 EXPLAIN,这类问题当场现形。

例三:历史日志表 —— 选错存储,迟早被数据压垮

这个坑最隐蔽,因为它上线头几个月完全正常。假设要记操作审计日志:

-- ❌ 反例:把无限增长的历史往 MySQL 里塞
CREATE TABLE audit_log (
  id       BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id  BIGINT,
  action   VARCHAR(50),
  detail   LONGTEXT,                    -- 大字段存详情
  gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

这张表的问题不在语法,在定位:审计日志是只追加、永不删、无限增长的。半年后它可能就是几亿行、几百 GB,那时候:

  • 想加个索引?大表 DDL,锁表半小时起。
  • 想按条件查历史?没有合适索引就是全表扫描,一查就把库拖垮。
  • LONGTEXT 让单行巨大,把 buffer pool 挤得七零八落。

正确的思路不是"把这张表设计得更好",而是"这类数据根本不该放 MySQL"

  • 无限增长的历史/审计,放到能廉价追加、易扩展的存储——对象存储、DynamoDB、专门的日志/时序系统。
  • MySQL 里如果非要留,也只留有界的、近期的热数据,配好保留期(比如只留 30 天),过期归档。
  • 大对象(附件、长文本详情)存对象存储,MySQL 里只存一个引用(URL 或 key)。

这条规则背后的哲学,值得单独抄下来:

MySQL 是给"当前的、基础的、要被频繁增删改查的结构化数据"用的,不是数据仓库,更不是日志归档。 选错存储类型,是那种"当下最省事、将来最要命"的决定。


四、被忽略的根因:很多禁令,是给"数据复制"让路的

前面那份禁用清单,很多人第一反应是"MySQL 明明支持 JSON / 生成列 / 外键,凭什么禁?"——因为你的数据往往不是只活在这一个库里。

真实的生产系统,MySQL 的数据几乎都要被复制/同步出去:主从复制做高可用、跨机房同步做多活、通过 CDC(Change Data Capture,变更数据捕获)灌进数据仓库或搜索引擎、用数据同步平台在多个环境间比对一致性。数据一旦要"流动",源库上一个看似无害的设计,就可能让下游同步链路对不上账。

理解了这条链路,前面一半的禁令就从"背规则"变成了"讲道理"。

复制是怎么工作的:先认识 binlog

MySQL 的复制核心是 binlog(二进制日志)——所有数据变更都记在这里,下游按它回放。binlog 有两种主要格式,坑就藏在这里:

  • Statement-Based(SBR,记 SQL 语句):把 UPDATE ... 这条语句原样发给下游重放。
  • Row-Based(RBR,记行变更):只记"这一行从什么变成了什么"。

SBR 有个致命问题:任何"结果不确定"的 SQL,在下游重放会得到不同结果。 比如 NOW()UUID()RAND()、自增值——源库和从库各算各的,数据当场分叉。这也是为什么现代生产环境基本都用 RBR 或 MIXED。而这个"不确定性"的坑,会顺着链路影响你的建表选择。

逐条对号入座:禁令 ↔ 复制约束

建表禁令 和数据复制的关系
每张表必须有主键 这是复制的硬要求。RBR 删/改一行时,下游要靠主键(或唯一键)定位到"哪一行"。没有主键,下游可能匹配到错误的行,MySQL 官方明确说这会让"源和副本静默分叉,破坏一致性"——连报错都没有。
主键别用二进制类型 跨环境的数据比对工具要拿主键做行对齐。binary 主键在不同工具/编码下表示不一致,比对时会冒出一堆"假差异",根本没法确认两边到底同没同步。用定长字符串 UUID 就没这问题。
禁生成列 生成列的值是源库算出来的。同步链路把这列当普通列传,还是让下游重算?不同工具行为不一,很容易两边算出不同结果。干脆禁掉,改成应用维护的普通列——所见即所存。
禁 JSON JSON 在复制里又大又麻烦:早期版本每次改一个字段都要把整个文档写进 binlog,体积暴涨;跨平台同步对 JSON 的支持还参差不齐。拆成标量列,复制清爽。
禁大字段 LONGBLOB/LONGTEXT 大字段让单行变得巨大,binlog 跟着膨胀,同步延迟飙升,一个大事务能把复制链路堵住。
charset/collation 要一致、别在表/列单独设 跨环境比对时,如果两边 collation 不同,同样的字符串会被判成"不相等"——又是"假差异"。统一继承库设置,比对才可信。
禁外键、禁触发器 它们都是"隐式的连锁写入"。同步链路只搬明面上的数据变更,下游未必会触发同样的连锁反应,两边越跑越偏。

看出来了吗?这些禁令的一大半,根本不是 MySQL 单机跑不动,而是"数据要复制出去"这件事对源库提出的纪律。 单机自己玩,你爱用 JSON 用 JSON;可一旦进了要做主从、多活、CDC 的生产环境,这些自由就成了同步链路的定时炸弹。

实践中要额外注意的几点

除了建表纪律,真正接上复制/同步链路时,还有几个坑值得提前想到:

  1. 确认 binlog 用的是 ROW 格式。这是让复制"可预测"的前提,别让 SBR 的不确定性毁掉一致性。
  2. 别在 SQL 里依赖"不确定函数"NOW()UUID()RAND() 这类,尽量在应用层算好了再写库——顺带一提,这也正是"主键用应用生成 UUID"的又一个理由。
  3. 大事务是复制的天敌。一次改几十万行,下游要么延迟暴涨要么直接卡住。呼应前面的"批量分批"——分批不只是为了本库的锁,也是为了下游追得上。
  4. schema 变更要考虑同步链路。源库加了个下游工具不认的类型(比如某种特殊列),同步可能直接报错中断。所以大表 DDL、加新类型,都要问一句"下游同步扛得住吗"。
  5. 给同步/比对留好"锚点"。稳定的主键、创建/修改时间戳,就是比对工具对齐两边数据的抓手——这也是"必有主键、必有时间戳"的隐藏用途。

一句话:

单机视角看是"限制",全链路视角看是"纪律"。 你建的不是一张孤立的表,而是一条数据流的源头。


五、一个容易被忽略的角度:规则也要允许"有据的例外"

讲了这么多"禁令",我最想强调的其实是这一点——好的规范,不是一刀切,而是给例外留了口子,但要求例外必须有证据。

比如"慎用唯一索引",但如果你的业务在高并发下确实需要数据库来做唯一性仲裁(应用层 check-then-insert 挡不住并发),那就该用唯一索引——前提是你在设计文档里写清楚:并发竞争在哪、为什么应用层挡不住、评审人是谁

再比如"避免大字段",但如果你确实要存一段有界的二进制(比如加密后的密文),那可以用有明确上限的 VARBINARY,但同样要写明上限、走审批。

这背后是一套我很认同的工程文化:

规则是默认值(default),不是铁律(law)。 遵守规则不需要理由,违反规则需要——而且这个理由要落在纸上、有人签字,而不是"我觉得没事"。

这样既避免了"新人乱来",又不至于让规范变成挡住合理创新的官僚主义。每条被"豁免"的规则,都对应一份写下来的、有人负责的决策。这才是评审清单真正的价值——它逼着你把"想当然"变成"想清楚"。


最后一句

数据库设计有个残酷的特点:代码写错了可以随时改,表设计错了,等有了数据就很难改了。 一张几千万行的表,加个索引可能要停机维护,改个字段类型可能要迁移一整晚。

所以这些"军规"看着琐碎、甚至有点不近人情,但它们几乎都是拿真实事故换来的。评审会上被打回来的时候别嫌烦——在建表阶段被拦下来,是最便宜的一次拦截。 等上了生产、涨了数据再发现,代价要翻几百倍。

老话说,凡事预则立,不预则废。建表这件事,就是典型的"预"——多花半天想清楚,能省掉后面几个通宵。

建表 & 查询自查清单(收藏版)

建表:

  • [ ] 每张表都有主键,且是应用生成的 UUID(非自增、非复合、非二进制)
  • [ ] 有创建时间 + 修改时间两个 DATETIME 列(不用 TIMESTAMP
  • [ ] 能 NOT NULL 的都 NOT NULL,留 NULL 的写明理由
  • [ ] 要建索引的字符串列控制在 255 字节内(utf8mb4 下约 VARCHAR(60)
  • [ ] 字符集/collation 继承库设置,不在表/列单独设
  • [ ] 无外键、无触发器、无视图、无全文索引、无生成列、无 JSON 列
  • [ ] 无大字段(MEDIUMTEXT/LONGTEXT/MEDIUMBLOB/LONGBLOB
  • [ ] 索引按查询设计(复合索引优先),命名用 u_/i_/c_ 前缀
  • [ ] 唯一索引和其他例外,都有书面理由

查询:

  • [ ] 不用 SELECT *,只查需要的列
  • [ ] 会增长的表无全表扫描,上线前跑过 EXPLAIN
  • [ ] 每个查询都有 limit(一页别超 2000 行)
  • [ ] JOIN 尽量少,复杂查询走单独评审
  • [ ] 复合索引查询带最左列
  • [ ] 批量操作分批、有上限,避免长事务
  • [ ] 不用 ON DUPLICATE KEY UPDATE / REPLACE INTO / INSERT INTO A SELECT * FROM B
  • [ ] 不更新主键值(要换身份就删了重插)
  • [ ] "写完立刻读"走主库,可容忍延迟的读走读库

参考资料

想深入或者拿去说服同事,下面这几份都是公开、可引用的权威资料。提醒一句:不同规范之间并不完全一致(后面会点出几处分歧),要结合自己的业务量级和部署形态判断,别照单全收。

综合规范类

  • 《阿里巴巴 Java 开发手册》(嵩山版)PDF —— 业界流传最广的一份,"MySQL 数据库"章节把规约分成【强制】【推荐】【参考】三档,覆盖建表、索引、SQL、ORM 映射。GitHub 上的 alibaba/p3c 还提供了配套的代码规约扫描插件(IDEA/Eclipse)。
  • 各家云厂商的《数据库设计规范》文档(阿里云、腾讯云开发者社区都有多篇),思路大同小异,可以横向对比着看。

MySQL 官方文档(最终裁判,遇到分歧以它为准)

几处值得注意的分歧

  • 自增主键:阿里手册对中小系统其实是允许 bigint unsigned 自增的,只在分库分表时才要求换成分布式 ID。而本文采用的是更激进的口径(默认就用应用生成的 UUID)——因为一旦考虑跨机房迁移,UUID 省去了后续换主键的痛。选哪种,取决于你未来会不会走到分布式那一步。
  • 主键类型:有的规范推荐 bigint(省空间、自增友好),有的推荐字符串 UUID(迁移/多活友好)。没有绝对答案,是一道 trade-off 题。

一句话:规范是别人拿事故换来的经验,但最终为你这张表负责的是你自己。 参考、理解、然后按自己的场景做决定。

全文思维导图

@startmindmap
<style>
mindmapDiagram {
  node {
    BackgroundColor #F8F9FA
    RoundCorner 10
    Padding 10
    FontSize 13
  }
  :depth(0) {
    BackgroundColor #1E3A5F
    FontColor white
    FontSize 18
    FontStyle bold
  }
  :depth(1) {
    FontSize 15
    FontStyle bold
  }
  :depth(2) {
    FontSize 13
  }
}
</style>

* MySQL 建表军规
** 主键
*** 不用自增,用应用 UUID
*** 必须有、避免复合、别改值
*** 别用二进制类型
** 字段
*** 能 NOT NULL 就 NOT NULL
*** 时间用 DATETIME 不用 TIMESTAMP
*** 索引串控制在 255 字节
*** 字符集继承库设置
** 禁用清单
*** 外键 / 触发器 / 视图
*** 全文索引 / 生成列 / JSON
*** 大字段 BLOB/TEXT
** 索引
*** 按查询设计,复合优先
*** 命名 u_/i_/c_
*** 慎用唯一索引与前缀索引
** 查询
*** 不 SELECT *、必加 limit
*** 增长表禁全表扫描
*** 复合索引带最左列
*** 批量分批、避免长事务
*** 写后即读走主库
** 实战对比
*** 用户表:主键/时间/NULL 三连坑
*** 订单表:复合索引最左前缀
*** 日志表:选错存储迟早压垮
** 工程文化
*** 规则是默认值不是铁律
*** 例外要有据、有人签字
*** 建表阶段拦截最便宜
@endmindmap

MySQL 建表军规 - 思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。