BEGIN;
EXPLAIN (ANALYZE, BUFFERS)
UPDATE orders SET status = 'archived' WHERE created_at < now() - interval '2 years';
ROLLBACK;
SQL & PostgreSQL
draftedIndexes, EXPLAIN, transactions & isolation, MVCC/VACUUM, locking, pooling, replication, zero-downtime migrations — kèm so sánh với SQL Server.
0/68 rated · mastery 0%
Bài học
13 bài · ~277 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~23 phút· 5 câuverify
Postgres lưu dữ liệu thế nào: page, heap, tuple, WAL
Từ file trên disk tới 8 KB page, heap tuple, TOAST và WAL — nền tảng để hiểu index, MVCC và ACID; và mental model khác SQL Server ở đâu.
- 02~19 phút· 3 câuverify
Một câu SQL được thực thi ra sao
Logical query order (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT), join algorithms, aggregation, window functions và CTE.
- 03~24 phút· 7 câuverify
B-tree index: composite, covering, partial, expression
Index giúp gì và tốn gì; cách chọn thứ tự cột, Index Only Scan, partial/expression index và query sargable.
- 04~17 phút· 4 câuverify
GIN, GiST, BRIN và JSONB
Khi B-tree không đủ: inverted index cho jsonb/array/full-text, GiST cho range/geo, BRIN cho data tăng dần; JSON vs JSONB và cách index.
- 05~24 phút· 7 câuverify
Query planner & đọc EXPLAIN
Planner ước lượng cost bằng statistics; đọc EXPLAIN (ANALYZE, BUFFERS), vì sao bỏ qua index, estimate sai, generic vs custom plan.
- 06~20 phút· 6 câuverify
Transaction & isolation levels
ACID trong thực tế, bốn isolation level, anomaly của từng level trong Postgres (lost update, write skew) và khác SQL Server/RCSI.
- 07~20 phút· 6 câuverify
MVCC, VACUUM, HOT và xid wraparound
Mỗi UPDATE tạo tuple version mới: snapshot, dead tuple, autovacuum, bloat, HOT update và vì sao wraparound có thể dừng database.
- 08~20 phút· 8 câuverify
Locking & concurrency control
Row lock (FOR UPDATE/SHARE…), table lock level, deadlock, advisory lock, SKIP LOCKED queue, optimistic locking và upsert an toàn.
- 09~19 phút· 5 câuverify
Connections & PgBouncer
Process-per-connection, vì sao cần pooler, session/transaction/statement mode, prepared statements và sizing pool khi scale pods.
- 10~20 phút· 6 câuverify
Replication, partitioning & scaling
Streaming vs logical replication, replica lag và read-your-writes, hot standby conflict, declarative partitioning và cách chọn hướng scale.
- 11~16 phút· 8 câuverify
Zero-downtime schema migrations
Lock queue, lock_timeout, ADD COLUMN instant vs rewrite, CREATE INDEX CONCURRENTLY, NOT VALID + VALIDATE, backfill theo batch và expand–contract.
- 12~29 phút· 5 câuverify
Data modeling: keys, types, time zones, multi-tenant schema
Chọn primary key (identity, UUIDv4/v7), timestamp vs timestamptz, normalization, soft delete và schema/index cho bảng multi-tenant.
- 13~27 phút· 6 câuverify
Truy cập dữ liệu từ app: N+1, pagination, bulk load
N+1 và cách phát hiện, OFFSET vs keyset pagination, COPY cho bulk load và các pattern Node/TypeScript an toàn.
Tóm tắt & cheat sheet của track
TL;DR
- PostgreSQL là MVCC thuần: mỗi
UPDATEtạo tuple version mới, bản cũ thành dead tuple chờ VACUUM dọn. Reader không block writer, writer không block reader — khác SQL Server mặc định (lock-basedREAD COMMITTED, trừ khi bật RCSI). - Postgres không có clustered index: mọi table là heap, mọi index (kể cả PK) là secondary index trỏ tới
ctid.CLUSTERchỉ sắp xếp lại một lần, không duy trì. - Senior interview xoáy vào: đọc
EXPLAIN (ANALYZE, BUFFERS), vì sao planner bỏ qua index, anomaly ở từng isolation level, bloat / long transaction / xid wraparound, lock queue khi migrate, PgBouncer transaction mode, replica lag. - Zero-downtime migration = expand → migrate (backfill theo batch) → contract, cộng
lock_timeout,CREATE INDEX CONCURRENTLY, constraintNOT VALID+VALIDATE.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| B-tree index | Cây cân bằng, sắp xếp theo key; hỗ trợ =, <, >, BETWEEN, ORDER BY, prefix LIKE 'abc%' (cần C collation hoặc text_pattern_ops) | CREATE INDEX ON orders (tenant_id, created_at DESC) |
| Composite index | Thứ tự cột quan trọng: equality trước, range/sort sau | (tenant_id, status, created_at) phục vụ WHERE tenant_id=? AND status=? ORDER BY created_at |
| Covering index | INCLUDE cột payload để có Index Only Scan (PG 11+) | CREATE INDEX ON orders (customer_id) INCLUDE (total) |
| Partial index | Index chỉ một tập con row | CREATE INDEX ON jobs (run_at) WHERE status = 'pending' |
| Expression index | Index trên biểu thức; query phải dùng đúng biểu thức | CREATE INDEX ON users (lower(email)) |
| GIN / GiST / BRIN | GIN: inverted (jsonb, array, full-text) · GiST: range/geo/nearest · BRIN: block range tóm tắt cho data tăng dần | CREATE INDEX ON events USING brin (created_at) |
| MVCC | Mỗi row version có xmin/xmax; transaction nhìn theo snapshot | SELECT xmin, xmax, ctid FROM t |
| VACUUM | Dọn dead tuple, cập nhật visibility map, freeze xid cũ | VACUUM (VERBOSE, ANALYZE) orders |
| HOT update | Update không đụng cột indexed + còn chỗ trên page → không cần sửa index | fillfactor = 90 cho table update nhiều |
| Row lock | Lưu trong tuple header, không lock escalation như SQL Server | SELECT ... FOR UPDATE SKIP LOCKED |
| Advisory lock | Lock theo số do app định nghĩa | SELECT pg_try_advisory_xact_lock(hashtext('job:daily')) |
| PgBouncer | Pooler nhẹ; mode session / transaction / statement | Transaction mode phá SET, session advisory lock, LISTEN |
| Streaming replication | Replica apply WAL; async mặc định → replica lag | pg_stat_replication.replay_lag |
| Declarative partitioning | Chia table theo range/list/hash; partition pruning | PARTITION BY RANGE (created_at) |
| Keyset pagination | Phân trang bằng WHERE (k) > (last_k) thay vì OFFSET | WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 50 |
Cơ chế hoạt động
Vòng đời một UPDATE dưới MVCC
SQL Server khác: update thường in-place trên page của clustered index; chỉ khi bật RCSI/SNAPSHOT thì bản cũ được copy vào version store (tempdb, hoặc Persistent Version Store khi bật ADR — SQL Server 2019+).
Zero-downtime column change (expand / contract)
Mỗi DDL chạy với SET lock_timeout = '3s' + retry: nếu ALTER TABLE phải chờ ACCESS EXCLUSIVE sau một transaction dài, mọi query mới sẽ xếp hàng sau nó → outage dù DDL "chỉ mất 1ms".
Đường đi của query qua pool & replica
Trade-offs
| Chủ đề | Lựa chọn A | Lựa chọn B | Khi nào chọn gì |
|---|---|---|---|
| Concurrency control | Postgres MVCC (heap + VACUUM) | SQL Server lock-based RC (mặc định on-prem) / RCSI | MVCC: đọc không block; trả giá bằng bloat & vacuum. Lock-based: không bloat nhưng reader/writer block nhau, dễ deadlock |
| Pagination | OFFSET | Keyset | OFFSET cho admin nhỏ, cần nhảy trang; keyset cho feed/API lớn |
| PK | bigint identity | UUID (v4 / v7) | bigint nhỏ & nhanh; UUID khi sinh ở client / merge nhiều nguồn; v4 random làm B-tree phân mảnh, v7 time-ordered tốt hơn |
| Isolation | Read Committed + lock tường minh | Serializable (SSI) + retry | RC cho đa số OLTP; SSI khi invariant phức tạp (write skew), chấp nhận retry |
| Pooler mode | Session | Transaction | Transaction scale tốt hơn nhiều nhưng mất session state |
| Replication | Physical streaming | Logical | Physical: HA/read replica cả cluster; Logical: chọn table, khác major version, CDC |
| Chia dữ liệu | Partitioning (1 node) | Sharding (nhiều node, vd Citus) | Partition cho retention/pruning; shard khi 1 node không đủ write/storage |
| JSON | Cột quan hệ | jsonb | jsonb cho attribute thưa/động; field hay filter/join nên là cột thật |
Pitfalls
EXPLAIN ANALYZEthực thi thật câu lệnh — vớiUPDATE/DELETEhãy bọc trongBEGIN; ... ROLLBACK;.- Hàm/cast trên cột (
WHERE date(created_at) = ..., so sánhvarcharvới tham số sai kiểu) làm index vô dụng. - Transaction mở lâu (kể cả
idle in transaction) giữxmin→ VACUUM không dọn được gì → bloat toàn DB. CREATE INDEXthường lock ghi cả table; dùngCONCURRENTLY— nhưng không chạy được trong transaction block (nhiều migration tool mặc định bọc transaction!).- Thêm cột
DEFAULT now()/gen_random_uuid()(volatile) vẫn rewrite table; default hằng số thì instant từ PG 11. - Đọc từ replica ngay sau khi ghi → dữ liệu cũ.
timestamp without time zonecho sự kiện thực tế → bug khi đổi DST / đa múi giờ; dùngtimestamptz.- Tin rằng
NOLOCK-style dirty read tồn tại trong Postgres:READ UNCOMMITTEDở Postgres hoạt động nhưREAD COMMITTED.
Cheat sheet
- Chẩn đoán query:
EXPLAIN (ANALYZE, BUFFERS)→ so estimated vs actual rows, tìm node tốn thời gian nhất,shared read(disk) vsshared hit(cache). - Top query:
pg_stat_statements(total_exec_time, calls, mean). Lock:pg_locks+pg_blocking_pids(pid). Activity:pg_stat_activity(state, xact_start, backend_xmin). - Bloat/vacuum:
pg_stat_user_tables(n_dead_tup, last_autovacuum). Wraparound:age(datfrozenxid)trongpg_database. - Isolation Postgres: RU≈RC (mặc định) · RR = snapshot isolation (không phantom, có write skew) · Serializable = SSI (lỗi
40001→ retry). - Lock hàng:
FOR UPDATE>FOR NO KEY UPDATE>FOR SHARE>FOR KEY SHARE; queue:FOR UPDATE SKIP LOCKED. - Migration an toàn:
lock_timeout,ADD COLUMNnullable,CREATE INDEX CONCURRENTLY,ADD CONSTRAINT ... NOT VALID→VALIDATE CONSTRAINT, backfill batch,SET NOT NULLsau khi có CHECK hợp lệ (PG 12+ bỏ qua full scan). - Timeouts nên có:
statement_timeout,lock_timeout,idle_in_transaction_session_timeout, (PG 17+)transaction_timeout. - SQL Server ↔ Postgres:
TOP↔LIMIT·IDENTITY↔GENERATED ... AS IDENTITY· filtered index ↔ partial index ·datetimeoffset↔timestamptz(Postgres không lưu offset, chỉ lưu instant UTC) ·MERGE↔INSERT ... ON CONFLICT(Postgres cóMERGEtừ PG 15) ·WITH (NOLOCK)↔ không có.
References
- PostgreSQL docs — Indexes ↗
- PostgreSQL docs — Using EXPLAIN ↗
- PostgreSQL docs — Transaction Isolation ↗
- PostgreSQL docs — Explicit Locking ↗
- PostgreSQL docs — Routine Vacuuming (incl. wraparound) ↗
- PostgreSQL docs — ALTER TABLE ↗
- PostgreSQL docs — CREATE INDEX (CONCURRENTLY) ↗
- PostgreSQL docs — Table Partitioning ↗
- PostgreSQL docs — High Availability & Hot Standby ↗
- PostgreSQL docs — JSON Types ↗
- PgBouncer — Features (pooling modes) ↗
- Microsoft Learn — Transaction locking and row versioning guide (SQL Server) ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 68 / 68
SELECT *
FROM (
SELECT o.*, row_number() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders o
WHERE tenant_id = $1
) t
WHERE rn = 1;
-- Postgres-only alternative:
SELECT DISTINCT ON (customer_id) * FROM orders
WHERE tenant_id = $1 ORDER BY customer_id, created_at DESC;
CREATE INDEX orders_customer_cover ON orders (tenant_id, customer_id) INCLUDE (total, status);
EXPLAIN (ANALYZE) SELECT total, status FROM orders WHERE tenant_id = 7 AND customer_id = 42;
-- Index Only Scan using orders_customer_cover ... Heap Fetches: 0
CREATE UNIQUE INDEX users_email_lower ON users (tenant_id, lower(email));
SELECT * FROM users WHERE tenant_id = $1 AND lower(email) = lower($2); -- dùng index
SELECT * FROM users WHERE tenant_id = $1 AND email ILIKE $2; -- không dùng
-- Chống 2 ca làm việc trùng giờ cho cùng nhân viên (cần extension btree_gist)
ALTER TABLE shifts ADD CONSTRAINT no_overlap
EXCLUDE USING gist (employee_id WITH =, tstzrange(starts_at, ends_at) WITH &&);
Nested Loop (cost=0.85..1250.40 rows=120 width=64) (actual time=0.05..3.20 rows=118 loops=1)
Buffers: shared hit=480
-> Index Scan using customers_tenant_email on customers c (rows=1) (actual rows=1 loops=1)
Index Cond: ((tenant_id = 7) AND (email = 'a@b.com'))
-> Bitmap Heap Scan on orders o (rows=120) (actual rows=118 loops=1)
Recheck Cond: (customer_id = c.id)
-> Bitmap Index Scan on orders_customer_id (actual rows=118 loops=1)
Index Cond: (customer_id = c.id)
const r = await db.query(
`UPDATE addresses SET line1 = $1, version = version + 1
WHERE id = $2 AND tenant_id = $3 AND version = $4`,
[line1, id, tenantId, expectedVersion],
);
if (r.rowCount === 0) throw new ConflictError('Address was modified by someone else');
WITH next AS (
SELECT id FROM jobs
WHERE status = 'pending' AND run_at <= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs j SET status = 'running', locked_at = now(), attempts = attempts + 1
FROM next WHERE j.id = next.id
RETURNING j.*;
CREATE TABLE events (
tenant_id bigint NOT NULL, id bigint GENERATED ALWAYS AS IDENTITY,
created_at timestamptz NOT NULL, payload jsonb,
PRIMARY KEY (created_at, id)
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_09 PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE INDEX products_attrs_gin ON products USING gin (attrs jsonb_path_ops);
SELECT id FROM products WHERE tenant_id = $1 AND attrs @> '{"brand":"acme","size":"M"}';
SELECT (date '2026-10-05' + time '09:00') AT TIME ZONE 'Australia/Sydney' AS starts_at_utc;
-- Ngày DST bắt đầu ở Sydney (2026-10-04) làm 02:00-03:00 không tồn tại.
-- index: CREATE INDEX orders_created_at ON orders (created_at);
SELECT id, total FROM orders
WHERE date(created_at) = current_date
AND tenant_id::text = $1 -- $1 bound as text by the driver
ORDER BY created_at DESC;
Nested Loop (cost=1.13..16.20 rows=1 width=40) (actual time=0.09..40211.55 rows=480112 loops=1)
-> Index Scan using products_tenant_category on products p (rows=1) (actual rows=480112 loops=1)
Index Cond: ((tenant_id = 12) AND (category_id = 900))
-> Index Scan using inventory_pkey on inventory i (rows=1) (actual rows=1 loops=480112)
-- migration 2026_09_01_add_gift_note.sql
ALTER TABLE orders ADD COLUMN gift_note text;
-- at the same time, pg_stat_activity shows:
-- pid 811 state: idle in transaction xact_start: 00:12 ago query: SELECT * FROM orders WHERE ...
async function reserve(tx: Tx, items: { productId: number; qty: number }[]) {
for (const it of items) { // cart order as the user added items
await tx.query(
`UPDATE inventory SET reserved = reserved + $1
WHERE product_id = $2 AND available - reserved >= $1`,
[it.qty, it.productId],
);
}
}
// Request A: items [101, 202] Request B: items [202, 101]
SELECT id, name, price, updated_at FROM products
WHERE tenant_id = $1
ORDER BY updated_at DESC
LIMIT 50 OFFSET $2; -- $2 = page * 50
// per request
await client.query(`SET search_path TO tenant_${tenantId}, public`);
await client.query({ name: 's_12', text: 'SELECT * FROM orders WHERE id = $1', values: [id] });
const orders = await orderRepo.find({ where: { tenantId }, take: 50 });
for (const o of orders) {
o.customer = await customerRepo.findOneBy({ id: o.customerId });
o.items = await itemRepo.findBy({ orderId: o.id });
}
SET lock_timeout = '3s';
ALTER TABLE orders ADD CONSTRAINT orders_currency_nn CHECK (currency IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_currency_nn;
ALTER TABLE orders ALTER COLUMN currency SET NOT NULL;
ALTER TABLE orders DROP CONSTRAINT orders_currency_nn;
-- lặp từ app/script với :last_id tăng dần
UPDATE orders SET currency_code = legacy_currency
WHERE id > :last_id AND id <= :last_id + 5000 AND currency_code IS NULL;
-- T1 và T2 chạy song song ở REPEATABLE READ
SELECT count(*) FROM on_call WHERE shift_id = 9 AND active; -- cả hai thấy 2
UPDATE on_call SET active = false WHERE shift_id = 9 AND doctor_id = :me;
COMMIT; -- cả hai thành công → 0 active