SQL Basics¶
SQL is tested in almost every data science interview because analysts and scientists spend a significant fraction of their work time querying databases. Interviewers use SQL basics to verify you can retrieve, filter, and transform data before trusting you with more complex problems.
Q1: Walk me through the order in which SQL clauses execute.¶
Show answer
SQL is written in a specific order, but the database engine executes it in a different order. Confusing the two is one of the most common sources of errors.
Written order: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
Execution order:
1. FROM — identify the source tables and perform joins
2. WHERE — filter rows before any grouping
3. GROUP BY — group the surviving rows
4. HAVING — filter groups after aggregation
5. SELECT — compute the output columns (aliases created here)
6. ORDER BY — sort the output (column aliases are visible here)
7. LIMIT — trim the result set
This explains two common errors:
- You cannot reference a SELECT alias in a WHERE clause — WHERE runs before SELECT.
- You cannot filter on an aggregate with WHERE — you must use HAVING.
Q2: What is the difference between WHERE and HAVING?¶
Show answer
Both clauses filter rows, but they operate at different stages of query execution.
WHERE filters individual rows before grouping. It cannot reference aggregate functions because aggregation has not happened yet.
HAVING filters groups after GROUP BY and aggregation. It can reference aggregates.
A useful mental model: WHERE is a pre-aggregation filter; HAVING is a post-aggregation filter.
-- Find departments with more than 5 high-salary employees
SELECT
department,
COUNT(*) AS high_earners
FROM employees
WHERE salary > 80000 -- row-level filter (before GROUP BY)
GROUP BY department
HAVING COUNT(*) > 5; -- group-level filter (after GROUP BY)
Common mistake: writing WHERE COUNT(*) > 5 — this raises an error because aggregate functions are not allowed in WHERE.
Interview tip: if an interviewer asks you to filter on a computed aggregate value, reach for HAVING without hesitation.
Q3: How do NULLs behave in SQL — arithmetic, comparisons, and aggregations?¶
Show answer
NULL represents an unknown value. It propagates in arithmetic and comparisons in ways that surprise most beginners.
Arithmetic: any expression involving NULL returns NULL.
Comparisons: comparing anything to NULL returns NULL (not TRUE or FALSE). This means = NULL never matches.
-- Wrong: this returns zero rows even if nulls exist
SELECT * FROM orders WHERE discount = NULL;
-- Correct
SELECT * FROM orders WHERE discount IS NULL;
SELECT * FROM orders WHERE discount IS NOT NULL;
Aggregations: aggregate functions (SUM, AVG, MIN, MAX, COUNT(col)) ignore NULL values. COUNT(*) counts all rows including those with nulls; COUNT(col) counts only non-null values in that column.
SELECT
COUNT(*) AS total_rows,
COUNT(discount) AS rows_with_discount,
AVG(discount) AS avg_nonull_discount
FROM orders;
COALESCE: substitute a default when a value is NULL.
NULLIF: returns NULL if two values are equal — useful to avoid division by zero.
Q4: What does DISTINCT do, and when should you be cautious with it?¶
Show answer
DISTINCT removes duplicate rows from the result set. It applies to the combination of all selected columns, not to any single column in isolation.
-- Unique combinations of city and country
SELECT DISTINCT city, country
FROM customers
ORDER BY country, city;
When to use it: - Verifying how many unique values exist in a column - Deduplicating a result when you know duplicates can arise from your joins
When to be cautious:
- DISTINCT is expensive — it forces a sort or hash to find duplicates. On large tables, this can be slow.
- If you're using DISTINCT to hide an incorrect join, you're masking a bug rather than fixing it. Investigate why duplicates are appearing before reaching for DISTINCT.
- COUNT(DISTINCT col) counts unique non-null values and is one of the slower aggregate operations.
Q5: Explain CASE WHEN — how do you use it for computed columns and conditional aggregation?¶
Show answer
CASE WHEN is SQL's conditional expression. It works anywhere an expression is valid: in SELECT, WHERE, ORDER BY, and inside aggregate functions.
Computed column:
SELECT
order_id,
total_amount,
CASE
WHEN total_amount >= 500 THEN 'high'
WHEN total_amount >= 100 THEN 'medium'
ELSE 'low'
END AS order_tier
FROM orders;
Conditional aggregation — count or sum only rows that meet a condition, without separate subqueries:
SELECT
product_category,
COUNT(*) AS total_orders,
COUNT(CASE WHEN status = 'returned' THEN 1 END) AS returns,
SUM(CASE WHEN status = 'returned' THEN total_amount ELSE 0 END) AS returned_revenue
FROM orders
GROUP BY product_category;
This pattern (aggregate + CASE WHEN inside) lets you pivot long data into wide form and compute multiple conditional aggregates in a single pass over the table — far more efficient than multiple separate queries.
Interview tip: if asked "how would you compute the return rate per category in one query", conditional aggregation is the answer.
Q6: What string functions does SQL provide? Give examples of TRIM, CONCAT, SUBSTRING, and LIKE.¶
Show answer
String manipulation is common in data cleaning tasks during interviews.
TRIM / LTRIM / RTRIM — remove whitespace (or specified characters):
UPPER / LOWER — normalise case for comparison:
CONCAT / || — join strings:
SELECT first_name || ' ' || last_name AS full_name FROM employees;
-- or in MySQL/SQL Server:
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM employees;
SUBSTRING / LEFT / RIGHT:
SELECT SUBSTRING(phone, 1, 3) AS area_code FROM contacts;
SELECT LEFT(postal_code, 4) AS district FROM addresses;
LENGTH / CHAR_LENGTH:
LIKE — pattern matching with % (any sequence) and _ (single character):
Q7: How do date and time functions work? Cover DATE_TRUNC, EXTRACT, and date arithmetic.¶
Show answer
Date functions are heavily used in time-series analysis and cohort queries.
DATE_TRUNC — round a timestamp down to a time boundary (PostgreSQL / BigQuery):
SELECT
DATE_TRUNC('month', order_date) AS month,
COUNT(*) AS orders
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
'year', 'quarter', 'month', 'week', 'day', 'hour'.
EXTRACT / DATE_PART — extract a single component from a date:
SELECT
EXTRACT(YEAR FROM order_date) AS order_year,
EXTRACT(MONTH FROM order_date) AS order_month,
EXTRACT(DOW FROM order_date) AS day_of_week -- 0=Sunday
FROM orders;
Date arithmetic — add or subtract intervals:
-- Orders placed in the last 30 days
SELECT * FROM orders
WHERE order_date >= NOW() - INTERVAL '30 days';
-- Days between two dates
SELECT order_id, delivery_date - order_date AS days_to_deliver
FROM orders;
NOW() / CURRENT_DATE / CURRENT_TIMESTAMP:
Interview tip: whenever you see a time-series or retention question, reach for DATE_TRUNC to group by period and EXTRACT to isolate components like month-of-year for seasonality analysis.
Q8: What is type casting in SQL and when do you need it?¶
Show answer
Type casting converts a value from one data type to another. You need it when the database stores a value in one type but your query requires another.
CAST syntax (ANSI SQL, portable):
SELECT CAST(price AS NUMERIC(10, 2)) FROM products;
SELECT CAST('2024-01-15' AS DATE) AS parsed_date;
SELECT CAST(user_id AS VARCHAR) AS user_id_text;
PostgreSQL shorthand with :::
SELECT price::NUMERIC(10, 2) FROM products;
SELECT '2024-01-15'::DATE AS parsed_date;
SELECT revenue::FLOAT / units AS revenue_per_unit FROM sales;
Common situations requiring a cast:
- A date stored as VARCHAR — you need to cast before date arithmetic
- Integer division producing an integer result (e.g., 1/4 = 0) — cast one operand to FLOAT
- Joining on columns with mismatched types (e.g., INT vs VARCHAR) — cast to match
Q9: Explain BETWEEN, IN, and IS NULL — and their NOT variants.¶
Show answer
These are shorthand filter operators that produce cleaner SQL than equivalent AND/OR expressions.
BETWEEN is inclusive on both ends:
-- Orders from Q1 2024 (includes Jan 1 and Mar 31)
SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';
-- Equivalent but more verbose
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date <= '2024-03-31';
IN — match against a list of values (more readable than many ORs):
SELECT * FROM orders
WHERE status IN ('shipped', 'delivered', 'returned');
-- NOT IN — exclude a list
SELECT * FROM products
WHERE category NOT IN ('discontinued', 'out_of_stock');
IS NULL / IS NOT NULL:
Critical warning: NOT IN with a subquery is dangerous if the subquery can return NULL. One NULL in the list causes NOT IN to return no rows, because any comparison with NULL is NULL (not TRUE). Prefer NOT EXISTS when working with subqueries that might produce NULLs.
Q10: How do you alias tables and columns, and why does it matter?¶
Show answer
Aliases give temporary names to columns or tables within a query. They improve readability and are required in certain situations.
Column aliases — rename output columns:
SELECT
first_name || ' ' || last_name AS full_name,
salary * 12 AS annual_salary,
EXTRACT(YEAR FROM hire_date) AS hire_year
FROM employees;
Table aliases — shorten table references (required for self-joins):
SELECT
o.order_id,
c.customer_name,
o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.id;
Rules to remember:
- Column aliases defined in SELECT are not visible in WHERE or HAVING (execution order: WHERE runs before SELECT). Use the full expression or a subquery/CTE instead.
- Column aliases are visible in ORDER BY (ORDER BY runs after SELECT).
- Table aliases, once defined, must be used consistently — you cannot mix the full table name and its alias in the same query.
Q11: How does ORDER BY work, and what happens with NULLs in sorted results?¶
Show answer
ORDER BY sorts the result set. By default the sort is ascending (ASC); use DESC for descending.
SELECT product_name, price, rating
FROM products
ORDER BY rating DESC, price ASC; -- sort by rating descending, break ties by price ascending
Sorting by position (less readable, avoid in production):
NULL ordering: the SQL standard does not define where NULLs sort. PostgreSQL and most ANSI databases place NULLs last in ascending order and first in descending order. You can override explicitly:
SELECT product_name, discontinued_date
FROM products
ORDER BY discontinued_date ASC NULLS LAST; -- NULLs at the bottom regardless
LIMIT (PostgreSQL, MySQL) / TOP (SQL Server) / FETCH FIRST n ROWS ONLY (ANSI) truncates the result after sorting:
Q12: What is the difference between COUNT(*), COUNT(col), and COUNT(DISTINCT col)?¶
Show answer
These three forms behave differently around NULLs and duplicates:
COUNT(*)— counts every row, including rows where all columns are NULLCOUNT(col)— counts non-NULL values in the specified columnCOUNT(DISTINCT col)— counts unique non-NULL values in the column
SELECT
COUNT(*) AS total_rows,
COUNT(email) AS rows_with_email, -- nulls excluded
COUNT(DISTINCT email) AS unique_email_addresses -- nulls and dupes excluded
FROM users;
Concrete example: if a table has 100 rows, 10 rows have email = NULL, and 15 rows share the same email address:
- COUNT(*) = 100
- COUNT(email) = 90 (10 NULLs excluded)
- COUNT(DISTINCT email) = 75 (10 NULLs + 15 duplicates excluded)
Interview tip: interviewers frequently use a scenario like this to test whether you know that COUNT(*) and COUNT(col) can return different numbers. Always clarify which you mean.
Q13: How do you handle conditional logic with COALESCE vs CASE WHEN?¶
Show answer
Both COALESCE and CASE WHEN can express conditional logic, but they serve different purposes.
COALESCE(val1, val2, ...) returns the first non-NULL value in the list. It is the idiomatic, concise tool for null substitution:
-- Use the shipping address; fall back to billing address if shipping is null
SELECT
order_id,
COALESCE(shipping_address, billing_address, 'No address on file') AS delivery_address
FROM orders;
CASE WHEN handles arbitrary conditions — not just nulls:
SELECT
customer_id,
CASE
WHEN total_orders > 100 THEN 'vip'
WHEN total_orders > 20 THEN 'loyal'
WHEN total_orders > 0 THEN 'regular'
ELSE 'inactive'
END AS customer_segment
FROM customer_stats;
Use COALESCE for null fallback — it's shorter and signals intent clearly. Use CASE WHEN for multi-branch logic based on conditions. Avoid nesting multiple CASE WHEN blocks for null handling when COALESCE can do the same job.
Q14: What are common filtering mistakes with LIKE and how do wildcards work?¶
Show answer
LIKE supports two wildcards:
- % — matches zero or more of any character
- _ — matches exactly one character
SELECT * FROM products WHERE name LIKE 'phone%'; -- starts with "phone"
SELECT * FROM products WHERE name LIKE '%phone'; -- ends with "phone"
SELECT * FROM products WHERE name LIKE '%phone%'; -- contains "phone"
SELECT * FROM products WHERE code LIKE 'A_C'; -- A, any one char, C
Common mistakes:
-
Case sensitivity:
LIKEis case-sensitive in PostgreSQL. UseILIKEfor case-insensitive matching, or normalise withLOWER(). -
Leading wildcard disables index:
LIKE '%phone'cannot use a B-tree index onname— the database must scan every row. Only patterns anchored at the start (LIKE 'phone%') can use an index. -
Escaping the wildcard character: if you need to match a literal
%or_, escape it. -
SIMILAR TO(PostgreSQL) supports full regex-like syntax ifLIKEis insufficient. For heavy pattern matching, consider~(regex match operator) in PostgreSQL.
Q15: How do you write a query to find the N most recent records per category?¶
Show answer
This is a classic interview pattern combining ORDER BY, LIMIT, or window functions. The naive version uses a subquery; the scalable version uses ROW_NUMBER().
For a single category — trivial:
For all categories at once — requires a window function (covered in depth in the window functions file, but the pattern is important here too):
SELECT *
FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY order_date DESC
) AS rn
FROM orders
) ranked
WHERE rn <= 5;
Why not just GROUP BY with MAX(order_date)? GROUP BY collapses rows — you lose all columns except the grouped/aggregated ones. The window function approach keeps all columns and lets you select exactly N rows per group.
Interviewers use this problem to see whether you reach for window functions when appropriate. Knowing ROW_NUMBER() OVER (PARTITION BY ...) is essential.