Skip to content

SELECT, WHERE, ORDER BY

Chapter 2 13 code blocks 3870 words

Everything an analyst does in SQL is a variation on one statement. Even a 200-line report query with six JOINs and three window functions is still, structurally, SELECT ... FROM ... WHERE ... ORDER BY .... So it pays to know these four clauses extremely well before adding anything on top.

Every query below runs on the school.db from Chapter 1.

Choosing columns

SELECT answers the question "which columns do you want to see?" — and the columns come out in the order you name them, not in the order they sit in the table.

python8 lines
-- all columns (exploring only)
SELECT * FROM students;

-- named columns, in the order YOU choose
SELECT last_name, first_name, city FROM students;

-- a column can appear more than once, and constants are allowed
SELECT first_name, first_name, 'student' FROM students;

Result 1

student_idfirst_namelast_namegendercitybirth_dateenrolled_date
1ShahnozaAlimovaFBukhara1998-10-102025-09-03
2DostonUmarovMFergana1999-11-232025-02-13
3ZulfiyaYusupovaFSamarkand2002-07-302025-02-13
4KamolaKarimovaFSamarkand2002-08-142025-02-05
5GulnoraSaidovaFNamangan1998-03-212025-09-07
6BekzodYusupovMSamarkand2002-02-212025-09-07
7DostonAlimovMKarshi2004-02-082025-09-10
8MadinaRahimovaFTashkent2000-05-292025-02-09
9SardorAlimovMBukhara2003-05-082025-02-04
10DilnozaAlimovaFAndijan2000-08-262025-09-08
11JasurErgashevMKarshi1999-08-052025-09-07
12RustamToshevMTashkent2001-02-122025-09-08
13BekzodQodirovMNamangan2002-05-052025-02-08
14OtabekSaidovMAndijan2000-10-282025-02-09
15MadinaUmarovaFNamangan2001-12-262025-02-05

… first 15 of 300 rows

300 rows

Result 2

last_namefirst_namecity
AlimovaShahnozaBukhara
UmarovDostonFergana
YusupovaZulfiyaSamarkand
KarimovaKamolaSamarkand
SaidovaGulnoraNamangan
YusupovBekzodSamarkand
AlimovDostonKarshi
RahimovaMadinaTashkent
AlimovSardorBukhara
AlimovaDilnozaAndijan
ErgashevJasurKarshi
ToshevRustamTashkent
QodirovBekzodNamangan
SaidovOtabekAndijan
UmarovaMadinaNamangan

… first 15 of 300 rows

300 rows

Result 3

first_namefirst_name'student'
ShahnozaShahnozastudent
DostonDostonstudent
ZulfiyaZulfiyastudent
KamolaKamolastudent
GulnoraGulnorastudent
BekzodBekzodstudent
DostonDostonstudent
MadinaMadinastudent
SardorSardorstudent
DilnozaDilnozastudent
JasurJasurstudent
RustamRustamstudent
BekzodBekzodstudent
OtabekOtabekstudent
MadinaMadinastudent

… first 15 of 300 rows

300 rows

2 ms

That last one looks pointless, but it teaches an important idea: SELECT builds a new result table. It does not "show you the table" — it constructs a fresh one, column by column, from whatever expressions you write.

📌 A query returns a table, always

The input is a table, the output is a table. That single fact is why a query can be nested inside another query (Chapter 6), and why a query can be joined to another query (Chapter 5). Say it early and repeat it often.

Computed columns (expressions)

Anything you can compute, you can put in SELECT. The result becomes a new column that exists only in the output — nothing is written back to the table.

sql7 lines
SELECT
    title,
    price,
    price * 0.9              AS discounted_price,
    price / credits          AS price_per_credit,
    credits * 30             AS total_hours
