SQL Notebook
Basic Query & Sorting
SELECT
SELECT license_plate, entry_time
FROM parking_history;
Using * gets everything.
Column Aliases
SELECT parking_id,
parked_hours * 40 AS amount
FROM parking_history;
When you calculate something, it is better using AS to give it a clear name.
Removing Duplicates
SELECT DISTINCT parking_type
FROM parking_history;
DISTINCT makes sure the same row doesn’t appear more than once.
ORDER BY Clause
SELECT *
FROM parking_history
ORDER BY entry_time ASC;
ORDER BY sorts the results. ASC means earliest first.
Filtering & Conditions
WHERE Clause
SELECT *
FROM parking_history
WHERE license_plate = 'AAA-1111';
WHERE filters the rows. Here, it returns the details for the license plate AAA-1111.
Conditions with AND, OR, NOT
SELECT license_plate, payment_method, amount, is_electric
FROM payment_history
WHERE (payment_method = 'Credit_Card' AND amount > 5000)
OR (is_electric = 1);
- AND: All conditions must be true.
- OR: At least one condition must be true.
- NOT: Reverses a condition.
SQL checks in this order: NOT → AND → OR. Always use parentheses () for complex logic to avoid mistakes.
Not Equal (<>)
SELECT parking_id, parking_type, entry_time, exit_time
FROM parking_history
WHERE parking_type <> 'monthly';
<> means "not equal". This returns all non-monthly parking records. Same as WHERE NOT parking_type = 'monthly'. <> is shorter and very common.
Check for Missing Values (IS NULL)
SELECT station_code, license_plate, entry_time
FROM parking_history
WHERE exit_time IS NULL;
- IS NULL finds rows with no value (car still parked).
- Never use = NULL or <> NULL — they don't work!
- Use IS NULL or IS NOT NULL only.
Date Range (BETWEEN or >= / <)
SELECT COUNT(*) AS total_cars
FROM parking_history
WHERE entry_time >= '2025-01-01'
AND entry_time < '2026-01-01';
- Best way for a full year: >= start AND < next_year_start.
- Safe even with timestamps (hours, minutes, seconds).
- BETWEEN also works, but can miss records on Dec 31 if the time is not exactly 00:00:00.
IN (Match a List)
SELECT station_code, station_name, open_date
FROM management_table
WHERE strftime('%Y', open_date) IN ('2025', '2024', '2023');
IN checks if the value is in the list. Cleaner than using many ORs.
Pattern Matching (LIKE)
SELECT station_code, license_plate
FROM parking_history
WHERE license_plate LIKE 'E%';
LIKE finds text patterns. - % = any characters (zero or more) - _ = exactly one character
Aggregation & Grouping
Aggregate Functions
SELECT station_code,
COUNT(*) AS vehicle_count,
AVG(TIMESTAMPDIFF(MINUTE, entry_time, exit_time)) AS avg_duration_mins,
MAX(TIMESTAMPDIFF(MINUTE, entry_time, exit_time)) AS max_duration_mins,
MIN(TIMESTAMPDIFF(MINUTE, entry_time, exit_time)) AS min_duration_mins
FROM parking_history
WHERE exit_time IS NOT NULL
GROUP BY station_code;
- Per group: COUNT(*) → row count, AVG() → average, MAX/MIN → highest/lowest value.
- Use WHERE first to filter data before grouping.
Basic Grouping (GROUP BY)
SELECT station_code, COUNT(*) AS total_entries
FROM parking_history
GROUP BY station_code
ORDER BY total_entries DESC;
Group by Multiple Columns
SELECT city, station_type, COUNT(*) AS station_count
FROM management_table
GROUP BY city, station_type
ORDER BY city, station_type;
GROUP BY city, THEN station_type.
Grouping via Expressions
SELECT HOUR(entry_time) AS entry_hour,
COUNT(*) AS traffic_volume
FROM parking_history
GROUP BY HOUR(entry_time)
ORDER BY entry_hour;
GROUP BY HOUR(entry_time) → hourly buckets (0-23). Analyze trends without extra columns.
Filter Groups (HAVING)
SELECT station_code, COUNT(*) AS total_visits
FROM parking_history
GROUP BY station_code
HAVING COUNT(*) > 1000;
- HAVING filters AFTER grouping.
- WHERE filters BEFORE (raw rows).
- Can't use HAVING on non-aggregates.
Add Totals (ROLLUP)
SELECT payment_method, COUNT(*) AS tx_count
FROM payment_history
GROUP BY payment_method WITH ROLLUP;
WITH ROLLUP → adds subtotal rows + grand total (NULL row). Quick reports without extra queries.
Table Joins
FROM Clause & Table Aliases
SELECT ph.station_code, ph.station_name, py.amount
FROM parking_history ph
JOIN payment_history py
ON py.parking_id = ph.parking_id;
FROM tells the query which table to read. Short aliases (like ph, py) make the query shorter and easier to read, especially when joining tables.
Inner Join (Most Common)
SELECT ph.license_plate, ph.entry_time, mt.bd_name, mt.city
FROM parking_history ph
INNER JOIN management_table mt
ON ph.station_code = mt.station_code;
INNER JOIN keeps only rows with matching keys in both tables.
Left Join (Keep All from Left Table)
SELECT mt.station_name,
COALESCE(SUM(pay.amount_received), 0) AS total_revenue
FROM management_table mt
LEFT JOIN payment_history pay
ON mt.station_code = pay.station_code
AND DATE(pay.paid_time) = CURRENT_DATE
GROUP BY mt.station_name;
Joining Three Tables
SELECT ph.license_plate, mt.area, pay.payment_method, pay.paid_time
FROM parking_history ph
INNER JOIN management_table mt
ON ph.station_code = mt.station_code
INNER JOIN payment_history pay
ON ph.station_code = pay.station_code
AND ph.entry_time = pay.entry_time;
Start with parking_history → add station info → add payment info.
Chain Multiple Outer Joins
SELECT mt.station_name, ph.entry_time, pay.amount_received
FROM management_table mt
LEFT JOIN parking_history ph
ON mt.station_code = ph.station_code
LEFT JOIN payment_history pay
ON ph.station_code = pay.station_code
AND ph.entry_time = pay.entry_time;
Self-Join
SELECT t1.station_name AS station_a, t1.bd_team AS team_a,
t2.station_name AS station_b, t2.bd_team AS team_b, t1.area
FROM management_table t1
INNER JOIN management_table t2
ON t1.area = t2.area
AND t1.bd_team <> t2.bd_team;
Use two aliases (t1, t2) for the same table. Find pairs of stations in the same area but different teams.
Cross Join (Every Combo)
SELECT mt.station_name, d.day_num
FROM management_table mt
CROSS JOIN (
SELECT 1 AS day_num UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5
) d
ORDER BY mt.station_name, d.day_num;
Set Operations
The UNION Operator
SELECT station_code FROM Parking_history
UNION
SELECT station_code FROM Payment_history;
UNION stacks two query results and removes duplicates. Rules: Same number of columns, Compatible data types, Column names from first query.
UNION ALL (Combine + Keep All Rows)
SELECT *
FROM (
SELECT entry_time AS event_time, 'Vehicle Entry' AS event_type
FROM parking_history
WHERE station_code = 'ST001'
UNION ALL
SELECT paid_time AS event_time, 'Payment Made' AS event_type
FROM payment_history
WHERE station_code = 'ST001'
)
ORDER BY event_time;
UNION ALL keeps everything (faster, no deduplication).
INTERSECT (Rows in Both Queries)
SELECT station_code, entry_time
FROM parking_history
INTERSECT
SELECT station_code, entry_time
FROM payment_history;
INTERSECT returns only rows that appear in BOTH results.
EXCEPT (Rows in First but Not Second)
SELECT station_code, entry_time
FROM parking_history
EXCEPT
SELECT station_code, entry_time
FROM payment_history;
EXCEPT returns rows only in the FIRST query.
Subqueries
Basic Subquery
SELECT station_code, station_name, open_date
FROM management_table
WHERE open_date = (
SELECT MAX(open_date)
FROM management_table
);
- Inner query runs first to find the latest open_date.
- Outer query gets stations opened on that date.
Scalar Subquery in SELECT
SELECT mt.station_name,
mt.city,
(SELECT COUNT(*)
FROM parking_history ph
WHERE ph.station_code = mt.station_code) AS total_visits
FROM management_table mt;
Subquery returns one value per row.
IN Subquery (Filter by List)
SELECT mt.station_name, ph.license_plate, ph.entry_time
FROM parking_history ph
JOIN management_table mt
ON mt.station_code = ph.station_code
WHERE ph.station_code IN (
SELECT station_code
FROM management_table
WHERE area = 'Xinyi Dist'
);
Non-correlated (subquery runs once). Subquery → list of station_codes in Xinyi.
Derived Table (Subquery in FROM)
SELECT station_code, day_of_record, daily_total
FROM (
SELECT station_code,
DATE(paid_time) AS day_of_record,
SUM(amount_received) AS daily_total
FROM payment_history
GROUP BY station_code, DATE(paid_time)
) AS daily_stats
WHERE daily_total > 10000;
- Inner → aggregate daily revenue per station.
- Outer → filter high-revenue days (>10,000).
NOT EXISTS (Find Unmatched Rows)
SELECT ph.license_plate, ph.station_code, ph.entry_time
FROM parking_history ph
WHERE NOT EXISTS (
SELECT 1
FROM payment_history pay
WHERE pay.station_code = ph.station_code
AND pay.entry_time = ph.entry_time
);
- Correlated subquery: It runs once for every row in the outer query (
parking_history). - If NO matching record is found in the inner query (
payment_history) → returns TRUE and keeps the row. - Use case: Finding unpaid parking sessions.
SELECT 1is used because the actual data doesn't matter, only its existence.
Conditional Logic
Simple CASE (Exact Match Mapping)
SELECT station_name,
CASE station_type
WHEN 1 THEN 'Indoor Garage'
WHEN 2 THEN 'Outdoor Lot'
WHEN 3 THEN 'Street Parking'
ELSE 'Unknown'
END AS type_description
FROM management_table;
Match exact values only.
Searched CASE (Ranges & Complex Rules)
SELECT ticket_id,
TIMESTAMPDIFF(HOUR, entry_time, exit_time) AS duration_hours,
CASE
WHEN TIMESTAMPDIFF(HOUR, entry_time, exit_time) < 2 THEN 'Short-term'
WHEN TIMESTAMPDIFF(HOUR, entry_time, exit_time) BETWEEN 2 AND 8 THEN 'Medium'
ELSE 'Long-term'
END AS duration_category
FROM parking_history
WHERE exit_time IS NOT NULL;
Check conditions freely (ranges, >, <, AND/OR).