The short version
- You say what, the engine says how. Two queries for the same rows can run at very different speeds.
- Five families, plus admin. DDL, DML, DQL, DCL and TCL say if a ticket is a change window, a privilege call or a plan review.
- A SELECT runs out of order. FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. WHERE never heard of your alias.
- MySQL DDL commits. CREATE, ALTER, DROP and TRUNCATE end your transaction; PostgreSQL can roll them back.
- One road, one stop per problem. Syntax errors at the parser, slow plans at the optimizer, lock waits in storage.
- Dialects differ in small things. Quoting, concatenation, generated ids, case sensitivity.
The five families of statements
Every statement belongs to a family, and the family tells you what it touches: structure, rows, permissions or the transaction.
| Family | Stands for | For | Statements |
|---|---|---|---|
| DDL | Data Definition Language | Structure: databases, tables, indexes, views | CREATE, ALTER, DROP, TRUNCATE, RENAME |
| DML | Data Manipulation Language | Add, change, remove rows | INSERT, UPDATE, DELETE, upserts |
| DQL | Data Query Language | Read rows | SELECT, UNION, INTERSECT, EXCEPT |
| DCL | Data Control Language | Who may do what | GRANT, REVOKE, CREATE USER, CREATE ROLE |
| TCL | Transaction Control Language | All-or-nothing units of work | START TRANSACTION / BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Admin | Administrative and utility | Steer the server itself | SHOW, EXPLAIN, SET, FLUSH, KILL (MySQL); VACUUM, ANALYZE, REINDEX (PostgreSQL) |
DQL is one statement, SELECT (MySQL added INTERSECT and EXCEPT in 8.0.31). DCL is yours to decide (chapter 14), TCL marks the unit of work (chapter 9), and the admin statements are much of your day (chapter 16). Client commands like psql's \dt are not SQL (chapter 6).
DDL: shaping the schema
CREATE, ALTER and DROP are the core; TRUNCATE empties a table and keeps its definition. Migrations and index changes are your hands-on time (chapter 3).
Careful: MySQL DDL commits
MySQL Every CREATE, ALTER, DROP and TRUNCATE implicitly commits the current transaction and cannot be rolled back. PostgreSQL DDL is transactional, so the same failed migration rolls back cleanly there.
DML: the upsert
INSERT, UPDATE and DELETE change rows. The one people forget is the upsert: insert a row, or update the existing one if its key is taken.
-- Reprice the mouse if it exists, add it if it does not.
INSERT INTO products (id, name, price)
VALUES (101, 'Mouse', 22.00) AS new
ON DUPLICATE KEY UPDATE price = new.price;
-- The same upsert. EXCLUDED is the row that failed to insert.
INSERT INTO products (id, name, price)
VALUES (101, 'Mouse', 22.00)
ON CONFLICT (id) DO UPDATE SET price = EXCLUDED.price
RETURNING id, price;
For you as a DBA
Developers live in DML and DQL; you live in DDL, DCL, TCL and the admin statements. Ask which family a ticket is in.
Anatomy of a SELECT
A SELECT is written in one order and evaluated in another, and most "why does this not work" questions come from that gap.
| Step | Clause | Its job | Note |
|---|---|---|---|
| 1 | FROM and JOIN | Name the tables and how they connect | Written second, evaluated first (chapter 7). |
| 2 | WHERE | Keep the rows that pass a test (a predicate), one row at a time | Runs before grouping, so no COUNT or SUM. |
| 3 | GROUP BY | Fold rows into groups | Chapter 8. |
| 4 | HAVING | Keep the groups that pass a test | May use aggregates: groups now exist. |
| 5 | SELECT | Compute the output columns, name them with AS | Written first, evaluated fifth. Aliases are born here. |
| 6 | DISTINCT | Remove duplicate output rows | Works on the finished rows. |
| 7 | ORDER BY | Sort the output | Can use aliases in both engines. Window functions run just before it. |
| 8 | LIMIT / OFFSET | Return only a slice | PostgreSQL also accepts FETCH FIRST. |
WHERE tests one row at a time, before grouping, which is why HAVING may say COUNT(*) > 1 and WHERE may not.
SELECT c.city, COUNT(*) AS orders, SUM(o.total) AS revenue
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.status <> 'cancelled'
GROUP BY c.city
HAVING SUM(o.total) > 30
ORDER BY revenue DESC
LIMIT 5;
| city | orders | revenue |
|---|---|---|
| Dubai | 2 | 245.00 |
| Cairo | 1 | 40.00 |
The alias rule
An alias from SELECT cannot be used in WHERE, in either engine. ORDER BY and GROUP BY can. To filter on a computed value, repeat the expression or use a subquery (chapter 13).
-- Fails in both engines: WHERE runs before SELECT names anything.
SELECT id, ROUND(total * 1.05, 2) AS with_tax
FROM orders
WHERE with_tax > 100;
-- Works: repeat the expression (or move the query into a subquery).
SELECT id, ROUND(total * 1.05, 2) AS with_tax
FROM orders
WHERE ROUND(total * 1.05, 2) > 100
ORDER BY with_tax;
| id | with_tax |
|---|---|
| 11 | 189.00 |
Remember
The logical order is FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. An alias is born in SELECT, so WHERE cannot see it and ORDER BY can.
How a query travels through the server
Every statement takes the same road, and each kind of problem belongs to one stop.
The parser rejects bad syntax and unknown columns before a row is read. The optimizer costs its plan from statistics, so stale statistics mean bad plans; EXPLAIN prints the plan it chose, and EXPLAIN ANALYZE adds real timings (chapter 10). Writes are logged before the server answers, which is what makes backups and replicas possible (chapter 15).
MySQL serves each connection with a thread on port 3306; PostgreSQL starts a process per connection on 5432, which is why poolers come up more often there.
Sessions and the settings that travel with them
A session is your connection's memory: who you are, which database, and settings that change how statements behave.
- Autocommit
- On in both engines: each statement commits the moment it succeeds. START TRANSACTION or BEGIN suspends that, so you can check the row count before a DELETE becomes permanent.
- MySQL sql_mode
- Flags that decide how strictly MySQL treats bad data. The default is strict, and ONLY_FULL_GROUP_BY is why old tutorials fail on a new server.
SET PERSISTsurvives a restart. - PostgreSQL search_path
- The schemas an unqualified name is looked up in,
"$user", publicby default. Two teams can each have their ownorders.
-- Where am I, and what mood is the session in?
USE shop;
SELECT DATABASE(), @@autocommit, @@sql_mode;
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
-- psql: \c is a client command, so no semicolon
\c shop
SELECT current_database(), current_user;
SHOW search_path;
SET search_path = reporting, public; -- this session only
Dialect differences that trip a returning person
Both engines take the standard core. The differences hide in quoting names, gluing strings and numbering rows (chapter 17).
| Thing | MySQL 8.4 / 9.7 / 26.x | PostgreSQL 17 / 18 |
|---|---|---|
| Quoting a name | Backticks: `order`. Double quotes make a string unless ANSI_QUOTES is set. | Double quotes: "order". Unquoted names fold to lowercase; a quoted name keeps its case. |
| String literals | Single quotes (double quotes also work). | Single quotes only. "paid" is a column name. |
| Concatenation | CONCAT(a, b). || means OR unless PIPES_AS_CONCAT is set. | a || b, and CONCAT() too (it skips NULLs; || does not). |
| Limiting rows | LIMIT 10 OFFSET 20, or older LIMIT 20, 10. | LIMIT 10 OFFSET 20, or OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY. |
| Auto-numbered ids | AUTO_INCREMENT, read back with LAST_INSERT_ID(). | GENERATED ALWAYS AS IDENTITY (older scripts: SERIAL), read back with RETURNING id. |
| Dates | NOW(), CURDATE(), DATE_ADD(d, INTERVAL 7 DAY), DATE_FORMAT(d, '%Y-%m'). | now(), CURRENT_DATE, d + INTERVAL '7 days', to_char(d, 'YYYY-MM'). Both do EXTRACT. |
| Table name case | Follows lower_case_table_names: 1 on Windows, 0 on Linux. Fixed at initialization. | Unquoted names are lowercase on every OS, so Orders and orders are one table. |
| Comparing text | Case-insensitive by default (utf8mb4_0900_ai_ci). | Case-sensitive: use ILIKE or lower(). |
| Boolean columns | BOOLEAN is TINYINT(1); TRUE and FALSE are 1 and 0. | A real boolean, shown as t and f; WHERE active works alone. |
| Upsert | INSERT … ON DUPLICATE KEY UPDATE, REPLACE. | INSERT … ON CONFLICT, MERGE (15+). |
-- MySQL spelling
SELECT CONCAT(name, ' (', city, ')') AS label FROM customers;
-- PostgreSQL spelling
SELECT name || ' (' || city || ')' AS label FROM customers;
| label |
|---|
| Amira (Dubai) |
| Ben (Cairo) |
| NULL |
| Dev (Riyadh) |
Your Windows server treats Orders and orders as one table, so a mixed-case script works for you and fails on a Linux replica. Keep names lowercase.
| Capability | MySQL 8.4 / 9.7 / 26.x | PostgreSQL 17 / 18 |
|---|---|---|
| ROLLBACK undoes CREATE, ALTER, DROP, TRUNCATE | No (implicit commit) | Yes |
| Upsert | Yes ON DUPLICATE KEY UPDATE, REPLACE | Yes ON CONFLICT, MERGE (15+) |
| RETURNING on INSERT, UPDATE, DELETE | No (LAST_INSERT_ID() for ids) | Yes |
| FETCH FIRST n ROWS ONLY | No (LIMIT only) | Yes (and LIMIT) |
| INTERSECT and EXCEPT | Yes since 8.0.31 | Yes |
| Native boolean type | Partial TINYINT(1) in disguise | Yes |
| Schemas inside a database | No (schema = database) | Yes (search_path picks) |
Asking the AI for the exact syntax
Let an assistant write the syntax. A prompt that works carries four things.
- Engine and version
- "MySQL 8.4" or "PostgreSQL 17". A feature may not exist in yours (MERGE before PostgreSQL 15).
- The table definitions
- Paste
SHOW CREATE TABLE ordersor\d orders. - The rows you expect
- "One row per customer, including customers with no orders" tells it you need an outer join.
- A request to explain
- You will be the one asked about it later.
Then verify: run it in a transaction you roll back, compare the rows, and EXPLAIN it before production (chapter 10, chapter 18).
Ask the AI
"MySQL 8.4. Tables: customers(id, name, city) and orders(id, customer_id, status, total); customer_id is NULL for guest checkouts. List every customer with the count and total of their paid orders in 2026, including customers with no orders. Explain each clause and its logical step."
- MySQL 8.4: SELECT statement
Every clause, and aliases.
- PostgreSQL: SELECT
LIMIT, OFFSET, FETCH FIRST.
- MySQL 8.4: statements that cause an implicit commit
What ends your transaction.
- MySQL 8.4: server SQL modes
Each flag and the default.
- PostgreSQL: schemas and the search path
How unqualified names resolve.