FROM courses;
titlepricediscounted_priceprice_per_credittotal_hours
Python Basics1 200 0001 080 000300 000120
SQL for Analytics800 000720 000266 66690
Statistics 1011 200 0001 080 000300 000120
Data Visualization900 000810 000300 00090
Machine Learning1 450 0001 305 000290 000150
Web Development1 150 0001 035 000287 500120
Excel for Business1 100 000990 000550 00060
Project Management1 500 0001 350 000500 00090
UX Design850 000765 000283 33390
Graphic Design950 000855 000316 66690
Linear Algebra1 200 0001 080 000300 000120
Deep Learning1 550 0001 395 000310 000150
Java Basics1 150 0001 035 000287 500120
Financial Analysis1 500 0001 350 000500 00090
Probability1 250 0001 125 000312 500120

15 rows

0 ms

:::gotcha ⚠️ Look at Data Visualization: 316666, not 316666.67 950000 / 3 gives 316666 because integer ÷ integer = integer in SQL — the decimal part is thrown away, not rounded. This silently corrupts averages and percentages. Force a decimal by multiplying by 1.0: price * 1.0 / credits, or cast: CAST(price AS REAL) / credits. We come back to this in Chapter 4, where it wrecks a percentage calculation. :::

Joining text together

In SQLite and PostgreSQL, || glues strings:

sql7 lines
SELECT
    first_name || ' ' || last_name        AS full_name,
    last_name  || ', ' || first_name      AS sorted_name,
    UPPER(city)                           AS city_upper,
    LENGTH(first_name)                    AS name_length
FROM students
LIMIT 4;
full_namesorted_namecity_uppername_length
Shahnoza AlimovaAlimova, ShahnozaBUKHARA8
Doston UmarovUmarov, DostonFERGANA6
Zulfiya YusupovaYusupova, ZulfiyaSAMARKAND7
Kamola KarimovaKarimova, KamolaSAMARKAND6

4 rows

0 ms

:::gotcha ⚠️ NULL swallows the whole string For Sevara, city is NULL, so UPPER(city) is NULL — and if you wrote city || ', Uzbekistan' the entire result would be NULL, not ', Uzbekistan'. Wrap it: COALESCE(city, 'unknown') || ', Uzbekistan'. Chapter 3 covers COALESCE properly. :::

Aliases (AS)

An alias renames a column (or a table) in the output. Without one, the header of a computed column is the raw expression — unreadable in a report.

python11 lines
-- header becomes: price * 0.9        ← ugly
SELECT title, price * 0.9 FROM courses;

-- header becomes: discounted_price   ← readable
SELECT title, price * 0.9 AS discounted_price FROM courses;

-- AS is optional, but write it anyway — it is far easier to read
SELECT title, price * 0.9 discounted_price FROM courses;

-- a name with a space needs double quotes (identifier quoting)
SELECT title AS "Course Title", price AS "Price (UZS)" FROM courses;

Result 1

titleprice * 0.9
Python Basics1 080 000
SQL for Analytics720 000
Statistics 1011 080 000
Data Visualization810 000
Machine Learning1 305 000
Web Development1 035 000
Excel for Business990 000
Project Management1 350 000
UX Design765 000
Graphic Design855 000
Linear Algebra1 080 000
Deep Learning1 395 000
Java Basics1 035 000
Financial Analysis1 350 000
Probability1 125 000

15 rows

Result 2

titlediscounted_price
Python Basics1 080 000
SQL for Analytics720 000
Statistics 1011 080 000
Data Visualization810 000
Machine Learning1 305 000
Web Development1 035 000
Excel for Business990 000
Project Management1 350 000
UX Design765 000
Graphic Design855 000
Linear Algebra1 080 000
Deep Learning1 395 000
Java Basics1 035 000
Financial Analysis1 350 000
Probability1 125 000

15 rows

Result 3

titlediscounted_price
Python Basics1 080 000
SQL for Analytics720 000
Statistics 1011 080 000
Data Visualization810 000
Machine Learning1 305 000
Web Development1 035 000
Excel for Business990 000
Project Management1 350 000
UX Design765 000
Graphic Design855 000
Linear Algebra1 080 000
Deep Learning1 395 000
Java Basics1 035 000
Financial Analysis1 350 000
Probability1 125 000

15 rows

Result 4

