Chapter 25 Advanced 58 Questions

Practice Questions — SQL Interview Masterclass - Top 30 Questions Solved

← Back to Notes
8 Easy
11 Medium
14 Hard

Topic-Specific Questions

Question 1
Easy
Find the 4th highest distinct salary from employees. Use DENSE_RANK.
DENSE_RANK handles ties.
SELECT DISTINCT salary FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) t WHERE rnk = 4;
Question 2
Easy
Find all products whose price is above the overall average.
Subquery in WHERE.
SELECT id, name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);
Question 3
Easy
List employees hired in March 2025.
BETWEEN or YEAR/MONTH extract.
SELECT id, name, hire_date FROM employees
WHERE hire_date BETWEEN '2025-03-01' AND '2025-03-31';
Question 4
Easy
Find duplicate emails in the users table along with their count.
GROUP BY + HAVING.
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Question 5
Easy
Return the count of employees per department, ordered by count descending.
GROUP BY + ORDER BY.
SELECT dept, COUNT(*) AS n
FROM employees
GROUP BY dept
ORDER BY n DESC;
Question 6
Medium
List employees whose name starts with 'A' or 'P' and whose salary is in the top 3 of their department.
Combine WHERE with a per-group window.
SELECT dept, name, salary FROM (
    SELECT dept, name, salary,
           ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
    FROM employees
    WHERE name LIKE 'A%' OR name LIKE 'P%'
) t
WHERE rn <= 3;
Question 7
Medium
For each department, return the average salary and the number of employees whose salary is above that average.
Join a grouped summary back to the base table.
WITH avgs AS (
    SELECT dept, AVG(salary) AS avg_sal FROM employees GROUP BY dept
)
SELECT e.dept, a.avg_sal, COUNT(*) AS above_avg_count
FROM employees e
JOIN avgs a ON a.dept = e.dept
WHERE e.salary > a.avg_sal
GROUP BY e.dept, a.avg_sal;
Question 8
Medium
Find the employee(s) with the longest tenure (earliest hire_date).
Subquery for MIN, then filter.
SELECT id, name, hire_date
FROM employees
WHERE hire_date = (SELECT MIN(hire_date) FROM employees);
Question 9
Medium
Return the top 2 highest-paid employees per department (break ties arbitrarily).
ROW_NUMBER with PARTITION BY dept.
SELECT dept, name, salary FROM (
    SELECT dept, name, salary,
           ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
    FROM employees
) t
WHERE rn <= 2
ORDER BY dept, rn;
Question 10
Medium
For each order, show the total and the percentage it contributes to the grand total.
Window SUM without PARTITION.
SELECT id, total,
       ROUND(100.0 * total / SUM(total) OVER (), 2) AS pct
FROM orders;
Question 11
Medium
List customers who ordered products from more than 3 distinct categories.
JOIN + GROUP BY customer + COUNT DISTINCT category_id.
SELECT o.customer_id
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
GROUP BY o.customer_id
HAVING COUNT(DISTINCT p.category_id) > 3;
Question 12
Hard
For each user, find the longest gap (in days) between consecutive login_dates.
LEAD or LAG to get next/previous login, then DATEDIFF.
WITH gaps AS (
    SELECT user_id, login_date,
           DATEDIFF(LEAD(login_date) OVER (PARTITION BY user_id ORDER BY login_date),
                    login_date) AS gap_days
    FROM logins
)
SELECT user_id, MAX(gap_days) AS longest_gap
FROM gaps
WHERE gap_days IS NOT NULL
GROUP BY user_id;
Question 13
Hard
Calculate the 14-day rolling sum of daily_sales.
ROWS BETWEEN 13 PRECEDING AND CURRENT ROW.
SELECT sale_date, daily_total,
       SUM(daily_total) OVER (
           ORDER BY sale_date
           ROWS BETWEEN 13 PRECEDING AND CURRENT ROW
       ) AS rolling_14d
