COUNT()统计所有行(含NULL),COUNT(字段)跳过该字段为NULL的行;JOIN导致重复时COUNT()虚高,应改用COUNT(DISTINCT主键)或EXISTS校验。
因为 COUNT(*) 统计所有行(包括含 NULL 的行),而 COUNT(字段) 会自动跳过该字段值为 NULL 的记录。这不是 bug,是 SQL 标准定义的行为。
NULL
WHERE 过滤,但业务上其实只关心非空状态(比如 COUNT(email) 统计“有邮箱的用户数”,不是“总用户数”)NULL 含义混乱本质是笛卡尔积副作用:主表一行匹配从表多行时,COUNT(*) 会把每条 JOIN 结果都算一次。
SELECT * + LIMIT 10 看实际 JOIN 后有多少重复主键COUNT(DISTINCT 主键) 是最直接的修复,但注意性能——大表上 DISTINCT 可能触发临时磁盘排序EXISTS 或 LEFT JOIN ... WHERE 右表.主键 IS NULL,比聚合更轻量用 >= AND 更可靠,尤其涉及 TIMESTAMP 或带毫秒的字段。
BETWEEN '2025-01-01' AND '2025-01-31' 实际等价于 (取决于类型精度),容易漏掉最后一秒的微秒级数据
created_at >= '2025-01-01' AND created_at 明确闭左开右,边界无歧义,也方便拼接时间窗口BETWEEN 在索引使用上更保守,可能导致本可走索引的查询退化为全表扫描索引存在 ≠ 查询会用。常见干扰项:
WHERE YEAR(order_time) = 2025,导致索引失效;应改写为 order_time >= '2025-01-01' AND order_time
WHERE status != 'cancelled' 匹配 95% 行),优化器判定全表扫描更快(a, b, c),但查询只用了 b 和 c,无法命中ANALYZ
E TABLE(MySQL)或 VACUUM ANALYZE(PostgreSQL)再看执行计划可信不是靠“写得出来”,是靠每次结果可复现、边界可推演、变更可预判。最容易被忽略的,是把“查得出来”当成“查得对”——而真正的断点,往往藏在 NULL、时区、隐式类型转换和统计信息滞后这些安静的地方。