SQL Joins
Combine rows from two or more tables based on a shared column — the foundation of nearly every real-world query, since real data almost never lives in one single table.
What & Why
Real business data is split across related tables. marketing.campaigns stores campaign details while marketing.leads stores lead activity, and a JOIN combines them by matching marketing.leads.campaign_id to marketing.campaigns.id.
The column names do not need to be identical. A foreign key and the primary key it references describe the relationship; similar-looking names alone do not.
See How It Works
Marketing wants campaign names beside lead counts.
| 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 |
SELECT
c.name,
COUNT(l.id) AS leads
FROM marketing.campaigns c
JOIN marketing.leads l ON l.campaign_id = c.id
GROUP BY c.name
ORDER BY leads DESC, c.name ASC;| name | leads |
|---|---|
| Spring Launch | 2 |
| Finance Retargeting | 1 |
| Retention Webinar | 1 |
The inner join omits Enterprise Search because it has no lead row.
Practice this concept
Marketing wants campaign names with their lead counts. Keep only campaigns that have leads, then order by lead count descending and campaign name ascending.
marketingPrefix tables with marketing.table_name.
nameleadsmarketing.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