SQL 从“能查”到“可信”的进化路径

COUNT()统计所有行(含NULL),COUNT(字段)跳过该字段为NULL的行;JOIN导致重复时COUNT()虚高,应改用COUNT(DISTINCT主键)或EXISTS校验。

为什么 COUNT(*) 和 COUNT(字段) 结果不一致?

因为 COUNT(*) 统计所有行(包括含 NULL 的行),而 COUNT(字段) 会自动跳过该字段值为 NULL 的记录。这

不是 bug,是 SQL 标准定义的行为。

  • 当你怀疑数据“少算”了,先检查目标字段是否存在大量 NULL
  • 聚合前没加 WHERE 过滤,但业务上其实只关心非空状态(比如 COUNT(email) 统计“有邮箱的用户数”,不是“总用户数”)
  • 在宽表或 ETL 中间层,字段补空逻辑不统一,导致同一语义字段在不同来源中 NULL 含义混乱

JOIN 后 COUNT 突然翻倍,怎么定位?

本质是笛卡尔积副作用:主表一行匹配从表多行时,COUNT(*) 会把每条 JOIN 结果都算一次。

  • 先用 SELECT * + LIMIT 10 看实际 JOIN 后有多少重复主键
  • 改用 COUNT(DISTINCT 主键) 是最直接的修复,但注意性能——大表上 DISTINCT 可能触发临时磁盘排序
  • 如果只是校验关联完整性,优先用 EXISTSLEFT JOIN ... WHERE 右表.主键 IS NULL,比聚合更轻量

时间字段用 BETWEEN 还是 >= AND

>= AND 更可靠,尤其涉及 TIMESTAMP 或带毫秒的字段。

  • BETWEEN '2025-01-01' AND '2025-01-31' 实际等价于 (取决于类型精度),容易漏掉最后一秒的微秒级数据
  • created_at >= '2025-01-01' AND created_at 明确闭左开右,边界无歧义,也方便拼接时间窗口
  • 某些数据库(如 MySQL 5.6)对 BETWEEN 在索引使用上更保守,可能导致本可走索引的查询退化为全表扫描

为什么加了索引,执行计划还是没走?

索引存在 ≠ 查询会用。常见干扰项:

  • 条件里对字段用了函数,比如 WHERE YEAR(order_time) = 2025,导致索引失效;应改写为 order_time >= '2025-01-01' AND order_time
  • 谓词选择率太高(比如 WHERE status != 'cancelled' 匹配 95% 行),优化器判定全表扫描更快
  • 复合索引字段顺序和查询条件不匹配,例如索引是 (a, b, c),但查询只用了 bc,无法命中
  • 统计信息陈旧,执行 ANALYZE TABLE(MySQL)或 VACUUM ANALYZE(PostgreSQL)再看执行计划

可信不是靠“写得出来”,是靠每次结果可复现、边界可推演、变更可预判。最容易被忽略的,是把“查得出来”当成“查得对”——而真正的断点,往往藏在 NULL、时区、隐式类型转换和统计信息滞后这些安静的地方。