Course TitlePrice (UZS)
Python Basics1 200 000
SQL for Analytics800 000
Statistics 1011 200 000
Data Visualization900 000
Machine Learning1 450 000
Web Development1 150 000
Excel for Business1 100 000
Project Management1 500 000
UX Design850 000
Graphic Design950 000
Linear Algebra1 200 000
Deep Learning1 550 000
Java Basics1 150 000
Financial Analysis1 500 000
Probability1 250 000

15 rows

1 ms

Table aliases Tables get aliases too. They become essential from Chapter 5 onward, when two tables both have a student_id column and you must say which one you mean.

sql3 lines
SELECT s.first_name, s.city
FROM students AS s
WHERE s.city = 'Tashkent';
first_namecity
MadinaTashkent
RustamTashkent
AzizaTashkent
AzizaTashkent
ZulfiyaTashkent
GulnoraTashkent
DilnozaTashkent
AzizTashkent
AzizaTashkent
MalikaTashkent
ShohruxTashkent
NilufarTashkent
OtabekTashkent
SardorTashkent
GulnoraTashkent

… first 15 of 82 rows

82 rows

0 ms

:::tip 💡 Alias naming Short but meaningful: s for students, e for enrollments, c for courses. Avoid a, b, t1, t2 — six months later nobody knows what t2 was. :::

⚠️ Remember from Chapter 1

An alias is created at step 5 (SELECT), so ORDER BY (step 7) can use it but WHERE (step 2) cannot. This is the single most common "no such column" error beginners hit.

WHERE and comparison operators

WHERE keeps a row only when the condition evaluates to TRUE. FALSE rows are dropped — and so are UNKNOWN rows, which is the NULL problem from Chapter 1.

Operator Meaning Example
= Equal WHERE city = 'Tashkent'
<> or != Not equal WHERE status <> 'active'
> < >= <= Comparison WHERE grade >= 80
BETWEEN a AND b Range, both ends included WHERE grade BETWEEN 80 AND 90
IN (...) Matches any value in a list WHERE city IN ('Tashkent','Bukhara')
LIKE Pattern match on text WHERE last_name LIKE '%ova'
IS NULL / IS NOT NULL Tests for a missing value WHERE grade IS NULL
matn4 lines
-- "Which enrollments scored below 60?"
SELECT enrollment_id, student_id, course_id, grade, status
FROM enrollments
WHERE grade < 60;
enrollment_idstudent_idcourse_idgradestatus
17751552failed
202741055failed
2633856failed
47179459failed
5393346failed
792111453failed
82212558failed
89217556failed
116235857failed
15030352failed
15356351failed
1721221555failed
17927348failed
205841552failed
222681557failed

… first 15 of 192 rows

192 rows

1 ms

⚠️ The `<>` trap that loses rows

SELECT * FROM students WHERE city <> 'Tashkent'; returns 6 rows, not 7. Sevara, whose city is NULL, disappears — because NULL <> 'Tashkent' is UNKNOWN, not TRUE. If you want her included, you must say so explicitly:

WHERE city <> 'Tashkent' OR city IS NULL; This is one of the most common causes of a silently wrong report, and it is worth showing students the row count both ways.

AND, OR, NOT — and precedence

python11 lines
-- both conditions must hold
SELECT first_name, city, gender FROM students
WHERE city = 'Tashkent' AND gender = 'M';        -- Jasur, Otabek

-- either condition is enough
SELECT first_name, city FROM students
WHERE city = 'Bukhara' OR city = 'Fergana';      -- Dilnoza, Ulugbek

-- negation
SELECT first_name, status FROM enrollments
WHERE NOT status = 'completed';                  -- careful: NULL statuses would vanish

:::gotcha ⚠️ AND binds tighter than OR Exactly like × binds tighter than + in arithmetic. These two queries are not the same:

python7 lines
-- reads as: Tashkent, OR (Samarkand AND female)
-- returns 3 Tashkent students (both genders) + Zarina = 4 rows
WHERE city = 'Tashkent' OR city = 'Samarkand' AND gender = 'F';

