beginner~3h

SQL Practice — Beginner (Problems 1-30)

30 warm-up problems covering SELECT, WHERE, ORDER BY, LIMIT, DISTINCT, filtering operators, and basic aggregates — every one solved, run against real data, and explained.

Learning objectives

  • Write correct SELECT/WHERE/ORDER BY/LIMIT queries confidently from a plain-English prompt.
  • Use BETWEEN, IN, LIKE, and IS NULL correctly instead of reaching for error-prone equivalents.
  • Apply COUNT/SUM/AVG/MIN/MAX with GROUP BY and HAVING, and know why HAVING exists at all.
  • Read a real computed output table and connect it back to exactly which clause produced which value.

◆ Before you start

All 150 problems in this practice section run against the exact same 9-table schema, modeled after a small e-commerce company that also tracks internal projects and employee hours. Nothing here is abstract — every problem below was actually executed against a populated copy of this schema, so the SQL, the output table, and the explanation you'll see under each problem are all real, not hand-typed guesses.

9 tables are shared by every problem in this practice set — the exact schema:

departmentsdepartment_id (PK, INT), department_name (VARCHAR)

employeesemployee_id (PK, INT), first_name, last_name, email (VARCHAR), department_id (FK → departments), manager_id (FK → employees), salary (DECIMAL), hire_date (DATE), city (VARCHAR)

customerscustomer_id (PK, INT), customer_name, email (VARCHAR), city, country (VARCHAR), signup_date (DATE)

productsproduct_id (PK, INT), product_name, category (VARCHAR), price (DECIMAL), stock_quantity (INT)

ordersorder_id (PK, INT), customer_id (FK → customers), employee_id (FK → employees, the sales employee), order_date (DATE), status (VARCHAR — Pending, Shipped, Delivered, Cancelled), total_amount (DECIMAL)

order_itemsorder_item_id (PK, INT), order_id (FK → orders), product_id (FK → products), quantity (INT), unit_price (DECIMAL — price at purchase time)

paymentspayment_id (PK, INT), order_id (FK → orders), payment_date (DATE), amount (DECIMAL), payment_method (VARCHAR — Card, UPI, Cash, Netbanking), payment_status (VARCHAR — Success, Failed, Refunded)

projectsproject_id (PK, INT), project_name (VARCHAR), start_date, end_date (DATE, nullable), budget (DECIMAL)

employee_projectsemployee_id (FK → employees), project_id (FK → projects), hours_worked (DECIMAL)

▲ Note on realism

The sample data behind these problems deliberately isn't clean. A handful of orders have a corrupted total_amount, an invalid status, or reference a customer_id that doesn't exist — on purpose, so that Chapter 4's data-quality problems (P149, P110) have something real to find. If a query's output looks slightly messy in places, that's the dataset doing its job, not an error.

💻 Code example

-- No query for this page — this is the shared schema reference. -- See the next problem for the first real query.

P1 — Select All Employees  ·  Beginner

Retrieve all columns from the employees table.

SQL

SELECT * FROM employees;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_nameemaildepartment_idmanager_idsalaryhire_datecity
1ArvindKrishnanarvind.krishnan@company.com1NULL285,0002019-01-10Bengaluru
2MeeraNairmeera.nair@company.com11215,0002019-06-15Bengaluru
3KaranMalhotrakaran.malhotra@company.com21198,0002019-08-01Mumbai
4PriyaChoprapriya.chopra@company.com31175,0002020-02-20Mumbai
5SureshIyersuresh.iyer@company.com41160,0002019-11-05Delhi
6AnilBhattanil.bhatt@company.com51205,0002020-01-15Pune
7SunitaRaosunita.rao@company.com61168,0002020-03-10Hyderabad
8RohanVermarohan.verma@company.com12152,0002020-05-12Bengaluru
9AnanyaSinghananya.singh@company.com12148,0002020-07-19Pune
10VikramReddyvikram.reddy@company.com23131,0002020-09-01Mumbai

(showing 10 of 40 rows)

Explanation

