PostgreSQL

Truy vấn PostgreSQL nâng cao

JOIN, LATERAL, CTE recursive, window function, MVCC, transaction, isolation, EXPLAIN — gốc Postgres.

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ới OVER.

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;
PG 19 IGNORE NULLS.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")';
-- 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

Câu PV cốt lõi:"Postgres dùng MVCC, MVCC là gì?"

"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

LevelDirtyNon-repPhantomSerial. 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;
Khác biệt Postgres vs SQL Server:
  • 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 ScanFull table scan — chậm với bảng lớn
Index ScanDùng index, lookup heap
Index Only ScanDùng index, không cần heap — tốt nhất
Bitmap Index Scan + Bitmap Heap ScanTốt khi nhiều row match một index
Nested LoopJOIN row-by-row
Hash JoinJOIN bằng hash
Merge JoinJOIN trên 2 nguồn đã sort
SortSắp xếp — đắt
Gather/ParallelParallel 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

PG 18 B-tree Skip Scan. Khi 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
PG 19 EXPLAIN Memoize.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

⭐ Postgres 18 release ghi nhận đến 3× faster cho Sequential Scan và Bitmap Heap Scan nhờ AIO subsystem. Workload IO-heavy nâng cấp lên 18 là quà free.

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
Câu PV:"VACUUM trong Postgres làm gì?"

"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)

Tính năng lớn nhất PG 19. SQL/PGQ cho phép viết graph query (pattern matching kiểu Cypher) trên chính bảng quan hệ hiện có — không extension, không storage engine mới, không migrate dữ liệu. Chuẩn SQL:2023.

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

PG 19 (initial): Fixed-depth pattern matching (số hop cố định). Variable-length path (-[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.
Câu PV:"Postgres hỗ trợ graph query chưa — có cần cài extension không?"

"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."

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.