Truy vấn PostgreSQL nâng cao
1. JOIN — giống chuẩn ANSI
Cú pháp y hệt SQL Server cho INNER, LEFT, RIGHT, FULL OUTER, CROSS. Bẫy LEFT JOIN + WHERE cũng tương tự (đặt vào ON).
Khác biệt nhỏ Postgres-specific
-- USING — JOIN trên cột cùng tên
SELECT u.id, u.name, o.total
FROM users u JOIN orders o USING (id); -- tự match user.id = orders.id
-- và chỉ trả 1 cột id
-- NATURAL JOIN — tự JOIN trên tất cả cột cùng tên (tránh dùng — nguy hiểm)
SELECT * FROM a NATURAL JOIN b;
2. LATERAL JOIN — đặc thù mạnh
-- LATERAL cho phép subquery bên phải REFERENCE bảng bên trái
-- Ví dụ: top 3 đơn mới nhất của mỗi user
SELECT u.id, u.name, o.*
FROM users u
LEFT JOIN LATERAL (
SELECT *
FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) o ON true;
LATERAL là sức mạnh độc đáo của Postgres. Tương đương CROSS APPLY của SQL Server. Cho phép giải các bài "top N per group" + parameterized subquery rất gọn.3. CTE — bao gồm Recursive
Cú pháp như SQL Server
WITH high_value AS (
SELECT * FROM orders WHERE total > 10000000
)
SELECT u.name, COUNT(*)
FROM high_value h JOIN users u ON u.id = h.user_id
GROUP BY u.name;
Postgres 12+ — Inline vs Materialized
-- MATERIALIZED — bắt CTE chạy 1 lần, cache (cũ default)
WITH cte AS MATERIALIZED (
SELECT * FROM big_table WHERE x > 100
)
SELECT ...;
-- NOT MATERIALIZED — inline (default 12+, planner tối ưu tốt hơn)
WITH cte AS NOT MATERIALIZED (
SELECT * FROM big_table WHERE x > 100
)
SELECT ...;
Recursive CTE
-- Build cây tổ chức từ root
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 0 AS lvl, name::TEXT AS path
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.lvl + 1, o.path || ' > ' || e.name
FROM employees e
JOIN org o ON o.id = e.manager_id
)
SELECT * FROM org ORDER BY path;
4. Window function
Cú pháp giống chuẩn (như SQL Server): OVER (PARTITION BY ... ORDER BY ...). Postgres hỗ trợ nhiều function nhất — full list:
- Ranking:
ROW_NUMBER,RANK,DENSE_RANK,PERCENT_RANK,CUME_DIST,NTILE. - Offset:
LAG,LEAD,FIRST_VALUE,LAST_VALUE,NTH_VALUE. - Aggregate over window:
SUM,AVG,COUNT, ... vớiOVER.
Postgres 19 — IGNORE NULLS
-- IGNORE NULLS — bỏ qua giá trị NULL trong window frame
-- Áp dụng cho: lead(), lag(), first_value(), last_value(), nth_value()
SELECT sensor_id, ts, reading,
last_value(reading) IGNORE NULLS OVER (
PARTITION BY sensor_id ORDER BY ts
) AS last_known_reading
FROM sensor_data;
-- RESPECT NULLS là default — trả NULL nếu gặp NULL
-- So sánh: IGNORE NULLS vs RESPECT NULLS
SELECT sensor_id, ts, reading,
last_value(reading) RESPECT NULLS OVER (
PARTITION BY sensor_id ORDER BY ts
) AS resp_nulls,
last_value(reading) IGNORE NULLS OVER (
PARTITION BY sensor_id ORDER BY ts
) AS ignore_nulls
FROM sensor_data;
LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE đều hỗ trợ. Cực kỳ hữu ích cho dữ liệu sensor/time-series có lỗ hổng NULL. SQL Server hỗ trợ từ 2025 (Azure SQL), Oracle có IGNORE NULLS từ lâu.-- Top N per group
SELECT id, user_id, total
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) rn
FROM orders
) t WHERE rn <= 3;
-- Running total
SELECT order_date, total,
SUM(total) OVER (ORDER BY order_date) AS running_total,
AVG(total) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM orders;
-- Compare with previous row
SELECT order_date, total,
LAG(total) OVER (ORDER BY order_date) AS prev_total,
total - LAG(total) OVER (ORDER BY order_date) AS delta
FROM orders;
5. Set operations
Cú pháp giống SQL Server: UNION, UNION ALL, INTERSECT, EXCEPT.
6. DISTINCT ON — đặc thù Postgres
-- Đơn hàng mới nhất của mỗi user — viết ngắn hơn ROW_NUMBER
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;
DISTINCT ON chỉ Postgres có. Thường nhanh hơn ROW_NUMBER + WHERE rn=1.7. Aggregate nâng cao
-- GROUPING SETS, ROLLUP, CUBE — giống SQL Server
SELECT city, status, COUNT(*)
FROM orders o JOIN users u ON u.id = o.user_id
GROUP BY GROUPING SETS ((city, status), (city), ());
-- FILTER — aggregate có điều kiện
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'paid') AS paid_count,
SUM(total) FILTER (WHERE status = 'paid') AS paid_revenue
FROM orders;
FILTER (WHERE ...) đặc thù Postgres — gọn hơn SUM(CASE WHEN ... THEN ... END). SQL Server không có.8. JSON query trong WHERE
-- Containment, fastest với GIN index
SELECT * FROM events WHERE payload @> '{"type": "click"}';
-- Path access
SELECT * FROM events WHERE payload->>'type' = 'click';
-- Array contains
SELECT * FROM posts WHERE tags @> ARRAY['vue'];
-- JSONB path query
SELECT * FROM events
WHERE payload @? '$.tags[*] ? (@ == "promo")';
9. Full-text search
-- Add tsvector column
ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || content)) STORED;
-- GIN index
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Query
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgres & 18');
10. Transaction & MVCC
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;
-- ROLLBACK nếu lỗi
BEGIN;
...
ROLLBACK;
-- Savepoint
BEGIN;
INSERT ...;
SAVEPOINT sp1;
UPDATE ...;
ROLLBACK TO sp1; -- chỉ rollback từ sp1
COMMIT;
MVCC — cốt lõi Postgres
"Multi-Version Concurrency Control. Mỗi UPDATE/DELETE không xoá row cũ ngay mà tạo version mới + đánh dấu version cũ là dead. Mỗi transaction thấy snapshot data tại thời điểm bắt đầu. Đọc không block ghi, ghi không block đọc.
Trade-off: row chết tích lũy → cần
VACUUM(auto-vacuum chạy nền) để reclaim space."
Isolation level
| Level | Dirty | Non-rep | Phantom | Serial. anomaly |
|---|---|---|---|---|
| Read Uncommitted | (giống Read Committed trong PG) | |||
| Read Committed (default) | ❌ | ✅ | ✅ | ✅ |
| Repeatable Read | ❌ | ❌ | ❌ (nhờ MVCC) | ✅ |
| Serializable | ❌ | ❌ | ❌ | ❌ |
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
...
COMMIT;
- Postgres không có dirty read thậm chí ở Read Uncommitted (vì MVCC).
- Postgres Repeatable Read = snapshot suốt transaction, bao luôn phantom.
- Postgres Serializable dùng SSI (Serializable Snapshot Isolation) — ít block, optimistic.
11. Deadlock
Tương tự SQL Server. Postgres tự detect và abort 1 transaction (deadlock_timeout default 1s).
-- Bật log
SET log_lock_waits = on;
SET deadlock_timeout = '1s';
12. EXPLAIN & query planning
Cú pháp
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 1;
ANALYZE thực sự chạy query và đo time. BUFFERS show số page đọc.
Node thường gặp
| Node | Ý nghĩa |
|---|---|
| Seq Scan | Full table scan — chậm với bảng lớn |
| Index Scan | Dùng index, lookup heap |
| Index Only Scan | Dùng index, không cần heap — tốt nhất |
| Bitmap Index Scan + Bitmap Heap Scan | Tốt khi nhiều row match một index |
| Nested Loop | JOIN row-by-row |
| Hash Join | JOIN bằng hash |
| Merge Join | JOIN trên 2 nguồn đã sort |
| Sort | Sắp xếp — đắt |
| Gather/Parallel | Parallel query |
Cost = startup_cost..total_cost
So sánh cost giữa các plan để chọn tối ưu hơn.
Postgres 18 — B-tree Skip Scan
DISTINCT hoặc GROUP BY trên cột đầu tiên của composite index, planner có thể chọn Skip Scan thay vì Seq Scan. Thấy trong EXPLAIN: -> Index Only Scan using idx_abc on tab (actual ... skip=...). Không cần thay đổi SQL — planner tự xài khi có lợi.Postgres 19 — Memoize estimates trong EXPLAIN
-- Memoize là cache node trong Nested Loop: nhớ kết quả subquery theo key
-- Postgres 19 hiển thị thêm thông tin ước lượng:
EXPLAIN (ANALYZE)
SELECT * FROM orders o
JOIN LATERAL (
SELECT * FROM users u WHERE u.id = o.user_id
) u ON true;
-> Memoize (cost=1.25..123.50 rows=10 width=100)
Cache Key: o.user_id
Estimates: capacity=2 distinct keys=2 lookups=1000 hit percent=99.80%
Hits: 998 Misses: 2 Evictions: 0
capacity, distinct keys, hit percent đều là ước lượng — planner dùng để quyết định có dùng Memoize hay không. hit percent càng cao càng có lợi. Thấy rõ hơn qua ANALYZE với Hits/Misses/Evictions thực tế.Postgres 18 — Async I/O accelerate
13. VACUUM & ANALYZE
-- Manual
VACUUM ANALYZE orders;
VACUUM FULL orders; -- rebuild bảng, lock dài — chỉ làm khi rất fragment
-- Statistics riêng
ANALYZE orders;
-- Auto-vacuum config (postgresql.conf)
autovacuum = on
autovacuum_naptime = 1min
"VACUUM reclaim space từ row dead (đã DELETE hoặc UPDATE cũ trong MVCC). Auto-vacuum chạy nền. VACUUM FULL rebuild bảng từ đầu (lock dài), VACUUM thường không lock. ANALYZE cập nhật statistics cho planner."
14. SQL/PGQ Property Graph Queries (PG 19)
Khái niệm
Property Graph là view logic trên bảng có sẵn: bảng là VERTEX (đỉnh), quan hệ khóa ngoại là EDGE (cạnh). Postgres rewrite graph query thành relational operation thông thường, dùng index có sẵn.
Cú pháp
-- 1. Định nghĩa graph từ bảng có sẵn
CREATE PROPERTY GRAPH social_graph
VERTEX TABLES (users LABEL person PROPERTIES (id, name, city))
EDGE TABLES (
follows
SOURCE KEY (follower_id) REFERENCES users (id)
DESTINATION KEY (followed_id) REFERENCES users (id)
LABEL follows
);
-- 2. Query bằng GRAPH_TABLE
SELECT * FROM GRAPH_TABLE (social_graph
MATCH (a IS person WHERE a.name = 'Alice')
-[IS follows]->(b IS person)
-[IS follows]->(c IS person)
COLUMNS (b.name AS friend, c.name AS friend_of_friend)
);
Ví dụ nâng cao — kết hợp với SQL thường
-- GRAPH_TABLE compose với JOIN, aggregate, CTE như bảng thường
WITH fof AS (
SELECT * FROM GRAPH_TABLE (social_graph
MATCH (a IS person WHERE a.name = 'Alice')
-[IS follows]->(b IS person)
-[IS follows]->(c IS person)
COLUMNS (c.id AS fof_id, c.name AS fof_name)
)
)
SELECT fof_name, COUNT(*) AS order_count
FROM fof JOIN orders o ON o.user_id = fof_id
GROUP BY fof_name
ORDER BY order_count DESC;
-- Tạo graph từ JOIN
CREATE PROPERTY GRAPH user_order_graph
VERTEX TABLES (
users LABEL user PROPERTIES (id, name),
orders LABEL "order" PROPERTIES (id, total, status)
)
EDGE TABLES (
-- Edge implicit từ FK
orders AS placed
SOURCE KEY (user_id) REFERENCES users (id)
LABEL placed_by
);
Giới hạn hiện tại
-[IS follows]->+, -[IS follows]->* hay -[IS follows]->{2,4}) sẽ có trong phiên bản sau. Không thay thế được Neo4j/JanusGraph nếu cần graph chuyên sâu, nhưng đủ cho bài toán "friend-of-friend", "mutual connection", "path detection" đơn giản — ngay trên bảng quan hệ hiện tại, zero migration."Từ PG 19, hỗ trợ chuẩn SQL/PGQ ngay trong core.
CREATE PROPERTY GRAPHđịnh nghĩa graph từ bảng có sẵn,GRAPH_TABLEđể query. Engine tự rewrite thành relational operation, không cần extension, không cần storage mới. Kết quả compose với JOIN, aggregate, CTE như bảng thường. Fixed-depth hiện tại, variable-length sẽ có trong tương lai."