Core Concepts of SQL JOINs
INNER JOIN Returns only the rows from both tables that satisfy the join condition.
SELECT column_list
FROM PrimaryTable P
INNER JOIN SecondaryTable S
ON P.MainID = S.MainID
LEFT JOIN (LEFT OUTER JOIN) Returns all rows from the left table, and the matched rows from the right table. Unmatched rows from the right table will contain NULL values.
SELECT column_list
FROM PrimaryTable P
LEFT JOIN SecondaryTable S
ON P.MainID = S.MainID
RIGHT JOIN (RIGHT OUTER JOIN) Returns all rows from the right table, and the matched rows from the left table. Unmatched rows from the left table will contain NULL values.
SELECT column_list
FROM PrimaryTable P
RIGHT JOIN SecondaryTable S
ON P.MainID = S.MainID
FULL OUTER JOIN Returns all rows when there is a match in either left or right table. Rows without a match in one table will have NULLs for columns from that table.
SELECT column_list
FROM PrimaryTable P
FULL OUTER JOIN SecondaryTable S
ON P.MainID = S.MainID
LEFT JOIN Excluding INNER JOIN Returns rows that exist only in the left table, excluding matching rows.
SELECT column_list
FROM PrimaryTable P
LEFT JOIN SecondaryTable S
ON P.MainID = S.MainID
WHERE S.MainID IS NULL
RIGHT JOIN Excluding INNER JOIN Returns rows that exist only in the right table, excluding matching rows.
SELECT column_list
FROM PrimaryTable P
RIGHT JOIN SecondaryTable S
ON P.MainID = S.MainID
WHERE P.MainID IS NULL
FULL OUTER JOIN Excluding INNER JOIN Returns rows that exist in either table but not in both.
SELECT column_list
FROM PrimaryTable P
FULL OUTER JOIN SecondaryTable S
ON P.MainID = S.MainID
WHERE P.MainID IS NULL OR S.MainID IS NULL
Practical Examples
Replacing Employee ID with Unique Identifier
SELECT UniqueID.unique_identifier, Staff.full_name
FROM Staff
LEFT JOIN UniqueID
ON Staff.employee_id = UniqueID.employee_id
Product Sales Analysis
SELECT Inventory.product_title, Transactions.sale_year, Transactions.sale_price
FROM Transactions
LEFT JOIN Inventory
ON Transactions.item_id = Inventory.item_id
Visitors Without Transactions
SELECT visitor_id, COUNT(visitor_id) AS no_transaction_count
FROM SiteVisits
LEFT JOIN PurchaseRecords
ON SiteVisits.session_id = PurchaseRecords.session_id
WHERE PurchaseRecords.transaction_id IS NULL
GROUP BY visitor_id
Alternative approach using a subquery:
SELECT visitor_id, COUNT(session_id) AS no_transaction_count
FROM SiteVisits
WHERE session_id NOT IN (SELECT DISTINCT session_id FROM PurchaseRecords)
GROUP BY visitor_id
Identifying Rising Temperatures
Using window functions:
SELECT measurement_id
FROM (
SELECT
measurement_id,
temperature_value,
measurement_date,
LAG(measurement_date, 1) OVER(ORDER BY measurement_date) AS previous_date,
LAG(temperature_value, 1) OVER(ORDER BY measurement_date) AS previous_temp
FROM WeatherReadings
) temp_data
WHERE temperature_value > previous_temp AND DATEDIFF(measurement_date, previous_date) = 1
Using self-join:
SELECT w2.measurement_id
FROM WeatherReadings w1
INNER JOIN WeatherReadings w2
WHERE DATEDIFF(w2.measurement_date, w1.measurement_date) = 1
AND w2.temperature_value > w1.temperature_value
Average Processing Time Per Machine
SELECT
act1.device_id,
ROUND(AVG(act2.event_time - act1.event_time), 3) AS avg_processing_duration
FROM DeviceActivity AS act1
JOIN DeviceActivity AS act2 ON
act1.device_id = act2.device_id
AND act1.job_id = act2.job_id
AND act1.activity_type = 'begin'
AND act2.activity_type = 'complete'
GROUP BY device_id
Employee Bonus Query
SELECT
emp.employee_name, bonus.incentive_amount
FROM Employees emp
LEFT JOIN BonusPool bonus ON
emp.employee_code = bonus.employee_code
WHERE bonus.incentive_amount < 1000 OR bonus.incentive_amount IS NULL
Student Exam Participation Count
SELECT
stud.student_code,
stud.student_name,
subj.course_name,
IFNULL(exam_counts.exam_participation, 0) AS exams_taken
FROM Students stud
CROSS JOIN Courses subj
LEFT JOIN (
SELECT student_code, course_name, COUNT(*) AS exam_participation
FROM ExamResults
GROUP BY student_code, course_name
) exam_counts
ON stud.student_code = exam_counts.student_code AND subj.course_name = exam_counts.course_name
ORDER BY stud.student_code, subj.course_name
Managers with Minimum Direct Reports
SELECT manager_name
FROM (
SELECT
mgr.employee_name AS manager_name,
COUNT(rep.employee_code) AS report_count
FROM EmployeeDirectory AS mgr
JOIN EmployeeDirectory AS rep
ON mgr.employee_code = rep.supervisor_id
GROUP BY mgr.employee_code
) AS ManagerReports
WHERE report_count >= 5
Alternative using HAVING clause:
SELECT mgr.employee_name AS manager_name
FROM EmployeeDirectory AS mgr
JOIN EmployeeDirectory AS rep
ON mgr.employee_code = rep.supervisor_id
GROUP BY mgr.employee_code
HAVING COUNT(mgr.employee_code) >= 5
Confirmation Rate Calculation
SELECT
reg.user_identifier,
ROUND(SUM(IF(action_type='confirmed', 1, 0)) / COUNT(reg.user_identifier), 2) AS confirmation_percentage
FROM UserRegistrations reg
LEFT JOIN ActionConfirmations conf
ON reg.user_identifier = conf.user_identifier
GROUP BY reg.user_identifier
Alternative using AVG function:
SELECT
reg.user_identifier,
ROUND(IFNULL(AVG(conf.action_type='confirmed'), 0), 2) AS confirmation_percentage
FROM UserRegistrations AS reg
LEFT JOIN ActionConfirmations AS conf
ON reg.user_identifier = conf.user_identifier
GROUP BY reg.user_identifier