cv.cm

Coding · Programming languages

SQL tutorial for beginners

7 min read

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 client
SELECT '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');
TypeStoresNotes
INTEGER, BIGINTwhole numbersuse BIGINT for large IDs
DECIMAL(p, s)exact decimalsuse for money, never FLOAT
VARCHAR(n), TEXTstringsTEXT is not in the standard but is widely supported
DATE, TIMESTAMPdates and timeswrite literals as '2026-01-05'
BOOLEANtrue or falseMySQL 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%'; -- Ann

JOIN: 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
JoinReturns
INNER JOINrows that match in both tables
LEFT JOINall left rows, plus matches from the right or NULL
RIGHT JOINall right rows, plus matches from the left or NULL
FULL OUTER JOINall rows from both sides; not supported by MySQL
CROSS JOINevery 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 | 2

COUNT(*) 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 | 2

CTEs 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.50

PostgreSQL, MySQL and SQLite differences

The core of SQL is portable, but each engine differs in details. These are the differences beginners meet first.

FeaturePostgreSQLMySQLSQLite
Auto-increment IDGENERATED ALWAYS AS IDENTITY or SERIALAUTO_INCREMENTINTEGER PRIMARY KEY
Quoting names"name"backticks around the name"name"
Limit rowsLIMIT n or FETCH FIRST n ROWS ONLYLIMIT nLIMIT n
String concatenationa || bCONCAT(a, b)a || b
Booleansreal BOOLEAN typeTINYINT(1)stored as 0 and 1
Typingstrictstrict in default strict modeflexible, unless the table is declared STRICT
UpsertON CONFLICT ... DO UPDATEON DUPLICATE KEY UPDATEON 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 BY nor inside an aggregate function.
  • Using FLOAT for money instead of DECIMAL.
  • Forgetting indexes on columns used in joins and filters; CREATE INDEX idx_orders_customer ON orders(customer_id); and EXPLAIN help 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.

All coding tutorials