FROM daily_sales
ORDER BY sale_date;
Question 14
Hard
Find 'active' customers: placed at least one order in each of the last 3 months.
Cross-join a calendar of the last 3 months with customers, then verify every month is covered.
WITH last3 AS (
    SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 0 MONTH), '%Y-%m') AS m UNION ALL
    SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m') UNION ALL
    SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 2 MONTH), '%Y-%m')
)
SELECT customer_id
FROM orders
WHERE DATE_FORMAT(order_date, '%Y-%m') IN (SELECT m FROM last3)
GROUP BY customer_id
HAVING COUNT(DISTINCT DATE_FORMAT(order_date, '%Y-%m')) = 3;
Question 15
Hard
Find products that were ordered in both 2024 AND 2025 but not in 2026.
INTERSECT-like pattern using GROUP BY + HAVING.
SELECT product_id
FROM order_items oi JOIN orders o ON o.id = oi.order_id
GROUP BY product_id
HAVING SUM(CASE WHEN YEAR(o.order_date) = 2024 THEN 1 ELSE 0 END) > 0
   AND SUM(CASE WHEN YEAR(o.order_date) = 2025 THEN 1 ELSE 0 END) > 0
   AND SUM(CASE WHEN YEAR(o.order_date) = 2026 THEN 1 ELSE 0 END) = 0;
Question 16
Hard
For a products table with (id, name, price, category_id), return the second most expensive product per category. If only one product exists in a category, return NULL for that row.
DENSE_RANK with PARTITION BY category_id.
WITH ranked AS (
    SELECT category_id, name, price,
           DENSE_RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS rnk
    FROM products
)
SELECT c.id AS category_id, r.name AS second_most_expensive
FROM (SELECT DISTINCT category_id AS id FROM products) c
LEFT JOIN ranked r ON r.category_id = c.id AND r.rnk = 2;
Question 17
Hard
A user is a 'churned' user if their last order was more than 180 days ago. Return one row per churned user with last_order_date.
MAX(order_date) per user in HAVING.
SELECT customer_id, MAX(order_date) AS last_order_date
FROM orders
GROUP BY customer_id
HAVING MAX(order_date) < DATE_SUB(CURDATE(), INTERVAL 180 DAY);

Mixed & Application Questions

Question 1
Easy
Count the number of employees in each department.
GROUP BY dept.
SELECT dept, COUNT(*) AS n FROM employees GROUP BY dept;
Question 2
Easy
Return employees whose salary is within 10% of the dept average.
Dept average via window function.
SELECT * FROM (
    SELECT e.*, AVG(salary) OVER (PARTITION BY dept) AS dept_avg
    FROM employees e
) t
WHERE salary BETWEEN dept_avg * 0.9 AND dept_avg * 1.1;
Question 3
Easy
Return the earliest login per user.
MIN(login_date) per user.
SELECT user_id, MIN(login_date) AS first_login
FROM logins GROUP BY user_id;
Question 4
Medium
For each post, return the number of comments and the number of likes.
Left join both, count each separately with DISTINCT.
SELECT p.id,
       COUNT(DISTINCT c.id) AS comment_count,
       COUNT(DISTINCT l.user_id) AS like_count
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
LEFT JOIN likes    l ON l.post_id = p.id
GROUP BY p.id;
Question 5
Medium
Find products priced within the top 20% of their category.
NTILE partitions into buckets.
SELECT category_id, name, price FROM (
    SELECT category_id, name, price,
           NTILE(5) OVER (PARTITION BY category_id ORDER BY price DESC) AS bucket
    FROM products
) t
WHERE bucket = 1;
Question 6
Medium
For every order, return the difference (in rupees) from the average total of the previous 5 orders (by order_date).
AVG with a ROWS frame.
SELECT id, order_date, total,
       AVG(total) OVER (
           ORDER BY order_date
           ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING
       ) AS avg_prev_5,
       total - AVG(total) OVER (
           ORDER BY order_date
           ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING
       ) AS diff
FROM orders;
Question 7
Medium
List all employees whose manager is in a different department.
Self-join + inequality.
SELECT e.name AS emp, e.dept AS emp_dept,
       m.name AS manager, m.dept AS mgr_dept