SELECT * asks the database for every column it knows about on employees, in whatever order they were defined in the schema, for every row that exists — there's no WHERE clause, so nothing is filtered out. In this dataset that returns all 40 employees, from the CEO Arvind Krishnan down to the newest individual contributor. SELECT * is convenient while you're exploring a table for the first time, but in application code it's usually better to name columns explicitly (as the next problem does) — a SELECT * breaks silently if someone adds a column later and your code wasn't expecting it.

💻 Code example

SELECT * FROM employees;

P2 — Select Specific Employee Columns  ·  Beginner

Display each employee's first name, last name, and salary.

SQL

SELECT first_name, last_name, salary FROM employees;

Output (computed against this section's live 9-table dataset)

first_namelast_namesalary
ArvindKrishnan285,000
MeeraNair215,000
KaranMalhotra198,000
PriyaChopra175,000
SureshIyer160,000
AnilBhatt205,000
SunitaRao168,000
RohanVerma152,000
AnanyaSingh148,000
VikramReddy131,000

(showing 10 of 40 rows)

Explanation

Naming first_name, last_name, salary instead of * returns only the three columns the caller actually needs, in that exact order — the database doesn't have to read or serialize the email, department_id, manager_id, hire_date, or city columns for this query at all. The result set has the same 40 rows as Problem 1, just narrower. This is the pattern you want in real applications: it documents intent, and it means adding a new column to employees later can never silently change what this query returns.

💻 Code example

SELECT first_name, last_name, salary FROM employees;

P3 — Find Employees in Bengaluru  ·  Beginner

Find all employees whose city is Bengaluru.

SQL

SELECT * FROM employees WHERE city = 'Bengaluru';

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_nameemaildepartment_idmanager_idsalaryhire_datecity
1ArvindKrishnanarvind.krishnan@company.com1NULL285,0002019-01-10Bengaluru
2MeeraNairmeera.nair@company.com11215,0002019-06-15Bengaluru
8RohanVermarohan.verma@company.com12152,0002020-05-12Bengaluru
14RohanRaorohan.rao14@company.com18167,0002023-03-15Bengaluru
22ArjunReddyarjun.reddy22@company.com31197,839.552022-09-16Bengaluru
30ManishKumarmanish.kumar30@company.com51248,719.112022-11-10Bengaluru
38RiyaChauhanriya.chauhan38@company.com1853,903.402024-04-18Bengaluru

(7 rows total)

Explanation

The WHERE city = 'Bengaluru' clause is evaluated once per row, before anything is returned — a row survives only if its city column is exactly 'Bengaluru' (SQL string comparisons are case-sensitive by default). Of the 40 employees, 7 are based in Bengaluru, including the CEO Arvind Krishnan and his direct report Meera Nair. Because there's no index on city in this simple schema, the engine has to scan every row and test the predicate — on a real production table you'd add an index on city if this filter ran often.

💻 Code example

SELECT * FROM employees WHERE city = 'Bengaluru';

P4 — Employees With Salary Above 60000  ·  Beginner

Find employees earning more than 60,000.

SQL

SELECT first_name, last_name, salary FROM employees WHERE salary > 60000;

Output (computed against this section's live 9-table dataset)

first_namelast_namesalary
ArvindKrishnan285,000
MeeraNair215,000
KaranMalhotra198,000
PriyaChopra175,000
SureshIyer160,000
AnilBhatt205,000
SunitaRao168,000
RohanVerma152,000
AnanyaSingh148,000
VikramReddy131,000

(showing 10 of 30 rows)

Explanation

salary > 60000 is a strict numeric inequality, so an employee earning exactly 60,000 would not qualify — only rows strictly above the threshold pass. 30 of the 40 employees clear that bar, which tells you most of the workforce (managers and above, plus a majority of the senior ICs) sits comfortably above ₹60k, while the newer, junior hires cluster below it.

💻 Code example

SELECT first_name, last_name, salary FROM employees WHERE salary > 60000;

P5 — Employees Hired After 2023  ·  Beginner

Find employees hired after January 1, 2023.

SQL

SELECT * FROM employees WHERE hire_date > '2023-01-01';

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_nameemaildepartment_idmanager_idsalaryhire_datecity
14RohanRaorohan.rao14@company.com18167,0002023-03-15Bengaluru
15VihaanThakurvihaan.thakur15@company.com2360,767.332023-11-11Mumbai
17RakeshShettyrakesh.shetty17@company.com4539,926.012024-07-09Pune
18SanjayArorasanjay.arora18@company.com51241,971.322024-04-21Hyderabad
19VikramRanavikram.rana19@company.com6767,953.882025-03-09Chennai
20DeepakJoshideepak.joshi20@company.com1846,377.822023-12-19Kolkata
21DiyaKulkarnidiya.kulkarni21@company.com2363,706.032024-06-08Ahmedabad
24RahulMenonrahul.menon24@company.com51275,646.762024-10-03Delhi
25MeeraKapoormeera.kapoor25@company.com6761,086.042025-09-09Pune
27NehaNaiduneha.naidu27@company.com2344,873.102023-11-11Chennai

(showing 10 of 18 rows)

Explanation

Dates in SQL can be compared with the same operators as numbers (>, <, >=, BETWEEN) once they're stored as a proper DATE type, and the string '2023-01-01' is implicitly cast to a date for the comparison. 18 employees were hired after that cutoff — this dataset's newer joiners, mostly individual contributors brought on through 2023-2025 as the company grew, while the founding leadership team (hired 2019-2020) is correctly excluded.

💻 Code example

SELECT * FROM employees WHERE hire_date > '2023-01-01';

P6 — Sort Employees by Salary  ·  Beginner

Display employees from highest salary to lowest.

SQL

SELECT first_name, last_name, salary FROM employees ORDER BY salary DESC;

Output (computed against this section's live 9-table dataset)

first_namelast_namesalary
ArvindKrishnan285,000
MeeraNair215,000
AnilBhatt205,000
KaranMalhotra198,000
PriyaChopra175,000
SunitaRao168,000
RohanRao167,000
SureshIyer160,000
RohanVerma152,000
AnanyaSingh148,000

(showing 10 of 40 rows)

Explanation

ORDER BY salary DESC sorts the full 40-row result set from the highest salary down, entirely independent of the SELECT list — you can sort by a column even without displaying every column used to sort. Arvind Krishnan (₹285,000, the CEO) leads, followed by Meera Nair and Anil Bhatt; sorting doesn't remove any rows, it only changes the order they're returned in.

💻 Code example

SELECT first_name, last_name, salary FROM employees ORDER BY salary DESC;

P7 — Top 5 Highest Paid Employees  ·  Beginner

Return the five employees with the highest salaries.

SQL

SELECT first_name, last_name, salary FROM employees ORDER BY salary DESC LIMIT 5;

Output (computed against this section's live 9-table dataset)

first_namelast_namesalary
ArvindKrishnan285,000
MeeraNair215,000
AnilBhatt205,000
KaranMalhotra198,000
PriyaChopra175,000

(5 rows total)

Explanation

This combines the ORDER BY salary DESC from the previous problem with LIMIT 5, which is applied after sorting — so it keeps only the first five rows of the already-sorted result, not five arbitrary high earners. The result is exactly the five most senior salaries: Arvind (₹285,000), Meera (₹215,000), Anil Bhatt (₹205,000), Karan Malhotra (₹198,000), and the fifth-highest department head. Without the preceding ORDER BY, LIMIT would just return whichever five rows the engine happened to read first, which is not a meaningful answer.

💻 Code example

SELECT first_name, last_name, salary FROM employees ORDER BY salary DESC LIMIT 5;

P8 — Customers From India  ·  Beginner

Find all customers whose country is India.

SQL

SELECT customer_id, customer_name, city FROM customers WHERE country = 'India';

Output (computed against this section's live 9-table dataset)

customer_idcustomer_namecity
1Rahul AgarwalBengaluru
2Sneha NairMumbai
3Amit BhatiaDelhi
4Pooja SinhaPune
5Vikas RanaHyderabad
6Neha GhoshChennai
7Rajesh PandeyKolkata
8Swati YadavAhmedabad
9Manoj KulkarniBengaluru
10Kirti ShettyMumbai

(showing 10 of 20 rows)

Explanation

The filter country = 'India' runs against the customers table this time, and returns 20 of the 30 customers — matching this dataset's deliberate mix of mostly-Indian customers with a handful of international ones (USA, UK, Canada, Germany, Australia, Singapore, UAE) sprinkled in for variety in later problems like P25's GROUP BY country.

💻 Code example

SELECT customer_id, customer_name, city FROM customers WHERE country = 'India';

P9 — Products Above ₹1000  ·  Beginner

Find products whose price is greater than 1000.

SQL

SELECT product_name, price FROM products WHERE price > 1000;

Output (computed against this section's live 9-table dataset)

product_nameprice
Mechanical Keyboard3,499
27-inch 4K Monitor24,999
USB-C Hub1,299
Noise Cancelling Headphones8,999
Smartwatch12,999
Portable SSD 1TB6,999
Bluetooth Speaker2,299
Men's Running Shoes3,299
Dumbbell Set 10kg2,599
Cricket Bat1,899

(showing 10 of 21 rows)

Explanation

price > 1000 filters the 28-row products table down to 21 products — everything except the handful of budget items (a ₹799 mouse, a ₹899 yoga mat, a couple of sub-₹1000 books and kitchen basics). The comparison works the same way for DECIMAL prices as it does for integer salaries in earlier problems.

💻 Code example

SELECT product_name, price FROM products WHERE price > 1000;

P10 — Delivered Orders  ·  Beginner

Find all orders that have been delivered.

SQL

SELECT * FROM orders WHERE status = 'Delivered';

Output (computed against this section's live 9-table dataset)

order_idcustomer_idemployee_idorder_datestatustotal_amount
17102025-07-18Delivered13,490
2532024-08-13Delivered7,797
419212025-09-26Delivered12,293
5832024-10-25Delivered14,792
621272025-04-07Delivered115,286
10232025-06-20Delivered76,385
117152025-10-20Delivered35,991
126392025-01-05Delivered4,995
1317392025-11-17Delivered10,388
146212024-11-06Delivered9,095

(showing 10 of 88 rows)

Explanation

Matching status = 'Delivered' returns 88 of the 148 orders in the table — the largest single bucket, since the dataset was generated with roughly 55% of orders landing in Delivered, 15% Shipped, 10% Pending, and 20% Cancelled (deliberately high enough to make cancellation-rate and revenue-exclusion problems later on meaningful).

💻 Code example

SELECT * FROM orders WHERE status = 'Delivered';

P11 — Unique Customer Countries  ·  Beginner

List every unique country represented in the customers table.

SQL

SELECT DISTINCT country FROM customers;

Output (computed against this section's live 9-table dataset)

country
India
UAE
USA
UK
Canada
Australia
Germany
Singapore

(8 rows total)

Explanation

DISTINCT runs after the rows are selected and collapses duplicate values in the country column down to one row per unique value — instead of returning all 30 customer rows, it returns just the 8 distinct countries represented among them (India, UAE, USA, UK, Canada, Germany, Australia, Singapore). It answers 'what values exist here', not 'how many rows have each value' — for that you'd add COUNT(*) and GROUP BY, as Problem 25 does.

💻 Code example

SELECT DISTINCT country FROM customers;

P12 — Products in Electronics  ·  Beginner

Find all products in the Electronics category.

SQL

SELECT product_name, price FROM products WHERE category = 'Electronics';

Output (computed against this section's live 9-table dataset)

product_nameprice
Wireless Mouse799
Mechanical Keyboard3,499
27-inch 4K Monitor24,999
USB-C Hub1,299
Noise Cancelling Headphones8,999
Smartwatch12,999
Portable SSD 1TB6,999
Bluetooth Speaker2,299

(8 rows total)

Explanation

This is the same filter pattern as Problem 3 and Problem 9, just applied to the category column of products. Electronics is this dataset's largest category by design (8 of the 28 products), spanning the ₹799 wireless mouse up to the ₹24,999 4K monitor.

💻 Code example

SELECT product_name, price FROM products WHERE category = 'Electronics';

P13 — Salary Range  ·  Beginner

Find employees earning between 40,000 and 80,000.

SQL

SELECT first_name, last_name, salary FROM employees WHERE salary BETWEEN 40000 AND 80000;

Output (computed against this section's live 9-table dataset)

first_namelast_namesalary
VihaanThakur60,767.33
SanjayArora41,971.32
VikramRana67,953.88
DeepakJoshi46,377.82
DiyaKulkarni63,706.03
TanviChatterjee43,454.56
RahulMenon75,646.76
MeeraKapoor61,086.04
NehaNaidu44,873.10
PriyaYadav44,693.13

(showing 10 of 15 rows)

Explanation

BETWEEN 40000 AND 80000 is inclusive on both ends — DuckDB (and standard SQL) treats it as shorthand for salary >= 40000 AND salary <= 80000, so an employee earning exactly 40,000 or exactly 80,000 would still match. 15 employees fall in that band, mostly the individual contributors hired over the last two years, since the company's more senior managers and department heads all clear ₹100k+.

💻 Code example

SELECT first_name, last_name, salary FROM employees WHERE salary BETWEEN 40000 AND 80000;

P14 — Customers From Selected Cities  ·  Beginner

Find customers from Bengaluru or Mumbai.

SQL

SELECT customer_name, city FROM customers WHERE city IN ('Bengaluru', 'Mumbai');

Output (computed against this section's live 9-table dataset)

customer_namecity
Rahul AgarwalBengaluru
Sneha NairMumbai
Manoj KulkarniBengaluru
Kirti ShettyMumbai
Imran KhanBengaluru
Jyoti DasMumbai

(6 rows total)

Explanation

IN ('Bengaluru', 'Mumbai') is exactly equivalent to city = 'Bengaluru' OR city = 'Mumbai', just more compact and, for longer lists, easier for the query planner to optimize. 6 customers are based in one of those two cities out of the 30 total.

💻 Code example

SELECT customer_name, city FROM customers WHERE city IN ('Bengaluru', 'Mumbai');

P15 — Names Starting With A  ·  Beginner

Find customers whose names start with the letter A.

SQL

SELECT customer_name FROM customers WHERE customer_name LIKE 'A%';

Output (computed against this section's live 9-table dataset)

customer_name
Amit Bhatia
Alok Bose

(2 rows total)

Explanation

LIKE 'A%' is a pattern match: % stands for 'zero or more of any character', so this matches any customer_name that starts with the literal letter A (case-sensitive in DuckDB's default collation) — it would match 'Amit Bhatia' but not 'alok bose' if that were lowercase, and it would not match a name where 'A' appears in the middle. Only 2 of the 30 customers happen to start with A in this dataset: Amit Bhatia and Alok Bose.

💻 Code example

SELECT customer_name FROM customers WHERE customer_name LIKE 'A%';

P16 — Products With Low Stock  ·  Beginner

Find products with fewer than 10 units in stock.

SQL

SELECT product_name, stock_quantity FROM products WHERE stock_quantity < 10;

Output (computed against this section's live 9-table dataset)

product_namestock_quantity
Smartwatch8
Cycling Helmet9

(2 rows total)

Explanation

stock_quantity < 10 finds products at real risk of stocking out — a strict inequality, so a product sitting at exactly 10 units would not be flagged yet. Only 2 products qualify here: the Smartwatch (8 units) and the Cycling Helmet (9 units) — not coincidentally, these are also the two products this dataset deliberately keeps out of every order in Problem 35, since a slow-moving, low-stock item is a realistic reason a product might never sell.

💻 Code example

SELECT product_name, stock_quantity FROM products WHERE stock_quantity < 10;

P17 — Cancelled Orders  ·  Beginner

Find all cancelled orders and display their IDs and amounts.

SQL

SELECT order_id, total_amount FROM orders WHERE status = 'Cancelled';

Output (computed against this section's live 9-table dataset)

order_idtotal_amount
86,294
913,891
168,392
1813,196
262,397
293,696
3024,688
3313,592
406,396
438,892

(showing 10 of 25 rows)

Explanation

Filtering on status = 'Cancelled' surfaces exactly the orders that never generated revenue — 25 of the 148 orders (roughly the 20% cancellation rate baked into this dataset), each shown with its total_amount even though that amount was never actually collected. This is the same set of rows Problem 24 and Problem 106 both have to explicitly exclude or measure.

💻 Code example

SELECT order_id, total_amount FROM orders WHERE status = 'Cancelled';

P18 — Orders Above 5000  ·  Beginner

Find orders whose total amount exceeds 5000.

SQL

SELECT order_id, customer_id, total_amount FROM orders WHERE total_amount > 5000;

Output (computed against this section's live 9-table dataset)

order_idcustomer_idtotal_amount
1713,490
257,797
399,092
41912,293
5814,792
621115,286
7114,494
886,294
9213,891
10276,385

(showing 10 of 119 rows)

Explanation

total_amount > 5000 is a simple numeric filter over orders, matching 119 of the 148 rows — most orders in this dataset clear ₹5,000 because each one bundles 1-4 line items (see order_items), so only the smallest single-item orders fall under that line.

💻 Code example

SELECT order_id, customer_id, total_amount FROM orders WHERE total_amount > 5000;

P19 — Employees in Two Departments  ·  Beginner

Find employees belonging to department 1 or department 2.

SQL

SELECT employee_id, first_name, department_id FROM employees WHERE department_id IN (1, 2);

Output (computed against this section's live 9-table dataset)

employee_idfirst_namedepartment_id
1Arvind1
2Meera1
3Karan2
8Rohan1
9Ananya1
10Vikram2
14Rohan1
15Vihaan2
20Deepak1
21Diya2

(showing 10 of 16 rows)

Explanation

department_id IN (1, 2) is the numeric-column version of Problem 14's city filter — it matches employees in Engineering (department_id = 1) or Sales (department_id = 2), 16 employees combined, without needing two separate OR conditions.

💻 Code example

SELECT employee_id, first_name, department_id FROM employees WHERE department_id IN (1, 2);

P20 — Employees Without Managers  ·  Beginner

Find employees who do not have a manager.

SQL

SELECT employee_id, first_name, last_name FROM employees WHERE manager_id IS NULL;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_name
1ArvindKrishnan

(1 row total)

Explanation

manager_id IS NULL is the only correct way to test for a missing value in SQL — manager_id = NULL would never match anything, because NULL represents 'unknown', and unknown = unknown itself evaluates to unknown (not true). Exactly one employee has no manager: Arvind Krishnan, the CEO — everyone else in the 40-person hierarchy ultimately reports up to him, which is exactly what makes the recursive-hierarchy problems in Chapter 4 (P126, P127) work.

💻 Code example

SELECT employee_id, first_name, last_name FROM employees WHERE manager_id IS NULL;

P21 — Count All Employees  ·  Beginner

Find the total number of employees.

SQL

SELECT COUNT(*) AS employee_count FROM employees;

Output (computed against this section's live 9-table dataset)

employee_count
40

(1 row total)

Explanation

COUNT(*) counts rows, not values in a particular column, so it correctly returns 40 regardless of whether any individual column (like manager_id, which is NULL for the CEO) has missing data. This is the simplest possible aggregate query: one row in, one row out, summarizing the whole table.

💻 Code example

SELECT COUNT(*) AS employee_count FROM employees;

P22 — Average Employee Salary  ·  Beginner

Calculate the average salary of all employees.

SQL

SELECT AVG(salary) AS average_salary FROM employees;

Output (computed against this section's live 9-table dataset)

average_salary
102,073.49

(1 row total)

Explanation

AVG(salary) sums every employee's salary and divides by the row count (40) in a single pass — it returns roughly ₹102,073, which sits well above the median individual-contributor salary because it's pulled upward by the CEO's ₹285,000 and the five-figure salaries of the department heads and managers. This is exactly why later problems (P44, P134) also compute the median — averages can be misleading when a dataset has a few very large outliers.

💻 Code example

SELECT AVG(salary) AS average_salary FROM employees;

P23 — Highest and Lowest Salary  ·  Beginner

Find the maximum and minimum employee salary.

SQL

SELECT MAX(salary) AS highest_salary, MIN(salary) AS lowest_salary FROM employees;

Output (computed against this section's live 9-table dataset)

highest_salarylowest_salary
285,00038,194.72

(1 row total)

Explanation

MAX and MIN can be computed in the same SELECT because each is an independent aggregate over the same column — the engine only needs one pass over the table to produce both. The spread here is enormous: ₹285,000 (the CEO) down to roughly ₹38,195 (this dataset's most junior individual contributor), a nearly 7.5x gap that reflects the org's multi-level hierarchy from CEO down to ICs.

💻 Code example

SELECT MAX(salary) AS highest_salary, MIN(salary) AS lowest_salary FROM employees;

P24 — Total Order Revenue  ·  Beginner

Calculate the total value of all orders.

SQL

SELECT SUM(total_amount) AS total_revenue FROM orders WHERE status <> 'Cancelled';

Output (computed against this section's live 9-table dataset)

total_revenue
2,705,025

(1 row total)

Explanation

SUM(total_amount) alone would double-count revenue that was never actually earned, since 25 of the 148 orders are Cancelled — the WHERE status <> 'Cancelled' clause runs before the aggregation, so those rows are excluded from the sum entirely. The result, roughly ₹2.7 million, is this dataset's real non-cancelled order revenue; contrast this with what you'd get by summing every row regardless of status, which would overstate revenue by the value of every cancelled order.

💻 Code example

SELECT SUM(total_amount) AS total_revenue FROM orders WHERE status <> 'Cancelled';

P25 — Count Customers by Country  ·  Beginner

Show how many customers belong to each country.

SQL

SELECT country, COUNT(*) AS customer_count FROM customers GROUP BY country ORDER BY customer_count DESC;

Output (computed against this section's live 9-table dataset)

countrycustomer_count
India20
UK2
USA2
Canada2
Australia1
UAE1
Singapore1
Germany1

(8 rows total)

Explanation

GROUP BY country collapses the 30 customer rows into one row per distinct country, and COUNT(*) counts how many original rows fell into each group. India dominates with 20 customers, while the seven international countries have 1-2 each — ORDER BY customer_count DESC then puts the largest group first so the concentration is immediately visible.

💻 Code example

SELECT country, COUNT(*) AS customer_count FROM customers GROUP BY country ORDER BY customer_count DESC;

P26 — Average Salary by Department  ·  Beginner

Calculate the average salary for each department.

SQL

SELECT department_id, AVG(salary) AS average_salary FROM employees GROUP BY department_id;

Output (computed against this section's live 9-table dataset)

department_idaverage_salary
1138,588.51
292,711.24
399,118.03
474,451.17
596,749.44
690,014.29

(6 rows total)

Explanation

Grouping by department_id and averaging salary within each group shows the org's pay structure department by department: Engineering averages the highest (₹138,589, pulled up by two senior heads plus several well-paid ICs), while HR sits lowest (₹74,451). Unlike Problem 22's single company-wide average, this reveals that the 'average salary' varies a lot depending on which team you're looking at.

💻 Code example

SELECT department_id, AVG(salary) AS average_salary FROM employees GROUP BY department_id;

P27 — Product Count by Category  ·  Beginner

Count products in every category.

SQL

SELECT category, COUNT(*) AS product_count FROM products GROUP BY category ORDER BY product_count DESC;

Output (computed against this section's live 9-table dataset)

categoryproduct_count
Electronics8
Sports6
Home & Kitchen6
Clothing4
Books4

(5 rows total)

Explanation

Grouping products by category and counting rows per group shows the catalog's composition: Electronics leads with 8 products, Sports and Home & Kitchen follow with 6 each, and Clothing and Books round out the remaining 8. ORDER BY product_count DESC puts the best-stocked category first.

💻 Code example

SELECT category, COUNT(*) AS product_count FROM products GROUP BY category ORDER BY product_count DESC;

P28 — Departments With More Than 5 Employees  ·  Beginner

Find departments having more than five employees.

SQL

SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id HAVING COUNT(*) > 5;

Output (computed against this section's live 9-table dataset)

department_idemployee_count
19
27
37
56
66

(5 rows total)

Explanation

HAVING is the equivalent of WHERE but for aggregated results — it can't run until after GROUP BY department_id and COUNT(*) have already produced one row per department, because COUNT(*) > 5 isn't a condition any single raw employee row could satisfy on its own. Five of this dataset's six departments clear that bar (only HR, the smallest team, falls at or under 5 employees), which is exactly why HAVING — not WHERE — has to be used here.

💻 Code example

SELECT department_id, COUNT(*) AS employee_count FROM employees GROUP BY department_id HAVING COUNT(*) > 5;

P29 — Categorize Employees by Salary  ·  Beginner

Classify employees as High, Medium, or Low salary.

SQL

SELECT first_name, salary, CASE WHEN salary >= 80000 THEN 'High' WHEN salary >= 50000 THEN 'Medium' ELSE 'Low' END AS salary_level FROM employees;

Output (computed against this section's live 9-table dataset)

first_namesalarysalary_level
Arvind285,000High
Meera215,000High
Karan198,000High
Priya175,000High
Suresh160,000High
Anil205,000High
Sunita168,000High
Rohan152,000High
Ananya148,000High
Vikram131,000High

(showing 10 of 40 rows)

Explanation

CASE WHEN ... THEN ... ELSE ... END evaluates each employee's salary against the conditions in order and stops at the first one that matches — a salary of ₹198,000 hits the >= 80000 branch and is labeled 'High' before the engine ever checks the >= 50000 branch, and anyone under 50,000 falls through to 'Low'. Because the org's leadership salaries are so far above 80,000, a large share of this 40-row result comes back labeled 'High', with 'Medium' and 'Low' concentrated among the more recently hired individual contributors.

💻 Code example

SELECT first_name, salary, CASE WHEN salary >= 80000 THEN 'High' WHEN salary >= 50000 THEN 'Medium' ELSE 'Low' END AS salary_level FROM employees;

P30 — Basic Employee Department Join  ·  Beginner

Display each employee's name and department name.

SQL

SELECT e.first_name, e.last_name, d.department_name FROM employees e JOIN departments d ON e.department_id = d.department_id;

Output (computed against this section's live 9-table dataset)

first_namelast_namedepartment_name
ArvindKrishnanEngineering
MeeraNairEngineering
KaranMalhotraSales
PriyaChopraMarketing
SureshIyerHuman Resources
AnilBhattFinance
SunitaRaoOperations
RohanVermaEngineering
AnanyaSinghEngineering
VikramReddySales

(showing 10 of 40 rows)

Explanation

A plain JOIN (shorthand for INNER JOIN) matches each employees row to the departments row whose department_id equals the employee's own department_id — since every employee in this dataset has a valid department_id, all 40 employees appear, now with a human-readable department_name (like 'Engineering' or 'Sales') standing in for the raw numeric foreign key. This is the foundational join pattern every later multi-table problem in this section builds on.

💻 Code example

SELECT e.first_name, e.last_name, d.department_name FROM employees e JOIN departments d ON e.department_id = d.department_id;

Want a visual for this concept?

Generate a diagram tailored to “SQL Practice — Beginner (Problems 1-30)” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to SQL Practice — Intermediate (Problems 31-70)← Back to all SQL Practice Problems chapters