Querying with SELECT
Reading data with SQL: choosing columns, filtering rows with WHERE, sorting with ORDER BY, and limiting and de-duplicating results.
SELECT and FROM
Reading data is the most common thing you will do with a database,
and it always uses one statement. A
SELECT statement asks the
database for a result table: you list the columns you want after
SELECT and the table they come from after
FROM. A * means "all columns":
SELECT *
FROM products
ORDER BY product_id;
product_id name category price stock ---------- ------------------------ ----------- ----- ----- 1 USB-C cable 2 m Cables 9 120 2 HDMI cable 3 m Cables 12 80 3 Wireless mouse Peripherals 25 45 4 Mechanical keyboard Peripherals 89 12 5 27-inch monitor Displays 249 7 6 Laptop stand Accessories 35 30 7 USB-C hub Accessories 45 25 8 Noise-cancelling headset Audio 129 9 9 Webcam 1080p Peripherals 59 0 10 Ethernet cable 5 m Cables 7 200
(ORDER BY sorts the result; it is explained below and
used in every example so that the order is predictable.) Usually you
name the columns you need, in the order you want them:
SELECT first_name, last_name, country
FROM customers
ORDER BY customer_id;
first_name last_name country ---------- --------- ------- Sanne de Vries NL Kwame Mensah NL Mei Chen NL Omar Haddad BE Lena Fischer DE Priya Patel NL Tom Jansen NL Ana Silva PT
A few rules of SQL's syntax: keywords are not case-sensitive (select
works too, capitals are a readability convention), line breaks and
indentation are free, a statement ends with a semicolon, and
-- starts a comment that runs to the end of the line.
Expressions and column aliases
A SELECT list can contain calculations as well as
columns. The database evaluates the expression for every row. To
give the result a readable name, add a
column alias with
AS; without one, the column would be called
price * stock. Here is the value of the stock of each
product:
SELECT name, price, stock, price * stock AS stock_value
FROM products
ORDER BY stock_value DESC, name;
name price stock stock_value ------------------------ ----- ----- ----------- 27-inch monitor 249 7 1743 Ethernet cable 5 m 7 200 1400 Noise-cancelling headset 129 9 1161 USB-C hub 45 25 1125 Wireless mouse 25 45 1125 USB-C cable 2 m 9 120 1080 Mechanical keyboard 89 12 1068 Laptop stand 35 30 1050 HDMI cable 3 m 12 80 960 Webcam 1080p 59 0 0
The alias exists only in the result; the table is not changed. The
word AS is optional in most products (price * stock stock_value
also works), but writing it makes the intention clear. An alias that
contains spaces needs double quotes: AS "stock value".
Functions for text, numbers and dates are covered in
Filtering, NULL and Functions.
Filtering rows with WHERE
The WHERE clause keeps only the rows for which a condition is true. The database tests the condition on each row of the table; rows that fail it are left out of the result.
| Operator | Meaning | Example |
|---|---|---|
= |
Equal to (one =, not two) |
country = 'NL' |
<> or != |
Not equal to (<> is the standard form)
|
status <> 'delivered' |
<, > |
Less than, greater than | price < 20 |
<=, >= |
Less or equal, greater or equal | signup_date >= '2026-03-01' |
SELECT name, price
FROM products
WHERE price < 20
ORDER BY price;
SELECT first_name, signup_date
FROM customers
WHERE signup_date >= '2026-03-01'
ORDER BY signup_date;
SELECT order_id, status
FROM orders
WHERE status <> 'delivered'
ORDER BY order_id;
name price ------------------ ----- Ethernet cable 5 m 7 USB-C cable 2 m 9 HDMI cable 3 m 12 first_name signup_date ---------- ----------- Priya 2026-03-05 Tom 2026-04-11 Ana 2026-05-30 order_id status -------- --------- 104 shipped 105 cancelled 107 shipped 108 pending 110 pending
Text values go between single quotes. Double quotes
are for names of columns and tables, so
WHERE status = "shipped" means something different in
standard SQL (and fails in PostgreSQL). Comparing text with
= is case-sensitive in SQLite and PostgreSQL:
'cables' does not match 'Cables'. MySQL
and SQL Server usually compare case-insensitively, depending on the
column's collation (its rules for comparing and sorting text). The
date comparison works because the shop stores dates as ISO 8601 text
(YYYY-MM-DD), which sorts in date order.
Combining conditions: AND, OR and NOT
AND requires both conditions to be true,
OR at least one, and NOT reverses a
condition. They follow the rules of
Boolean logic, with
one twist for missing values that is covered in
Three-valued logic.
SELECT first_name, city
FROM customers
WHERE NOT country = 'NL'
ORDER BY customer_id;
first_name city ---------- ------- Omar Antwerp Lena Cologne Ana Lisbon
The precedence trap
Suppose the warehouse asks for "cables and peripherals with fewer than 50 in stock". The obvious translation gives the wrong answer:
-- Wrong: AND is evaluated before OR
SELECT name, category, stock
FROM products
WHERE category = 'Cables' OR category = 'Peripherals' AND stock < 50
ORDER BY product_id;
name category stock ------------------- ----------- ----- USB-C cable 2 m Cables 120 HDMI cable 3 m Cables 80 Wireless mouse Peripherals 45 Mechanical keyboard Peripherals 12 Webcam 1080p Peripherals 0 Ethernet cable 5 m Cables 200
Cables with 120 and 200 in stock? AND has a higher
precedence than OR, just as multiplication comes before
addition, so the database read the condition as
category = 'Cables' OR (category = 'Peripherals' AND stock <
50): all cables, plus peripherals with low stock. Parentheses say what
was meant:
SELECT name, category, stock
FROM products
WHERE (category = 'Cables' OR category = 'Peripherals') AND stock < 50
ORDER BY product_id;
name category stock ------------------- ----------- ----- Wireless mouse Peripherals 45 Mechanical keyboard Peripherals 12 Webcam 1080p Peripherals 0
Sorting with ORDER BY
A table has no built-in order, and neither does a query result:
without an explicit sort, the database returns rows in whatever
order is fastest, which can change when data or indexes change.
ORDER BY sorts the result:
ASC (ascending, the default) or DESC
(descending). With several columns, the second decides only between
rows that are equal in the first:
SELECT category, name, price
FROM products
ORDER BY category, price DESC;
category name price ----------- ------------------------ ----- Accessories USB-C hub 45 Accessories Laptop stand 35 Audio Noise-cancelling headset 129 Cables HDMI cable 3 m 12 Cables USB-C cable 2 m 9 Cables Ethernet cable 5 m 7 Displays 27-inch monitor 249 Peripherals Mechanical keyboard 89 Peripherals Webcam 1080p 59 Peripherals Wireless mouse 25
DESC applies only to the column right before it:
categories are sorted A to Z, prices within each category from high
to low. You can sort by a column alias, as the stock value example
above did with ORDER BY stock_value DESC, name; the
second column there breaks the tie between the two products worth
1125.
LIMIT and OFFSET
LIMIT n returns at most n rows;
OFFSET m skips the first m rows first.
Together with ORDER BY they answer "top 3" questions
and split long lists into pages:
-- The three most expensive products
SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 3;
-- The next three ("page 2")
SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 3 OFFSET 3;
name price ------------------------ ----- 27-inch monitor 249 Noise-cancelling headset 129 Mechanical keyboard 89 name price ------------ ----- Webcam 1080p 59 USB-C hub 45 Laptop stand 35
LIMIT and OFFSET work in SQLite,
PostgreSQL and MySQL. SQL Server uses TOP, or the
standard OFFSET … FETCH clause (which requires
an ORDER BY):
-- SQL Server
SELECT TOP (3) name, price
FROM products
ORDER BY price DESC;
-- SQL Server, and standard SQL (PostgreSQL, Oracle 12c and later, Db2)
SELECT name, price
FROM products
ORDER BY price DESC
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;
The standard also allows
FETCH FIRST 3 ROWS ONLY without an offset; PostgreSQL
and Oracle accept it, SQL Server needs the
OFFSET 0 ROWS part.
Removing duplicates with DISTINCT
A query returns one result row per table row, so the same value can
appear many times. DISTINCT, written
right after SELECT, removes duplicate result rows:
SELECT country
FROM customers
ORDER BY country;
SELECT DISTINCT country
FROM customers
ORDER BY country;
country ------- BE DE NL NL NL NL NL PT country ------- BE DE NL PT
DISTINCT applies to the whole result row, not to the
first column only. With two columns, each unique
combination appears once. Missing values count as equal to
each other here, so the one customer without a city gives one
NULL row:
SELECT DISTINCT city, country
FROM customers
ORDER BY country, city;
city country --------- ------- Antwerp BE Cologne DE NULL NL Amsterdam NL Rotterdam NL Utrecht NL Lisbon PT
To count how many customers each country has, rather than just
listing the countries, use GROUP BY (see
Aggregation and Grouping).
The logical order of evaluation
SQL is written in one order and evaluated, logically, in another. Knowing the second order explains many error messages:
| Step | Clause | What happens |
|---|---|---|
| 1 | FROM |
Take the rows of the table (or joined tables) |
| 2 | WHERE |
Keep the rows for which the condition is true |
| 3 | GROUP BY, HAVING |
Form and filter groups, if the query has them (see Aggregation and Grouping) |
| 4 | SELECT |
Compute the result columns and their aliases; then
DISTINCT removes duplicates
|
| 5 | ORDER BY |
Sort the result (aliases exist now) |
| 6 | LIMIT / OFFSET |
Cut out the requested rows |
The database may execute the query differently internally (it can use an index to avoid sorting, for instance), but the result is always as if it followed these steps.
Why an alias does not belong in WHERE
WHERE (step 2) runs before SELECT (step 4)
has created the alias, so in standard SQL a column alias cannot be
used in WHERE, only in ORDER BY. Repeat
the expression instead. This version works in every product:
SELECT name, price * stock AS stock_value
FROM products
WHERE price * stock > 1150
ORDER BY stock_value DESC;
name stock_value ------------------------ ----------- 27-inch monitor 1743 Ethernet cable 5 m 1400 Noise-cancelling headset 1161
Writing WHERE stock_value > 1150 fails in PostgreSQL
(column "stock_value" does not exist), in SQL Server
(Invalid column name 'stock_value') and in MySQL (Unknown column 'stock_value' in 'where clause'). SQLite is the exception: as a non-standard extension it looks up
an unknown name in WHERE among the result column
aliases, so this runs and returns the same three rows:
-- Works in SQLite only
SELECT name, price * stock AS stock_value
FROM products
WHERE stock_value > 1150
ORDER BY stock_value DESC;
name stock_value ------------------------ ----------- 27-inch monitor 1743 Ethernet cable 5 m 1400 Noise-cancelling headset 1161
Do not rely on it. Apart from not being portable, it has a trap:
when an alias has the same name as a real column, SQLite's
WHERE uses the column. Below, the result
column called price holds double the price, but the
filter still tests the original price (249 and 129 are over 100; the
keyboard's 89 would double to 178, yet it is not included):
SELECT name, price * 2 AS price
FROM products
WHERE price > 100
ORDER BY name;
name price ------------------------ ----- 27-inch monitor 498 Noise-cancelling headset 258