On this page
SQL is the language you use to ask a database questions. "How many orders came from Pune?" "Which customers spent the most?" You write the question as a short piece of text, the database runs it, and it sends back a table of answers. Almost every data engineering job uses it every day, which is why this is the first skill to learn.
This guide uses one small table. Every query was run in DuckDB, and the output under each query is exactly what it printed, so you can check your own results against it. The same queries work in MySQL, PostgreSQL, Snowflake and BigQuery unless a note says otherwise.
What is a table?
A table is a grid. Each row is one thing (here, one order) and each column is one fact about it (the customer, the city, the amount). Create the table below and fill it with 8 orders. You do not need to understand the CREATE and INSERT lines yet. They just build the data.
CREATE TABLE orders (
order_id INTEGER,
customer VARCHAR,
city VARCHAR,
order_date DATE,
amount INTEGER,
status VARCHAR
);
INSERT INTO orders VALUES
(1, 'Asha', 'Pune', '2026-03-01', 1200, 'paid'),
(2, 'Ravi', 'Mumbai', '2026-03-01', 800, 'paid'),
(3, 'Meena', 'Chennai', '2026-03-02', 450, 'refunded'),
(4, 'Kiran', 'Pune', '2026-03-02', 2500, 'paid'),
(5, 'Asha', 'Pune', '2026-03-03', 300, 'paid'),
(6, 'Divya', 'Bengaluru', '2026-03-03', 1500, 'pending'),
(7, 'Ravi', 'Mumbai', '2026-03-04', 950, 'paid'),
(8, 'Meena', 'Chennai', '2026-03-05', 2200, 'paid');SELECT: choose which columns to see
SELECT * means "show me every column". The star is a shortcut for "all of them". FROM orders says which table to read.
SELECT * FROM orders;order_id | customer | city | order_date | amount | status ---------+----------+-----------+------------+--------+--------- 1 | Asha | Pune | 2026-03-01 | 1200 | paid 2 | Ravi | Mumbai | 2026-03-01 | 800 | paid 3 | Meena | Chennai | 2026-03-02 | 450 | refunded 4 | Kiran | Pune | 2026-03-02 | 2500 | paid 5 | Asha | Pune | 2026-03-03 | 300 | paid 6 | Divya | Bengaluru | 2026-03-03 | 1500 | pending 7 | Ravi | Mumbai | 2026-03-04 | 950 | paid 8 | Meena | Chennai | 2026-03-05 | 2200 | paid (8 rows)
8 rows and 6 columns, the whole table. Usually you want only some columns. Name them, separated by commas:
SELECT customer, amount FROM orders;customer | amount ---------+------- Asha | 1200 Ravi | 800 Meena | 450 Kiran | 2500 Asha | 300 Divya | 1500 Ravi | 950 Meena | 2200 (8 rows)
Now only 2 columns come back. The rows are in no promised order unless you ask for one, which is the next steps.
Pick the table to read.
Keep only the rows that match.
Choose the columns and make any aliases.
Sort what is left.
Keep the first 3 rows.
You write SELECT first, but the database reads it third. That is why WHERE cannot use an alias created in SELECT.
WHERE: choose which rows to see
WHERE keeps only the rows that match a condition. Text goes in single quotes, numbers do not, and a comparison uses one equals sign.
SELECT order_id, customer, amount
FROM orders
WHERE city = 'Pune';order_id | customer | amount ---------+----------+------- 1 | Asha | 1200 4 | Kiran | 2500 5 | Asha | 300 (3 rows)
3 orders are from Pune. You can compare numbers with >, <, >=, <= and <> (not equal):
SELECT order_id, customer, amount
FROM orders
WHERE amount > 1000;order_id | customer | amount ---------+----------+------- 1 | Asha | 1200 4 | Kiran | 2500 6 | Divya | 1500 8 | Meena | 2200 (4 rows)
Join conditions with AND (both must be true) and OR (at least one must be true):
SELECT order_id, customer, city, amount
FROM orders
WHERE city = 'Pune' AND amount > 1000;order_id | customer | city | amount ---------+----------+------+------- 1 | Asha | Pune | 1200 4 | Kiran | Pune | 2500 (2 rows)
Only the orders that are both from Pune and above 1000 survive. IN is a tidy way to say "any of these":
SELECT order_id, customer, city
FROM orders
WHERE city IN ('Mumbai', 'Chennai');order_id | customer | city ---------+----------+-------- 2 | Ravi | Mumbai 3 | Meena | Chennai 7 | Ravi | Mumbai 8 | Meena | Chennai (4 rows)
And LIKE matches patterns in text. A percent sign % stands for "anything":
SELECT DISTINCT customer
FROM orders
WHERE customer LIKE 'M%';customer -------- Meena (1 row)
Names starting with M. DISTINCT removed the repeat, because Meena has 2 orders and you asked for each name once.
ORDER BY and LIMIT: sort the answer and keep the top few
Without ORDER BY, the order of rows is not promised. Ask for what you want:
SELECT customer, amount
FROM orders
ORDER BY amount DESC;customer | amount ---------+------- Kiran | 2500 Meena | 2200 Divya | 1500 Asha | 1200 Ravi | 950 Ravi | 800 Meena | 450 Asha | 300 (8 rows)
DESC means largest first. The default is ASC, smallest first. To keep only the top three, add LIMIT:
SELECT customer, amount
FROM orders
ORDER BY amount DESC
LIMIT 3;customer | amount ---------+------- Kiran | 2500 Meena | 2200 Divya | 1500 (3 rows)
"The three biggest orders" is one of the most common questions people ask a database. You can sort by 2 columns too, and the second one breaks ties in the first:
SELECT city, customer, amount
FROM orders
ORDER BY city, amount DESC;city | customer | amount ----------+----------+------- Bengaluru | Divya | 1500 Chennai | Meena | 2200 Chennai | Meena | 450 Mumbai | Ravi | 950 Mumbai | Ravi | 800 Pune | Kiran | 2500 Pune | Asha | 1200 Pune | Asha | 300 (8 rows)
Cities come out in alphabetical order, and inside Pune the biggest amount comes first.
COUNT, SUM, AVG, MIN and MAX: many rows, one answer
These aggregate functions look at a group of rows and return one number.
SELECT COUNT(*) AS orders_placed,
SUM(amount) AS total_amount,
AVG(amount) AS average_amount,
MIN(amount) AS smallest,
MAX(amount) AS largest
FROM orders;orders_placed | total_amount | average_amount | smallest | largest --------------+--------------+----------------+----------+-------- 8 | 9900 | 1237.5 | 300 | 2500 (1 row)
AS gives a column a friendlier name, called an alias. 1 row came back, because 8 rows were folded into five numbers. Combine an aggregate with WHERE to answer a narrower question:
SELECT COUNT(*) AS paid_orders, SUM(amount) AS paid_total
FROM orders
WHERE status = 'paid';paid_orders | paid_total ------------+----------- 6 | 7950 (1 row)
6 orders were paid. The refunded and pending orders were filtered out before anything was counted, which is exactly the order shown in Figure 1.
Common beginner mistakes
Read these once. They cause most first-week errors.
- Using a double equals. SQL compares with
=, not==. - Forgetting quotes on text.
WHERE city = Punemakes the database look for a column calledPune. Write'Pune'. - Using a column alias in WHERE.
WHERE total > 100fails iftotalis an alias made inSELECT, becauseWHEREruns first, before the alias exists. - Mixing a column and an aggregate.
SELECT customer, SUM(amount)fails without aGROUP BY, which the GROUP BY guide explains. - Trusting the row order. If the order matters, say it with
ORDER BY.
What to learn next
You can now read a table and ask simple questions of it. The next steps, in order:
- GROUP BY and HAVING: one answer per city or per customer, instead of one answer for everything.
- Joins: combine 2 tables, such as orders and customers.
- NULL: what a missing value does to your queries.
Practise right away in the browser with Filter Engineering salaries and Names starting with A. Both are free, and they use exactly the ideas from this guide.