高并发下复合索引字段顺序怎么排
字段顺序直接决定索引是否能被 WHERE、ORDER BY、GROUP BY 用上。MySQL 从左到右匹配索引列,一旦遇到范围查询(
>、
BETWEEN、
LIKE 'abc%')或
IS NULL,右侧字段就失效。
常见错误是把高区分度字段放前面却忽略查询模式。比如用户表常查
WHERE status = ? AND created_at > ? ORDER BY id DESC,这时
(status, created_at, id)比
(id, status, created_at)有效得多——因为
status是等值,
created_at是范围,
id是排序,刚好满足最左前缀+覆盖排序。 等值条件字段放最左(如
user_id = 123) 多个等值时,把区分度高、过滤性强的放前面(但别只看
CARDINALITY,要看实际查询分布) 范围条件字段紧接等值字段之后,且只能有一个(再往后字段无法用于索引查找) 排序字段可放在最后,前提是前面全是等值或单个范围;否则会触发 filesort
唯一索引 vs 普通索引在写入热点上的表现差异
高并发 INSERT/UPDATE 场景下,唯一索引要求每次写都做唯一性校验,需访问完整索引树甚至回表,而普通索引只需定位到插入位置。当业务能接受应用层去重(如用 Redis 预判 + 数据库兜底),优先用普通索引可显著降低锁等待。
典型反例:秒杀库存扣减用
UNIQUE (order_id)防重,但大量请求同时 INSERT 同一
order_id(因网络重试或前端重复提交),导致唯一冲突+死锁频发。换成先 INSERT IGNORE + 检查
ROW_COUNT(),配合普通索引,吞吐能提升 3–5 倍。 高频写入字段尽量避免建唯一索引,除非强一致性不可妥协 若必须唯一约束,考虑将校验逻辑前置(如分库分表键哈希后查缓存)
INSERT ... ON DUPLICATE KEY UPDATE在唯一索引冲突时仍要加 GAP 锁,比普通索引更易阻塞
如何判断一个查询是否真的走索引而不是全表扫描
不能只看
EXPLAIN的
type字段是
ref或
range就放心——还要看
key_len是否符合预期、
rows是否远大于实际返回行数、有没有出现
Using filesort或
Using temporary。
真实案例:某订单表有索引
(shop_id, status, created_at),查询
WHERE shop_id = 100 AND status IN ('paid', 'shipped') ORDER BY created_at DESC,EXPLAIN显示用了该索引,但
key_len = 8(只用了前两列),
created_at无法用于排序,最终触发 filesort。改用
(shop_id, status, created_at)并确保
status是等值(如拆成两个 UNION 查询),才真正消除排序开销。 用
EXPLAIN FORMAT=JSON查看
used_key_parts,确认哪些列实际生效 对慢查询开启
slow_query_log并设置
log_queries_not_using_indexes = ON,但注意它不捕获已用索引却效率低的情况 线上验证务必用
SELECT ... FOR UPDATE或真实负载压测,避免执行计划因数据量变化而漂移
覆盖索引在高并发读场景下的取舍边界
覆盖索引能避免回表,减少 IO 和锁竞争,但索引本身体积变大,更新成本上升,且可能挤占 buffer pool。不是所有“能覆盖”的字段都该加进索引——尤其当其中包含 TEXT、BLOB 或长 VARCHAR 时。
比如日志表常查
SELECT user_id, action, ts FROM log WHERE app_id = ? AND ts BETWEEN ? AND ?,若把
action(平均长度 200 字节)和
ts全放进索引,单条索引记录超 250 字节,1000 万行就多占近 2.5GB 索引空间,InnoDB page split 更频繁,反而拖慢写入。 优先覆盖整型、短字符串(如状态码、枚举 ID)、时间戳等紧凑字段 避免在高频更新表上为低频查询字段建覆盖索引 用
SELECT COUNT(*)对比
SELECT COUNT(1)在覆盖索引下的执行计划,可快速验证是否真正免回表
索引不是越多越好,高并发场景下每个多余的二级索引都在悄悄增加每一行 INSERT 的加锁路径和缓冲区压力。设计时得盯着慢查日志里真实的
Rows_examined和
Innodb_row_lock_waits,而不是凭经验猜“这个字段以后可能会查”。
