Skip to main content

Intermediate SQL for Data Engineering Interviews



Take your SQL skills beyond the basics with practical examples covering joins, subqueries, CTEs, set operations, date and string functions, conditional aggregation, and filtering with EXISTS or IN.

These patterns are used every day in data pipelines, warehouse transformations, analytics models, and technical interviews. Work through the examples and try the practice questions to build confidence.

Sample datasets used in this chapter

We’ll use two small tables: customers and orders.

customers

customer_idcustomer_namecity
1AliceBangalore
2BobMumbai
3CharlieDelhi
4DianaPune

orders

order_idcustomer_idamountorder_datestatus
101112002026-01-05Completed
10218002026-01-15Pending
103220002026-02-02Completed
10455002026-02-10Cancelled

Notice that customer 4 has no orders, while order 104 belongs to customer ID 5, which is not present in the customers table. These unmatched records will help us understand joins.

1. INNER, LEFT, RIGHT and FULL JOIN

A JOIN combines rows from two tables using a related column. Joins are essential when building data models from normalized source tables.

1.1 INNER JOIN

An INNER JOIN returns only rows that have a match in both tables.

SELECT
    c.customer_id,
    c.customer_name,
    o.order_id,
    o.amount
FROM customers c
INNER JOIN orders o
    ON c.customer_id = o.customer_id;

Example result:

customer_idcustomer_nameorder_idamount
1Alice1011200
1Alice102800
2Bob1032000

Charlie and Diana are excluded because they have no matching orders. Order 104 is excluded because its customer ID does not exist in customers.

1.2 LEFT JOIN

A LEFT JOIN returns every row from the left table and matching rows from the right table. If no match exists, right-table columns contain NULL.

SELECT
    c.customer_id,
    c.customer_name,
    o.order_id,
    o.amount
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id;

This includes Alice, Bob, Charlie, and Diana. Charlie and Diana appear with NULL order details.

Common Data Engineering use case: Find customers who have never placed an order.
SELECT c.customer_id, c.customer_name
FROM customers c
LEFT JOIN orders o
    ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

Result: Charlie and Diana.

1.3 RIGHT JOIN

A RIGHT JOIN returns every row from the right table and matching rows from the left table.

SELECT
    c.customer_name,
    o.order_id,
    o.amount
FROM customers c
RIGHT JOIN orders o
    ON c.customer_id = o.customer_id;

This includes all orders, including order 104. Since customer ID 5 has no matching customer record, customer_name is NULL for that order.

Tip: A RIGHT JOIN can usually be rewritten as a LEFT JOIN by swapping the table order. Many teams prefer LEFT JOIN for consistency and readability.

1.4 FULL OUTER JOIN

A FULL OUTER JOIN returns all matching rows plus unmatched rows from both tables. Missing values on either side are filled with NULL.

SELECT
    c.customer_id,
    c.customer_name,
    o.order_id,
    o.customer_id AS order_customer_id,
    o.amount
FROM customers c
FULL OUTER JOIN orders o
    ON c.customer_id = o.customer_id;

The result includes customers without orders (Charlie and Diana) and orders without a matching customer (order 104).

Support for FULL OUTER JOIN varies by database. For example, BigQuery and PostgreSQL support it; some engines require an alternative approach.

JOIN comparison

Join typeWhat it returnsTypical use case
INNER JOINOnly matching rowsOrders with valid customer records
LEFT JOINAll left rows plus matchesKeep all customers, even without orders
RIGHT JOINAll right rows plus matchesKeep all orders, even without customers
FULL OUTER JOINAll rows from both sidesReconciliation and identifying unmatched records
Interview trap: A join can produce multiple rows per key if either table has duplicate keys. Always understand the relationship—one-to-one, one-to-many, or many-to-many—before joining.

2. Subqueries and Common Table Expressions (CTEs)

2.1 Subqueries

A subquery is a query nested inside another SQL statement. It can be used in a WHERE, FROM, or SELECT clause.

Example: Find orders whose amount is greater than the average order amount.

