SQL โ Complete Beginner Tutorial
6 modules ยท 17 examples ยท Click โถ Run Query to see the result table
๐ students
| id | name | age | city | grade | score |
|---|---|---|---|---|---|
| 1 | Alice | 20 | Cape Town | A | 95 |
| 2 | Bob | 17 | Johannesburg | B | 82 |
| 3 | Charlie | 22 | Cape Town | A | 91 |
| 4 | Diana | 16 | Durban | C | 68 |
| 5 | Edward | 19 | Johannesburg | B | 79 |
| 6 | Fiona | 21 | Cape Town | A | 88 |
| 7 | George | 18 | Durban | D | 55 |
๐ courses
| id | title | teacher |
|---|---|---|
| 1 | Web Development | Mr Adams |
| 2 | Data Science | Ms Botha |
| 3 | Python Basics | Mr Cloete |
๐ enrolments
| student_id | course_id |
|---|---|
| 1 | 1 |
| 1 | 3 |
| 2 | 2 |
| 3 | 1 |
| 5 | 2 |
| 5 | 3 |
SELECT Basics
Every SQL query starts with SELECT. It tells the database which columns you want to see. FROM tells it which table to look in. Think of it like asking a question: "Show me the name and age FROM the students table."
SELECT * โ Get Everything
The asterisk * means "all columns". This is the quickest way to see everything in a table. In real apps you usually pick specific columns instead โ fetching everything wastes resources when tables have millions of rows.
SELECT *
FROM students;SELECT * is great for exploring a new table. But in production code, always name the exact columns you need โ it is faster and your code still works if someone adds new columns later.
Choosing Specific Columns
List only the columns you need, separated by commas. You can rename a column in the results using AS โ this is called an "alias". The original table is not changed โ it only affects how the result looks.
SELECT name, city, score
FROM students;
-- With aliases (rename columns in the result)
SELECT
name AS student_name,
score AS exam_score,
grade AS letter_grade
FROM students;SQL keywords (SELECT, FROM, WHERE) are usually written in capitals by convention โ it makes queries easier to read. Your database does not care about the case, but your colleagues will thank you.
DISTINCT โ Remove Duplicates
DISTINCT keeps only unique values โ great for finding out what unique options exist in a column without seeing the same city listed ten times.
-- Without DISTINCT โ shows every city, repeated
SELECT city FROM students;
-- With DISTINCT โ each city only once
SELECT DISTINCT city
FROM students;
-- DISTINCT on multiple columns
SELECT DISTINCT city, grade
FROM students
ORDER BY city;DISTINCT looks at the combination of ALL selected columns. If you SELECT DISTINCT city, grade โ it keeps rows where city+grade together are unique, not just the city alone.
Filtering with WHERE
WHERE filters the rows you get back. Only rows where the condition is TRUE are included. Think of it as adding "but only show me rows where..." to your question. Without WHERE you get every row.
Basic WHERE Conditions
After WHERE, you write a condition using comparison operators. = checks equality (note: one equals sign in SQL, not two like in JavaScript). You can also use >, <, >=, <=, and != (not equal).
-- Students from Cape Town only
SELECT name, age, city
FROM students
WHERE city = 'Cape Town';
-- Students aged 18 or older
SELECT name, age
FROM students
WHERE age >= 18;
-- Students who did NOT get an A
SELECT name, grade
FROM students
WHERE grade != 'A';In SQL, text values (strings) go in single quotes: WHERE city = 'Cape Town'. Numbers do not need quotes: WHERE age = 20. Mixing them up is one of the most common beginner mistakes.
AND, OR, NOT โ Combining Conditions
AND requires both conditions to be true. OR requires at least one to be true. NOT flips a condition. You can use parentheses () to group conditions just like maths.
-- AND: both conditions must be true
SELECT name, age, city
FROM students
WHERE city = 'Cape Town'
AND age >= 20;
-- OR: either condition can be true
SELECT name, city
FROM students
WHERE city = 'Cape Town'
OR city = 'Durban';
-- Combining AND + OR with parentheses
SELECT name, grade, score
FROM students
WHERE grade = 'A'
AND (city = 'Cape Town' OR city = 'Durban');AND has higher priority than OR โ just like multiplication before addition in maths. Always use parentheses when mixing AND and OR to make your intention crystal clear.
LIKE โ Pattern Matching
LIKE searches for a text pattern. The % wildcard means "any number of any characters". The _ wildcard means "exactly one character". Great for searching names, emails, or any text field.
-- Names that start with "A"
SELECT name FROM students
WHERE name LIKE 'A%';
-- Names that end with "e"
SELECT name FROM students
WHERE name LIKE '%e';
-- Names containing "ar" anywhere
SELECT name FROM students
WHERE name LIKE '%ar%';
-- IN: match any value from a list
SELECT name, city
FROM students
WHERE city IN ('Cape Town', 'Durban');
-- BETWEEN: range of values (inclusive)
SELECT name, score
FROM students
WHERE score BETWEEN 75 AND 90;BETWEEN 75 AND 90 includes both 75 and 90 themselves. IN ('a', 'b', 'c') is a cleaner alternative to writing OR three times. Both are very common in real SQL queries.
Sorting & Limiting
By default, SQL returns rows in no guaranteed order. ORDER BY lets you sort results. LIMIT caps how many rows come back โ essential for pagination (showing 10 results per page) and performance on huge tables.
ORDER BY โ Sorting Results
ORDER BY sorts your results by one or more columns. ASC = ascending (AโZ, 0โ9) and is the default. DESC = descending (ZโA, 9โ0). You can sort by multiple columns โ the second sort breaks ties from the first.
-- Sort by score, highest first
SELECT name, score
FROM students
ORDER BY score DESC;
-- Sort alphabetically by name
SELECT name, city
FROM students
ORDER BY name ASC;
-- Sort by city first, then by score within each city
SELECT name, city, score
FROM students
ORDER BY city ASC, score DESC;When you ORDER BY two columns, think of it as a tiebreaker. First it sorts by city. When multiple rows have the same city, it then sorts those by score descending.
LIMIT โ Control Row Count
LIMIT sets the maximum number of rows returned. OFFSET skips a number of rows before starting โ this powers pagination. Together they let you show "page 1, page 2, page 3..." of results.
-- Top 3 highest scoring students
SELECT name, score
FROM students
ORDER BY score DESC
LIMIT 3;
-- Page 2: skip the first 3, get the next 3
SELECT name, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 3;
-- The single lowest score
SELECT name, score
FROM students
ORDER BY score ASC
LIMIT 1;LIMIT without ORDER BY gives you random rows โ the database picks whichever it finds first. Always pair LIMIT with ORDER BY so you know exactly which rows you are getting.
Aggregate Functions
Aggregate functions calculate a single summary value from many rows โ like the total, average, or count. GROUP BY splits the rows into groups first, then runs the aggregate on each group separately.
COUNT, SUM, AVG, MIN, MAX
These five functions are used constantly in data analysis. COUNT(*) counts all rows. COUNT(column) counts non-null values in that column. SUM, AVG, MIN, MAX work on number columns.
-- How many students are there?
SELECT COUNT(*) AS total_students
FROM students;
-- Average, min, and max score
SELECT
AVG(score) AS average_score,
MIN(score) AS lowest_score,
MAX(score) AS highest_score
FROM students;
-- Only count students aged 18+
SELECT COUNT(*) AS adult_students
FROM students
WHERE age >= 18;AVG ignores NULL values automatically โ same as SUM, MIN, and MAX. COUNT(*) counts every row including NULLs, but COUNT(column_name) skips rows where that column is NULL.
GROUP BY โ Summarise by Category
GROUP BY splits your rows into groups and runs the aggregate function on each group. The classic use: "how many students per city?" or "what is the average score per grade?".
-- How many students in each city?
SELECT city, COUNT(*) AS num_students
FROM students
GROUP BY city
ORDER BY num_students DESC;
-- Average score per grade
SELECT
grade,
COUNT(*) AS students,
AVG(score) AS avg_score,
MIN(score) AS min_score,
MAX(score) AS max_score
FROM students
GROUP BY grade
ORDER BY grade;Rule: every column in your SELECT must either be inside an aggregate function (COUNT, SUM, AVG...) OR be listed in GROUP BY. Breaking this rule causes an error in most databases.
HAVING โ Filter After Grouping
WHERE filters individual rows BEFORE grouping. HAVING filters groups AFTER the aggregate is calculated. Use HAVING when your condition involves an aggregate like COUNT or AVG.
-- Cities with MORE than 1 student
SELECT city, COUNT(*) AS num_students
FROM students
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY num_students DESC;
-- Grades where average score is above 75
SELECT
grade,
ROUND(AVG(score), 1) AS avg_score
FROM students
GROUP BY grade
HAVING AVG(score) > 75
ORDER BY avg_score DESC;Remember: WHERE โ before grouping (filters rows). HAVING โ after grouping (filters groups). You can use both in the same query! The order is: WHERE filters rows, then GROUP BY groups them, then HAVING filters the groups.
JOINs
Real databases split data across multiple tables to avoid repeating information. JOINs combine rows from two or more tables based on a matching column โ usually a shared ID. This is one of the most powerful and important SQL skills.
INNER JOIN โ Only Matching Rows
INNER JOIN returns only rows that have a match in BOTH tables. If a student has no enrolments, they are left out. If a course has no students enrolled, it is left out too. Both sides must match.
-- Which students are enrolled in which courses?
SELECT
students.name AS student,
courses.title AS course,
courses.teacher
FROM students
INNER JOIN enrolments
ON students.id = enrolments.student_id
INNER JOIN courses
ON enrolments.course_id = courses.id
ORDER BY students.name;The ON clause says HOW the tables connect โ usually matching an ID from one table to a foreign key in another. students.id = enrolments.student_id means "find me rows where the student's id matches the student_id stored in the enrolments table".
LEFT JOIN โ Keep All Left Rows
LEFT JOIN returns all rows from the LEFT (first) table, plus matching rows from the right table. If there is no match on the right side, those columns show NULL. Use it when you want to see everything, even unmatched rows.
-- All students, even those with no enrolments
-- (Diana and George have no courses โ NULL)
SELECT
students.name,
courses.title AS enrolled_in
FROM students
LEFT JOIN enrolments
ON students.id = enrolments.student_id
LEFT JOIN courses
ON enrolments.course_id = courses.id
ORDER BY students.name;LEFT JOIN is used far more often than RIGHT JOIN โ just swap which table you write first. A very common use: "show me all customers, and if they have an order show that too โ but include customers with zero orders as well".
Modifying Data
So far you have only read data. SQL can also create tables and add, change, or delete data. Be careful with UPDATE and DELETE โ without a WHERE clause they affect EVERY row in the table. Always double-check before running them.
INSERT โ Add New Rows
INSERT INTO adds one or more new rows to a table. List the columns you are filling in, then the matching values. You can insert multiple rows in one statement by separating them with commas.
-- Insert a single student
INSERT INTO students (name, age, city, grade, score)
VALUES ('Hannah', 19, 'Cape Town', 'B', 84);
-- Insert multiple students at once
INSERT INTO students (name, age, city, grade, score)
VALUES
('Ivan', 23, 'Johannesburg', 'A', 92),
('Julia', 17, 'Durban', 'C', 71);
-- After inserting, SELECT to confirm
SELECT name, age, city, grade, score
FROM students
WHERE name IN ('Hannah', 'Ivan', 'Julia');You do not need to include the id column if it is set to AUTO_INCREMENT (MySQL) or SERIAL (PostgreSQL) โ the database assigns the next number automatically.
UPDATE โ Change Existing Rows
UPDATE changes values in rows that already exist. SET says which columns to change. The WHERE clause limits which rows are affected. WITHOUT a WHERE clause, EVERY row in the table gets updated โ a very common and painful mistake!
-- Update Alice's score
UPDATE students
SET score = 97, grade = 'A'
WHERE name = 'Alice';
-- Raise everyone's score by 5
-- (in their city only)
UPDATE students
SET score = score + 5
WHERE city = 'Cape Town';
-- After updating, check the result
SELECT name, city, score, grade
FROM students
WHERE city = 'Cape Town'
ORDER BY score DESC;โ ๏ธ ALWAYS write the WHERE clause FIRST, before SET, when composing an UPDATE. Then go back and fill in SET. This habit prevents the devastating "I updated every row" mistake that has ended careers.
DELETE & CREATE TABLE
DELETE removes rows. Without WHERE it clears the entire table. CREATE TABLE defines a new table with its columns and data types. This is how database schemas are built from scratch.
-- Delete a specific student
DELETE FROM students
WHERE name = 'George';
-- Delete all students with a failing grade
DELETE FROM students
WHERE grade = 'F';
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-- CREATE TABLE โ build a new table from scratch
-- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CREATE TABLE teachers (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
subject VARCHAR(100),
email VARCHAR(150) UNIQUE,
hired_on DATE
);
-- Insert into the new table
INSERT INTO teachers (id, name, subject, email)
VALUES
(1, 'Mr Adams', 'Web Development', 'adams@school.edu'),
(2, 'Ms Botha', 'Data Science', 'botha@school.edu'),
(3, 'Mr Cloete', 'Python', 'cloete@school.edu');
SELECT * FROM teachers;Common data types: INTEGER (whole numbers), VARCHAR(n) (text up to n chars), TEXT (unlimited text), DECIMAL(p,s) (exact decimals like money), DATE, BOOLEAN. NOT NULL means the column cannot be empty. PRIMARY KEY is the unique identifier for each row.
You finished the SQL tutorial!
You now know how to SELECT, filter with WHERE, sort, group, join tables, and modify data. These skills work in MySQL, PostgreSQL, SQLite and almost every other database system.