CREATE TABLE 建表全指南:从字段类型到索引设计,避开慢SQL的坑

发布时间:2026/9/28 6:18:01
CREATE TABLE 建表全指南:从字段类型到索引设计,避开慢SQL的坑 如果你问我 SQL 里最基础但最容易翻车的是哪条命令我会选 CREATE TABLE。刚入行那会儿我也觉得它没什么可学的不就是写个表名、列几个字段吗后来在生产环境里把订单金额字段设成了 FLOAT上线第二天对账就差了 0.01我才真正意识到CREATE TABLE 不是一段普通的建表语句它是在给整个系统的数据地基画图纸。图纸歪了后面所有查询、索引、报表、数据迁移都得跟着买单。这篇文章我会结合实际案例把 CREATE TABLE 从语法、字段类型选型、约束设计、索引规划到日常最容易踩的坑完整地讲一遍。适合刚接触 SQL 的初学者也适合写过不少 SQL 但没有认真梳理过建表细节的开发者和数据分析师。哪怕你用的是 MySQL、SQL Server 还是 PostgreSQL核心思路都相通差异点我会在对应位置标出来。1. 建表之前先弄懂表结构设计的核心思维1.1 一张表到底是由什么组成的很多人把表理解成 Excel行列结构确实一模一样但关系数据库要比 Excel 严格得多每一列必须有明确的数据类型每一行都必须满足表上定义的约束。你可以把一张表想象成带类型检查的仓库货架每个格子能放什么东西在上架之前就先规定好了。CREATE TABLE 做的就是“规定货架”这一步。一张表真正的组成远不止“列名 类型”这么简单。它至少包含表名、列名、数据类型、是否允许 NULL、默认值、主键、唯一约束、检查约束、外键、索引以及 MySQL 里的存储引擎和字符集、SQL Server 里的文件组等物理属性。很多新人写 CREATE TABLE 时只关心列名和类型忽略后面这一大堆因素结果表是建出来了真正跑业务的时候却各种扯皮。1.2 为什么字段设计比语法更容易毁掉一张表CREATE TABLE 的语法学起来真的很快你十分钟就能背下来但字段设计这件事没有半年实战经验基本拿不准。举个例子有人把订单状态字段设计成 VARCHAR(16)存“未支付”“已支付”“已退款”这种中文词看着直观等到你要做统计报表时就难受了每次都要 CASE WHEN 转义写出来的 SQL 又长又慢。更合理的做法是用 TINYINT 存状态码配合一张状态字典表去解释含义。再比如日期字段用 VARCHAR(20) 存写入方便但查询的时候根本无法高效排序和范围筛选后台一查近三十天订单就会慢到怀疑人生。还有金额字段用 FLOAT 存更是大坑后面我会单独展开。这里我想说的核心就一句CREATE TABLE 时你在设计的是未来所有查询和数据逻辑的底层结构设计阶段多留一天认真讨论业务后面能少加十天班去洗数据和调慢 SQL。2. CREATE TABLE 标准语法从最小可用语句拆起2.1 一条最小建表语句的标准长相先看一段最省事的建表语句我用 MySQL 语法写其他数据库的差异后面补CREATE TABLE IF NOT EXISTS user_info ( id INT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这段 SQL 已经是生产可用级别了不要以为最简就是CREATE TABLE user_info (id INT, name VARCHAR(20))那种等你上线跑数据时一定会后悔。拆开来看IF NOT EXISTS用来保证脚本重复执行时不报错id INT AUTO_INCREMENT PRIMARY KEY是 MySQL 的自增主键写法SQL Server 对应的是INT IDENTITY(1,1)NOT NULL表示字段不允许为空DEFAULT CURRENT_TIMESTAMP让创建时间自动填当前时间。有一点特别重要列之间必须用英文逗号分隔最后一个列定义后面不能有逗号不然很多数据库会直接报语法错误。这个错误我在新人代码里见得太多了。2.2 一个包含常见选项的订单表示例在实际系统里单表往往有几十个字段这里我用一个订单表做模板把最常见的建表选项都放进去CREATE TABLE IF NOT EXISTS order ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, remark VARCHAR(255) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里面有几个设计点订单号必须加唯一约束防止重复金额用DECIMAL(12,2)而不是FLOAT后面单独解释状态用TINYINT用数字语义而不是存中文updated_at加了ON UPDATE CURRENT_TIMESTAMP每次更新记录时自动刷新时间MySQL 支持但 SQL Server 不支持这个写法需要改用触发器维护这是跨数据库经常遇到的坑。order是保留字这里用了反引号包裹MySQL 支持这种写法。不过我更建议在业务命名阶段就规避保留字比如表名干脆叫t_order或trade_order省得后面写 SQL 到处加反引号。2.3 大小写、保留字和命名规范这些细节不能忽略先说保留字。order、user、group、default、condition这些看起来很适合当表名或列名但它们多半是 SQL 保留字或关键字直接使用会导致 SQL 语法错误。MySQL 里可以用反引号转义SQL Server 里用方括号[]PostgreSQL 里用双引号都能解决但每个查询都得写转义符号非常繁琐能避免就避免。再说命名。表名用单数还是复数团队内部通常会有约定我个人倾向单数user、order_item不要一会儿users一会儿order_items。字段统一小写 snake_caseJava 后端自动映射成驼峰也方便不要混用多种命名风格。最后说大小写。MySQL 在 Linux 上默认区分表名大小写在 Windows 上不区分这会导致同一个建表脚本在两个环境表现不一致。最稳妥的做法是全程统一小写表名字段名也是一样。SQL Server 的默认排序规则通常不区分大小写但你也别指望靠这个兜底规范统一才是正解。3. 字段类型没选对等于给慢SQL埋雷3.1 数值型选型整数、定点数还是浮点数数值类型看起来简单实际上是最容易埋雷的地方。先看一张对照表类型适合场景注意点TINYINT / SMALLINT状态码、年龄、枚举范围小省空间INT / INT UNSIGNED用户ID、数量常规选择BIGINT订单号、流水号、大表主键空间翻倍别滥用DECIMAL(p,s)金额、汇率、精确计算整数位必须预留足够FLOAT / DOUBLE科学计算、坐标近似值不要用于业务金额最经典的翻车案例就是把金额存成 FLOAT。FLOAT 是二进制浮点数无法精确表示所有十进制小数0.1 0.2 的结果在计算机里不是 0.3而是 0.30000000000000004。一次两次看不出问题订单量一大对账差几分钱是很正常的事。金额一律用 DECIMAL比如DECIMAL(12,2)表示总位数 12 位小数 2 位也就是整数部分最多 10 位绝大多数业务足够用了。还有个坑是手机号用 INT 或 BIGINT 存。手机号看上去是数字但它不参与任何算术运算而且可能会有前导 0 或者 86 这种形态一旦转成数值前导 0 直接丢失。正确做法是存VARCHAR(20)。类似的还有银行卡号、证件号都当字符串处理。3.2 字符串与字符集VARCHAR、CHAR 和 TEXT字符串类型在业务表里用得最多也最容易踩坑。VARCHAR(n)是变长字符串存多长用多长CHAR(n)是定长存不满会补空格MySQL 比较时通常会忽略尾部空格SQL Server 也有类似行为所以能用 VARCHAR 尽量用 VARCHAR。TEXT这种大字段能不用就不用。它在数据库里存储和检索的开销都比普通字段大而且在排序、去重、分组时很容易驱动临时表落盘性能差得离谱。如果你的字段确实需要存很长的文本要么拆到单独的大字段表通过主键关联要么直接用对象存储保存内容表里只存地址查询体验完全不同。字符集是很多乱码问题的根源。MySQL 建表时请固定用utf8mb4它不是utf8的升级版那么简单utf8在 MySQL 里最多存 3 字节字符很多特殊字符和 emoji 都存不进去写入直接报错或者变问号。utf8mb4是完整的 UTF-8 支持。SQL Server 没有这个烦恼你建表时选nvarchar和nchar就能正确存储 Unicode但要注意所有相关列都统一使用n前缀类型混用照样会出现字符集转换损耗。3.3 日期时间类型别用字符串存时间日期时间的选型不同数据库差异很大。MySQL 常用DATE、DATETIME、TIMESTAMPSQL Server 用datatime、datetime2、datetimeoffsetPostgreSQL 用date、timestamp、timestamptz。但有一条铁律是通用的不要让日期时间变成字符串。用字符串存日期写入时确实很顺想怎么存怎么存但索引会失效范围查询会变成字符串比较时区处理更是灾难。比如你存2025-06-01 10:30:00看起来像时间三个表里三种格式第二个表存20250601第三个表存2025/06/01 10:30等你要做月平均统计的时候就知道有多痛苦了。关于DATETIME和TIMESTAMP的区别MySQL 里TIMESTAMP支持自动更新但是范围只到 2038 年DATETIME范围远很多业务表一般优先DATETIME。默认值可以直接写DEFAULT CURRENT_TIMESTAMPSQL Server 对应写DEFAULT GETDATE()PostgreSQL 写DEFAULT now()。任何日期字段都不建议允许 NULL宁可给一个默认时间。3.4 字段类型、慢SQL与注入风险的隐含关系很多人觉得慢 SQL 优化是查询阶段的事跟建表没关系其实大错特错。最常见的慢 SQL 根因之一就是“隐式转换”表里某列是VARCHAR类型查询条件却传了一个数字数据库只能先把每一行的字符串转成数字再比较索引直接作废全表扫描跑起来数据量一上去反应时间立刻指数级上升。建表时把类型定准了比后面加索引管用得多。再说 SQL 注入。注入的本质是拼接不可信内容进 SQL 语句最常见发生在查询和插入阶段但 DDL 同样需要警惕——永远不要用用户输入直接拼接表名、列名去执行CREATE TABLE、DROP TABLE这类操作。所以建表时明确约定字段语义、限制长度尽可能用参数化查询这不仅是安全要求也是让慢 SQL 无处遁形的必要前提。4. 约束条件主键、去重与数据完整性防线4.1 主键到底怎么选自增、UUID 还是业务键主键是表的灵魂先看常见方案的对比方案优点缺点适用场景自增主键写入有序索引小性能好分布式合并会冲突泄露业务量单体系统、内部表UUID/GUID全局唯一跨库合并方便随机写入导致索引碎片存储大分布式、多区域合并雪花ID趋势递增分布式唯一需要额外实现分配器中大型分布式系统业务键天然有意义省字段业务可能变化主键不可变更非常稳定的业务标识我的建议是绝大多数业务表都别把业务字段当主键。比如用户表不要用email做主键因为邮箱可能改订单表不要用order_no做主键因为订单号规则可能调整。正解是用代理主键比如自增或者雪花 ID再把order_no、email加一个UNIQUE KEY保证业务唯一。这样两者各司其职不会互相拖累。主键还有个硬性要求不允许为空这一点在 CREATE TABLE 里一定要写明白。有些数据库允许主键建在可空列上但行为极其诡异宁可报错也别放任。4.2 NOT NULL 和 DEFAULT别怕麻烦老老实实加上很多新人为了省事字段全部允许 NULL结果后面写统计 SQL 时一脸茫然。NULL 在 SQL 里有特殊含义它代表“未知”而不是空字符串。NULL 参与COUNT、SUM、GROUP BY、ORDER BY计算时行为都和其他值不同查出来的结果往往让人摸不着头脑。所以建表时默认逻辑是业务上一定存在的字段全部NOT NULL并且加上合理的DEFAULT。比如状态字段status TINYINT NOT NULL DEFAULT 0创建时间created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP。那些真正可选的字段才允许 NULL同时一定想清楚你对“空字符串”和“NULL”的语义区分是什么。如果在业务里两者含义不同就要在数据字典里写清楚否则后面洗数据的人会崩溃。4.3 唯一约束让去重从源头发生提到“SQL 语句去重”大多数人的第一反应是SELECT DISTINCT或者GROUP BY但那是查询阶段的事后补救。真正靠谱的做法是在建表阶段就通过唯一约束让重复数据根本写不进去。语法很简单一列唯一CREATE TABLE user_info ( id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL, UNIQUE KEY uk_email (email) );多列联合唯一也常见比如同一个用户对同一商品只能有一条评价CREATE TABLE review ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, content TEXT, UNIQUE KEY uk_user_product (user_id, product_id) );有了这个联合唯一约束即便应用层因为并发导致重复提交数据库也会拦下第二条这是最可靠的一层兜底。还有一个细节唯一索引里的多个 NULL 是不冲突的因为 NULL 表示未知MySQL 认为两个未知值不一定相同。如果业务上“空值也算一种重复”那你得额外设计其他约束来补位。4.4 外键和CHECK约束用还是不用外键能保证子表引用主表的数据一定存在学术上叫引用完整性。但生产环境里很多互联网团队默认不用外键原因很简单高并发插入和更新时外键会带来额外的锁检查和校验开销而且分布式分库分表后外键根本没法跨库生效。我的建议是核心事务系统可以用高并发读多写少的系统尽量在设计文档里定义好关系由应用层保证数据一致性再配合定时对账兜底。CHECK 约束的支持情况要留意。MySQL 8.0.16 之前只解析不生效8.0.16 之后才真正落地SQL Server 和 PostgreSQL 一直支持得很好。比如限制状态值范围CREATE TABLE t_order ( status TINYINT NOT NULL DEFAULT 0, CONSTRAINT chk_order_status CHECK (status IN (0, 1, 2)) );这种约束能挡掉很多应用层漏进来的脏数据成本远低于后面一遍遍清洗。核心问题不是“用不用”而是“别滥用”约束越多写入时要做的检查越多所以要让团队里每一个人都明白每条约束的业务含义而不是堆一堆别人根本不知道的规则上去。5. 复用和迁移CREATE TABLE AS 与 LIKE 的实战差异5.1 从查询结果直接建表最快的临时表生产方式新建表不一定每次都要从零手写有一类需求是想把一张或几张表的查询结果落成一张新表常见于报表、数据备份、临时分析。MySQL 和 PostgreSQL 的语法是CREATE TABLE new_table AS SELECT ...简写 CTASSQL Server 则用SELECT ... INTO new_table。举个例子-- MySQL / PostgreSQL CREATE TABLE order_bak_20250601 AS SELECT * FROM t_order WHERE created_at 2025-06-01; -- SQL Server SELECT * INTO order_bak_20250601 FROM t_order WHERE created_at 2025-06-01;这条语句日常非常顺手尤其适合把核心库的大表抽一部分数据到分析库。但你必须清楚CTAS 创建的新表只是数据的快照不会复制源表的约束、默认值、主键、索引和自增属性。也就是说这张新表除了字段类型跟着源表走其他什么都没带走。如果后续要在这张备份表上做高频查询还得自己补主键和索引。5.2 带着“去重逻辑”建新表一招解决重复数据建表同时做数据清洗最常见的场景是源表里存在重复记录需要生成一张去重后的新表。用窗口函数配合 CTAS 是最好的做法。比如用户导入表里同一个邮箱有多条记录想保留 ID 最小的那条CREATE TABLE user_dedup AS SELECT id, user_name, email, created_at FROM ( SELECT id, user_name, email, created_at, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM user_raw ) t WHERE rn 1;这里先用窗口函数ROW_NUMBER()按邮箱分组给每一组分一个序号ID 最小的排第 1然后只保留rn 1的行。这个模式在数据分析里很常用比GROUP BY去重更灵活因为你能同时保留其他字段不会丢失明细信息。去重只是第一步表建好后别急着高兴记得回到源表把唯一索引补上否则过几天新数据又会重复进来。这种“建表不设防靠事后 distinct”的做法本质上是在给未来埋雷。5.3 CREATE TABLE LIKE 与临时表只复制结构还是只存在一会儿MySQL 中有一种特殊语法用于完全复制某张表的表结构但不带数据CREATE TABLE new_table LIKE old_table;LIKE会复制源表的列定义、索引、自增属性比 CTAS 保留的内容完整得多但它不会复制数据有些版本也不会完整复制外键约束。这个玩法常用于做影子表在业务高峰期给一个大表创建一个结构一模一样的空表先建好索引再在低峰期把数据切过去避免直接在大表上长时间加锁。临时表又是另一个概念。MySQL 和 PostgreSQL 支持CREATE TEMPORARY TABLESQL Server 用#tmp前缀它只对当前会话可见会话结束表自动删除特别适合存中间结果。不过记住临时表再方便也只是临时方案如果哪一天你在生产环境里发现需要频繁重建临时表应该回头检查正式表的数据模型是不是设计不合理而不是继续给临时表加一堆索引。6. 建表之后的索引与性能设计从源头减少慢查询6.1 哪些字段值得建索引哪些建了反而拖慢写入建表之后紧接着要考虑索引。很多慢 SQL 的根子就在建表阶段高频查询的字段没有索引或者索引建得乱七八糟查询优化器根本用不上。先明确基本规则出现在WHERE、JOIN ON、ORDER BY、GROUP BY里的字段是优先建索引的对象。特别是那些区分度高的字段比如order_no、email、user_id天然适合做索引。相反状态字段如果只有 0、1、2 三种值区分度太低单独建索引效果很差查询优化器很可能还是选择全表扫描因为逐个索引回表反而更慢。索引不是越多越好。每加一个索引写入数据时就要多维护一棵索引树。我见过一张业务表被开发叠了 12 个索引正常的订单插入都慢成龟速这是典型的无脑优化。建索引前先问自己这个字段真的会被高频查询吗这个查询真的需要单独建索引吗能不能复用已有索引6.2 联合索引与最左前缀顺序错了等于白建多条件查询场景联合索引的字段顺序非常重要。假设评论表有(user_id, created_at)联合索引那它能高效支持WHERE user_id ?也支持WHERE user_id ? AND created_at ?甚至支持ORDER BY user_id, created_at。但如果你查询条件只有created_at这个联合索引就用不上了因为索引最左前缀原则要求必须从最左边的字段开始匹配。所以设计联合索引时把等值条件字段放前面范围条件放后面。比如订单查询常见user_id status created_at那(user_id, status, created_at)通常比(status, user_id, created_at)更合理因为user_id和status的等值匹配可以直接定位到一个小范围再用created_at做范围过滤和排序。另一个技巧是覆盖索引如果查询只需要返回user_id、status、created_at这几个字段而它们恰好都在同一个索引里MySQL 就可以只扫描索引树、不回表取数据性能直接上一个台阶。这也是为什么建表时不要轻易把大字段塞进冗余索引里去的原因。6.3 EXPLAIN 验证建表后第一件事是看执行计划写任何建表语句和查询语句之后我都建议你跑一下EXPLAIN确认查询不是全表扫描。比如EXPLAIN SELECT order_no, status FROM t_order WHERE user_id 10086;看执行计划里type字段的值如果出现ALL说明全表扫描如果看到ref、range、index说明索引用上了。另外看key列是不是你预期的索引名有时候查询优化器会选一个和你想象完全不同的索引这时候要思考是不是联合索引顺序没设计好。有一点必须提醒数据量太小时优化器可能觉得走全表扫描比走索引更快这并不代表建表设计错了。别拿十几条测试数据来判断性能要拿生产环境的真实数据量或接近真实的数据量做压测结论才靠谱。建表阶段多花半小时做这个验证后面排查慢 SQL 的时间能少一大半。7. 我踩过的几个建表坑都是生产环境里真实发生过的7.1 字符集不一致中文写入后乱码有一次某个业务模块上线后发现用户填写的备注信息到了数据库里全是问号和乱码。查了一圈发现表是默认latin1字符集而应用连接用的 UTF-8写入时一路转码转成了乱码。最坑的是乱码一旦落库直接改字符集并不能把已经乱掉的数据恢复回来。解决办法其实很朴素建表语句里强制写DEFAULT CHARSETutf8mb4连接串里也统一 UTF-8并让 DBA 把数据库默认字符集也改成utf8mb4。如果你的表已经建了不要只修改表定义要先用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4转换数据再修改连接配置否则数据可能二次损坏。统一字符集这件事最好在建表第一天就做到而不是等出现乱码再救火。7.2 DECIMAL 精度看错对账差一分钱有位同事在设计结算表时用了DECIMAL(10,2)算日流水没问题某天突然出现一条报错提示数值超出列范围。查原因才发现DECIMAL(10,2)的整数部分最大只有 8 位也就是 99999999如果某笔结算单金额超过这个数插入直接失败。另外DECIMAL定点数能精确表示有限位的十进制小数但它也不建议用来做复杂的小数运算后反复取整因为中间计算截断和取整规则不同。正解是金额字段预留足够的位数比如DECIMAL(18,2)同时所有金额运算统一用相同精度类型避免隐式转换。还有业务上如果涉及汇率这种多位小数需要更长的比例尺这时候只能用DECIMAL(18,8)之类的高精度方案应用层再做金额换算。7.3 上线脚本没做幂等跑第二次直接报错很多建表脚本第一次执行成功第二次执行的时候就报“表已存在”。这在小项目里可能无所谓但进入 CI/CD 自动化发布流程后同一个脚本可能被重复执行报错轻则导致发布失败重则中断整个流水线后面的人还一脸懵。让脚本变安全的最简单方法就是加IF NOT EXISTSCREATE TABLE IF NOT EXISTS t_order (...);如果你的团队用 Flyway、Liquibase 这类迁移工具它们靠版本号管理脚本本身就能避免重复执行但前提是每个版本号对应的脚本内容不能改。在生成环境加表我建议永远配套写回滚脚本建表脚本执行前先确认表不存在回滚脚本就是DROP TABLE IF EXISTS。哪怕你不用迁移工具把建表语句全部收敛到版本化目录里也比随便在运维平台手动执行安全得多。7.4 用保留字当表名写查询时处处碰壁我曾接手过一个老系统里面有个表叫userMySQL 下所有相关 SQL 全都要写成SELECT * FROM user WHERE ...这套代码所有地方都加了反引号写起来烦而且团队新人经常忘记反引号一执行就是语法错误。后来我们花了两个星期做重命名迁移把user改成user_account才彻底消停了。所以建表命名阶段就应该用业务前缀或者更明确的语义词比如t_user、sys_user、user_info避免order、group、condition这类保留字。列名也一样别用order这种词用order_no、sort_no这种具体含义明确的命名后面写 SQL 的体验完全不一样。7.5 外键 ON DELETE CASCADE 引发的“级联翻车”这个坑是最吓人的。某次上线一个删除功能管理员在后台删除一个用户结果用户的历史订单、订单明细、评价记录全部被外键级联删了个干净。幸好当时还有备份恢复数据花了一个晚上但整个团队都被吓得不轻。ON DELETE CASCADE在数据模型看来很优雅但实际业务里“删用户”和“删订单明细”往往不应该是同一条物理操作。用户可能只是被禁用订单需要保留用于审计你要做的是逻辑删除而不是物理删除。我的经验是业务表的删除尽量用状态字段软删除即使要做物理清理也写成显式的批量任务逐张表可控地处理坚决不依赖数据库级联。这不算约束的错而是设计时要把每个删除场景都想清楚。7.6 盲目建索引写入慢到没法用最后再讲一个很常见的过度设计。为了提升查询速度业务方一口气给一张表建了一堆索引觉得“反正数据库空间够”。结果这张表本来就是高频写入表每天几十万次插入每个索引都是一棵独立的 B 树数据写入时要同时写入十几棵索引树最终一个简单的插入操作被拖到几百毫秒订单积压严重。正确的做法是先基于真实查询模式建立必要索引每多一个索引前都问一句“少了它会怎么样”。同时如果确实需要做大批量数据灌入通常先删除非必要索引、导入完成后再重建索引这样整体耗时反而更短。建索引不是在建收藏夹建得越多越心安它是有成本的写放大和空间膨胀都是代价。那些索引热词下面频繁出现的“慢 SQL 优化”大多数时候根本不是优化器的问题而是建表阶段索引策略就没想清楚。这些坑我不止踩过一次而且往往是同一种类型的错误在不同项目里再次出现。现在每次执行 CREATE TABLE 前我至少会问自己三件事这个字段的容量和类型在三年后还够用吗这个约束能不能在源头挡住脏数据这套索引真的覆盖了高频查询吗想清楚再回车后面省下来的全是治慢 SQL 和洗数据的宝贵时间。