SELECT order_id, customer_id, amount
FROM orders
WHERE amount > (
    SELECT AVG(amount)
    FROM orders
);

The inner query calculates the average amount. The outer query returns orders above that average.

Subquery in the FROM clause:

SELECT customer_id, total_amount
FROM (
    SELECT customer_id, SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
) customer_totals
WHERE total_amount > 1000;

This first aggregates orders by customer and then filters the aggregated results.

2.2 Common Table Expressions (CTEs)

A CTE is a named temporary result set defined using WITH. It exists for the duration of a single SQL statement and can make complex queries easier to read and maintain.

WITH customer_totals AS (
    SELECT
        customer_id,
        SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
)
SELECT customer_id, total_amount
FROM customer_totals
WHERE total_amount > 1000;

The CTE creates a readable logical step called customer_totals, which the main query then uses.

2.3 Multiple CTEs

You can define multiple CTEs and chain them together.

WITH completed_orders AS (
    SELECT *
    FROM orders
    WHERE status = 'Completed'
),
customer_revenue AS (
    SELECT
        customer_id,
        SUM(amount) AS revenue
    FROM completed_orders
    GROUP BY customer_id
)
SELECT *
FROM customer_revenue
WHERE revenue > 1000;

This query first filters completed orders, then aggregates revenue by customer, and finally filters customers by revenue.

SubqueryCTE
Nested directly inside another queryDefined using WITH
Can be concise for simple logicOften easier to read when logic has multiple steps
May be harder to reuse within a statementCan be referenced multiple times in the same statement, subject to database rules
Performance depends on the optimizer and queryNot automatically faster; materialization and inlining behavior vary by database
Interview tip: A CTE improves organization and readability, but it does not guarantee that the result is materialized or computed only once. Check your database’s execution plan when performance matters.

3. UNION vs. UNION ALL

UNION and UNION ALL combine the result sets of two or more SELECT statements. Each SELECT must return the same number of columns with compatible data types in corresponding positions.

3.1 UNION

UNION combines the results and removes duplicate rows.

SELECT city FROM customers
UNION
SELECT city FROM customers_archive;

If both tables contain Bangalore, the combined result includes Bangalore only once.

3.2 UNION ALL

UNION ALL combines all rows, including duplicates.

SELECT city FROM customers
UNION ALL
SELECT city FROM customers_archive;

If Bangalore appears in both tables, it appears twice in the result.

UNIONUNION ALL
Removes duplicate result rowsPreserves duplicates
Usually requires extra work to deduplicateUsually faster because it does not deduplicate
Useful when unique output rows are requiredUseful for combining append-only data or partitions
Data Engineering tip: When combining monthly or daily partitions that are designed to be appended, UNION ALL is often appropriate. Use UNION only when removing duplicates is part of the requirement.

Important: SQL set operations match columns by position, not by column name. Use explicit column lists and compatible data types.

4. Date and String Functions

Date and string functions are frequently used to clean source data, build date dimensions, standardize text, and derive reporting attributes. Function names and syntax differ across SQL engines, so check your target database.

4.1 Common date operations

Extract a year or month: In BigQuery, use EXTRACT.

SELECT
    order_id,
    EXTRACT(YEAR FROM order_date) AS order_year,
    EXTRACT(MONTH FROM order_date) AS order_month
FROM orders;

Add an interval: In BigQuery:

SELECT DATE_ADD(order_date, INTERVAL 7 DAY) AS date_plus_7_days
FROM orders;

Calculate a date difference: In BigQuery:

SELECT DATE_DIFF(
    DATE '2026-02-10',
    DATE '2026-02-02',
    DAY
) AS days_between;

Result: 8 days. The order of the arguments matters: this calculates the first date minus the second date in the specified unit.

Filter a date range:

SELECT *
FROM orders
WHERE order_date >= DATE '2026-01-01'
  AND order_date <  DATE '2026-02-01';
Best practice: For timestamp columns, half-open ranges—start inclusive, end exclusive—are often safer than filtering with an end-of-day time. Be mindful of time zones when converting timestamps to dates.

4.2 Common string functions