FROM employees e
JOIN employees m ON m.id = e.manager_id
WHERE e.dept <> m.dept;
Question 8
Medium
Return the median order total.
ROW_NUMBER + COUNT OVER () + FLOOR/CEIL.
SELECT AVG(total) AS median
FROM (
    SELECT total,
           ROW_NUMBER() OVER (ORDER BY total) AS rn,
           COUNT(*)     OVER ()                AS n
    FROM orders
) t
WHERE rn IN (FLOOR((n + 1) / 2), CEIL((n + 1) / 2));
Question 9
Hard
A post is 'trending' if it received more than 100 likes in the last 24 hours. Return all trending post ids.
Filter likes by timestamp, group by post.
SELECT post_id
FROM likes
WHERE liked_at >= NOW() - INTERVAL 24 HOUR
GROUP BY post_id
HAVING COUNT(*) > 100;
Question 10
Hard
Count, for each customer, the number of orders they placed on weekends.
DAYOFWEEK: 1=Sunday, 7=Saturday.
SELECT customer_id,
       SUM(CASE WHEN DAYOFWEEK(order_date) IN (1, 7) THEN 1 ELSE 0 END) AS weekend_orders
FROM orders
GROUP BY customer_id;
Question 11
Hard
For each user, find the time (in minutes) between their first and second login.
Window function to number logins, self-join or pivot.
WITH ranked AS (
    SELECT user_id, login_ts,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_ts) AS rn
    FROM logins
)
SELECT r1.user_id,
       TIMESTAMPDIFF(MINUTE, r1.login_ts, r2.login_ts) AS minutes_between
FROM ranked r1
JOIN ranked r2 ON r1.user_id = r2.user_id AND r1.rn = 1 AND r2.rn = 2;
Question 12
Hard
For each employee, list the names of their direct reports, comma-separated.
GROUP_CONCAT with self-join.
SELECT m.name AS manager,
       GROUP_CONCAT(e.name ORDER BY e.name SEPARATOR ', ') AS reports
FROM employees e
JOIN employees m ON m.id = e.manager_id
GROUP BY m.id, m.name;
Question 13
Hard
Pivot: for each customer, show the total they spent in each of the last 3 calendar months.
Conditional aggregation with DATE_FORMAT.
SELECT customer_id,
       SUM(CASE WHEN DATE_FORMAT(order_date,'%Y-%m') = DATE_FORMAT(CURDATE(),'%Y-%m')                           THEN total ELSE 0 END) AS this_month,
       SUM(CASE WHEN DATE_FORMAT(order_date,'%Y-%m') = DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH),'%Y-%m') THEN total ELSE 0 END) AS last_month,
       SUM(CASE WHEN DATE_FORMAT(order_date,'%Y-%m') = DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 2 MONTH),'%Y-%m') THEN total ELSE 0 END) AS two_months_ago
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 3 MONTH)
GROUP BY customer_id;
Question 14
Hard
Return the percentage of orders that have at least one item costing more than Rs. 1000.
CASE with aggregation over a subquery/EXISTS.
SELECT 100.0 * SUM(
    CASE WHEN EXISTS (
        SELECT 1 FROM order_items oi
        WHERE oi.order_id = o.id AND oi.unit_price > 1000
    ) THEN 1 ELSE 0 END
) / COUNT(*) AS pct
FROM orders o;
Question 15
Hard
For each employee, show their salary, the average salary of the whole company, and the difference.
Window function with empty frame.
SELECT name, salary,
       AVG(salary) OVER () AS company_avg,
       salary - AVG(salary) OVER () AS diff
FROM employees;
Question 16
Hard
Identify employees whose salary is higher than the average salary of every other department.
ALL subquery.
SELECT name, dept, salary FROM employees e
WHERE salary > ALL (
    SELECT AVG(salary) FROM employees e2
    WHERE e2.dept <> e.dept
    GROUP BY e2.dept
);

Multiple Choice Questions

MCQ 1
Which function handles ties naturally when finding the Nth highest salary?
  • A. ROW_NUMBER
  • B. DENSE_RANK
  • C. LIMIT/OFFSET
  • D. LAG
