Coding · Programming languages
SQL tutorial for beginners
Short answer
SQL is the standard language for creating, querying and changing data in relational databases such as PostgreSQL, MySQL and SQLite. You write declarative statements like SELECT, INSERT, UPDATE and DELETE, and the database decides how to run them. This page teaches table design, filtering, joins, grouping, subqueries, window functions and transactions, and lists the main differences between PostgreSQL, MySQL and SQLite.
What SQL is and what it is used for
SQL (Structured Query Language) is the standard language for working with relational databases, where data is stored in tables of rows and columns. You describe what data you want, and the database engine decides how to find it. SQL is standardized by ISO/IEC 9075, and every major database implements the core of the standard plus its own extensions.
Developers, data analysts, data engineers and database administrators use SQL every day: to store application data, to answer business questions, to build reports and to feed data pipelines. The most common open-source engines are PostgreSQL, MySQL (and its fork MariaDB) and SQLite; commercial ones include Microsoft SQL Server and Oracle Database.
Get a database and run your first query
The easiest way to start is SQLite, a serverless database stored in a single file. It is preinstalled on macOS and most Linux systems. PostgreSQL and MySQL run as servers and come with the command-line clients psql and mysql.
sqlite3 shop.db # SQLite: opens or creates a file
psql -U postgres -d shop # PostgreSQL client
mysql -u root -p shop # MySQL clientSELECT 'Hello, world!' AS greeting;
-- greeting
-- Hello, world!SQL keywords are case-insensitive. Writing keywords in uppercase and names in lowercase is a common convention that makes queries easier to read. A statement ends with a semicolon, and -- starts a comment.
Create tables: data types and constraints
A table definition lists columns, their types, and constraints that keep the data valid. This page uses two tables, customers and orders, in every example.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
city VARCHAR(50)
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
amount DECIMAL(10, 2) NOT NULL,
ordered_on DATE NOT NULL
);
INSERT INTO customers (id, name, city) VALUES
(1, 'Ann', 'Oslo'), (2, 'Bo', 'Rome'), (3, 'Cy', 'Oslo');
INSERT INTO orders (id, customer_id, amount, ordered_on) VALUES
(1, 1, 30.00, '2026-01-05'),
(2, 1, 45.50, '2026-02-10'),
(3, 2, 12.00, '2026-02-11');| Type | Stores | Notes |
|---|---|---|
INTEGER, BIGINT | whole numbers | use BIGINT for large IDs |
DECIMAL(p, s) | exact decimals | use for money, never FLOAT |
VARCHAR(n), TEXT | strings | TEXT is not in the standard but is widely supported |
DATE, TIMESTAMP | dates and times | write literals as '2026-01-05' |
BOOLEAN | true or false | MySQL treats it as TINYINT(1) |
Constraints include PRIMARY KEY (unique row identifier), NOT NULL, UNIQUE, CHECK (condition) and FOREIGN KEY / REFERENCES (the value must exist in another table). Strings use single quotes; double quotes are for identifiers in standard SQL.
SELECT: filter, sort and limit
SELECT reads data. WHERE filters rows, ORDER BY sorts them, and LIMIT (or the standard FETCH FIRST n ROWS ONLY) restricts how many come back.
SELECT name, city
FROM customers
WHERE city = 'Oslo'
ORDER BY name;
-- Ann | Oslo
-- Cy | Oslo
SELECT id, amount,
CASE WHEN amount >= 40 THEN 'large'
WHEN amount >= 20 THEN 'medium'
ELSE 'small' END AS size
FROM orders
WHERE amount BETWEEN 10 AND 50
ORDER BY amount DESC
LIMIT 2;
-- 2 | 45.50 | large
-- 1 | 30.00 | medium
SELECT name FROM customers WHERE city IS NULL; -- not: city = NULL
SELECT name FROM customers WHERE name LIKE 'A%'; -- AnnJOIN: combine tables
A join matches rows from two tables using a condition, usually a foreign key. INNER JOIN (or just JOIN) keeps only matching rows; LEFT JOIN keeps every row of the left table and fills missing columns with NULL.
SELECT c.name, o.amount
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
ORDER BY o.amount DESC;
-- Ann | 45.50
-- Ann | 30.00
-- Bo | 12.00
SELECT c.name, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL; -- customers with no orders
-- Cy | NULL| Join | Returns |
|---|---|
INNER JOIN | rows that match in both tables |
LEFT JOIN | all left rows, plus matches from the right or NULL |
RIGHT JOIN | all right rows, plus matches from the left or NULL |
FULL OUTER JOIN | all rows from both sides; not supported by MySQL |
CROSS JOIN | every combination of rows (a Cartesian product) |
GROUP BY and aggregate functions
Aggregate functions such as COUNT, SUM, AVG, MIN and MAX summarize many rows into one. GROUP BY makes one result row per group, and HAVING filters groups after aggregation, while WHERE filters rows before it.
SELECT c.name,
COUNT(o.id) AS orders,
COALESCE(SUM(o.amount), 0) AS total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY total DESC;
-- Ann | 2 | 75.50
-- Bo | 1 | 12.00
-- Cy | 0 | 0
SELECT city, COUNT(*) AS n
FROM customers
GROUP BY city
HAVING COUNT(*) > 1;
-- Oslo | 2COUNT(*) counts rows, while COUNT(column) counts only non-null values, which is why Cy gets 0 orders above. COALESCE returns its first non-null argument.
Subqueries, CTEs and window functions
A subquery is a query inside another query. A common table expression (CTE), written with WITH, gives a subquery a name and makes long queries readable. Window functions such as ROW_NUMBER, RANK and SUM(...) OVER compute values across related rows without collapsing them into groups.
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE amount > 20);
-- Ann
WITH totals AS (
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
)
SELECT c.name, t.total,
RANK() OVER (ORDER BY t.total DESC) AS rnk
FROM totals AS t
JOIN customers AS c ON c.id = t.customer_id;
-- Ann | 75.50 | 1
-- Bo | 12.00 | 2CTEs and window functions are supported by PostgreSQL, SQLite 3.25 and later, and MySQL 8.0 and later.
Change data: INSERT, UPDATE, DELETE and transactions
UPDATE and DELETE affect every row that matches the WHERE clause. A transaction groups several statements so that they all succeed or all fail together.
UPDATE customers SET city = 'Paris' WHERE id = 2;
DELETE FROM orders WHERE amount < 15;
BEGIN;
UPDATE orders SET amount = amount - 10 WHERE id = 1;
UPDATE orders SET amount = amount + 10 WHERE id = 2;
COMMIT; -- or ROLLBACK; to undo both
SELECT id, amount FROM orders;
-- 1 | 20.00
-- 2 | 55.50PostgreSQL, MySQL and SQLite differences
The core of SQL is portable, but each engine differs in details. These are the differences beginners meet first.
| Feature | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
| Auto-increment ID | GENERATED ALWAYS AS IDENTITY or SERIAL | AUTO_INCREMENT | INTEGER PRIMARY KEY |
| Quoting names | "name" | backticks around the name | "name" |
| Limit rows | LIMIT n or FETCH FIRST n ROWS ONLY | LIMIT n | LIMIT n |
| String concatenation | a || b | CONCAT(a, b) | a || b |
| Booleans | real BOOLEAN type | TINYINT(1) | stored as 0 and 1 |
| Typing | strict | strict in default strict mode | flexible, unless the table is declared STRICT |
| Upsert | ON CONFLICT ... DO UPDATE | ON DUPLICATE KEY UPDATE | ON CONFLICT ... DO UPDATE |
Other differences include date functions (NOW() in PostgreSQL and MySQL, datetime('now') in SQLite), case-sensitivity of string comparisons, and full outer join support, which MySQL lacks.
Common mistakes and where to go next
- Building queries by concatenating user input, which allows SQL injection. Always use parameterized queries (placeholders such as
?or$1) from your programming language. - Selecting a column in a grouped query that is neither in
GROUP BYnor inside an aggregate function. - Using
FLOATfor money instead ofDECIMAL. - Forgetting indexes on columns used in joins and filters;
CREATE INDEX idx_orders_customer ON orders(customer_id);andEXPLAINhelp here. - Assuming rows come back in a fixed order without
ORDER BY.
For further study, read the official manuals: the PostgreSQL tutorial, the MySQL Reference Manual and the SQLite SQL language reference. Practice on a real dataset by writing questions in plain English and turning each into a query.
FAQ
Is SQL a programming language?
SQL is a declarative query language: you describe the result you want rather than the steps to compute it. Vendor extensions such as PL/pgSQL and T-SQL add variables, loops and procedures.
Which database should a beginner start with?
SQLite needs no server and is ideal for learning. PostgreSQL is a strong choice for learning standard-compliant SQL for real applications, and MySQL is common in web hosting.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping, and HAVING filters groups after aggregate functions are computed.
What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only rows that have a match in both tables. LEFT JOIN returns every row from the left table and fills the right table's columns with NULL when there is no match.
How do I prevent SQL injection?
Never insert user input into SQL text. Use parameterized queries or prepared statements, where the database receives the query and the values separately.