SELECT adc.ID AS county_id, NVL(stats.shouldReport, 0) AS shouldReport, -- 应报企业数 NVL(stats.reported, 0) AS reported, -- 已报企业数(没填就是 0) ROUND( NVL(stats.reported, 0) / NULLIF(NVL(stats.shouldReport, 1), 0), 4 ) AS report_rate -- 填报率 FROM ACIM_DIC_COUNTY adc LEFT JOIN ( -- 核心统计:每个区的应报数和已报数 SELECT oewrt.COUNTY_ID, COUNT(*) AS shouldReport, -- 所有已备案企业 COUNT(oemr.RECORD_ID) AS reported -- 只统计填了月报的 FROM OIL_ENTER_WHSLEWHSE_RECORD_TAB oewrt LEFT JOIN OIL_ENTER_MONTH_REPORT oemr ON oewrt.id = oemr.RECORD_ID AND oemr.MONTH = '2025-09' -- ✅ 关键:条件在 ON WHERE oewrt.ENT_TYPE IN ('1', '3') AND oewrt.CONFIRM_STATUS = '2' GROUP BY oewrt.COUNTY_ID ) stats ON adc.ID = stats.COUNTY_ID WHERE shouldReport > 0 ; -- 或 SUBSTR('910101', 1, 2) || '%'为什么
AND oemr.MONTH = '2025-06'写在ON上“不过滤左表”,而写在WHERE上就“过滤掉左表行”?
🧠 核心原因:ON和WHERE在 SQL 执行流程中的作用完全不同
✅ SQL 的逻辑执行顺序(简化版):
FROM+JOIN→ 先把两张表“拼”成一张大表- 这个阶段由
ON条件决定哪些右表行能和左表匹配
- 这个阶段由
WHERE→ 对上一步生成的大表进行行级过滤GROUP BY,SELECT,ORDER BY...
🔑
ON控制“怎么连”,WHERE控制“连完后留哪些”
🌰 举个具体例子
假设数据如下:
左表:企业表(oewrt)
| id | COUNTY_ID | ENT_TYPE | CONFIRM_STATUS |
|---|---|---|---|
| 1 | A | 1 | 2 |
| 2 | B | 1 | 2 |
右表:月报表(oemr)
| RECORD_ID | MONTH |
|---|---|
| 1 | 2025-06 |
| 1 | 2025-05 |
企业 B 没有 2025-06 月报!
情况一:条件写在ON(✅ 正确保留左表)
SELECT * FROM oewrt LEFT JOIN oemr ON oewrt.id = oemr.RECORD_ID AND oemr.MONTH = '2025-06';执行过程:
- 对左表每一行,找右表中同时满足
id=RECORD_ID且MONTH='2025-06'的行 - 结果:
id COUNTY_ID RECORD_ID MONTH 1 A 1 2025-06 2 B NULL NULL
✅左表所有行都在,只是右表匹配不到就填NULL
情况二:条件写在WHERE(❌ 过滤掉左表)
SELECT * FROM oewrt LEFT JOIN oemr ON oewrt.id = oemr.RECORD_ID WHERE oemr.MONTH = '2025-06';执行过程:
- 先做
LEFT JOIN(不带 MONTH 条件):id COUNTY_ID RECORD_ID MONTH 1 A 1 2025-06 1 A 1 2025-05 2 B NULL NULL - 再执行
WHERE oemr.MONTH = '2025-06':- 第一行:
2025-06 = 2025-06→ ✅ 保留 - 第二行:
2025-05 = 2025-06→ ❌ 过滤 - 第三行:
NULL = 2025-06→ ❌UNKNOWN(不是 true),被过滤!
- 第一行:
最终结果:
| id | COUNTY_ID | RECORD_ID | MONTH |
|---|---|---|---|
| 1 | A | 1 | 2025-06 |
❌企业 B 完全消失了!
📌 关键区别总结
条件在ON | 条件在WHERE | |
|---|---|---|
| 作用阶段 | 连接时(决定右表哪些行能匹配) | 连接后(对整行做过滤) |
| 对左表影响 | 永不丢左表行(LEFT JOIN 语义) | 会丢左表行(如果右表为 NULL) |
| NULL 处理 | 允许右表为 NULL | NULL = 'xxx'→ false/unknown → 被过滤 |
| 适用场景 | 需要保留左表所有记录(如你的需求) | 只要匹配成功的记录(相当于 INNER JOIN) |
💡 一句话记住:
ON是“连接规则”,WHERE是“生存规则”。
左连接中,左表行天生“存活”;但如果WHERE判它“死刑”(比如要求右表字段等于某值,但它却是 NULL),它就会被干掉。
✅ 正确认知(精炼版):
当使用
LEFT JOIN时:
- 过滤右表的条件应写在
ON子句中,以保留左表所有行;- 用
COUNT(右表.某字段)(如主键)来统计右表实际匹配的记录数;- 因为
COUNT(字段)会自动忽略NULL值,所以未匹配的行不会被计入,结果就是“有效关联数”。
📌 举个标准模式(你的场景)
SELECT a.COUNTY_ID, COUNT(*) AS shouldReport, -- 左表总行数(含未匹配) COUNT(b.RECORD_ID) AS reported -- 右表非 NULL 行数(自动排除未匹配) FROM OIL_ENTER_WHSLEWHSE_RECORD_TAB a LEFT JOIN OIL_ENTER_MONTH_REPORT b ON a.id = b.RECORD_ID AND b.MONTH = '2025-06' -- ✅ 右表过滤条件放 ON WHERE a.ENT_TYPE IN ('1','3') AND a.CONFIRM_STATUS = '2' GROUP BY a.COUNTY_ID;✅ 这样:
- 每个有备案企业的区都会出现;
reported自动 = 填了 2025-06 月报的企业数(没填的就是 0);- 不需要
WHERE b.MONTH = ...,否则会丢数据!
⚠️ 但要注意两个细节(避免误解)
1. ❌ 不是“把 null 去掉从而统计总数”,而是:
COUNT(*)→ 统计所有行(包括右表为 NULL 的)COUNT(b.xxx)→ 统计b.xxx 非 NULL 的行
所以你不是“去掉 null 再统计总数”,而是分别统计“总行数”和“有效关联行数”。
2. ✅ 字段选择很重要:
- 推荐用
COUNT(b.RECORD_ID)或COUNT(b.id)(右表主键/非空字段) - 避免用
COUNT(b.MONTH),万一MONTH本身允许为 NULL(即使有记录),也会被忽略
🔁 对比错误做法
-- ❌ 错误:条件放 WHERE LEFT JOIN b ON a.id = b.RECORD_ID WHERE b.MONTH = '2025-06'→ 未填报的企业整行消失,COUNT(*)和COUNT(b.xxx)都只算填报的,且无填报的区不出现。
-- ❌ 错误:用 COUNT(*) COUNT(*) AS reported -- 即使没匹配,也会计 1!→ 因为LEFT JOIN后每行都存在,COUNT(*)永远 = 左表行数。
✅ 总结口诀(背下来!)
左连要保左,右表条件放 ON;
统计匹配数,COUNT(右表字段);
若用 WHERE 滤,左表数据全丢光;
COUNT(*) 是总数,别拿它当已报量。
你现在完全掌握了这个关键技巧!👏
只要记住:ON控连接,WHERE控生死,COUNT(字段)忽略 NULL,就能写出正确的统计 SQL。