时间字段跨时区或粒度不一致最常见,如TIMESTAMP WITH TIME ZONE与DATE混用导致数据遗漏;JOIN键存在NULL或隐式类型转换(如TEXT与INTEGER的user_id)引发匹配失败;多表JOIN致行膨胀使SUM失真;上游ETL覆盖写入或去重逻辑污染数据;同名字段业务含义不同(如含税/不含税revenue)。

查 WHERE 条件里的时间字段是否跨时区或未对齐粒度
时间类口径偏差最常见——比如一张表用 created_at(带时区的 TIMESTAMP WITH TIME ZONE),另一张用 date 字段(仅日期,无时分秒),做 JOIN 或 GROUP BY 时表面匹配,实际漏掉当天后半天的数据。
实操建议:
• 用 EXTRACT(YEAR FROM ...)、DATE_TRUNC('day', ...) 等统一截断到相同粒度再比对
• 查看两表该字段的 data_type 和 column_default(如是否默认转为 UTC)
• 在 WHERE 中临时加 AND created_at::date = '2024-06-01' 和 AND date = '2024-06-01' 分别跑,看行数是否一致
核对 JOIN 键是否隐含空值或类型隐式转换
NULL 值在 JOIN 中不匹配,但业务上可能被当作“未知客户”或“未归因渠道”,导致下游统计少算;更隐蔽的是字符串 ID 和整型 ID 混用,比如 user_id 在 A 表是 TEXT(含前导零如 '00123'),B 表是 INTEGER(存为 123),ON a.user_id = b.user_id 看似成立,实则因隐式转换失败而跳过整行。
实操建议:
• 对所有 JOIN 字段执行 COUNT(*) FILTER (WHERE field IS NULL),确认空值比例
• 显式 cast:把 user_id::TEXT 或 user_id::BIGINT 写进 ON 条件,避免依赖数据库自动推断
• 用 USING 替代 ON 时尤其小心,它会自动忽略类型不一致的列
检查聚合逻辑中是否漏了 DISTINCT 或重复计数
多表 JOIN 后直接 SUM(revenue) 是重灾区:比如订单主表关联 3 条订单明细,又关联 2 个优惠券记录,一条订单会被膨胀成 6 行,SUM 就翻 6 倍。
实操建议:
• 先 SELECT COUNT(*) 和 COUNT(DISTINCT order_id) 对比,若远大于 1,说明存在膨胀
• 关键指标优先在单表完成聚合(如先 SELECT order_id, SUM(item_price) AS order_amount FROM items GROUP BY order_id),再与其他维度表 JOIN
• 不得不跨表聚合时,用 SUM(DISTINCT ...) 要谨慎——它只适用于可哈希标量,且不同数据库支持度不一(PostgreSQL 支持,MySQL 不支持)
验证上游 ETL 任务是否覆盖全量且无去重逻辑污染
口径问题常不在 SQL 本身,而在数据进仓前就被“加工”过了。例如:某张宽表的 etl_job 每次运行都执行 DELETE FROM table WHERE dt = '2024-06-01'; INSERT INTO ...,但上游源库当天有 2 次写入,ETL 只取最后一次快照,丢失中间变更;又或者清洗脚本里写了 GROUP BY user_id HAVING COUNT(*) = 1,直接过滤掉多设备登录用户。
实操建议:
• 查该表的 INSERT 语句或 Airflow DAG 日志,确认是追加(INSERT INTO)还是覆盖(TRUNCATE + INSERT)
• 在目标表加一列 _source_row_count,存原始抽取条数,和 COUNT(*) 对比
• 找出清洗 SQL 中所有 GROUP BY、DISTINCT、ROW_NUMBER(),逐条确认业务含义是否允许丢弃数据
revenue,一个含税一个不含税;都叫 status,一个用字符串 'success',一个用数字 1。动手前,先花五分钟看清楚字段注释和上游血缘,比改十次 SQL 更省时间。










