游客发表
SQL里查“不包含”某个值,包含写法远比你想的某值容易踩坑。我见过太多人直接写 WHERE name != '张三',包含结果数据里藏着几个NULL,某值查询结果莫名其妙少了几行——这就是包含最经典的陷阱。
先看最基础的某值场景。假设你有一张用户表,包含要排除掉所有状态为“禁用”的某值账号。直观写法是包含:
SELECT * FROM users WHERE status != '禁用'; 这语法没错,但问题在于——如果status列里存在NULL值,某值这些行会被自动过滤掉。包含NULL不等于“禁用”,某值但也不等于“非禁用”,包含它代表“未知”。某值数据库的包含逻辑里,NULL与任何值比较都返回“不是真”,所以status != '禁用'对NULL行结果是“非真”,行就被丢了。
解决方式有两种。要么写WHERE status IS NULL OR status != '禁用',要么用WHERE status NOT IN ('禁用')——但后者同样会忽略NULL行。这里有个细节:NOT IN子句只要子查询或列表里出现一个NULL,整个查询结果就会变成空集。比如你想排除两个值:
SELECT * FROM orders WHERE status NOT IN ('已取消', '已退款'); 如果表里某些行status是NULL,它们不会出现在结果里。更糟的情况是,如果列表本身是子查询返回的,且子查询结果包含NULL,那这个NOT IN直接返回零行——没有任何报错,就是静默地空。这种bug极难排查,因为逻辑上看着完全正确。
还有个常见场景:排除“包含某关键词”的记录。比如日志表里要筛掉所有含“debug”字样的消息。新手会写:
SELECT * FROM logs WHERE message NOT LIKE '%debug%'; 这写法没错,但性能堪忧——LIKE前置通配符会让索引失效,全表扫描跑起来慢得让人抓狂。如果表数据量过百万,这条查询可能拖垮整个库。更聪明的做法是考虑业务逻辑,比如把日志级别单独存一列,直接WHERE level != 'DEBUG',或者用全文索引。
再谈一个容易混淆的点:排除“不包含”和“包含但不等于”是两回事。比如你想找所有“没有关联订单”的用户,反连接(anti-join)才是正解。用NOT EXISTS:
SELECT * FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id ); 这比WHERE id NOT IN (SELECT user_id FROM orders)更安全——后者再次面临NULL问题,而且子查询结果集大时性能反而更差。我以前维护过一个老系统,开发图省事用NOT IN,结果订单表里有个user_id为NULL的历史脏数据,导致所有用户都查不出来。那周我花了整整两天定位这个“灵异事件”。
实际工作中,我还发现很多人忽略“排除集合”的场景。比如要排除最近30天内有退款行为的用户,同时排除内部测试账号。两条条件叠加时,别急着用AND拼,先想想逻辑关系:
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM refunds WHERE refund_date > NOW() - INTERVAL 30 DAY) AND is_test_account = 0; 这写法可行,但子查询结果可能很大——几万条refund记录塞进NOT IN里,内存直接爆掉。改成LEFT JOIN加IS NULL更高效:
SELECT u.* FROM users u LEFT JOIN refunds r ON u.id = r.user_id AND r.refund_date > NOW() - INTERVAL 30 DAY WHERE r.user_id IS NULL AND u.is_test_account = 0; JOIN条件里放时间过滤,避免全表关联后再筛。这是经验之谈——SQL优化的本质往往不是技巧多花哨,而是减少无效数据的流动。
最后说个大多数人不知道的:NOT LIKE和NOT ILIKE(PostgreSQL)在大小写不敏感场景下容易踩坑。你排除'debug',结果'Debug'混进来了。要么用LOWER函数包一层:WHERE LOWER(message) NOT LIKE '%debug%',但函数又会杀索引。折中方案是存数据时就统一小写,或者用citext扩展。
写SQL排除逻辑,本质是跟数据里的“不确定性”搏斗。NULL、大小写、隐式转换、子查询空集——每个都像地雷。我的建议很朴素:能用NOT EXISTS就别用NOT IN,能显式写IS NULL就别依赖隐式行为,能提前过滤JOIN条件就别等最后WHERE。多花三十秒想清楚边界情况,比上线后被用户投诉“数据少了”要划算得多。
友情链接