1. 表设计的第一个坎:业务没吃透就动手建表
我接过最痛苦的一次数据库表设计,是一张 80 多个字段的单表,里面同时塞了订单信息、用户地址、支付流水号和发票抬头。这活儿不是外包干出来的,而是几个工程师为了“先上线再说”硬生生拼出来的。后来业务要拆分,光是清洗和迁移数据就折腾了两周。所以别小看“数据库表设计”这五个字,它本质上不是写几条建表语句,而是对整个业务的建模。
1.1 先理清业务关系,再谈建表
很多新手拿到需求就直接开表,把需求文档里的字段名挨个抄一遍,然后就开始写 CRUD。这样做的问题在于,你根本分不清“订单”和“订单明细”是应该拆两张表,还是塞在同一张表;“用户地址”是用户表的字段,还是独立的一张地址表;“支付流水”到底要不要落地。这些选择全都来自业务规则,不是来自字段清单。
我现在的习惯是:先画一张简单的实体关系草图,把“谁拥有什么”“谁依赖谁”“谁会频繁查询哪些数据”标出来,再动手写 DDL。可以不用画到标准 ER 图那么复杂,哪怕手写一张卡片都行,但这一步能规避掉后面 80% 的返工。数据库表设计的核心不是你会不会写 CREATE TABLE,而是你清不清楚每一张表代表的业务实体到底是什么。
1.2 三范式不该是教条,而是一把检查尺
第一范式要求字段原子性,第二范式要求消除部分依赖,第三范式要求消除传递依赖。教科书定义背出来很容易,但实际设计时,大部分人要么完全不拆,要么拆得过度。
举个例子。订单表里保存一份“用户等级快照”算不算违反第三范式?从纯理论角度,它确实违反,因为用户等级本应存到用户表中,订单表只要关联用户 ID 就行。但现实中,用户等级在下单后就可能发生变化,而订单的历史统计需要当时的等级。这种“带快照的冗余”恰恰是合理的业务需要。
我的判断标准就一句话:冗余字段是否会因为源头数据更新而出现不一致?如果会,就要么通过事务同步,要么干脆不冗余;如果是历史快照,不存在更新联动,那冗余就是可接受的。三范式真正的作用是让你在写冗余时心里有数,而不是直接禁掉所有冗余。
1.3 一次订单表重构的复盘
之前我接手过一个电商项目,原来的订单表里有商品名称、商品分类、商品品牌、供应商名称、供应商联系人、仓库名称、仓库地址……这些字段全部来自商品和供应商两张主数据表。当时这么做的原因很朴素:查询订单列表时不想再 JOIN 商品表了。
等到需求变成“供应商改名为一个新名字后,历史订单要显示旧名字”时,整张表被业务逼疯了。因为订单表里的供应商名称是实时冗余,根本没想过做快照,改一次主数据,历史订单全跟着变。最后我们重构时,把订单表切成订单主表、订单明细表,并明确规定:商品名称、供应商名称在订单里只能以“快照”形式存在,订单保存后不允许再被主数据更新影响。
这次重构让我明白,表设计的本质是回答“这个业务事实在某个时间点是什么状态”,而不是把当前页面要展示的字段全堆在一起。一张表只描述一个业务实体,字段只存这个实体直接拥有的属性,这条规则到现在仍然是我设计数据库表的第一原则。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字段类型选错,后面所有的优化都是在还债
字段类型的选择是数据库表设计里最容易被忽略、却最影响后期性能的一环。很多人建表时只考虑“这个字段存什么内容”,没考虑“这个字段未来会被怎么查询、怎么计算、怎么比较”。类型选错,等到数据量上去了,索引也建了,发现还是慢,那大概率是类型本身的问题。
2.1 数值字段:选错类型给统计挖坑
整数类型里,TINYINT 占 1 字节,SMALLINT 占 2 字节,INT 占 4 字节,BIGINT 占 8 字节。对于状态位、类型位这种取值不超过几十个的字段,TINYINT 就够了,完全没必要用 INT。但反过来,用户 ID、订单 ID 这类未来可能过亿的字段,最好直接用 BIGINT,不要等到快溢出时再改字段类型,那种 DDL 操作在大表上是灾难。
金额字段有一个铁律:不要用 FLOAT 和 DOUBLE,要用 DECIMAL。原因很简单,浮点数在二进制下无法精确表示 0.1 这类小数,做累计求和时误差会越来越大。DECIMAL(10,2) 表示总长度 10 位、小数 2 位,最大能存 99999999.99,一般业务足够。如果你做的是分账、结算这种高精度场景,把小数位放宽到 4 位,展示层再去做四舍五入也行。
还有个容易被坑的点是 MySQL 里的 INT UNSIGNED。看起来只是“不允许负数”而已,但它会导致比较操作和部分 ORM 框架的兼容性问题,比如 Java 的 Long 类型映射还好,但某些框架会把 UNSIGNED INT 映射成 Long 以外的类型,很容易出问题。所以除非有压倒性理由,我一般建议直接只用 BIGINT,省掉一堆麻烦。
2.2 字符串字段:varchar 长度不是拍脑袋
VARCHAR 和 CHAR 的区别大家都很熟,一个变长一个定长。但实际设计里,很多人把 VARCHAR(255) 当成万能选项,不管什么内容都往 255 里塞。这里有个隐藏问题:VARCHAR 的长度上限是按“字符数”算的,但索引有字节长度限制。MySQL 5.7 默认的索引单列最大是 767 字节,使用 utf8mb4 时一个字符最多占 4 字节,所以 VARCHAR(191) 刚好能建立完整索引,VARCHAR(255) 在某些情况下就会触碰索引长度限制,除非你用的是 3072 字节的 DYNAMIC 行格式。
另外,TEXT 类型的坑在于:它不能有默认值,而且 MySQL 对 TEXT 字段的索引必须指定前缀长度,不能直接建普通索引。这就导致你后期如果要在 Text 字段上做 LIKE 查询,执行计划基本就是全表扫描。所以大文本内容,比如商品详情、文章正文,我的建议是放独立的扩展表,或者干脆走对象存储,数据库里只放一个文件 Key。
2.3 时间字段:datetime、timestamp 与时区
DATETIME 和 TIMESTAMP 的区别大家应该都听过:DATETIME 占用 5 到 8 字节,不受时区影响;TIMESTAMP 占用 4 字节,范围只能到 2038 年,而且会随数据库时区变化而换算。如果你做的是国际化业务,建议统一用 DATETIME 以 UTC 存储,应用层负责时区转换;如果只是国内业务,用 DATETIME 存储北京时间也能接受,但要明确规定“所有接口传参都带时区”或者约定为北京时间。
还有一点,时间字段的默认值一定要设置。MySQL 5.6 之后支持 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP,这个功能可以直接在数据库层维护创建时间和更新时间,省得每一次 INSERT 和 UPDATE 都在应用层手动填。
2.4 布尔、枚举、JSON:能不用就不用的系列
布尔字段我用 TINYINT(1),存 0 和 1,配合字段注释写明含义。MySQL 8.0 也支持 BOOLEAN 类型,但底层仍然是 TINYINT(1),所以使用上差别不大。
枚举字段我基本不用 ENUM。原因有三点:一是 ENUM 的排序是按照定义顺序而不是字母顺序,容易出错;二是改动枚举值需要做 DDL,不是普通的增改数据;三是 ENUM 的定义绑死在表结构里,代码和数据的耦合很高。替代方案是建一张字典表,或者直接用 TINYINT 存状态码,配合字段注释说明每个值对应的含义。
JSON 字段要辩证看。MySQL 5.7 的 JSON 类型可以做部分查询,但它最大的问题是无法像普通字段一样建索引,查询性能有限。适合存那种“业务上偶尔看、不需要频繁检索”的扩展信息,比如后台配置项、前端展示元数据。凡是需要做 WHERE 条件、JOIN 关联、统计分组的字段,一律不要塞进 JSON。
3. 主键、唯一约束和索引:查询快慢在建表那刻就定了
索引不是后期发现问题再补的,而是建表时就应该规划好的。大多数性能问题,根子都在数据库表设计的索引策略上。你后期可以加索引,但加到一定量之后,更新性能下行,索引维护成本反而成为新的瓶颈。
3.1 主键:自增、UUID、雪花 ID 各有利弊
主键策略直接决定数据写入性能和分布式扩展能力。MySQL InnoDB 是聚簇索引,主键顺序直接决定数据行的物理存储顺序。自增主键是单调递增的,新行插入永远在末尾,页分裂少,写入性能最好。但它的问题是跨库合并时容易冲突,而且自增 ID 会暴露业务量。
UUID 适合跨库合并,但随机字符串做主键会让聚簇索引频繁页分裂,写入性能下降明显。雪花 ID 本质上是一个 64 位整数,既有全局唯一性,又能相对有序,适合分布式场景。如果你没有强一致的全局唯一要求,单机项目用自增没毛病;一旦确定未来会分库分表或者多活,建议直接上雪花 ID 或类似方案,避免后期迁移主键的绝对痛苦。
还要提醒一句:不要用“业务主键”当数据库主键。比如订单号,它在业务上是唯一的,但未来可能因为格式调整或生成规则变化导致碰撞,而且订单号一般比较长,作为聚簇索引会浪费空间。正确做法是 id 做代理主键,order_no 加唯一索引做业务唯一键。
3.2 唯一约束加了报错?先处理存量重复数据
热搜里有一个词叫“mysql设置唯一已经有重复数据库”,这个场景太常见了。你想给 order_no 加唯一索引,系统直接报错,原因是表里已经存在重复的订单号。这时候最蠢的做法是先删索引加字段强行插入,最后又发现业务越来越乱。
正确流程分三步。第一步查询确认重复范围:
sql复制SELECT order_no, COUNT(*) AS cnt
FROM orders
GROUP BY order_no
HAVING cnt > 1
ORDER BY cnt DESC;
第二步,保留其中一条记录,一般是保留最小 ID 那条,删除其他重复行:
sql复制DELETE o1 FROM orders o1
INNER JOIN orders o2
ON o1.order_no = o2.order_no AND o1.id > o2.id;
第三步再添加唯一索引:
sql复制ALTER TABLE orders ADD UNIQUE KEY uk_order_no (order_no);
这里有个重要提醒:删除重复数据前,一定要先确认这些重复行关联的子表数据,比如订单明细、支付流水。不然主表删掉一条记录,子表可能变成孤儿数据。我是吃过这个亏的,所以现在处理重复数据之前,一定会先看外键关联。
3.3 联合索引的顺序:最左前缀是硬规则
联合索引的生效规则是“最左前缀”,查询条件必须包含索引最左边的列,才能命中索引。比如联合索引 (user_id, status, create_time),能命中 user_id 单独查询、(user_id, status) 查询、(user_id, status, create_time) 查询,但无法命中只有 status 和 create_time 的查询。
所以联合索引的列顺序不是随便排的,而是按以下规则来:等值条件列放最前,区分度高的列放前面,范围条件列放最后。比如用户订单列表的查询条件是 user_id = ? AND status = ? AND create_time > ?,那索引应该设计成 (user_id, status, create_time),而不是反过来。因为 create_time 是范围条件,把它放在前面,后面的列就无法用于查询了,只能用于排序或覆盖索引。
3.4 索引数量要克制,EXPLAIN 说了算
“给所有查询字段都建索引”是很多刚入门的人最容易犯的错。索引不是越多越好,每个索引都会增加 INSERT、UPDATE 的写放大成本,还会占用存储空间。如果你有 5 个单列索引,其中好几个从不被使用,那整个表的写性能都会被拖累。
我判断索引是否有效的唯一标准是 EXPLAIN。建完索引后,把核心查询语句跑一遍,看 key 字段是否走了预期索引、rows 字段估算扫描行数是否大幅下降。如果 EXPLAIN 结果显示 type 是 ALL,说明还是全表扫描,索引大概率没用上,要么是索引列与查询条件不匹配,要么是查询写法导致索引失效,比如对索引列用了函数或者隐式类型转换。
4. 命名规范与注释:表结构本身就是团队文档
数据库表设计不只是技术资产,更是团队协作里的公共文档。你离职之后,后来者看着几十张表和几百个字段没有任何注释,第一反应往往是骂人。命名规范和注释不是一个加分项,而是必须项。
4.1 从库名到索引名,一套能直接用五年的规则
我用的规则很简单:库名全部小写,用下划线分隔,比如 trade_order、user_center;表名也全部小写,复数形式视团队习惯而定,但不要一会单数一会复数。字段名禁止驼峰,统一小写加下划线。为什么要这样?因为 Linux 下 MySQL 表名是区分大小写的,如果表名一会儿 UserInfo 一会儿 user_info,DBA 排查问题时会想打人。
索引命名也有固定套路:普通索引用 idx_ 前缀,唯一索引用 uk_ 前缀,外键用 fk_ 前缀,后面跟字段名组合,比如 idx_user_create(user_id, create_time)。这样看着索引名就能知道它是什么类型、覆盖了哪些列,不用去看建表语句。
还要注意别用数据库保留字当字段名。比如 name、desc、order、level 这些词,在 MySQL 里虽然不一定报错,但在部分 SQL 语句和 ORM 映射里容易出问题。非要用的时候,必须用反引号包起来,我一般直接避开,换成用户昵称、描述内容、排序号这类更明确的命名。
4.2 COMMENT 里写什么才不算废话
字段注释最怕写“备注”“信息”“状态”这种等于没写的注释。我要求团队成员写 COMMENT 时,至少要写清楚三件事:业务含义、单位或取值范围、特殊情况说明。
举个例子:
- 字段名 total_amount,注释写“订单总金额,单位元,保留两位小数,含运费”
- 字段名 status,注释写“订单状态:0待支付,1已支付,2已发货,3已完成,4已取消”
- 字段名 user_id,注释写“下单用户ID,对应 user_info.id”
这样一张表建完后,注释就是一本字段字典,不需要再单独维护一份 Word 文档。很多人问“注释写这么细会不会很啰嗦”,我的经验是:等你三个月后回来看这张表,你只会嫌注释不够。
表级 COMMENT 也建议写清楚这张表的核心业务场景和特殊约定。比如订单明细表,可以注明“商品名称和单价为下单时快照,不随商品表变更”。这类信息放在代码里没人看,放在建表语句的 COMMENT 里,反而是最显眼的。
4.3 让 IDEA、DBeaver 正确显示字段注释
主流数据库工具本身都能显示 COMMENT。IDEA 的 Database 面板里,选中表之后按 F4 打开表结构,列栏目里能看到注释,前提是你连接数据库时用的账号有读取注释的权限。如果看不到注释,在 Database 连接的高级配置里,检查“remarks reporting”或者“get remarks”选项是否打开。
DBeaver 里就比较直接,左侧表节点展开后,列节点的 Properties 标签里会显示注释。另外 DBeaver 的“ER Diagram”功能可以快速把所有表关系和字段注释一起展示出来,我做表评审的时候非常依赖这个功能。工具本身只是呈现,关键在于你的 DDL 里确实写了 COMMENT,否则工具再智能也没有内容可显示。
5. 表结构变更与迁移:真正的 DDL 手艺活
数据库表设计不是一次性工作,表建完不叫结束,后面还有字段变更、索引优化、数据导入、跨库迁移。这些实操环节,才是最考验功力的地方。
5.1 同一套表,MySQL、Oracle、达梦怎么建
用 MySQL 建订单表是很多人的第一位老师:
sql复制CREATE TABLE order_info (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
order_no VARCHAR(32) NOT NULL COMMENT '订单号',
user_id BIGINT UNSIGNED NOT NULL COMMENT '下单用户ID',
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额,单位元',
status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付,1已支付,2已发货,3已完成,4已取消',
pay_time DATETIME DEFAULT NULL COMMENT '支付时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_create (user_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='订单信息表';
但如果换到 Oracle 或者达梦、人大金仓,这套写法就要改。Oracle 不认 AUTO_INCREMENT,要用 IDENTITY 列或者序列;字段类型上,字符串用 VARCHAR2,数字用 NUMBER,大对象用 CLOB;时间类型用 DATE 或 TIMESTAMP;也没有 ENGINE 和 COMMENT 这种物理属性,字段注释要单独执行 COMMENT ON COLUMN 语句:
sql复制CREATE TABLE order_info (
id NUMBER(20) GENERATED BY DEFAULT AS IDENTITY,
order_no VARCHAR2(32) NOT NULL,
user_id NUMBER(20) NOT NULL,
total_amount NUMBER(10,2) NOT NULL,
status NUMBER(2) DEFAULT 0 NOT NULL,
pay_time TIMESTAMP NULL,
create_time TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
update_time TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT pk_order_info PRIMARY KEY (id),
CONSTRAINT uk_order_no UNIQUE (order_no)
);
COMMENT ON COLUMN order_info.order_no IS '订单号';
COMMENT ON COLUMN order_info.status IS '订单状态:0待支付,1已支付,2已发货,3已完成,4已取消';
国产数据库基本走 Oracle 风格,所以如果团队可能涉及国产化适配,表设计阶段就要尽量避免 MySQL 特征过于明显的语法,比如反引号、AUTO_INCREMENT、ENGINE 这些。字段类型统一用 NUMBER 和 VARCHAR2 这类通用性强的设计,迁移时反而省事。
5.2 ALTER TABLE 大表加字段的锁与坑
很多人在生产环境给大表加字段,直接一条 ALTER TABLE 跑下去,结果表被锁了半小时,业务线全卡住。MySQL 8.0 对 ADD COLUMN 支持了 INSTANT 算法,可以在元数据层面完成操作,但前提是添加的列在表末尾,且是允许 NULL 或者有默认值。如果你在表中间插入一列,或者修改字段长度,就只能用 INPLACE 或 COPY 算法,前者需要重建表,后者全表复制,耗时和锁影响都很大。
所以在数据库表设计阶段就要考虑:主表和热表不要频繁改 DDL。业务需求不可能完全预判,但可以通过“预留扩展列”的方式尽量推迟变更。注意,预留列也不是随便加一堆 a1、a2 备用字段,那种做法太丑了。更合理的做法是:预留一个 JSON 扩展字段,或者把易变信息拆分到子表,这样大部分变更不需要动主表结构。
5.3 Excel 导入建表时最容易翻车的 3 个点
Excel 导入数据库看似简单,实际坑很多。第一是数字与文本的混淆,Excel 里的 18 位身份证号或者订单号经常变成科学计数法,导入数据库后全变成 1.23E+17 这种格式字符串,直接把唯一约束搞崩。解决办法是导入前在 Excel 里把这类列设置成文本格式,或者先导入到临时表,字段一律用 VARCHAR,清洗后再转正式表。
第二是日期格式不统一。Excel 里的日期可能是 2025/1/1、2025-01-01 01:23:45 这类格式,导入到 MySQL 时如果字段是 DATETIME,可能因为格式不识别而失败。建议先导入临时表,字段用 VARCHAR,再用 STR_TO_DATE 统一清洗,最后导入正式表。
第三是空值处理。Excel 里“空单元格”在导入时可能变成 NULL,也可能变成空字符串,这会导致唯一索引下出现多个空字符串数据,因为某些数据库中空字符串和 NULL 去重逻辑不同。所以导入前必须约定:哪些字段允许为空,空单元格映射成 NULL 还是空字符串,这一步想清楚了再导入。
5.4 DDL 导出与跨库迁移的注意事项
跨数据库迁移,很多人第一反应是跑同步工具,但真正的第一步应该是把表结构字段弄对。DBeaver 里右键表,选择 Generate DDL,可以快速导出建表脚本,但它生成的是适合当前目标库语法的脚本,不同数据库之间的字段类型映射还是需要人工核对。比如 MySQL 的 TINYINT 到 Oracle 应该映射成 NUMBER(2) 还是 NUMBER(3)?MySQL 的 DATETIME 到 Oracle 应该用 DATE 还是 TIMESTAMP?这些细节直接决定迁移后数据是否准确。
冷迁移的操作思路是:停止写入,备份数据文件,迁移后升级版本或路径。这种方式适合停机窗口充足的场景,步骤相对简单,但要注意数据库版本一致性,以及表空间文件路径的差异。全程数据库表设计文档和 DDL 脚本都要保留,尤其是在多源异构数据库环境下,没有一份准确的表结构清单,迁移到一半你会发现原表某个字段是自增,新库却忘了建序列,这种问题最难受。
6. 一次并发订单表的设计复盘:从字段到索引再到死锁
前面讲的都是方法论,最后我用一个实际的并发订单表设计过程,把完整的思考链路串一遍。这是一个典型的电商下单场景:用户在“我的订单”列表看订单、下单扣库存、支付后更新状态、运营后台做对账。
6.1 先拆需求,再列字段清单
我先拆业务实体,拆出来三张主表:订单主表、订单明细表、库存表。订单主表存订单级信息,订单明细表存每个商品项,库存表单独保持一份独立的可扣减数量。
订单主表的字段这样定:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键,代理主键 |
| order_no | VARCHAR(32) | 业务订单号,全局唯一 |
| user_id | BIGINT | 下单用户 |
| total_amount | DECIMAL(10,2) | 订单总金额,单位元 |
| status | TINYINT | 0待支付,1已支付,2已发货,3已完成,4已取消 |
| receiver_name | VARCHAR(50) | 收货人姓名 |
| receiver_phone | VARCHAR(20) | 收货人手机号 |
| address | VARCHAR(255) | 收货地址 |
| pay_time | DATETIME | 支付时间,允许为空 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
订单明细表字段包含 id、order_id、product_id、product_name、product_price、quantity、total_amount、create_time。注意 product_name 和 product_price 在订单明细里是快照,因为商品信息后续可能调整,但历史订单必须保持下单时的样子。
6.2 索引不是全表建一遍,而是按查询路径走
从查询路径反推索引,不要从字段反推。订单主表上我建了三个索引:
- uk_order_no(order_no):唯一索引,对应下单幂等和订单号查询
- idx_user_create(user_id, create_time):用户订单列表,按用户和时间排序
- idx_status_pay(status, pay_time):运营后台对账,按状态和支付时间范围筛选
订单明细表只建一个 idx_order_id(order_id),因为订单详情的进入方式只有一种:通过 order_id 去查明细列表。有没有必要再建一个 product_id 索引?如果有一个功能是“查某个商品的所有历史订单”,那可以加;没有这个功能就不加,避免无谓的写放大。
6.3 并发扣库存的锁冲突与死锁复盘
下单场景里最麻烦的是并发扣库存。当一个事务里先更新库存,再插入订单数据,如果两个用户同时抢同一个商品,就会出现行锁竞争。InnoDB 的行锁机制是好的,但事务处理不当,就会死锁。
以前我遇到过这种死锁:事务 A 扣减 SKU_1 库存后更新订单状态,事务 B 也是先扣 SKU_1 再更新另一个表,但因为两个事务更新其他表的顺序不同,导致互相等待。解决死锁的办法不是数据库层面加超时就行,而是在代码层约定:同一批资源的加锁顺序必须全局一致。
具体到表设计上,可以做的优化有两个。一是把库存更新放到事务最前面,缩短持锁时间;二是库存表单独拆出来,不要让库存字段和订单字段挤在一张表里,否则一次高并发抢购会把整张表锁得死死的。库存表结构也很简单:sku_id 做主键,stock_qty 做库存数量,version 做乐观锁版本,更新时用条件 UPDATE 而不是先查再改。
sql复制UPDATE stock
SET stock_qty = stock_qty - #{quantity},
version = version + 1
WHERE sku_id = #{skuId}
AND stock_qty >= #{quantity};
这种写法把“检查库存是否充足”和“扣减库存”合并成一条原子语句,不需要额外加 SELECT FOR UPDATE,并发性能好很多。每次更新会返回影响行数,如果影响行数为 0,就代表库存不足或者版本冲突,应用层直接返回失败即可。
6.4 连接池被拖垮的一半原因在表设计
连接池参数调得再好,也救不了慢 SQL 拖死数据库。之前排查过一起生产故障:Druid 连接池里 maxActive 设置了 50,结果高峰期连接数直接打满,业务全线卡死。数据库本身的 CPU 并不高,问题出在一张订单明细表的查询走了全表扫描,一分钟跑了几万次,每个查询都长时间占用连接。
那次排查完之后,我做的第一件事不是调大连接池,而是用 EXPLAIN 找到那条慢 SQL,发现它的 WHERE 条件里对 create_time 用了 DATE() 函数,导致索引失效,改成范围比较之后就恢复正常了。这个教训到现在都很深刻:表设计阶段设计好索引,查询阶段尽量保持索引列干净,不要用函数包裹索引列,否则连接池无论配多大,都会在高并发时被慢 SQL 拖垮。
数据库表设计这件事,最大的敌人不是数据库本身,而是“先上线再说”的侥幸心理。表结构一旦上线,后面每一次修改都在还账。我现在的习惯是:每张表设计完,把建表 SQL 打印出来当说明书通读一遍,重点看注释是否完整、索引是否与查询路径匹配、字段类型是否合理。多看几遍,坑就能少踩几个。
