在 MySQL 或 MariaDB 开发中,NULL值是处理 “缺失数据” 的常用方式,但包含IS NULL的查询往往隐藏着性能隐患。尤其当多个字段同时使用IS NULL条件时,查询可能从毫秒级骤降至分钟级。本文基于 350 万行数据的实测案例,拆解 NULL 值与索引的交互逻辑,揭秘性能暴跌的根源,并提供可落地的优化方案。
先看三组对比实验(测试表含 350 万行数据,建有联合索引idx_a_b(a,b)和单字段索引idx_b(b)):
结论:单个字段搭配IS NULL时,查询仍能高效利用索引,执行速度极快。
结论:仅增加一个字段的IS NULL条件,查询速度暴跌 8000 倍,从毫秒级沦为分钟级。
将表中所有NULL值替换为0(逻辑上代表 “无数据”),重复实验 2 的查询逻辑:
结论:用具体默认值替代NULL后,即使返回数据量更多,查询速度仍大幅提升,仅需 1.93 秒。
为什么多字段IS NULL会导致性能灾难?核心原因是 MySQL 索引优化器对IS NULL的处理机制存在限制,关键在于ref_or_null检索类型的局限性。
当查询中仅一个字段含IS NULL条件时,优化器会使用ref_or_null检索类型。这种类型能利用索引快速定位 “指定值 + NULL 值” 的记录,本质是对索引的高效扫描,因此查询速度快。
MySQL 的ref_or_null检索类型不支持同时作用于多个索引列。当两个字段都包含IS NULL条件时:
- 优化器无法使用联合索引
idx_a_b进行跨列优化;
- 只能选择单字段索引
idx_b,先筛选b=5或b IS NULL的记录,再通过WHERE子句过滤a字段的条件;
- 最终导致 “索引失效 + 全表扫描” 的低效执行计划,这也是实验 2 耗时 81 秒的核心原因。
通过EXPLAIN对比实验 2 和实验 3 的执行计划,能直观看到问题所在:
EXPLAIN SELECT COUNT(*) FROM t WHERE (a=2 OR a IS NULL) AND (b=5 OR b IS NULL) G
关键输出:
type: ref_or_null(仅支持单字段);
key: idx_b(仅使用单字段索引idx_b);
rows: 1815359(扫描 181 万行,需二次过滤a字段);
Extra: Using where; Using index(需额外过滤条件,未充分利用联合索引)。
EXPLAIN SELECT COUNT(*) FROM t WHERE (a=2 OR a=0) AND (b=5 OR b=0) G
关键输出:
type: range(范围检索,支持多字段);
key: idx_a_b(使用联合索引idx_a_b);
rows: 1908763(扫描 190 万行,但无需二次过滤);
Extra: Using where; Using index(仅通过索引即可完成查询,无需回表)。
核心差异:range检索类型支持联合索引的多字段优化,而ref_or_null仅支持单字段,多字段时会导致索引选择失效。
针对多字段IS NULL的性能问题,最优解决方案是 “避免使用 NULL 值”,用具体默认值替代;若必须使用 NULL,则需通过查询改写优化。
这是最彻底的优化方式,从数据存储层面消除 NULL 值,从根源避免索引优化限制。
- 设计表结构时,为可能 “无数据” 的字段设置默认值:
- 数值型字段(如
a、b):默认值0;
- 字符串型字段(如
status):默认值空字符串'';
- 日期型字段(如
create_time):默认值1970-01-01 00:00:00。
- 应用逻辑中,用默认值代表 “无数据”,而非存储 NULL。
- 完全兼容
range检索,联合索引能充分发挥作用;
- 避免
ref_or_null的限制,查询性能稳定;
- 无需修改查询语句,仅需调整数据存储逻辑。
若已存在大量 NULL 数据,无法直接修改表结构,可通过拆分查询、使用UNION改写,让优化器能利用索引:
原理:将多字段IS NULL的复杂条件,拆分为多个单字段IS NULL的简单查询,每个子查询都能利用ref_or_null高效检索,再通过UNION合并结果,总体性能远优于原查询。
若查询中包含>、<等范围条件,即使字段含IS NULL,优化器仍可能使用range检索,无需过度担心:
结论:ref_or_null的限制仅在 “多字段同时含 IS NULL + 等值条件” 时触发,含范围条件时优化器会自动切换为range检索,性能不受影响。
- 优先用默认值替代 NULL:表设计阶段避免字段允许 NULL,用具体默认值(0、''、1970-01-01)代表 “无数据”,从根源消除性能隐患;
- 单字段 IS NULL 可放心用:仅单个字段含 IS NULL 条件时,性能不受影响,无需刻意避免;
- 多字段 IS NULL 必优化:若业务必须使用多字段 IS NULL 查询,要么改写查询语句,要么替换 NULL 为默认值,切勿直接使用
OR a IS NULL AND OR b IS NULL的写法。
MySQL/MariaDB 中,NULL 值的性能坑不在 “单字段使用”,而在 “多字段同时使用”。核心原因是ref_or_null检索类型无法跨列优化,导致联合索引失效,触发全表扫描。
优化的关键是 “避开多字段 IS NULL 的组合”:优先选择用默认值替代 NULL(根治方案),其次通过UNION拆分查询(临时方案)。掌握这一原则,就能避免 NULL 值导致的查询性能暴跌,让索引始终发挥高效作用