Skip to content

100+ SQL Practice Questions with Solutions

    SQL Practice Questions with Solutions

    Last Updated on: 25th March 2026, 08:21 pm

    100+ SQL Practice Questions · brutalist query lab

    If you are learning SQL and still feel confused solving questions, then honestly you are not alone. Most people read theory, but when real query comes… mind just goes blank little bit.

    Why Practicing SQL Questions is Important

    SQL is not something you “memorize”. It’s something you practice, mess up, fix, repeat again. If you only read SELECT, WHERE, JOIN — you think you understand… but when question comes, confusion start happens. So best way is solving questions.

    Database Setup (Used in All Questions)

  • StudentsStudents(id, name, marks, city, dept_id)
  • DepartmentsDepartments(id, dept_name)
  • EmployeesEmployees(id, name, salary, dept_id)
  • SECTION 1: Basic SQL Questions (1–25)

    Start simple. Build confidence first. These questions cover SELECT, WHERE, ORDER BY, basic functions and filters.

    #1 · Get all records from Students
    SELECT * FROM Students;
    📖 Why? The asterisk * means “all columns”. This returns every row and column from the Students table. Perfect for initial data inspection.
    #2 · Get only student names
    SELECT name FROM Students;
    📖 Column projection: instead of all columns, we select only the name column. Useful when you need specific attributes.
    #3 · Students with marks greater than 80
    SELECT * FROM Students WHERE marks > 80;
    📖 WHERE clause filters rows based on a condition. Here we keep only rows where marks exceed 80. Comparison operators: >, <, =, >=, <=.
    #4 · Get students from Lahore
    SELECT * FROM Students WHERE city = 'Lahore';
    📖 String filtering: use single quotes for text values. Only rows where city exactly matches ‘Lahore’.
    #5 · Students between 60 and 90 marks
    SELECT * FROM Students WHERE marks BETWEEN 60 AND 90;
    📖 BETWEEN operator is inclusive shorthand for marks >= 60 AND marks <= 90. Great for range filters.
    #6 · Names starting with ‘A’
    SELECT * FROM Students WHERE name LIKE 'A%';
    📖 LIKE + wildcard: % matches any sequence of characters. 'A%' finds names beginning with ‘A’. Case‑sensitive depends on database collation.
    #7 · Names ending with ‘n’
    SELECT * FROM Students WHERE name LIKE '%n';
    📖 '%n' → any characters before the final letter ‘n’.
    #8 · Sort students by marks (ascending)
    SELECT * FROM Students ORDER BY marks;
    📖 ORDER BY sorts results. Default is ascending (lowest to highest). Use ASC explicitly if needed.
    #9 · Sort students by marks descending
    SELECT * FROM Students ORDER BY marks DESC;
    📖 DESC keyword reverses order → highest marks first.
    #10 · Count total students
    SELECT COUNT(*) FROM Students;
    📖 Aggregate function: COUNT(*) returns the total number of rows in the table.
    #11 · Find maximum marks
    SELECT MAX(marks) FROM Students;
    📖 MAX() returns the highest value in the column.
    #12 · Find minimum marks
    SELECT MIN(marks) FROM Students;
    📖 MIN() returns the lowest value.
    #13 · Find average marks
    SELECT AVG(marks) FROM Students;
    📖 AVG() calculates arithmetic mean (ignores NULLs).
    #14 · Find total marks (sum)
    SELECT SUM(marks) FROM Students;
    📖 SUM() adds all numeric values in the column.
    #15 · Get distinct cities
    SELECT DISTINCT city FROM Students;
    📖 DISTINCT removes duplicate values → returns unique city names.
    #16 · Students not from Karachi
    SELECT * FROM Students WHERE city != 'Karachi';
    📖 != (or <>) means “not equal”. Excludes a specific city.
    #17 · Students with NULL department
    SELECT * FROM Students WHERE dept_id IS NULL;
    📖 NULL handling: use IS NULL (not = NULL). NULL means missing/unknown value.
    #18 · Students with NOT NULL department
    SELECT * FROM Students WHERE dept_id IS NOT NULL;
    📖 IS NOT NULL keeps rows where department is assigned.
    #19 · Students with marks >= 90
    SELECT * FROM Students WHERE marks >= 90;
    📖 >= includes the boundary value.
    #20 · Students with marks not equal 50
    SELECT * FROM Students WHERE marks != 50;
    📖 Excludes the exact value 50.
    #21 · Top 5 students (first 5 rows)
    SELECT * FROM Students LIMIT 5;
    📖 LIMIT restricts output rows (MySQL/PostgreSQL). In SQL Server use TOP 5.
    #22 · Students sorted by name (A–Z)
    SELECT * FROM Students ORDER BY name;
    📖 Alphabetical order based on the name column.
    #23 · Count distinct cities
    SELECT COUNT(DISTINCT city) FROM Students;
    📖 COUNT(DISTINCT column) counts unique non‑NULL values.
    #24 · Students with marks less than 40
    SELECT * FROM Students WHERE marks < 40;
    📖 Low‑performing students filter.
    #25 · Students with marks in (50,60,70)
    SELECT * FROM Students WHERE marks IN (50,60,70);
    📖 IN operator matches any value inside the list. Cleaner than multiple OR conditions.

    SECTION 2: Intermediate SQL Questions (26–60)

    Now things start getting little tricky… but don't worry. GROUP BY, HAVING, subqueries, string functions.

    #26 · AND condition (marks>70 AND city=Lahore)
    SELECT * FROM Students WHERE marks > 70 AND city='Lahore';
    📖 Logical AND: both conditions must be true.
    #27 · OR condition (marks>70 OR city=Karachi)
    SELECT * FROM Students WHERE marks > 70 OR city='Karachi';
    📖 Logical OR: at least one condition true.
    #28 · Count students per city (GROUP BY)
    SELECT city, COUNT(*) FROM Students GROUP BY city;
    📖 GROUP BY groups rows with same city, then COUNT(*) counts each group.
    #29 · Average marks per city
    SELECT city, AVG(marks) FROM Students GROUP BY city;
    📖 Average marks calculated per city group.
    #30 · Cities having avg marks > 70 (HAVING)
    SELECT city, AVG(marks) FROM Students GROUP BY city HAVING AVG(marks) > 70;
    📖 HAVING filters groups (WHERE filters rows before grouping).
    #31 · Find duplicate cities (appear more than once)
    SELECT city, COUNT(*) FROM Students GROUP BY city HAVING COUNT(*) > 1;
    📖 Identifies cities with multiple students.
    #32 · Second highest marks using subquery
    SELECT MAX(marks) FROM Students WHERE marks < (SELECT MAX(marks) FROM Students);
    📖 Inner subquery gets highest marks, outer finds max below that → second highest.
    #33 · Students above average marks
    SELECT * FROM Students WHERE marks > (SELECT AVG(marks) FROM Students);
    📖 Subquery returns the global average, outer query selects above‑average students.
    #34 · Students below average
    SELECT * FROM Students WHERE marks < (SELECT AVG(marks) FROM Students);
    📖 Opposite: students scoring less than average.
    #35 · Names in uppercase
    SELECT UPPER(name) FROM Students;
    📖 String function UPPER() converts text to all caps.
    #36 · Names in lowercase
    SELECT LOWER(name) FROM Students;
    📖 LOWER() converts to lowercase (useful for case‑insensitive comparisons).
    #37 · Concatenate name and city
    SELECT CONCAT(name,' - ',city) FROM Students;
    📖 CONCAT() joins strings; creates a formatted label like "Ali - Lahore".
    #38 · Names longer than 5 characters
    SELECT * FROM Students WHERE LENGTH(name) > 5;
    📖 LENGTH() returns number of characters; filter on that.
    #39 · Replace 'Lahore' with 'LHR'
    SELECT REPLACE(city,'Lahore','LHR') FROM Students;
    📖 REPLACE() substitutes substrings (does not modify original data).
    #40 · First 3 letters of names
    SELECT SUBSTRING(name,1,3) FROM Students;
    📖 SUBSTRING(name, start, length) extracts prefix (e.g., "Ali" → "Ali", "Ahmed" → "Ahm").
    #41 · Students with marks divisible by 5
    SELECT * FROM Students WHERE marks % 5 = 0;
    📖 Modulo operator % gives remainder. Remainder zero → divisible by 5.
    #42 · Top 3 highest marks
    SELECT * FROM Students ORDER BY marks DESC LIMIT 3;
    📖 Sort descending, then pick first 3 rows.
    #43 · Longest name (by length)
    SELECT name FROM Students ORDER BY LENGTH(name) DESC LIMIT 1;
    📖 Order by length descending, limit 1 gives the longest name.
    #44 · Students not in Lahore (alternative syntax)
    SELECT * FROM Students WHERE city <> 'Lahore';
    📖 <> is the standard SQL not‑equal operator (same as !=).
    #45 · Cities with more than 2 students
    SELECT city, COUNT(*) FROM Students GROUP BY city HAVING COUNT(*) > 2;
    📖 Group by city, keep only groups where count exceeds 2.
    #46 · Count students per department
    SELECT dept_id, COUNT(*) FROM Students GROUP BY dept_id;
    📖 Shows distribution of students across departments (including NULL if present).
    #47 · Sort by city, then by marks descending
    SELECT * FROM Students ORDER BY city, marks DESC;
    📖 Multi‑column sort: first alphabetical by city, then highest marks within each city.
    #48 · Complex filter (marks>60 and city != Karachi)
    SELECT * FROM Students WHERE marks > 60 AND city != 'Karachi';
    📖 Combines numeric and text exclusion.
    #49 · Names containing letter 'a'
    SELECT * FROM Students WHERE name LIKE '%a%';
    📖 Wildcard before and after → 'a' anywhere in the name.
    #50 · Count students with marks > 60
    SELECT COUNT(*) FROM Students WHERE marks > 60;
    📖 Conditional count using WHERE.
    #51 · Maximum marks per city
    SELECT city, MAX(marks) FROM Students GROUP BY city;
    📖 Highest score achieved in each city.
    #52 · Minimum marks per city
    SELECT city, MIN(marks) FROM Students GROUP BY city;
    📖 Lowest score per city.
    #53 · Average marks per department
    SELECT dept_id, AVG(marks) FROM Students GROUP BY dept_id;
    📖 Department‑wise academic average.
    #54 · Student with highest marks (global)
    SELECT * FROM Students WHERE marks = (SELECT MAX(marks) FROM Students);
    📖 Subquery finds top marks, outer gets the student(s).
    #55 · Student with lowest marks
    SELECT * FROM Students WHERE marks = (SELECT MIN(marks) FROM Students);
    📖 Same pattern for minimum.
    #56 · Names sorted by length
    SELECT name FROM Students ORDER BY LENGTH(name);
    📖 Shortest names first.
    #57 · Distinct department IDs (students)
    SELECT DISTINCT dept_id FROM Students;
    📖 Lists unique department references in Students table.
    #58 · Marks between 40 and 60 inclusive
    SELECT * FROM Students WHERE marks BETWEEN 40 AND 60;
    📖 Range filter with boundaries included.
    #59 · Names where second character is 'a'
    SELECT * FROM Students WHERE name LIKE '_a%';
    📖 Underscore _ matches exactly one character, then 'a', then anything.
    #60 · Sort by marks desc, then name asc
    SELECT * FROM Students ORDER BY marks DESC, name ASC;
    📖 Tie‑breaker: if marks equal, names in ascending order.
    SQL
    JOINs · windows · ranking
    Think → break → solve → repeat

    SECTION 3: JOIN Queries (61–90)

    Joins = most important topic (and most confusing also sometimes). These combine data from multiple tables.

    #61 · Inner Join: Students + Departments
    SELECT s.name, d.dept_name FROM Students s INNER JOIN Departments d ON s.dept_id = d.id;
    📖 INNER JOIN returns only rows where department exists in both tables. Aliases s and d make query shorter.
    #62 · Left Join (all students, optional dept)
    SELECT s.name, d.dept_name FROM Students s LEFT JOIN Departments d ON s.dept_id = d.id;
    📖 LEFT JOIN keeps every student, even if no matching department (shows NULL for dept_name).
    #63 · Students without department (anti‑join)
    SELECT s.name FROM Students s LEFT JOIN Departments d ON s.dept_id = d.id WHERE d.id IS NULL;
    📖 Left join + NULL filter → finds students with no department mapping.
    #64 · Count students per department name
    SELECT d.dept_name, COUNT(*) FROM Students s JOIN Departments d ON s.dept_id = d.id GROUP BY d.dept_name;
    📖 Join then group by department name to get student counts per real department.
    #65 · Employees with department names
    SELECT e.name, d.dept_name FROM Employees e JOIN Departments d ON e.dept_id = d.id;
    📖 Similar join pattern using Employees table.
    #66 · Highest salary per department
    SELECT dept_id, MAX(salary) FROM Employees GROUP BY dept_id;
    📖 Aggregation per department (shows department ID).
    #67 · Average salary per department
    SELECT dept_id, AVG(salary) FROM Employees GROUP BY dept_id;
    📖 Department‑wise average compensation.
    #68 · Employees above company average salary
    SELECT * FROM Employees WHERE salary > (SELECT AVG(salary) FROM Employees);
    📖 Subquery returns global average, outer selects higher earners.
    #69 · Department with most employees
    SELECT dept_id, COUNT(*) FROM Employees GROUP BY dept_id ORDER BY COUNT(*) DESC LIMIT 1;
    📖 Group, sort by count descending, take top row.
    #70 · Employees with NULL department
    SELECT * FROM Employees WHERE dept_id IS NULL;
    📖 Employees not assigned to any department.
    #71 · Departments with no employees (LEFT JOIN)
    SELECT d.dept_name FROM Departments d LEFT JOIN Employees e ON d.id=e.dept_id WHERE e.id IS NULL;
    📖 Anti‑join: departments that never appear in Employees table.
    #72 · Sort employees by salary descending
    SELECT name, salary FROM Employees ORDER BY salary DESC;
    📖 Highest paid employees first.
    #73 · Departments with more than 2 employees
    SELECT dept_id, COUNT(*) FROM Employees GROUP BY dept_id HAVING COUNT(*) > 2;
    📖 HAVING filters groups with count > 2.
    #74 · Employee + department name (inner join)
    SELECT e.name, d.dept_name FROM Employees e JOIN Departments d ON e.dept_id = d.id;
    📖 Joining employee records to department names.
    #75 · Cross‑table: students and employees in same dept
    SELECT s.name, e.name FROM Students s JOIN Employees e ON s.dept_id = e.dept_id;
    📖 Pairs students and employees who share the same department.
    #76 · Distinct department IDs in Employees
    SELECT DISTINCT dept_id FROM Employees;
    📖 Which departments have at least one employee.
    #77 · Order employees by department ID
    SELECT * FROM Employees ORDER BY dept_id;
    📖 Groups employees by department visually.
    #78 · Total number of employees
    SELECT COUNT(*) FROM Employees;
    📖 Simple row count.
    #79 · Average salary of all employees
    SELECT AVG(salary) FROM Employees;
    📖 Company‑wide average.
    #80 · Maximum salary
    SELECT MAX(salary) FROM Employees;
    📖 Highest earner's salary.
    #81 · Minimum salary
    SELECT MIN(salary) FROM Employees;
    📖 Lowest salary in company.
    #82 · Employees with salary between 20000 and 50000
    SELECT * FROM Employees WHERE salary BETWEEN 20000 AND 50000;
    📖 Mid‑range salary filter.
    #83 · Names starting with 'S'
    SELECT * FROM Employees WHERE name LIKE 'S%';
    📖 Employees whose name begins with S.
    #84 · Salary greater than 30000
    SELECT * FROM Employees WHERE salary > 30000;
    📖 Above‑threshold filter.
    #85 · Sum of salaries per department
    SELECT dept_id, SUM(salary) FROM Employees GROUP BY dept_id;
    📖 Total payroll per department.
    #86 · Sort employees by name alphabetically
    SELECT name FROM Employees ORDER BY name;
    📖 A–Z order.
    #87 · First 10 employees
    SELECT * FROM Employees LIMIT 10;
    📖 Restrict output to first ten rows.
    #88 · Employees in department 1 or 2
    SELECT * FROM Employees WHERE dept_id IN (1,2);
    📖 IN operator for multiple possible values.
    #89 · Employees not in department 1 or 2
    SELECT * FROM Employees WHERE dept_id NOT IN (1,2);
    📖 Exclude two specific departments.
    #90 · Count distinct departments with employees
    SELECT COUNT(DISTINCT dept_id) FROM Employees;
    📖 How many different departments have employees.

    SECTION 4: Advanced SQL Queries (91–110)

    Now real interview level things start… window functions, ranking, complex subqueries, duplicates removal.

    #91 · Second highest salary
    SELECT MAX(salary) FROM Employees WHERE salary < (SELECT MAX(salary) FROM Employees);
    📖 Find max salary, then max below that → second highest.
    #92 · Third highest salary (OFFSET)
    SELECT DISTINCT salary FROM Employees ORDER BY salary DESC LIMIT 1 OFFSET 2;
    📖 OFFSET 2 skips first two distinct salary values → third highest.
    #93 · RANK() window function
    SELECT name, salary, RANK() OVER (ORDER BY salary DESC) FROM Employees;
    📖 Assigns rank with gaps (1,2,2,4…) based on salary.
    #94 · DENSE_RANK()
    SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) FROM Employees;
    📖 No gaps: 1,2,2,3…
    #95 · ROW_NUMBER()
    SELECT name, ROW_NUMBER() OVER () FROM Employees;
    📖 Simple sequential numbering across all rows.
    #96 · Running total (cumulative sum)
    SELECT salary, SUM(salary) OVER (ORDER BY id) FROM Employees;
    📖 Adds salaries progressively based on ID order.
    #97 · Duplicate salaries
    SELECT salary, COUNT(*) FROM Employees GROUP BY salary HAVING COUNT(*) > 1;
    📖 Identify salary values that appear more than once.
    #98 · Top 3 earners per department
    SELECT * FROM (SELECT e.*, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) rn FROM Employees e) t WHERE rn <= 3;
    📖 PARTITION BY resets ranking per department, outer filter keeps top 3.
    #99 · Delete duplicate rows (keep smallest ID)
    DELETE FROM Students WHERE id NOT IN (SELECT MIN(id) FROM Students GROUP BY name, city);
    📖 Keep one row per (name,city) combination (the smallest id), delete others.
    #100 · Self join example (employees with same salary)
    SELECT a.name, b.name, a.salary FROM Employees a JOIN Employees b ON a.salary = b.salary AND a.id != b.id;
    📖 Self‑join to find pairs of employees earning same salary.
    #101 · Recursive CTE (simple counting)
    WITH RECURSIVE nums(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM nums WHERE n<10) SELECT * FROM nums;
    📖 Generate series 1 to 10 using recursive common table expression.
    #102 · Pivot / Crosstab (conditional aggregation)
    SELECT city, SUM(CASE WHEN marks>=80 THEN 1 ELSE 0 END) as high_scorers FROM Students GROUP BY city;
    📖 Count of high‑scorers per city using CASE inside SUM.
    #103 · LAG() window (previous salary)
    SELECT name, salary, LAG(salary) OVER (ORDER BY id) FROM Employees;
    📖 Get previous row’s salary; useful for comparisons.
    #104 · LEAD() window (next salary)
    SELECT name, salary, LEAD(salary) OVER (ORDER BY id) FROM Employees;
    📖 Next row’s salary.
    #105 · Running average (window)
    SELECT salary, AVG(salary) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) FROM Employees;
    📖 Moving average of last 3 rows.

    How to Solve Any SQL Question (Real Trick)

    • 1️⃣ What data needed? (columns)
    • 2️⃣ From which table(s)? (JOINs)
    • 3️⃣ Conditions? (WHERE / HAVING)
    • 4️⃣ Grouping? (GROUP BY)
    • 5️⃣ Sorting / limit? (ORDER BY / LIMIT)

    Break it… then solve.

    Common Mistakes (Everyone Does 😅)

    • Forget GROUP BY with aggregations
    • Wrong JOIN condition (cartesian product)
    • Mixing WHERE and HAVING
    • Missing NULL handling (IS NULL vs = NULL)

    FAQs (Most Asked SQL Questions)

    How many SQL questions should I practice?
    At least 100+, but honestly more better.
    Is SQL hard to learn?
    Not really… but logic part little tricky.
    What is the hardest SQL topic?
    Joins and window functions mostly.
    Difference between WHERE and HAVING?
    WHERE filters rows, HAVING filters grouped data (after GROUP BY).
    What is JOIN?
    Combining data from multiple tables based on related columns.
    How to get job using SQL?
    Practice + projects + real datasets.
    Is SQL enough for data jobs?
    Not alone… need tools like Power BI, Python.
    Best way to learn SQL fast?
    Practice daily and solve real problems.
    Do companies ask SQL?
    Yes, almost every tech/data job.
    Which platform is best?
    HackerRank, LeetCode, Mode Analytics.

    Final Thoughts

    SQL is not about syntax… it is about thinking clearly. At start you struggle… then suddenly everything starts making sense. Just keep practicing… even if query is wrong, its okay. That is how real learning happens.

    Share this post on social!

    Comment on Post

    Your email address will not be published. Required fields are marked *