-- what was actually meant: (Tashkent OR Samarkand) AND female
-- returns Aziza + Zarina = 2 rows
WHERE (city = 'Tashkent' OR city = 'Samarkand') AND gender = 'F';

Rule for students: the moment a condition mixes AND with OR, put brackets in. Even where the brackets are redundant, they cost nothing and remove all doubt. :::

ORDER BY

Rows in a table have no order, so if the output order matters you must ask for it. ASC (ascending) is the default; DESC reverses it.

matn15 lines
-- one column
SELECT first_name, enrolled_date FROM students
ORDER BY enrolled_date DESC;

-- several columns: sort by city, then by name inside each city
SELECT city, first_name FROM students
ORDER BY city ASC, first_name DESC;

-- you may sort by an expression, or by an alias
SELECT title, price / credits AS per_credit FROM courses
ORDER BY per_credit DESC;

-- you may even sort by a column you did NOT select
SELECT first_name FROM students
ORDER BY birth_date;

Result 1

first_nameenrolled_date
Zulfiya2025-09-24
Sardor2025-09-22
Nilufar2025-09-21
Otabek2025-09-19
Shohrux2025-09-19
Bekzod2025-09-17
Sevara2025-09-16
Bekzod2025-09-16
Doston2025-09-15
Doston2025-09-14
Jasur2025-09-13
Shahnoza2025-09-13
Dilnoza2025-09-13
Shohrux2025-09-12
Bekzod2025-09-12

… first 15 of 300 rows

300 rows

Result 2

cityfirst_name
NoneZulfiya
NoneUlug'bek
NoneShohrux
NoneMadina
NoneJasur
NoneJasur
NoneDoston
NoneAkmal
AndijanUlug'bek
AndijanUlug'bek
AndijanUlug'bek
AndijanSevara
AndijanSevara
AndijanRustam
AndijanOtabek

… first 15 of 300 rows

300 rows

Result 3

titleper_credit
Excel for Business550 000
Project Management500 000
Financial Analysis500 000
Graphic Design316 666
Probability312 500
Deep Learning310 000
Python Basics300 000
Statistics 101300 000
Data Visualization300 000
Linear Algebra300 000
Machine Learning290 000
Web Development287 500
Java Basics287 500
UX Design283 333
SQL for Analytics266 666

15 rows

Result 4

first_name
Ulug'bek
Gulnora
Aziz
Shohrux
Kamola
Kamola
Zulfiya
Doston
Doston
Aziz
Malika
Shahnoza
Otabek
Zulfiya
Jasur

… first 15 of 300 rows

300 rows

1 ms

:::gotcha ⚠️ Where do NULLs sort? ORDER BY city on our students puts Sevara (NULL) first in SQLite and MySQL, because they treat NULL as smaller than everything. PostgreSQL and Oracle put NULLs last by default. If the position matters, be explicit — PostgreSQL supports ORDER BY city NULLS LAST; a portable trick that works everywhere is:

python1 lines
ORDER BY (city IS NULL), city;   -- 0 sorts before 1, so NULLs go last

:::

⚠️ Don't sort by column number

ORDER BY 2 means "sort by the second selected column". It works, but the day somebody inserts a column into the SELECT list, the query silently starts sorting by something else. Name the column.

LIMIT and OFFSET

LIMIT caps how many rows come back. It is the last step of all, so it applies after sorting.

matn6 lines
-- top 3 grades
SELECT enrollment_id, student_id, grade
FROM enrollments
WHERE grade IS NOT NULL
ORDER BY grade DESC
LIMIT 3;
enrollment_idstudent_idgrade
135247100
322125100
500114100

3 rows

0 ms

enrollment_idstudent_idgrade

136100 6395 1192

OFFSET skips rows first — this is how pagination works:

sql3 lines
SELECT first_name FROM students ORDER BY student_id LIMIT 3;             -- page 1
SELECT first_name FROM students ORDER BY student_id LIMIT 3 OFFSET 3;   -- page 2
SELECT first_name FROM students ORDER BY student_id LIMIT 3 OFFSET 6;   -- page 3

