mysql的b-tree索引默认不存储null值,导致where col is null无法使用单列索引,需用生成列(如processed_at_is_null)加索引优化;联合索引中null会中断最左前缀匹配,影响后续列的索引下推与范围扫描。

NULL值会让MySQL跳过普通B-Tree索引
MySQL的B-Tree索引默认不存储NULL值(InnoDB和MyISAM都如此),所以WHERE col IS NULL或WHERE col = ?(?为NULL)这类查询无法利用单列索引走range或ref访问。哪怕该列建了索引,执行计划里key字段也常是NULL,实际全表扫描。
常见错误现象:EXPLAIN显示type: ALL、key: NULL,但明明写了INDEX(col);或者col = 'a'走索引,col IS NULL却变慢十倍。
- 唯一例外:联合索引中,如果
NULL出现在非最左前缀位置(如INDEX(a, b)),WHERE a = 1 AND b IS NULL仍可用索引 -
IS NOT NULL在某些版本(5.7+)可走索引,但不保证,别依赖 - 函数索引(8.0.13+)也不能索引
NULL——CAST(col AS CHAR)遇到NULL结果仍是NULL,照样被跳过
想让IS NULL走索引?加一个非空计算列 + 索引
本质是把“是否为NULL”这个逻辑转成可索引的确定值。MySQL不支持函数索引直接索引IS NULL,但支持生成列(generated column)+ 普通索引。
使用场景:日志表里processed_at常为NULL,要高频查WHERE processed_at IS NULL。
实操建议:
- 添加生成列:
ALTER TABLE logs ADD COLUMN processed_at_is_null TINYINT GENERATED ALWAYS AS (processed_at IS NULL) STORED; - 在该列建索引:
CREATE INDEX idx_processed_null ON logs(processed_at_is_null); - 改写查询:
WHERE processed_at_is_null = 1—— 这时EXPLAIN会明确显示用了idx_processed_null
注意:STORED必须写,VIRTUAL列不能建索引;类型选TINYINT足够,避免用BOOLEAN(底层是TINYINT,但语义易混淆)。
联合索引中NULL对最左前缀匹配的影响
联合索引INDEX(a, b, c)要求从左到右连续匹配。一旦某列值为NULL,且该列在查询条件中参与等值判断,它之后的列就无法用于索引下推(index condition pushdown)或范围扫描。
例如:WHERE a = 1 AND b IS NULL AND c > 10 —— a可用,b IS NULL虽命中索引结构,但c > 10无法利用索引排序,只能回表后过滤。
- 对比:
WHERE a = 1 AND b = 2 AND c > 10能用ref+range,c部分走索引 -
IS NULL在联合索引中等价于一个“存在但无值”的占位,不提供比较能力 - 如果业务真需要按
b IS NULL高效筛选,不如把b拆成b_value和b_is_null两个物理列,后者建索引
覆盖索引 + IS NULL查询的陷阱
你以为SELECT id FROM t WHERE status IS NULL只要INDEX(status, id)就能覆盖?不一定。因为status IS NULL本身不走索引查找,优化器可能放弃使用该索引,即使它理论上能覆盖。
性能影响明显:当表大、NULL比例高时,优化器倾向选择主键扫描(type: index)而非联合索引(type: ALL但key: NULL),反而更慢。
- 验证方法:
EXPLAIN FORMAT=JSON看used_key_parts是否包含status - 强制走索引:
USE INDEX (idx_status_id),但需确认实际rows没暴增 - 更稳方案:还是用生成列索引,避免优化器“猜错”
真正容易被忽略的是:NULL不是值,是缺失标记——所有基于“值比较”的索引机制,对它天然失效。设计阶段就要想清楚哪些字段NULL频次高、是否值得为它单独建索引路径。