UPPER and LOWER: Standardize letter casing.

SELECT
    UPPER(customer_name) AS name_upper,
    LOWER(city) AS city_lower
FROM customers;

TRIM: Remove leading and trailing spaces.

SELECT TRIM('  Alice  ') AS clean_name;

Result: Alice.

LENGTH: Get the length of a string. Exact function names and character-versus-byte behavior vary by engine.

SELECT LENGTH('Bangalore') AS city_length;

CONCAT: Combine strings.

SELECT CONCAT(customer_name, ' - ', city) AS customer_location
FROM customers;

SUBSTRING: Extract part of a string. This syntax is supported by many databases:

SELECT SUBSTRING('DataEngineering', 1, 4) AS prefix;

Result: Data.

REPLACE: Replace occurrences of a substring.

SELECT REPLACE('Data Engineering', 'Engineering', 'Analytics') AS updated_text;

Result: Data Analytics.

Interview tip: When cleaning strings, consider NULLs, leading/trailing spaces, inconsistent casing, empty strings, and unexpected characters. Cleaning logic should match the business definition of a valid value.

5. Conditional Aggregation

Conditional aggregation combines aggregate functions with conditions, often using CASE WHEN. It lets you calculate multiple metrics in one grouped query rather than writing separate queries for each metric.

5.1 Count by status for each customer

SELECT
    customer_id,
    COUNT(CASE WHEN status = 'Completed' THEN 1 END) AS completed_orders,
    COUNT(CASE WHEN status = 'Pending' THEN 1 END) AS pending_orders,
    COUNT(CASE WHEN status = 'Cancelled' THEN 1 END) AS cancelled_orders
FROM orders
GROUP BY customer_id;

This produces one row per customer, with separate counts for completed, pending, and cancelled orders.

5.2 Calculate conditional revenue

SELECT
    customer_id,
    SUM(CASE WHEN status = 'Completed' THEN amount ELSE 0 END) AS completed_revenue,
    SUM(CASE WHEN status = 'Pending' THEN amount ELSE 0 END) AS pending_amount
FROM orders
GROUP BY customer_id;

Conditional aggregation is useful for building KPI tables, finance reports, sales summaries, and wide analytical datasets.

5.3 Conditional aggregation with a filter

Some databases support aggregate FILTER syntax; support varies. For example, PostgreSQL supports:

SELECT
    customer_id,
    COUNT(*) FILTER (WHERE status = 'Completed') AS completed_orders,
    SUM(amount) FILTER (WHERE status = 'Completed') AS completed_revenue
FROM orders
GROUP BY customer_id;

The CASE-based approach is more widely portable across SQL engines.

Common pitfall: In SUM(CASE WHEN condition THEN amount ELSE 0 END), rows that do not match contribute zero. If all matching amounts are NULL, the result may still depend on the exact CASE expression and data. Decide whether missing amounts should remain NULL or count as zero.

6. EXISTS vs. IN

EXISTS and IN are both used to filter rows based on values in another query. They often solve similar problems, but their behavior with NULLs and their query plans can differ.

6.1 IN

IN checks whether a value matches any value in a list or subquery result.

SELECT customer_id, customer_name
FROM customers
WHERE customer_id IN (
    SELECT customer_id
    FROM orders
);

This returns customers whose IDs appear in the orders table: Alice and Bob.

6.2 EXISTS

EXISTS checks whether the subquery returns at least one row. It is commonly written as a correlated subquery.

SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
);

For each customer, the database checks whether at least one matching order exists. The query returns Alice and Bob.

6.3 NOT IN vs. NOT EXISTS

A key interview topic is how NULL values affect negated membership tests.

SELECT customer_id, customer_name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
);

This safely expresses “customers with no matching orders” and returns Charlie and Diana.

By contrast, NOT IN can behave unexpectedly if the subquery returns even one NULL: comparisons may evaluate to UNKNOWN, causing rows to be filtered out. If using NOT IN, ensure the subquery excludes NULL values where appropriate.

