SQL 索引与表设计优化实践

where条件用索引字段却未走索引,主因是隐式类型转换(如varchar字段用数字比较)、索引列上使用函数、前导通配符模糊匹配(like ‘%abc’)、联合索引跳过首列等;order by不走索引则因排序字段与where未对齐索引结构或含表达式、null值;覆盖索引适用于高频固定字段查询,需按where、order by、select顺序设计;复合主键适合强分区场景,但增大二级索引体积;索引设计须兼顾数据生命周期。

SQL 索引与表设计优化实践

为什么 WHERE 条件用了索引字段却没走索引

常见现象是执行 EXPLAIN 看到 typeALLindex,而不是预期的 ref/range。根本原因往往不是索引没建,而是查询写法触发了隐式类型转换或函数包裹。

  • 字符串字段用数字比较:user_id = 123(而 user_idVARCHAR)→ MySQL 会把整列转为数字,索引失效
  • 在索引列上用函数:WHERE YEAR(created_at) = 2024 → 索引无法直接定位,得全表扫描
  • 前导通配符模糊匹配:LIKE '%abc' → B+ 树索引从左匹配,% 开头就跳过索引
  • 联合索引顺序错位:INDEX (a, b, c),但查询只用了 b = ?c = ? → 跳过首列,索引失效

ORDER BY 很慢,加索引也不生效

排序不走索引,通常是因为 ORDER BY 字段和 WHERE 条件没对齐索引结构,或者涉及多表、表达式、NULL 值处理。

  • 联合索引中,ORDER BY 必须按索引最左前缀连续使用,且方向一致(如 INDEX (status, created_at) 支持 WHERE status = 'done' ORDER BY created_at,但不支持 ORDER BY created_at DESC 如果索引是 ASC)
  • SELECT * + ORDER BY 可能触发 Using filesort:如果索引不能覆盖所有 SELECT 字段,MySQL 仍需回表再排序
  • NULL 的字段参与排序时,注意 ORDER BY col NULLS LAST(PostgreSQL 支持,MySQL 8.0.22+ 才支持),否则默认 NULL 排最前,可能打乱索引天然顺序

什么时候该用覆盖索引

覆盖索引本质是让查询只靠 B+ 树的叶子节点完成,避免回表。它不是“越多越好”,而是针对高频、轻量、固定字段组合的查询做精准优化。

  • 适用场景:分页列表(SELECT id, title, updated_at FROM post WHERE status = ? ORDER BY updated_at DESC LIMIT 20),可建 INDEX (status, updated_at, id, title)
  • 注意字段顺序:WHERE 条件字段在前,ORDER BY 在中,SELECT 中的非条件字段在后
  • 别过度覆盖:加太多字段会让索引体积暴增,写入变慢,尤其有 TEXT/BLOB 列时,MySQL 实际只存前 768 字节,覆盖效果打折
  • 验证是否生效:看 EXPLAINExtra 是否含 Using index(不是 Using index condition

复合主键 vs 单列自增主键的取舍

这问题没有银弹,关键看数据访问模式。盲目用 (tenant_id, id) 当主键,可能让单租户查询变快,但跨租户统计、后台管理、外键引用都更难。

  • 复合主键适合强分区场景:比如日志表按 (day, server_id) 主键,查询某天某机器日志时,聚簇索引天然局部性好
  • 但要注意二级索引代价:InnoDB 的二级索引叶子存的是主键值,复合主键越长,所有二级索引体积越大、缓存效率越低
  • 自增主键并非万能:高并发插入下可能产生热点(集中在最后一个页),但可通过 innodb_autoinc_lock_mode = 2 缓解;而 UUID 主键会导致插入随机、页分裂严重,除非你真需要分布式生成 ID 且已做好预分配
  • 外键约束实际很少用:多数业务用应用层保证一致性,所以“主键必须被其他表引用”这个理由越来越站不住脚

索引和主键设计里,最容易被忽略的是数据生命周期——冷热分离策略、归档逻辑、删旧机制,都会反向决定索引要不要保留、字段要不要冗余、甚至表要不要拆。没想清楚数据怎么死,就先别急着给它配索引。