SQL ON vs WHERE
For an INNER JOIN, putting a filter in ON or WHERE gives the same answer. For an OUTER JOIN, it absolutely does not — one of the most consequential gotchas in this entire series.
What & Why
With a plain INNER JOIN, it genuinely doesn't matter whether an extra filtering condition lives inside ON or inside WHERE. The moment an outer join is involved, that stops being true. ON decides which rows count as a match while the join is happening. WHERE filters the result after the join has already completed — including any NULL-filled rows the outer join added to preserve unmatched rows. If a WHERE condition checks a column that's NULL for one of those rows, the comparison evaluates to NULL (not true), and WHERE quietly throws that row away — cancelling out the entire reason the outer join was used.
See How It Works
Marketing wants every campaign retained while attaching only leads that have been qualified.
| id | name | channel | spend | start_date | end_date | status | target_segment |
|---|---|---|---|---|---|---|---|
| 1 | Spring Launch | google_ads | 55000.00 | 2024-01-15 | 2024-03-31 | active | smb |
| 2 | Retention Webinar | 45000.00 | 2024-02-10 | 2024-04-15 | active | enterprise | |
| 3 | Finance Retargeting | 50000.00 | 2024-03-12 | 2024-05-31 | active | enterprise | |
| 4 | Enterprise Search | google_ads | 60000.00 | 2024-04-01 | 2024-06-30 | active | enterprise |
| id | campaign_id | created_at | qualified_at | converted_at | lead_score | source | country | |
|---|---|---|---|---|---|---|---|---|
| 301 | 1 | ana@example.com | 2024-01-21 09:10:00+00 | 2024-01-22 11:00:00+00 | 2024-02-02 10:00:00+00 | 86 | google_ads | US |
| 302 | 1 | ben@example.com | 2024-01-24 12:40:00+00 | NULL | NULL | 52 | google_ads | CA |
| 303 | 2 | chloe@example.com | 2024-02-16 08:30:00+00 | 2024-02-18 14:20:00+00 | NULL | 74 | GB | |
| 304 | 3 | dev@example.com | 2024-03-20 17:15:00+00 | 2024-03-21 09:00:00+00 | 2024-04-04 16:00:00+00 | 91 | US |
| id | name |
|---|---|
| 1 | Spring Launch |
| 2 | Retention Webinar |
| 3 | Finance Retargeting |
| 4 | Enterprise Search |
| id | campaign_id | qualified_at |
|---|---|---|
| 301 | 1 | 2024-01-22 11:00:00+00 |
| 302 | 1 | NULL |
| 303 | 2 | 2024-02-18 14:20:00+00 |
| 304 | 3 | 2024-03-21 09:00:00+00 |
| campaign_name | qualified_lead_id |
|---|---|
| Spring Launch | 301 |
| Retention Webinar | 303 |
| Finance Retargeting | 304 |
| Enterprise Search | NULL |
The ON condition limits matching leads, while LEFT JOIN still preserves Enterprise Search with a NULL lead.
SELECT
c.name AS campaign_name,
l.id AS qualified_lead_id
FROM marketing.campaigns c
LEFT JOIN marketing.leads l
ON l.campaign_id = c.id
AND l.qualified_at IS NOT NULL
ORDER BY c.id, qualified_lead_id;Practice this concept
Marketing wants every campaign retained while attaching only leads that have been qualified.
marketingPrefix tables with marketing.table_name.
campaign_namequalified_lead_idmarketing.campaigns| Column | Type |
|---|---|
| id | integer |
| name | text |
| channel | text |
| spend | numeric |
| start_date | date |
| end_date | date |
| status | text |
| target_segment | text |
| legacy_id | text |
marketing.leads| Column | Type |
|---|---|
| id | integer |
| campaign_id | integer |
| text | |
| created_at | timestamp with time zone |
| qualified_at | timestamp with time zone |
| converted_at | timestamp with time zone |
| lead_score | integer |
| source | text |
| country | text |
| archive_status | text |
Sign up free to try it on a real business scenario