INEXISTS
Tests whether a value matches any returned valueTests whether at least one matching row exists
Often straightforward for value membershipOften natural for correlated existence checks
NULLs can affect IN/NOT IN logicEXISTS depends on whether rows match the correlation condition
Performance depends on the engine and query planPerformance depends on the engine and query plan
Interview answer: Don’t claim EXISTS is always faster than IN. Modern optimizers may transform both into similar semi-join plans. Choose the expression that communicates the intended logic, handle NULLs correctly, and inspect the execution plan when performance matters.

Final Interview Practice

Try solving each question before expanding the answer.

Question 1: JOINs

Find customers who have never placed an order.

Question 2: Subqueries

Find orders whose amount is greater than the average order amount.

Question 3: CTEs

Calculate total order value by customer, then return customers whose total exceeds 1,000.

Question 4: UNION

What is the key difference between UNION and UNION ALL?

Question 5: Date functions

In BigQuery, return orders placed during January 2026.

Question 6: Conditional aggregation

Count completed orders for each customer.

Question 7: EXISTS

Find customers who have at least one order.

Chapter Summary

By the end of this chapter, you should be able to:

  • Choose the right join type and recognize unmatched records and duplicate-key risks.
  • Write subqueries and organize multi-step logic with CTEs.
  • Explain when to use UNION versus UNION ALL.
  • Apply date and string functions to clean and transform data.
  • Build multiple metrics with conditional aggregation.
  • Use EXISTS and IN correctly, including understanding NULL-related pitfalls.
What comes next? Advanced SQL: window functions, ranking, running totals, LAG and LEAD, deduplication, and slowly changing dimensions (SCD Type 2) patterns frequently tested in Data Engineering interviews.

Comments

Popular posts from this blog

Tricky Questions or Puzzles in C ( Updated for 2026)

Updated for 2026 This article was originally written when C/C++ puzzles were commonly asked in interviews. While such language-specific puzzles are less frequent today, the problem-solving and logical reasoning skills tested here remain highly relevant for modern Software Engineering, Data Engineering, SQL, and system design interviews . Why These Puzzles Still Matter in 2026 Although most Software &   Data Engineering interviews today focus on Programming, SQL, data pipelines, cloud platforms, and system design , interviewers still care deeply about how you think . These puzzles test: Logical reasoning Edge-case handling Understanding of execution flow Ability to reason under pressure The language may change , but the thinking patterns do not . How These Skills Apply to Data Engineering Interviews The same skills tested by C/C++ puzzles appear in modern interviews as: SQL edge cases and NULL handling Data pipeline failure scenarios Incremental vs ...

Decorators in Python

Decorators provide additional functionality to the functions without directly changing their definition of them. Basically, it takes the functions as an argument, adds functionality to them, and returns it.  Before diving deep into the concept of Decorators, let's first try to understand  What are functions in Python and what is an inner function? In Python everything is Objects, be it Class, Variables, and Functions . So functions are python first-class objects that can be used or passed as an argument. You can store the functions in variables, you can pass a function to another function as parameters, and you can also return the function from the function. Below is one simple example where we are treating functions as objects . def make_me_lowercase ( str ): return str .lower() print (make_me_lowercase( "HELLO World" )) copy_of_you = make_me_lowercase print (copy_of_you( "HELLO World" )) The output of the above calls for both the functio...

The Vignere cipher

The Vignere cipher is a method of encryption which uses caesar cipher on each letter based on the letters of keyword.In caeser cipher each letter of the plaintext is shifted number of tomes depending upon the key value for examlple if the key=2 then the letter A is shifted two times and encrypted as C.Vignere cipher uses this technique on each of the letters but with different keys each time.That is the reason it appears unbreakable at first.But it is quite easy to understand and implement. For encrypting the plaintext vignere cipher uses vignere square or tabula recta which is like 26 X 26 table consisting of alphabets A-Z on ech row and columns.The columns in a table are such that each next column first alphabet is obtained by shifting previous column alphabet by one. Here is the figure of vignere square or tabular recta For the Encryption process the cipher uses different alphabets from the rows each time depending upon the alphabet key is having.So the process of encr...