ARTICLE DETAIL

资讯详情

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

MySQL数据库设计规范:从命名到索引,避开线上事故的实战指南

MySQL数据库设计规范:从命名到索引,避开线上事故的实战指南 1. 为什么要有一份“能抄作业”的 MySQL 设计规范先讲一次线上事故我到现在都还记得那次事故。凌晨两点监控群突然炸了告警刷了一屏订单表全表扫描数据库 CPU 直接顶到百分之百所有写请求开始排队。等我登录上去一看问题出在一个很不起眼的地方新同事在查询条件里用了一张表的 varchar 字段而那字段连索引都没有表数据量已经破了千万。更无奈的是这张表设计的时候字段名叫user_name另一张关联表叫username业务代码里 Join 时写错了一个没人发现因为这个字段从一开始就没按统一规范设计。这类事故不是偶然的。数据库设计规范看起来像是一堆琐碎的规定实际上它是把团队里每个人的经验、教训沉淀成一套共同语言。它解决的问题不是某一个 SQL 写得不好而是所有人的建表方式、命名方式、字段类型、索引策略都保持一致让系统在数据量上来之后依然可预期、可维护、可排查。如果你是一个两三人的小项目规范看起来是多余的。可一旦表数量超过五十张参与开发的超过三个人没有规范的数据库就是一座随时会塌的积木塔。你会看到这些真实场景同一个含义的字段有人叫created_at有人叫create_time还有人叫gmt_create联表查询全靠猜。有的表用自增主键有的用 UUID有的干脆没有主键。金额字段有人用decimal有人用float算到最后对不上账。时间字段一半是datetime一半是int存时间戳排序、格式化全靠脑子换算。这些不是纯技术问题是管理问题。规范的真正价值是让数据库在变成事故之前就先规避掉大概率会踩的坑。所以这篇文章我打算从一线实际使用的角度把一套可直接复制的 MySQL 设计规范拆开讲清楚覆盖命名、类型、索引、范式、约束、变更这几个核心环节每一条都会说清楚为什么这么定不这么定会出什么事。2. 库、表、字段命名大小写、分隔符与保留字这几条底线2.1 库名和表名小写字母加下划线一个业务域一个前缀命名规范是所有规范里最容易被忽视、又最容易引发连锁问题的一环。我见过不少项目库名用驼峰表名用大写字段名中英混排最后业务代码里写 SQL 时全靠 IDE 提示来补全换一个环境就报错。MySQL 在 Linux 上是区分大小写的在 Windows 上默认不区分这个差异就是埋雷的地方。如果开发同学在 Windows 本地建了一张UserOrder表提交的 SQL 脚本到 Linux 生产环境执行user_order是完全不同的两个对象。为了彻底绕开这个坑我建议直接规定库名、表名、字段名统一使用小写字母单词之间用下划线分隔。同一业务域的表使用统一前缀比如用户中心uc_、订单中心ord_、商品中心pms_一眼就能看出表归属哪个模块。表名的长度控制在 30 个字符以内过长的表名在 ER 图和日志里都很影响阅读。禁止使用驼峰命名、禁止中英文混搭、禁止大小写混用。这里有个容易被忽略的点MySQL 的库名表名在 Linux 文件系统里对应的是目录和文件如果你用了大写字母某些大小写敏感的文件系统下备份恢复、主从切换时可能直接找不到表。我经历过一次迁移事故就是导出导入后表名大小写不一致导致应用全部报table doesnt exist后来统一成小写加下划线这类问题再没出现过。2.2 字段名的语义一致同一个词全世界都一个含义字段命名最考验团队的抽象能力。同一个概念在不同表里叫法必须一致。我建议把常用的字段名固定下来形成一张团队内部的“字段字典”字段语义标准命名类型说明创建时间created_atdatetime记录插入时间默认当前时间更新时间updated_atdatetime记录最近修改时间更新时刷新生效记录状态statustinyint数值状态配合字典表解释含义删除标记is_deletedtinyint0 未删除1 已删除对应逻辑删除主键idbigint unsigned单表自增主键统一叫 id外键关联关联目标表名加相应语义与关联主键类型一致避免出现两张表主键类型一个 int 一个 bigint字段名统一的直接收益是写 SQL 不用查表结构。你看到一个created_at不需要怀疑它有别的写法JOIN 条件和 WHERE 条件也少了一堆低级错误。2.3 保留字与关键词建表前先过一遍字典这个坑可以说是新人高频踩坑点。order、group、desc、select、rank、key、index这些都是 MySQL 的保留字或内置函数名。你建一张order表在绝大多数场景下不报错但在某些 SQL 写法里必须用反引号包起来非常烦人。规范里我要求所有表名、字段名在定稿前必须做一次保留字检查。MySQL 官方文档里有完整的保留字列表也可以用一条 SQL 查SELECT * FROM information_schema.KEYWORDS;如果字段确实需要用到类似语义的词比如业务里就是有个字段叫rank我建议改名成rank_no或sort_level而不是让所有查询都背着一堆反引号。要记住数据库对象的命名是为十年后的维护者服务的不是为你今天写代码方便服务的。3. 字段类型选择整数、小数、字符和时间到底怎么定3.1 整数类型不用省那几个字节MySQL 的整数类型有tinyint、smallint、mediumint、int、bigint字节数分别是 1、2、3、4、8。很多同学在设计表时为了“省空间”把主键设成int状态位设成tinyint(1)这本身没错但要务必要想清楚边界。我见过一个表主键用的int有符号最大 21 亿多。业务当时觉得这辈子用不完结果三年后达到十亿量级被迫停机改表结构把主键换成bigint整个过程痛苦不堪。所以从规范上我直接要求主键统一用bigint unsigned除非你能证明这张表生命周期内数据量一定小于 21 亿。状态字段用tinyint unsigned取值范围 0~255多状态枚举完全够用。数量、计数类字段优先int unsigned量级特别大的用bigint。允许为 NULL 的数值字段建议显式给出 DEFAULT 值避免 NULL 参与计算时产生意外结果。值得说明的是unsigned本身不会让性能更好它只是扩大了正数取值范围。如果status只用 0 和 1tinyint和tinyint(1)在存储上没有区别显示宽度只是给客户端展示用的不影响存储。这个认知要清楚别被“长度”这个概念误导。3.2 小数类型金额和比率必须用 DECIMAL这是最不该含糊的地方。float和double是浮点数存在精度损失。金额字段如果用float当你存入 0.1 时实际可能是 0.100000001490116。十几个字段累加之后误差就会暴露出来对不上账。我的规定很简单金额、价格、费率、余额等小数类型一律使用decimal。decimal的定义要写明精度比如decimal(10,2)表示最长 10 位小数点后 2 位。金额字段建议至少预留到decimal(10,2)如果以后可能做大额交易或分账直接上decimal(20,6)也不亏。禁止用float、double存任何跟钱有关的字段。小数比较、求和时注意decimal的精度与业务舍入规则是否一致必要时在应用层统一四舍五入策略。有人会问那float还有用吗有比如一些科学计算、经纬度、统计分析中允许近似值的地方可以用。但凡是需要精确表示的业务字段一律decimal。3.3 字符串类型VARCHAR 长度不是越大越好varchar是最常用的字符串类型它存储的是变长字符加上 1~2 字节的长度记录。新人常犯的错是把所有字符串字段都设成varchar(255)理由是“不差这点空间”。实际上这个习惯会造成两个问题varchar过大会影响索引效率。在 utf8mb4 字符集下一个字符最多占 4 字节varchar(255)需要 1020 字节的索引空间而 InnoDB 单列索引最大长度默认是 767 字节或取决于页面大小。业务上无法约束数据长度。比如订单号本来最长 32 位你给了 255结果上游塞进来一串 200 位的垃圾数据入库不报错后期查询和展示都出问题。我这边给出的标准是根据业务真实最大长度 30% 的余量来定varchar长度。比如手机号最长 11 位我建议varchar(20)邮箱最长常见不超过 50 位建议varchar(64)订单号按业务规则最长 32 位建议varchar(50)。文本内容超过 512 字符的考虑用text或者拆分子表。text字段要注意它不能在非前缀索引情况下正常建普通索引平时也不建议拿来当查询条件。大文本搜索应该走全文索引或搜索引擎不要指望 MySQL 在千万级text字段上做 LIKE 模糊查询还能快。3.4 日期时间类型首选 DATETIME别再用时间戳日期时间类型有date、time、datetime、timestamp、year还有很多人喜欢用int或bigint存 unix 时间戳。我的态度很明确新表一律使用datetime原因有三个。datetime可读性好直接就能看出来是什么时间排查问题不用做换算。datetime取值范围从 1000-01-01 到 9999-12-31足够覆盖绝大多数业务。时间戳int只能表示 1970 到 2038 年2038 问题对长周期系统而言是真实的炸弹。timestamp也有它的归属场景它自带时区转换如果系统需要按用户时区展示时间可以更灵活。但带来的问题是数据实际存的是 UTC 时间如果周边系统时区配置不一致很容易出现“查出来的时间差八小时”的经典事故。所以我建议默认datetime确需时区自动转换的场景单独评审。这里还涉及一个细节到底字段叫什么。统一为created_at、updated_at之后默认值也要规范。MySQL 从 5.6 开始支持DATETIME类型带默认当前时间所以建表时可以这样写CREATE TABLE uc_users ( id bigint unsigned NOT NULL AUTO_INCREMENT, nick_name varchar(30) NOT NULL COMMENT 昵称, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT用户表;ON UPDATE CURRENT_TIMESTAMP这个特性很多人不敢用怕时间不准。实际上 MySQL 对updated_at的更新是发生在行数据被 UPDATE 时自动刷新的只要不是手动显式赋值它就能保持正确。当然如果你用了 MyBatis Plus 这类 ORM 的自动填充可以在应用层做两种方案取其一即可不要跟数据库的自动更新同时生效否则容易覆盖。4. 主键与索引设计让 EXPLAIN 替你说话4.1 主键选择自增还是 UUID这是一道必须提前答完的题主键设计直接决定 InnoDB 表的物理存储结构。InnoDB 是聚簇索引组织表数据行按照主键顺序物理排布。自增主键天然是顺序递增的插入时总是在页的末尾追加不会导致页分裂和碎片化。这是最推荐的方式。UUID 作为主键的问题在于它是随机字符串插入时主键顺序不连续极端情况下导致大量页分裂、随机 IO写入性能断崖式下降。而且 UUID 占用 36 个字符作为聚簇索引会让二级索引的体积成倍增大。但有一种情况我明确建议用业务唯一键而不是自增主键分布式场景或多系统合并数据的场景。届时自增主键可能出现冲突用雪花算法生成的bigint主键更为合理。这也是为什么我把主键统一成bigint unsigned——雪花ID正好可以存在bigint里。结论性建议单库单表自增bigint主键名统一叫id。分库分表或者需要跨系统合并应用层生成雪花 ID存bigint unsigned。禁止使用 UUID 字符串作为主键禁止无主键表。4.2 每一张表都必须有主键这是底线没有主键的 InnoDB 表MySQL 会生成一个隐藏主键这个隐藏主键你碰不到也不会知道结果就是数据复制、主从切换、归档时全都变得不可控。规范必须写死任何表都要有主键且主键要满足非空、唯一、稳定这三个条件。我见过有人把“业务主键”和“代理主键”搞混。比如订单表业务上order_no是唯一的于是直接把order_no设成主键。这在单库时代没毛病但一旦未来要分表order_no可能就不是全局唯一了。更稳妥的做法是id作为代理主键order_no上建唯一索引两者各司其职。4.3 索引设计少而精覆盖优先索引不是越多越好。每个索引都会占用磁盘并拖慢写入性能。我评审表结构时经常看到一张表挂了十几个索引实际上很多是冗余的。索引设计有一条非常实用的原则联合索引要遵循最左前缀尽量让一条索引覆盖多个查询路径。举一个实际例子。订单表有user_id、shop_id、status、created_at四个常用查询字段。如果分别建四个单列索引效率远不如建两组联合索引KEY idx_user_status (user_id, status, created_at), KEY idx_shop_status (shop_id, status, created_at)这样设计之后查询“某个用户某状态下的订单按时间排序”就能完全用索引覆盖避免文件的排序。很多人忽略的是索引中字段的顺序决定了可用性。把区分度高的字段放前面把范围查询字段尽量放后面这些规则都必须写进规范并且要求开发在提交 SQL 前跑一次EXPLAIN确认type不是ALL、key不是 NULL。4.4 常见索引设计记录建立一张索引 checklist我把索引相关的审查项做成了一张团队内必查表审查项标准WHERE 条件高频字段建立索引必须ORDER BY / GROUP BY 字段参与索引尽量利用最左前缀JOIN 字段类型与长度一致必须否则索引不生效单表索引数量控制在 5 个以内区分度低的字段不建索引如性别status除非配合联合索引索引前缀长度大 varchar 列使用前缀索引唯一索引业务唯一性用唯一索引强制兜底5. 范式与反范式拆表还是冗余必须有一个明确标准5.1 范式是理论基础但不能生搬硬套教科书里讲三大范式、BCNF很多同学记住了“要消除冗余、要拆分表”。实际业务里如果严格执行第三范式你会发现查询时要关联七八张表性能惨不忍睹。所以规范里我给的是混合策略核心交易数据倾向规范化查询密集的数据允许反范式冗余。第三范式本质上解决的是“数据一致性”问题一条数据只在一个地方维护避免更新时出现多处不一致。这个思想对订单、账务、库存这些强一致场景完全适用该拆就拆。但对于用户昵称、商品名称这种更新频率低、查询频率极高的信息冗余到订单表里往往是更明智的。比如订单快照表里就冗余了goods_name和goods_price因为用户下单后商家改了商品名和价格历史订单必须显示当时的信息这种冗余不是缺点是业务要求。5.2 冗余字段的边界不可变或低频变 高频读什么字段适合冗余判断标准我总结为三句话冗余字段的值变化频率极低比如商品名称、下单时的收货地址。冗余字段的更新可以由明确的事件触发且能保证最终一致。冗余带来的收益要明显即显著减少高频查询的 JOIN 开销。要避免冗余的是那些高频率更新的字段。比如用户积分、库存余量这类如果到处复制一份分布式一致性问题会让你生不如死。5.3 大字段拆分的判断什么时候分表什么时候只拆字段还有一种常见需求是单表字段过多。有的表建了七八十个字段说实话在 MySQL 中字段多并不会直接造成性能问题真正的问题是一行数据过大更新时锁的范围、缓冲池的占用都会变大。如果一个表里既有频繁访问的热点字段又有大量很少读取的文本大字段我建议做垂直拆分把大文本挪到扩展表比如user_profile_ext表通过主键一对一关联。水平拆分则不要在一开始就做。我的建议是先按规范设计一个垂直可扩展的模型单表数据量到两千万以后再考虑水平分表。过早分表带来的跨表查询、聚合统计问题远比收益更麻烦除非你有明确的容量规划否则不要轻易上。6. 约束与默认值数据库层防呆设计的最后防线6.1 外键默认不用但唯一约束必须用很多从书本上学数据库的人会惊讶于一线团队“禁用外键”的做法。原因很简单外键约束在 MySQL InnoDB 里会带来锁竞争高并发写入时容易成为性能瓶颈而且一旦跨库分表数据库外键就没法用了。业界主流方案是在应用层保证引用完整性。但这不等于不设约束。所有业务上的唯一性比如订单号、手机号、用户登录名都必须建唯一索引由数据库兜底。应用层两台机器并发插入只有一个能成功这是唯一索引存在的意义。我在评审时反复强调这一点应用层可以做逻辑校验但唯一性最终必须落在数据库约束上这才是最后一道防线。6.2 默认值和 NOT NULL避免 NULL 的三态地狱NULL在 SQL 里是一个很特殊的“三态”概念它不等于 0不等于空字符串也不等于 false。对NULL做比较运算时结果依然是NULL这会让WHERE a NULL永真或永假也会让COUNT(字段)统计不到 NULL 行。我的规范是业务字段尽量全部NOT NULL并且显式提供默认值。字符串字段默认不要给NULL给空字符串除非你有明确的语义需要区分“未填写”和“填空字符串”。数值字段默认给 0 或业务定义的最小值。时间字段默认给CURRENT_TIMESTAMP或固定时间。确实需要 NULL 语义的字段要单独评审并注释说明。这条规定执行到位之后应用代码里各种if (xxx null)的空指针少掉一大半SQL 统计也干净很多。6.3 字段注释写清楚是给未来同事留门字段注释看起来不起眼但它的价值在半年后你重新接手一个系统时完全体现。规范要求每一张表都要有COMMENT说明表用途每一个字段都要有COMMENT说明含义、单位、取值范围。状态字段还要额外注明每个数值代表什么含义。如果团队的枚举定义放在代码里那么建表 SQL 的字段注释里必须写清“0 待支付 / 1 已支付 / 2 已取消”不要只是干巴巴写一个“状态”。这能直接减少以后大量翻代码确认枚举值的时间。7. 字符集、排序规则与存储引擎全局默认最容易埋雷7.1 字符集统一使用 utf8mb4关于字符集我的立场非常简单新库新表一律utf8mb4。utf8mb4是真正的 UTF-8能完整表示 emoji 和大部分生僻汉字老的utf8在 MySQL 中最多只有 3 字节遇到 emoji 直接报错或乱码。建库时就要固定CREATE DATABASE your_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;线上环境如果已经存在大量老表用utf8也要排期完成字符集迁移。迁移过程需要注意索引长度问题因为utf8mb4下索引会比utf8更长部分表可能需要同时调整字段长度。7.2 排序规则unicode_ci 还是 general_ci别选错utf8mb4_unicode_ci和utf8mb4_general_ci都能用差别很小主要是排序和对某些字符的相等性判断规则。general_ci性能略好但精确度上unicode_ci更符合 Unicode 标准。我统一推荐utf8mb4_unicode_ci因为实际差别在常规业务里几乎感知不到但后者在多语言场景更稳妥。要注意的是一张表里的字段可以单独指定排序规则如果表排序规则和字段排序规则不一致查询时可能出现无法使用索引的情况。所以建表语句里就统一显式写出字符集和排序规则不要依赖实例级默认值避免从测试环境到生产环境配置不同导致行为漂移。7.3 存储引擎InnoDB 是唯一选择从 MySQL 5.7 开始InnoDB 就是默认引擎但规范里依然要写死全部使用 InnoDB。理由很简单支持事务、支持外键虽然我们默认不用外键但万一需要、支持行级锁崩溃恢复能力强属于通用业务最稳妥的选择。MyISAM 在只读报表类场景可能还有一点历史遗留但新的业务表一律不许用因为它不支持事务表锁会导致并发写入严重串行化一旦异常断电还容易损坏表。8. 从设计到落地评审流程、版本化 DDL 与常见坑8.1 表结构评审把规范塞进流程靠人肉不如靠工具规范写在文档里没人看等于零。真正能落地的办法是把评审做成强制节点。我团队的操作是新表或表结构变更必须走一个简短的评审流程至少包含这几个检查项是否遵循命名规范有没有使用保留字。是否每条字段都有注释。索引是否能覆盖主要查询是否存在重复索引。字段类型是否合理有没有隐式转换风险。唯一性约束是否由数据库兜底。另外可以用工具辅助比如pt-online-schema-change、gh-ost来做大表 DDLSchemaCI或自研脚本做数据库 CI 扫描我没法推荐某一个特定工具但核心思路是将规范检查自动化到发布管道不能只靠人自觉。8.2 版本化 DDL表结构必须跟代码一起进 Git数据库表结构是最容易被版本管理忽视的部分。很多人改完表结构只在测试环境执行一遍没有留下任何痕迹。三个月后生产环境要做迁移只能翻开发记录里零碎的 SQL 片段。我要求所有建表、改表 SQL 必须放进项目根目录的migration目录按版本号命名比如V20250101__create_uc_users.sql。每次发布都从空库执行一遍所有 migration 脚本保证环境同步。这个习惯最大的受益点是新同事拉下代码后能快速搭建完整本地环境测试环境和生产环境结构差异一目了然。8.3 在线变更大表 DDL 的锁表问题表结构变更在 MySQL 5.6 之后支持了在线 DDL但“在线”并不代表零风险。修改字段类型、增加索引、修改字符集这些操作仍然可能触发表的重建期间产生的锁和 IO 压力足以拖垮线上业务。我给出的操作规范是单表数据量超过 500 万行的结构变更一律使用pt-osc或gh-ost工具在低峰期执行。变更前先评估磁盘空间因为很多工具需要复制一份临时表至少要有原表 1.5 倍的空间。变更窗口设置充分的时间余量避免 binlog 积压导致主从延迟。这里有同学会问为什么不能直接用ALTER TABLE因为业务高峰期锁等待直接导致连接数打满我踩过的教训就是一张千万级表增加一个普通索引在线执行的十来分钟里写请求全部阻塞最后连接池耗尽整个服务雪崩。从那以后我宁愿慢一点也要用在线工具。8.4 执行规范后最大的收益团队沟通成本直线下降规范最难的不是定规矩而是坚持执行。团队里总有人说“这个字段很急先加进去等以后再规范”这种口子一开三个月后表结构又回到混沌状态。我个人的经验是规范可以迭代但执行不能打折。一旦发现违反规范的表立刻排期整改越早处理成本越低。每次新人入职我都会让他先读一遍数据库设计规范然后试着评审两三张老表指出不合规的地方。这个过程比培训效果好得多因为他会真正意识到规范不是束缚而是一张避坑地图。9. 最后分享几个我自己常用的兜底技巧写到这里干货已经讲得差不多我最后补充几个实际工作中非常常用、但很多人不知道的小技巧。第一个是关于EXPLAIN的误判。很多开发习惯只看type是不是ALL实际上typeALL不一定完全不可接受如果表只有几百行全表扫描反而比走索引快。真正要警惕的是key为 NULL、rows估算值明显放大、以及Extra里出现Using filesort和Using temporary这三个信号才说明索引设计有问题。评审时尽量把这三项也写进 checklist。第二个是关于隐式类型转换。数据库规范里一定要强调字段类型和查询条件类型必须一致。比如user_id是bigint却传了个字符串123来查MySQL 会将整表字段都转成字符串做比较索引直接失效。线上查不到数据或者慢查询一半以上是这个问题。排查方式就是看 EXPLAIN 的key是否为 NULL。第三个是定期做一次 Schema Review。不要等到出事故再反思我建议每两三个月由一个人通读一遍所有表的 DDL顺手把废掉索引、注释缺失、类型不合理的表标记出来。这件事在数据量小的早期做成本极低收益极高。等数据量上来再改就是每一条都要演一场刚才说的那种大表变更的惊悚剧。我个人用了这套规范带队近十年最大的感受不是数据库“变快”了多少而是整个系统变得可预期了。任何一个后来者接手不需要猜不需要翻代码看建表语句就能读懂业务模型。这大概就是数据库设计规范存在的真正意义吧。
返回列表