Answer: B
B is correct. DENSE_RANK assigns the same rank to tied salaries and does not skip — ideal for 'Nth distinct'.
MCQ 2
Which clause filters groups in a GROUP BY query?
  • A. WHERE
  • B. HAVING
  • C. FILTER
  • D. GROUP FILTER
Answer: B
B is correct. HAVING filters after aggregation. WHERE filters rows before aggregation.
MCQ 3
What does LAG(x) OVER (ORDER BY t) return on the first row?
  • A. 0
  • B. An error
  • C. NULL
  • D. The same value as x
Answer: C
C is correct. There is no previous row for row 1, so LAG returns NULL. Use LAG(x, 1, 0) to default to 0.
MCQ 4
Which is the correct way to find customers who never placed an order?
  • A. SELECT FROM customers WHERE id NOT IN (SELECT customer_id FROM orders)
  • B. LEFT JOIN orders and filter WHERE orders.id IS NULL
  • C. NOT EXISTS (SELECT 1 FROM orders WHERE ...)
  • D. Both B and C (A is dangerous if customer_id can be NULL)
Answer: D
D is correct. NOT IN with NULL in the subquery silently returns zero rows. Prefer NOT EXISTS or LEFT JOIN ... IS NULL.
MCQ 5
What does SUM(total) OVER () compute?
  • A. An error
  • B. The grand total repeated on every row
  • C. The running total
  • D. The per-row total
Answer: B
B is correct. An empty window is the whole result set — SUM over the whole set, placed on every row.
MCQ 6
Which window function breaks ties arbitrarily (no two rows share a rank)?
  • A. RANK
  • B. DENSE_RANK
  • C. ROW_NUMBER
  • D. NTILE
Answer: C
C is correct. ROW_NUMBER assigns distinct sequential numbers; ties are broken by the query optimiser.
MCQ 7
Which function pivots rows into columns in MySQL?
  • A. PIVOT keyword
  • B. Conditional aggregation using SUM(CASE WHEN ...)
  • C. Stored procedure only
  • D. CROSS JOIN
Answer: B
B is correct. MySQL has no PIVOT keyword. SUM(CASE WHEN condition THEN value ELSE 0 END) is the universal pivot.
MCQ 8
How do you find the median of a column without a MEDIAN function?
  • A. AVG(column)
  • B. ROW_NUMBER() + COUNT() OVER () + pick the middle position(s)
  • C. PERCENTILE_CONT(0.5)
  • D. You can't
Answer: B
B is correct. Number the sorted rows, pick position FLOOR((n+1)/2) and CEIL((n+1)/2), AVG the result.
MCQ 9
Which is TRUE about ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW?
  • A. Includes only the current row
  • B. Includes all rows from the start of the partition to the current row (cumulative)
  • C. Includes only preceding rows, not current
  • D. Throws an error
Answer: B
B is correct. This is the canonical cumulative / running total frame.
MCQ 10
A correlated subquery is one that:
  • A. Always returns a single row
  • B. References columns from the outer query; is re-evaluated for each outer row
  • C. Must use EXISTS
  • D. Cannot be used in WHERE
Answer: B
B is correct. Correlation = row-by-row dependency. Powerful but potentially slow — measure before using on large tables.
MCQ 11
Which is TRUE about GROUP_CONCAT?
  • A. Joins rows from different tables
  • B. Aggregates a column into a comma-separated string per group; can have ORDER BY and DISTINCT inside
  • C. Returns the first value in each group
  • D. Splits a CSV column
Answer: B
B is correct. MySQL's answer to string_agg. Watch group_concat_max_len for large groups.
MCQ 12
To find employees earning more than their manager, you use:
  • A. A CTE
  • B. A self-join on employees aliased twice (e, m)
  • C. UNION
  • D. CROSS JOIN
Answer: B
B is correct. Self-join with aliases is the classic pattern.
MCQ 13
Why is the 'date minus row_number' trick used for consecutive-day problems?
  • A. It's faster to type
  • B. Consecutive dates produce the same difference, so GROUP BY that difference creates 'islands' of consecutive days
  • C. It avoids using window functions
  • D. It works only in PostgreSQL
