AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 06 — SQL Fundamentals: SELECT, Filter, Modify

06 — SQL Fundamentals: SELECT, Filter, Modify

August 13, 20266 min read
Download as Markdown

Syntax to memorize was how I treated SQL, and it never explained the why behind the rules. The framing that organized it: SQL divides into DDL, which shapes the tables, and DML, which moves the data inside them, and every query follows a fixed logical evaluation order that is not the order I write it in. [1][2] Once the evaluation order landed, the confusing parts — why I can't put an aggregate alias in WHERE, why HAVING exists separately from WHERE — resolved themselves.

DDL: shaping the table

Data Definition Language changes the shape of the database. The core verbs are CREATE TABLE, ALTER TABLE, and DROP TABLE [3]:

CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

ALTER TABLE users ADD COLUMN display_name TEXT;
DROP TABLE users;

Each column gets a type and optional constraints; BIGSERIAL is shorthand for an auto-incrementing bigint primary key. DDL in Postgres is transactional — I can run CREATE TABLE inside a BEGIN/ROLLBACK, which is invaluable for testing migrations.

DML: reading and writing data

Data Manipulation Language is the four-verb core of everyday work [4]:

  • SELECT — read rows. The query I write 95% of the time.
  • INSERT — add rows.
  • UPDATE — change existing rows (writes new versions under MVCC).
  • DELETE — remove rows (flags them dead under MVCC).
INSERT INTO users (email) VALUES ('ave@example.com');
UPDATE users SET display_name = 'Ave' WHERE id = 1;
DELETE FROM users WHERE id = 1;

The evaluation order that clarifies everything

The single insight that unblocked me: SQL's logical evaluation order is not the order I type. A query is evaluated FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT [1]. This is why I can't reference a SELECT alias in WHERE — at the time WHERE runs, the SELECT list hasn't been computed yet.

FROM WHERE GROUP BY HAVING SELECT ORDER BY LIMIT pick tables filter rows bucket rows filter groups project cols sort cap count SELECT aliases are NOT visible to WHERE — WHERE already ran that's why HAVING exists: to filter on aggregates built in GROUP BY

Filtering: WHERE and the NULL trap

WHERE filters rows by a boolean condition [5]. The operators are the usual ones (=, !=, <, >), plus IN, BETWEEN, LIKE/ILIKE for patterns, and IS NULL/IS NOT NULL. The trap, covered in the relational-model notes, is that comparisons with NULL yield unknown, not false — so WHERE status != 'paid' silently drops rows where status is NULL. IS DISTINCT FROM is the NULL-safe inequality.

Postgres also offers a FILTER clause on aggregates, cleaner than a CASE for conditional counts:

SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'paid') AS paid_count
FROM orders;

Grouping: GROUP BY and HAVING

GROUP BY buckets rows by the listed columns, collapsing each bucket into one output row via aggregates (COUNT, SUM, AVG, MAX, MIN) [6]. HAVING filters the buckets — it's WHERE for groups, and it exists precisely because WHERE runs before GROUP BY and can't see aggregates.

SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

This counts orders per user and keeps only users with more than five. Moving the COUNT(*) > 5 condition into WHERE would fail, because at WHERE time the per-user count doesn't exist yet.

Bulk load and unload: COPY

For moving large volumes of data in or out, COPY is dramatically faster than row-by-row INSERT [7]:

COPY users (email, display_name) FROM '/tmp/users.csv' WITH (FORMAT csv, HEADER true);
COPY (SELECT * FROM users WHERE created_at > now() - interval '30 days') TO '/tmp/recent.csv' WITH CSV HEADER;

COPY streams directly between a file and the table, bypassing much of the per-row overhead. The server-side COPY requires the file to be readable by the Postgres process; the \copy variant in psql runs client-side and reads files the local user can see. For initial data loads and exports, this is the tool.

How I use this

The evaluation order is the habit. When a query won't compile or gives a weird result, I rewrite it in evaluation order in my head — what does FROM produce, what survives WHERE, what do the groups look like — and the bug usually surfaces. For any bulk insert, I reach for COPY (or its ORM equivalent) instead of looping INSERTs, because the speed difference is easily two orders of magnitude. And I keep DDL inside transactions when testing migrations, so a bad migration rolls back cleanly. The syntax is memorization; the evaluation order is the model.

References

[1] PostgreSQL Global Development Group, "The SELECT Statement," 2024. [Online]. Available: https://www.postgresql.org/docs/current/sql-select.html

[2] SQLTutorial.org, "SQL Tutorial — Essential SQL for the Beginners," 2024. [Online]. Available: https://www.sqltutorial.org/

[3] PostgreSQL Global Development Group, "CREATE TABLE," 2024. [Online]. Available: https://www.postgresql.org/docs/current/sql-createtable.html

[4] PostgreSQL Global Development Group, "Modifying Data," 2024. [Online]. Available: https://www.postgresql.org/docs/current/sql-insert.html

[5] Prisma, "How to filter query results in PostgreSQL," 2024. [Online]. Available: https://www.prisma.io/dataguide/postgresql/reading-and-querying-data/filtering-data

[6] PostgreSQLTutorial.com, "PostgreSQL GROUP BY," 2024. [Online]. Available: https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-group-by/

[7] PostgreSQL Global Development Group, "COPY," 2024. [Online]. Available: https://www.postgresql.org/docs/current/sql-copy.html

Knowledge check · Question 1 of 5

In what logical order does PostgreSQL evaluate a SELECT query?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!