Result 1

first_name
Shahnoza
Doston
Zulfiya

3 rows

Result 2

first_name
Kamola
Gulnora
Bekzod

3 rows

Result 3

first_name
Doston
Madina
Sardor

3 rows

1 ms

:::gotcha ⚠️ LIMIT without ORDER BY is meaningless LIMIT 10 alone means "any 10 rows the database happens to hand me" — and that set can change between runs. LIMIT is only meaningful with ORDER BY. The same applies to pagination: without a stable sort, page 2 can repeat rows from page 1. :::

💡 Habit worth teaching

On an unfamiliar table, always start with SELECT * FROM big_table LIMIT 5;. You see the column names and the shape of the data instantly, and you do not pull ten million rows into your session by accident.

DISTINCT

DISTINCT removes duplicate rows from the result. It is the fastest way to answer "what values actually occur in this column?"

SELECT DISTINCT city FROM students ORDER BY city;

city

NULL Andijan Bukhara Fergana Namangan Samarkand Tashkent

Seven rows from ten students. Note that DISTINCT keeps NULL as one of the values — it treats all NULLs as duplicates of each other, even though NULL = NULL is UNKNOWN. This is an inconsistency in SQL itself; just tell students it is a special case.

⚠️ DISTINCT applies to the whole row, not one column

Students read SELECT DISTINCT city, gender FROM students as "distinct cities, plus gender". It is not. It returns every distinct combination of city and gender — Tashkent appears twice, once with M and once with F. There is no way to make DISTINCT apply to a single column while returning others; that job belongs to GROUP BY (Chapter 4) or a window function (Chapter 7).

💡 DISTINCT is not free

To remove duplicates the database must sort or hash the entire result. On a large table that is expensive. If you find yourself adding DISTINCT to make a JOIN's duplicate rows go away, the JOIN is usually the real bug — see Chapter 5.

Practice set

Give these to students in order; each one adds exactly one new idea.

python37 lines
-- 1. Female students, alphabetically by last name
SELECT first_name, last_name FROM students
WHERE gender = 'F'
ORDER BY last_name;

-- 2. Courses costing more than 1,300,000, most expensive first
SELECT title, price FROM courses
WHERE price > 1300000
ORDER BY price DESC;

-- 3. Full name + city, unknown city shown as 'unknown'
SELECT first_name || ' ' || last_name AS full_name,
       COALESCE(city, 'unknown')      AS city
FROM students;

-- 4. The 3 lowest grades that were actually given
SELECT enrollment_id, grade FROM enrollments
WHERE grade IS NOT NULL
ORDER BY grade ASC
LIMIT 3;

-- 5. Which statuses exist in enrollments?
SELECT DISTINCT status FROM enrollments;

-- 6. Data-category courses with 4 or more credits
SELECT title, credits FROM courses
WHERE category = 'Data' AND credits >= 4
ORDER BY credits DESC, title;

-- 7. Students from Tashkent or Samarkand who are female (brackets!)
SELECT first_name, city FROM students
WHERE (city = 'Tashkent' OR city = 'Samarkand') AND gender = 'F';

-- 8. Course price per credit, as a proper decimal
SELECT title, ROUND(price * 1.0 / credits, 2) AS per_credit
FROM courses
ORDER BY per_credit DESC;

Result 1

first_namelast_name
ShahnozaAlimova
DilnozaAlimova
AzizaAlimova
GulnoraAlimova
SevaraAlimova
NilufarAlimova
KamolaAlimova
NilufarAlimova
ZulfiyaAlimova
ShahnozaAlimova
GulnoraAlimova
SevaraAlimova
ZulfiyaAlimova
NilufarAlimova
SevaraErgasheva

… first 15 of 154 rows

154 rows

Result 2

titleprice
Deep Learning1 550 000
Project Management1 500 000
Financial Analysis1 500 000
Machine Learning1 450 000

4 rows

Result 3