Answer: B
B is correct. The difference is constant for consecutive runs; GROUP BY that constant gives you each run.
MCQ 14
Which is the correct way to get top 3 per group (ties broken arbitrarily)?
  • A. LIMIT 3 per GROUP BY
  • B. RANK() OVER PARTITION BY g ORDER BY v DESC, then WHERE rnk <= 3
  • C. ROW_NUMBER() OVER PARTITION BY g ORDER BY v DESC, then WHERE rn <= 3
  • D. Three separate queries
Answer: C
C is correct. ROW_NUMBER always yields exactly 3 rows per group. RANK / DENSE_RANK can give more than 3 if there are ties.
MCQ 15
Why should you use NOT EXISTS over NOT IN when the subquery column can contain NULL?
  • A. NOT EXISTS is always faster
  • B. NOT IN silently returns zero rows if the subquery contains a NULL value
  • C. NOT IN is deprecated
  • D. The two are equivalent
Answer: B
B is correct. NOT IN ... NULL yields UNKNOWN for every comparison, excluding all outer rows.
MCQ 16
Which is NOT a valid way to compute a 7-day moving average?
  • A. AVG(x) OVER (ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
  • B. AVG(x) OVER (ORDER BY d RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW)
  • C. Correlated subquery that AVGs the last 7 rows
  • D. GROUP BY d HAVING x = AVG(x)
Answer: D
D is correct. The others compute a moving average; D is a nonsense construct.
MCQ 17
When is a cumulative-percentage query useful?
  • A. Transactional writes
  • B. Pareto/80-20 analysis: 'which products account for 80% of revenue?'
  • C. Index maintenance
  • D. Replication lag monitoring
Answer: B
B is correct. Cumulative percentages are the fastest way to spot which N items drive most of the total.

Coding Challenges

Challenge 1: Top Customer Per City

Easy
Given customers(id, name, city) and orders(customer_id, total), return the highest-spending customer in each city along with their total spend.
Sample Input
customers: (1,'Aarav','Pune'),(2,'Priya','Pune'),(3,'Rohan','Mumbai') orders: (1,5000),(1,2000),(2,4000),(3,9000)
Sample Output
Pune: Aarav 7000; Mumbai: Rohan 9000
Use window function partitioned by city.
WITH totals AS (
    SELECT c.city, c.name, SUM(o.total) AS spend
    FROM customers c JOIN orders o ON o.customer_id = c.id
    GROUP BY c.id, c.city, c.name
),
ranked AS (
    SELECT city, name, spend,
           ROW_NUMBER() OVER (PARTITION BY city ORDER BY spend DESC) AS rn
    FROM totals
)
SELECT city, name, spend FROM ranked WHERE rn = 1;

Challenge 2: Department Salary Band

Easy
For each department, return min, avg, and max salary.
Sample Input
employees with dept and salary.
Sample Output
ENG 50000 75000 110000 etc.
Single GROUP BY.
SELECT dept,
       MIN(salary) AS min_sal,
       ROUND(AVG(salary), 2) AS avg_sal,
       MAX(salary) AS max_sal
FROM employees
GROUP BY dept;

Challenge 3: Email Domain Popularity

Easy
Given users(id, email), return the top 3 email domains by user count.
Sample Input
emails like 'aarav@gmail.com', 'priya@yahoo.com', 'rohan@gmail.com'...
Sample Output
gmail.com 2, yahoo.com 1
Use SUBSTRING_INDEX on '@'.
SELECT SUBSTRING_INDEX(email, '@', -1) AS domain, COUNT(*) AS n
FROM users
GROUP BY domain
ORDER BY n DESC
LIMIT 3;

Challenge 4: Active Users Last 30 Days

Medium
Return distinct users who logged in in the last 30 days, along with their login count.
Sample Input
logins table with user_id and login_date.
Sample Output
user_id, login_count
Use DATE_SUB.
SELECT user_id, COUNT(*) AS login_count
FROM logins
WHERE login_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY user_id
ORDER BY login_count DESC;

Challenge 5: Manager With Most Direct Reports

Medium
Find the manager who directly manages the largest number of employees.
Sample Input
employees(id, name, manager_id).
Sample Output
Aarav manages 3 reports.
Self-join; GROUP BY manager; ORDER BY COUNT DESC; LIMIT 1.
SELECT m.id AS manager_id, m.name AS manager_name, COUNT(*) AS n_reports
FROM employees e
JOIN employees m ON m.id = e.manager_id
GROUP BY m.id, m.name
ORDER BY n_reports DESC
LIMIT 1;

Challenge 6: New vs Returning Customers Per Month

Medium
For each month in 2025, count how many orders came from first-time customers vs returning customers. A customer is 'new' in the month of their very first order.
Sample Input
orders(customer_id, order_date).
Sample Output
month | new_orders | returning_orders
Compute each customer's first-order month, then classify.
WITH first_orders AS (
    SELECT customer_id, DATE_FORMAT(MIN(order_date), '%Y-%m') AS first_month
    FROM orders
    GROUP BY customer_id
)
SELECT DATE_FORMAT(o.order_date, '%Y-%m') AS month,
       SUM(CASE WHEN DATE_FORMAT(o.order_date,'%Y-%m') = f.first_month THEN 1 ELSE 0 END) AS new_orders,
       SUM(CASE WHEN DATE_FORMAT(o.order_date,'%Y-%m') > f.first_month THEN 1 ELSE 0 END) AS returning_orders
FROM orders o
JOIN first_orders f ON f.customer_id = o.customer_id
WHERE YEAR(o.order_date) = 2025
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m')
ORDER BY month;

Challenge 7: Products Whose Revenue Grew Month-over-Month

Hard
For each product, return the months where its revenue grew vs the previous month (strictly). Output product, month, growth_pct.
Sample Input
order_items + orders joined on order_id, grouped by product and month.
Sample Output
product | month | growth_pct
Use LAG with PARTITION BY product.
WITH monthly AS (
    SELECT oi.product_id,
           DATE_FORMAT(o.order_date, '%Y-%m') AS month,
           SUM(oi.qty * oi.unit_price) AS rev
    FROM order_items oi JOIN orders o ON o.id = oi.order_id
    GROUP BY oi.product_id, DATE_FORMAT(o.order_date, '%Y-%m')
),
lagged AS (
    SELECT product_id, month, rev,
           LAG(rev) OVER (PARTITION BY product_id ORDER BY month) AS prev_rev
    FROM monthly
)
SELECT product_id, month,
       ROUND((rev - prev_rev) * 100.0 / prev_rev, 2) AS growth_pct
FROM lagged
WHERE prev_rev IS NOT NULL AND rev > prev_rev
ORDER BY product_id, month;

Challenge 8: Session Detection: 30-Minute Gap Rule

Hard
Given events(user_id, event_ts), assign each event a session number such that a gap of more than 30 minutes between consecutive events starts a new session.
Sample Input
events for a user at 10:00, 10:05, 11:00, 11:10, 12:00
Sample Output
10:00 -> session 1; 10:05 -> session 1; 11:00 -> session 2; 11:10 -> session 2; 12:00 -> session 3.
Use LAG + cumulative SUM of a 'new-session' flag.
WITH flagged AS (
    SELECT user_id, event_ts,
           CASE WHEN TIMESTAMPDIFF(MINUTE,
                        LAG(event_ts) OVER (PARTITION BY user_id ORDER BY event_ts),
                        event_ts) > 30
                THEN 1 ELSE 0 END AS new_session
    FROM events
)
SELECT user_id, event_ts,
       1 + SUM(new_session) OVER (
           PARTITION BY user_id ORDER BY event_ts
           ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS session_id
FROM flagged
ORDER BY user_id, event_ts;

Need to Review the Concepts?

Go back to the detailed notes for this chapter.

Read Chapter Notes

Want to master SQL and databases with a mentor?

Explore our MySQL Masterclass