含NOT IN操作符的SQL语句的5个基本优化方法
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
NOT IN是 SQL 中的一种条件操作符,它的主要作用是在查询时进行过滤,在子查询含 NULL 时可能返回空结果,基本优化要点为:
下面模拟电商缺货产品查询,针对 products 与 inventory 表的大数据场景,解析 NOT EXISTS、LEFT JOIN 等多种优化方法,解决 NULL 值陷阱与索引失效问题,提升查询效率。 先构建一个电商缺货产品查询系统,包含表结构设计、索引设置和模拟数据填充。系统存在两个核心表:products(百万级产品数据)和inventory(千万级库存记录),其中库存表5%的product_id为NULL。
一、方法1:使用 |
+----+--------------------+-------+------------+----------------+---------------+-----------------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+----------------+---------------+-----------------+---------+------+--------+----------+-------------+
| 1 | PRIMARY | p | NULL | ALL | status | NULL | 2 | NULL | 998107 | 50.00 | Using where |
| 2 | DEPENDENT SUBQUERY | i | NULL | index_subquery | idx_product_id| idx_product_id | 5 | func | 15 | 10.00 | Using where |
+----+--------------------+-------+------------+----------------+---------------+-----------------+---------+------+--------+----------+-------------+
关键指标:
index_subquery
表示使用索引关联子查询,rows=15
显示平均扫描15行即返回
LEFT JOIN
替代-----------------------------------------
-- 优化方法:LEFT JOIN + NULL检查
-- 核心优势:
-- 1. 避免子查询嵌套,执行路径更线性
-- 2. 适合大结果集批量处理
-- 注意事项:
-- 过滤条件必须放在JOIN条件中!
-- 错误示例:LEFT JOIN ... WHERE i.stock>0 会导致全表关联
-----------------------------------------
SELECT /*+ LEFT JOIN优化示例 */
p.id,
p.name
FROM products p
LEFT JOIN inventory i
ON p.id = i.product_id -- 关联条件(产品ID匹配)
AND i.stock > 0 -- 关键!库存过滤条件必须在此处
WHERE
p.status = 1 -- 可用产品
AND i.product_id IS NULL; -- 未找到匹配库存记录(NULL检查)
使用LEFT JOIN + NULL检查
优化缺货产品查询,逻辑是:查找所有可用但无库存的产品。优势为:
关键点:
ON p.id=i.product_id AND i.stock>0
p.status=1 AND i.product_id IS NULL
错误示例:若将i.stock>0
放在WHERE子句,会导致先全量关联再过滤,丧失优化效果。
相比NOT EXISTS
,此方案更适合OLAP场景的批量分析,执行路径更清晰可控。在150万产品+300万库存记录的场景下,合理利用索引可获得稳定性能。
-----------------------------------------
-- 适用场景:子查询结果集超大(百万级)
-- 优化步骤:
-- 1. 创建内存临时表存储中间结果
-- 2. 添加主键/唯一索引加速检索
-- 3. 将子查询转为INNER JOIN
-----------------------------------------
-- 步骤1:创建内存临时表
CREATE TEMPORARY TABLE tmp_valid_inventory (
product_id INT PRIMARY KEY -- 主键保证唯一性
) ENGINE=MEMORY;
-- 步骤2:插入有效库存产品ID
INSERT INTO tmp_valid_inventory
SELECT DISTINCT product_id -- 去重减少数据量
FROM inventory
WHERE
stock > 0
AND product_id IS NOT NULL; -- 显式排除NULL
-- 步骤3:关联查询
SELECT /*+ 临时表优化方案 */
p.id, p.name
FROM products p
LEFT JOIN tmp_valid_inventory tmp -- 改用LEFT JOIN避免重复过滤
ON p.id = tmp.product_id
WHERE
p.status = 1
AND tmp.product_id IS NULL; -- 无有效库存
-- 清理临时表(会话结束自动清理)
DROP TEMPORARY TABLE IF EXISTS tmp_valid_inventory;
使用内存临时表优化缺货产品查询,适用于子查询结果集超大(百万级)的场景。核心思路是:预计算并缓存频繁使用的中间结果,减少重复扫描。具体步骤为:
创建内存临时表CREATE TEMPORARY TABLE tmp_valid_inventory ENGINE=MEMORY
product_id
确保唯一性预计算有效库存IDINSERT INTO tmp_valid_inventory SELECT DISTINCT product_id FROM inventory WHERE stock>0
product_id
非空的记录DISTINCT
去重减少临时表数据量关联临时表替代子查询LEFT JOIN tmp_valid_inventory ON p.id=tmp.product_id WHERE tmp.product_id IS NULL
LEFT JOIN+NULL检查
高效筛选无库存产品优势:
inventory
表的重复扫描(百万级数据只需扫描一次)适用场景:
NULL
值陷阱与规避-- 危险!原始NOT IN查询(含NULL时返回空结果)
SELECT id FROM products
WHERE id NOT IN (
SELECT product_id FROM inventory WHERE stock > 0
);
-- 结果:0 rows (因subquery包含NULL值)
-- 解决方案1:子查询中过滤NULL
SELECT id FROM products
WHERE id NOT IN (
SELECT product_id
FROM inventory
WHERE stock > 0
AND product_id IS NOT NULL -- 显式排除NULL
);
-- 解决方案2:主查询添加NOT NULL
SELECT id FROM products
WHERE
id IS NOT NULL -- 确保主键非空
AND id NOT IN (...);
展示了NOT IN
操作符在子查询包含NULL
值时的陷阱及解决方案:
原始查询:
WHERE id NOT IN (SELECT product_id FROM inventory WHERE stock > 0)
若子查询结果包含任何NULL
值,整个NOT IN
条件会返回NULL
,导致主查询永远无结果(即使逻辑上存在匹配数据)。这是SQL三值逻辑(TRUE/FALSE/NULL)的特性所致。
WHERE id NOT IN (
SELECT product_id
FROM inventory
WHERE stock > 0
AND product_id IS NOT NULL -- 关键过滤
);
NULL
,使NOT IN
按预期工作。NULL
,但业务逻辑需排除NULL
。WHERE
id IS NOT NULL -- 主键通常非空,显式声明可优化执行计划
AND id NOT IN (...);
id
)本身非空时,id IS NOT NULL
是冗余条件,但可帮助优化器提前过滤。NULL
,需结合方案1使用。优先使用NOT EXISTS
:
WHERE NOT EXISTS (
SELECT 1 FROM inventory
WHERE product_id = products.id
AND stock > 0
);
NULL
值,逻辑更清晰,性能通常更优。谨慎使用NOT IN
:
仅当确认子查询结果不含NULL
时使用,否则必须显式过滤。
索引优化:
确保inventory.product_id
有索引,加速子查询或连接操作。
-- 低效场景:类型不匹配导致索引失效
EXPLAIN
SELECT * FROM products
WHERE CAST(id AS CHAR) NOT IN ( -- 类型转换导致索引失效
SELECT product_id FROM inventory
);
-- 优化方案:保持字段类型一致
ALTER TABLE inventory
MODIFY product_id INT NOT NULL; -- 不允许NULL
-- 添加覆盖索引(Covering Index)
CREATE INDEX idx_inventory_check
ON inventory(product_id, stock); -- 包含查询所需所有字段
-- 优化后执行计划:
+----+-------------+-------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | p | range | PRIMARY,status | status | 2 | NULL | 45% | 100.00 | Using where; Using index|
| 2 | DEPENDENT | i | ref | idx_inventory_check | idx_inventory_check | 8 | func | 2 | 100.00 | Using index |
+----+-------------+-------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
关键指标:
Using index
表示索引覆盖,无需回表查数据
展示因字段类型不匹配导致索引失效及优化方案:
低效原因为CAST(id AS CHAR)
转换产品ID类型,使products
表主键索引失效,触发全表扫描。
优化步骤:
inventory.product_id
为INT NOT NULL
,确保与products.id
类型一致,避免隐式转换idx_inventory_check(product_id, stock)
,包含查询所需字段,无需回表优化后执行计划显示:
products
表使用status
索引(type:range
),过滤45%数据inventory
表使用覆盖索引(Using index
),通过ref
类型快速匹配结果:避免全表扫描,利用索引加速查询,尤其适合150万产品+300万库存的大数据场景。
通过上述优化策略,在千万级数据量下:
以上通过适配不同场景的优化策略,结合索引优化与 NULL 值处理,使电商缺货查询性能大幅提升,为百万级数据高效检索提供实用指南,助力业务决策。
阅读原文:原文链接