你写的 SQL 跑出来 0 行结果,但逻辑明明没问题——99% 的情况,是 NULL 在作怪。今天把 SQL Server 开发中最高频的 10 个坑,一次性全部讲清楚。
引言
生产事故往往不是复杂逻辑写错了。
是一个 NULL,一个隐式转换,一个忘记加 ORDER BY。
在 SQL Server 里,有些行为和直觉完全相反。你以为 NOT IN 能过滤数据,结果返回空。你以为日期比较没问题,结果漏了时间部分。你以为 COUNT(列名) 和 COUNT(*) 一样,结果差了几千行。
这篇文章,把 10 个最容易翻车的场景整理出来。每一个都附有反例和正确写法,直接收藏备用。
坑 1:NOT IN 里有 NULL,结果永远是空
这是 SQL Server 里最经典、危害最大的陷阱。
看这个场景:找出不在黑名单里的用户。
-- 看似没问题
SELECT user_id
FROM users
WHERE user_id NOT IN (
SELECT blocked_user_id FROM blacklist
);
如果 blacklist 里 blocked_user_id 有哪怕一行 NULL,这个查询返回 0 行。
原因:NOT IN 等价于 <> value1 AND <> value2 AND ... AND <> NULL。而任何值与 NULL 比较,结果都是 UNKNOWN(不是 TRUE,也不是 FALSE)。整个 AND 链只要有一个 UNKNOWN,最终结果就是空。
正确写法——改用 NOT EXISTS:
SELECT user_id
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b
WHERE b.blocked_user_id = u.user_id
);
NOT EXISTS 天然忽略 NULL 行,安全可靠。
铁律:子查询结果可能含 NULL 时,禁用 NOT IN,改用 NOT EXISTS。
坑 2:NULL 参与运算,结果还是 NULL
这个坑更隐蔽,不会报错,只会悄悄给你错误数据。
-- 假设 discount 列有些行是 NULL
SELECT order_id,
amount + discount AS final_amount -- 这里有问题!
FROM orders;
当 discount 为 NULL 时,amount + NULL = NULL,不是 amount。
同样,字符串拼接也一样:
-- 用户名或姓氏为 NULL,整个结果就是 NULL
SELECT first_name + ' ' + last_name AS full_name
FROM users;
正确写法——用 ISNULL 或 COALESCE 兜底:
SELECT order_id,
amount + ISNULL(discount, 0) AS final_amount
FROM orders;
SELECT ISNULL(first_name, '') + ' ' + ISNULL(last_name, '') AS full_name
FROM users;
铁律:字段参与计算或拼接前,先处理 NULL。
坑 3:COUNT(列名) 会跳过 NULL
COUNT(*) 统计行数。COUNT(列名) 统计该列非 NULL 的行数。
-- 假设 orders 有 100 行,remark 列有 30 行是 NULL
SELECT COUNT(*) -- 返回 100
, COUNT(remark) -- 返回 70!
FROM orders;
这两个数字不一样!
很多人用 COUNT(某列) 来统计业务量,如果这列有 NULL,数字就会偏小,而且不会有任何提示。
明确你的意图:
-- 统计总行数
COUNT(*)
-- 统计某列填写了值的行数
COUNT(列名)
-- 统计去重后的非 NULL 值个数
COUNT(DISTINCT 列名)
铁律:统计行数用 COUNT(*),统计有效填写数才用 COUNT(列名),两者含义不同。
坑 4:BETWEEN 日期,时间部分被忽略
这个坑在查当天数据时极其常见。
-- 想查 2026-06-01 当天的订单
SELECT * FROM orders
WHERE order_time BETWEEN '2026-06-01' AND '2026-06-01';
问题:'2026-06-01' 在 SQL Server 里默认是 2026-06-01 00:00:00.000。
结果:只会查到恰好在 00:00:00 这一刻的记录,整天的数据几乎全部漏掉。
正确写法:
-- 方式 1:右边用下一天
WHERE order_time >= '2026-06-01'
AND order_time < '2026-06-02'
-- 方式 2:用 CAST 截断时间部分
WHERE CAST(order_time AS DATE) = '2026-06-01'
方式 1 更推荐,因为 CAST(order_time AS DATE) 会让索引失效(对列做了函数操作)。
铁律:日期范围查询,右端点用 < 下一天,不要用 BETWEEN。
坑 5:TOP 不加 ORDER BY,结果不确定
-- 取最新 10 笔订单?错了!
SELECT TOP 10 * FROM orders;
没有 ORDER BY 的情况下,TOP 返回的是引擎扫描时碰到的前 N 行,顺序取决于存储结构和执行计划,每次执行都可能不同。
这在开发环境通常没问题(数据少,顺序相对稳定),但上线后数据量大、碎片化,结果就乱了。
正确写法:
SELECT TOP 10 *
FROM orders
ORDER BY create_time DESC;
铁律:TOP 必须配 ORDER BY,否则结果无意义。
坑 6:字符串比较大小写,结果因排序规则而异
在 SQL Server 中,字符串比较是否区分大小写,取决于数据库的排序规则(Collation)。
-- 在 Chinese_PRC_CI_AS(不区分大小写)排序规则下:
WHERE username = 'Admin' -- 能查到 'admin'、'ADMIN'、'Admin'
-- 在 Latin1_General_CS_AS(区分大小写)排序规则下:
WHERE username = 'Admin' -- 只能查到 'Admin',查不到 'admin'
CI = Case Insensitive(不区分大小写),CS = Case Sensitive(区分大小写)。
大多数 SQL Server 默认是 CI(不区分大小写),但如果你的代码迁移到另一个数据库实例,行为可能突然变了。
如果需要明确控制,用 COLLATE:
-- 强制区分大小写的比较
WHERE username COLLATE Latin1_General_CS_AS = 'Admin'
-- 强制不区分大小写的比较
WHERE username COLLATE Chinese_PRC_CI_AS = 'Admin'
铁律:跨环境部署的系统,不要依赖默认排序规则,显式指定 COLLATE 或在文档中记录排序规则假设。
一张速查表
| | |
|---|
NOT IN | | |
| | |
COUNT(列名) | | |
BETWEEN | | |
TOP | | TOP |
| | |
写在最后
SQL Server 的这些坑,不是语言设计缺陷,是严格遵守 SQL 标准的结果。
NULL 的三值逻辑(TRUE / FALSE / UNKNOWN)在 ANSI SQL 规范里写得清清楚楚,只是大多数人学 SQL 时没人告诉过他们。
好消息是:踩一次就记住了。坏消息是:踩在生产环境代价很大。
所以把这张速查表收藏起来。下次写 SQL 之前,对着扫一眼——可能省掉一个深夜的故障排查。
该文章在 2026/6/5 16:18:46 编辑过