full_namecity
Shahnoza AlimovaBukhara
Doston UmarovFergana
Zulfiya YusupovaSamarkand
Kamola KarimovaSamarkand
Gulnora SaidovaNamangan
Bekzod YusupovSamarkand
Doston AlimovKarshi
Madina RahimovaTashkent
Sardor AlimovBukhara
Dilnoza AlimovaAndijan
Jasur ErgashevKarshi
Rustam ToshevTashkent
Bekzod QodirovNamangan
Otabek SaidovAndijan
Madina UmarovaNamangan

… first 15 of 300 rows

300 rows

Result 4

enrollment_idgrade
25638
55038
1 43041

3 rows

Result 5

status
completed
active
failed
dropped

4 rows

Result 6

titlecredits
Deep Learning5
Machine Learning5

2 rows

Result 7

first_namecity
ZulfiyaSamarkand
KamolaSamarkand
MadinaTashkent
ShahnozaSamarkand
AzizaTashkent
AzizaTashkent
GulnoraSamarkand
ZulfiyaTashkent
GulnoraTashkent
DilnozaTashkent
AzizaTashkent
GulnoraSamarkand
MalikaTashkent
NilufarTashkent
MalikaSamarkand

… first 15 of 67 rows

67 rows

Result 8

titleper_credit
Excel for Business550 000
Project Management500 000
Financial Analysis500 000
Graphic Design316 666.67
Probability312 500
Deep Learning310 000
Python Basics300 000
Statistics 101300 000
Data Visualization300 000
Linear Algebra300 000
Machine Learning290 000
Web Development287 500
Java Basics287 500
UX Design283 333.33
SQL for Analytics266 666.67

15 rows

2 ms

Interview questions

Q: Can you use a column alias in WHERE?

No. WHERE runs at step 2 and SELECT — where the alias is created — runs at step 5. Repeat the expression in WHERE, or wrap the query in a CTE/subquery. ORDER BY (step 7) can use the alias.

Q: What is the difference between DISTINCT and GROUP BY?

For simply listing unique values they give the same answer, and most engines execute them identically. GROUP BY is more powerful: it can compute aggregates per group (COUNT, AVG) and be filtered with HAVING. Use DISTINCT when you only want unique rows; use GROUP BY when you want something calculated per group.

Q: Does DISTINCT apply to one column or all of them?

All the selected columns together. SELECT DISTINCT a, b returns unique pairs of (a, b).

Q: WHERE city != 'Tashkent' — will rows with a NULL city be returned?

No, and this is the classic trick question. NULL != 'Tashkent' is UNKNOWN, and WHERE keeps only TRUE. Write WHERE city != 'Tashkent' OR city IS NULL.

Q: What does LIMIT 10 return without ORDER BY?

Ten arbitrary rows — whichever ones the engine produced first. It is not guaranteed to be stable between runs, so it is only safe for eyeballing data, never for reporting.

Q: Why does SELECT 7/2 return 3?

Integer ÷ integer = integer; the fractional part is truncated, not rounded. Use 7 * 1.0 / 2 or CAST(7 AS REAL) / 2.

Q: What is the difference between ORDER BY name and ORDER BY 2?

The second sorts by the second column in the SELECT list, by position. It works but is fragile: editing the SELECT list silently changes the sort.

Summary

  • SELECT builds a new table: columns come out in the order you name them, and can be expressions, constants or concatenations.
  • Integer division truncates — price / credits loses the decimals. Multiply by 1.0 or CAST.
  • Aliases (AS) rename output columns; they are usable in ORDER BY but never in WHERE.
  • WHERE keeps only rows that are TRUE. NULL rows are UNKNOWN, so <> silently drops them — add OR col IS NULL when you need them.
  • AND binds tighter than OR. Mixing them without brackets is a bug waiting to happen.
  • ORDER BY is the only way to guarantee row order; NULL placement differs by database.
  • LIMIT is meaningless without ORDER BY; LIMIT ... OFFSET ... is pagination.
  • DISTINCT applies to the whole selected row and keeps NULL as one value.

Next: Chapter 3 goes deeper into filtering — IN, BETWEEN, LIKE patterns, and the NULL traps that quietly break real reports.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents