SQL in 30 days for a final-year student: a day-by-day plan
In this article (7 sections)
Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).
You can learn the SQL that entry-level data tests check in 30 days at one hour a day, if every session is spent writing queries. The plan below uses SQLite, which ships with Python, and moves from single-table queries (days 1–7) to aggregation (8–13), joins (14–19), subqueries and CTEs (20–24) and window functions (25–28), with two days of timed revision. Each week ends with a checkpoint query you should be able to write without help.
Set up the practice database once
Run this Python script to create college.db, a synthetic database of students, courses and enrolments. It uses only the standard library. Every checkpoint below runs against it.
import sqlite3, random
random.seed(7)
con = sqlite3.connect("college.db")
cur = con.cursor()
cur.executescript("""
DROP TABLE IF EXISTS students; DROP TABLE IF EXISTS courses; DROP TABLE IF EXISTS enrolments;
CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT, branch TEXT, city TEXT, join_year INTEGER);
CREATE TABLE courses (course_id TEXT PRIMARY KEY, title TEXT, credits INTEGER);
CREATE TABLE enrolments (student_id INTEGER, course_id TEXT, semester INTEGER, marks INTEGER);
""")
branches = ["CSE", "ECE", "ME", "CE", "BCom"]
cities = ["Noida", "Ghaziabad", "Delhi", "Meerut", None]
cur.executemany("INSERT INTO students VALUES (?,?,?,?,?)",
[(i, f"Student{i:03d}", random.choice(branches), random.choice(cities), random.choice([2023, 2024]))
for i in range(1, 121)])
cur.executemany("INSERT INTO courses VALUES (?,?,?)",
[("DB101", "Databases", 4), ("ST201", "Statistics", 3), ("PY101", "Python", 4), ("EC105", "Economics", 3)])
rows = []
for sid in range(1, 121):
for cid in random.sample(["DB101", "ST201", "PY101", "EC105"], k=random.randint(1, 4)):
marks = random.randint(25, 98) if random.random() > 0.05 else None # a few missing marks
rows.append((sid, cid, random.choice([5, 6]), marks))
cur.executemany("INSERT INTO enrolments VALUES (?,?,?,?)", rows)
con.commit()
print(cur.execute("SELECT COUNT(*) FROM students").fetchone()[0], "students,",
cur.execute("SELECT COUNT(*) FROM enrolments").fetchone()[0], "enrolments")
con.close()Open it in DB Browser for SQLite, or query it from Python with sqlite3.connect("college.db"). The deliberate NULL cities and missing marks are there so you meet the traps early.
Week 1 (days 1–7): one table, precisely
| Day | Topic | Practise |
|---|---|---|
| 1 | SELECT, column aliases | List names and branches; rename columns |
| 2 | WHERE with =, <>, >, BETWEEN | Students who joined in 2024 |
| 3 | AND, OR, parentheses | ECE or ME students from Noida — check the brackets |
| 4 | IN, LIKE, wildcards | Names starting with "Student00" |
| 5 | NULL handling: IS NULL, COALESCE | Students with no city; replace NULL with "Unknown" |
| 6 | ORDER BY, LIMIT, DISTINCT | Distinct branches, sorted |
| 7 | Checkpoint and revision | Write the query below from memory |
Checkpoint 1: list students from Noida or Delhi who joined in 2024, sorted by name.
SELECT student_id, name, city
FROM students
WHERE city IN ('Noida', 'Delhi') AND join_year = 2024
ORDER BY name;Week 2 (days 8–13): counting correctly
| Day | Topic |
|---|---|
| 8 | COUNT(*), COUNT(column), SUM, AVG, MIN, MAX |
| 9 | GROUP BY one column |
| 10 | GROUP BY two columns |
| 11 | HAVING versus WHERE |
| 12 | CASE WHEN for buckets (distinction, pass, fail) |
| 13 | Checkpoint |
Checkpoint 2: average marks per course, only for courses with at least 50 marked enrolments. Notice that AVG and COUNT(marks) both ignore NULLs.
SELECT course_id, COUNT(marks) AS marked, ROUND(AVG(marks), 1) AS avg_marks
FROM enrolments
GROUP BY course_id
HAVING COUNT(marks) >= 50
ORDER BY avg_marks DESC;Week 3 (days 14–19): joins without losing rows
| Day | Topic |
|---|---|
| 14 | INNER JOIN on one key |
| 15 | LEFT JOIN and finding unmatched rows (WHERE right.key IS NULL) |
| 16 | Joining three tables |
| 17 | Row-count checks before and after every join |
| 18 | Join plus GROUP BY |
| 19 | Checkpoint |
On day 17, make it a habit: count rows in each table, then count rows after the join, and explain the difference before you trust any total.
Checkpoint 3: credit-weighted average marks per branch.
SELECT s.branch,
ROUND(SUM(e.marks * c.credits) * 1.0 / SUM(c.credits), 1) AS weighted_avg
FROM enrolments e
JOIN students s ON s.student_id = e.student_id
JOIN courses c ON c.course_id = e.course_id
WHERE e.marks IS NOT NULL
GROUP BY s.branch
ORDER BY weighted_avg DESC;The WHERE e.marks IS NOT NULL matters: without it, credits for unmarked enrolments would be counted in the denominator but contribute nothing to the numerator.
Week 4 (days 20–28): subqueries, CTEs and windows
| Day | Topic |
|---|---|
| 20 | Subquery in WHERE (above-average students) |
| 21 | Subquery in FROM |
| 22 | CTEs with WITH |
| 23 | Chaining two CTEs |
| 24 | Checkpoint on CTEs |
| 25 | ROW_NUMBER and RANK |
| 26 | PARTITION BY |
| 27 | Running totals with SUM() OVER |
| 28 | Checkpoint |
Checkpoint 4: the top two students by marks in each course. SQLite supports window functions from version 3.25, which current Python releases include.
WITH ranked AS (
SELECT course_id, student_id, marks,
ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY marks DESC, student_id) AS rn
FROM enrolments
WHERE marks IS NOT NULL
)
SELECT course_id, student_id, marks
FROM ranked
WHERE rn <= 2
ORDER BY course_id, rn;The student_id tie-breaker makes the result repeatable when two students have equal marks. Explain in one sentence why you might use RANK instead.
Days 29–30: timed revision
Day 29: redo all four checkpoints against a 15-minute timer, then write five new questions about college.db and answer them. Day 30: take a mixed set of ten problems from a practice site, timed, and log each mistake by type (NULL, join, grouping, window). That log is your revision list for placement season.
If you miss days
Do not skip ahead. Merge two lighter days (days 4 and 6, or 25 and 26) rather than dropping joins, which is the week screeners test hardest. Missing marks and NULL cities in this database are also the kind of thing the Excel test for freshers checks, from the spreadsheet side. When you are ready to see how your SQL fits into complete analyst work — dashboards, reconciliations and written findings — look at the Data Analyst programme, or check where your resume stands with the free skill check.
Frequently asked questions
Is one hour a day enough to learn SQL in 30 days?
It is enough to reach the level most entry-level analyst tests check — filtering, grouping, joins, subqueries and basic window functions — if the hour is spent writing queries rather than watching videos. Fluency under interview pressure takes further practice after the 30 days.
Which SQL database should a student practise on?
SQLite is the easiest start because it comes with Python and needs no server. The core syntax transfers to MySQL, PostgreSQL and SQL Server, with small differences in date functions and some window-function support.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI