在百万级数据量的数据库查询场景中,需要排除已关联到其他表的数据时,通常会想到使用not in或者left join结合is null的写法,两种实现方式的逻辑一致,但底层执行机制和性能表现存在明显差异,下面通过实际测试来对比两者的表现。

测试环境准备
本次测试使用MySQL 8.0版本,创建两张测试表,分别是主表user_info和关联表user_order,两张表都通过存储过程插入百万级测试数据,具体表结构如下:
-- 用户主表,存储用户基础信息
CREATE TABLE user_info (
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(50) NOT NULL,
age INT NOT NULL,
create_time DATETIME NOT NULL
);
-- 用户订单关联表,存储有订单的用户id
CREATE TABLE user_order (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
order_no VARCHAR(50) NOT NULL,
order_time DATETIME NOT NULL,
INDEX idx_user_id (user_id)
);
向user_info表插入100万条测试数据,向user_order表插入30万条不重复的user_id数据,模拟有30%的用户存在订单记录,剩余70%用户无订单记录。
两种查询写法实现
需求是查询所有没有订单的用户信息,分别使用not in和left join实现:
not in写法
SELECT *
FROM user_info
WHERE id NOT IN (
SELECT user_id FROM user_order
);
left join写法
SELECT u.* FROM user_info u LEFT JOIN user_order o ON u.id = o.user_id WHERE o.user_id IS NULL;
性能测试对比
多次执行两种查询语句,记录平均执行耗时、执行计划扫描行数等指标,结果如下:
| 查询方式 | 平均执行耗时(毫秒) | 全表扫描次数 | 索引使用情况 |
|---|---|---|---|
| not in | 1260 | 2次(主表全扫描+子查询全扫描) | 仅用户订单表user_id索引生效 |
| left join | 230 | 0次(均走索引) | 用户订单表user_id索引、主表主键索引均生效 |
从测试结果可以看出,在百万级数据量下,left join写法的执行效率远高于not in写法,主要原因如下:
- not in子查询会先执行子查询获取所有user_id集合,再对主表每一条数据进行匹配,当子查询结果集较大时,匹配效率会明显下降,且如果子查询结果中存在null值,not in会直接返回空结果,存在逻辑风险。
- left join写法可以通过索引快速匹配关联数据,过滤掉有订单的用户,扫描效率更高,且不存在null值导致的逻辑问题。
注意事项
如果关联表的user_id存在大量重复值,或者数据量较小时,两者的性能差异会缩小,此时可以根据代码可读性选择写法。但如果数据量达到百万级,且关联字段有索引,优先选择left join的写法来保证查询性能。
另外在实际开发中,如果not in后面的子查询结果集很小,也可以考虑使用not exists写法,其性能通常介于not in和left join之间,具体可以根据实际执行计划选择最优方案。