Cú pháp PostgreSQL cơ bản
1. Kiểu dữ liệu — đa dạng hơn SQL Server
Numeric
| Kiểu | Range | Storage | Khi nào |
|---|---|---|---|
SMALLINT | -32K → 32K | 2 bytes | Số nhỏ |
INTEGER / INT | -2.1B → 2.1B | 4 bytes | ID nhỏ |
BIGINT | ±9.2 × 10¹⁸ | 8 bytes | ID lớn, timestamp |
DECIMAL(p, s) / NUMERIC(p, s) | Chính xác | Variable | Tiền |
REAL | Float 32-bit | 4 bytes | Khoa học |
DOUBLE PRECISION | Float 64-bit | 8 bytes | Khoa học |
SERIAL / BIGSERIAL | Auto-increment | 4/8 | Legacy — giờ ưu tiên IDENTITY |
-- Postgres recommend cú pháp IDENTITY chuẩn ANSI hơn SERIAL
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(18, 2) NOT NULL CHECK (price >= 0)
);
Chuỗi
| Kiểu | Ghi chú |
|---|---|
CHAR(n) | Cố định n char, pad space. Hiếm dùng. |
VARCHAR(n) | Giới hạn n. Không nhanh hơn TEXT ở Postgres. |
TEXT | Không giới hạn. Mặc định nên dùng. |
"Khác SQL Server/MySQL, Postgres không phân biệt performance giữa
VARCHAR(n)vàTEXT. Cả 2 đều lưu cùng cách (TOAST cho data lớn). DùngVARCHAR(n)chỉ để enforce length limit ở DB. Còn không thìTEXTcho linh hoạt."
Thời gian
| Kiểu | Ý nghĩa |
|---|---|
DATE | Ngày, không giờ |
TIME | Giờ, không ngày |
TIMESTAMP | Ngày + giờ, không timezone |
TIMESTAMPTZ | Ngày + giờ + timezone, lưu UTC. Mặc định nên dùng. |
INTERVAL | Khoảng thời gian |
TIMESTAMPTZ cho mọi cột thời gian user-facing. Postgres lưu UTC, convert theo session timezone khi đọc.SET TIMEZONE TO 'Asia/Ho_Chi_Minh';
SELECT NOW(); -- output sẽ ở +07:00
Kiểu Postgres đặc trưng
-- Array
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title TEXT,
tags TEXT[] -- array of TEXT
);
INSERT INTO posts (title, tags) VALUES ('Hello', ARRAY['vue', 'nuxt']);
SELECT * FROM posts WHERE 'vue' = ANY(tags);
-- JSON / JSONB (JSONB nén nhị phân, query nhanh, index được)
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
payload JSONB NOT NULL
);
INSERT INTO events (payload)
VALUES ('{"type": "click", "user": "alice", "tags": ["promo"]}');
SELECT payload->>'type' AS event_type,
payload->'user' AS user_json
FROM events;
-- UUID — Postgres 18 có uuidv7() built-in
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT uuidv7(), -- 18+
-- DEFAULT gen_random_uuid() cho version < 18
email TEXT UNIQUE
);
-- INET / CIDR
CREATE TABLE logs (
ip INET,
subnet CIDR
);
-- Range type
CREATE TABLE reservations (
id BIGSERIAL PRIMARY KEY,
room_id INT,
during TSRANGE NOT NULL,
EXCLUDE USING GIST (room_id WITH =, during WITH &&) -- chống overlap!
);
EXCLUDE constraint: ngăn 2 row có range overlap cùng room_id. Không hệ DB nào khác có ngon thế này.JSON vs JSONB
| JSON | JSONB | |
|---|---|---|
| Lưu | Text chuẩn | Nhị phân nén |
| Insert | Nhanh | Chậm hơn chút (parse + encode) |
| Query/Index | Chậm | Nhanh hơn nhiều, hỗ trợ GIN index |
| Giữ whitespace, key order | ✅ | ❌ |
| Khuyến nghị | Audit log raw | Mặc định nên dùng |
2. DDL — Data Definition
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
age INT CHECK (age BETWEEN 0 AND 150),
status TEXT NOT NULL DEFAULT 'active',
country_id INT REFERENCES countries(id) ON DELETE SET NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Add column
ALTER TABLE users ADD COLUMN avatar_url TEXT;
-- Drop column
ALTER TABLE users DROP COLUMN avatar_url;
-- Rename
ALTER TABLE users RENAME COLUMN status TO state;
-- Change type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- Drop table
DROP TABLE IF EXISTS users CASCADE;
Generated columns
-- VIRTUAL (Postgres 18 default) — tính khi đọc
ALTER TABLE products
ADD COLUMN price_with_tax NUMERIC(18, 2)
GENERATED ALWAYS AS (price * 1.1) VIRTUAL;
-- STORED — lưu vật lý, index được
ALTER TABLE products
ADD COLUMN price_with_tax_stored NUMERIC(18, 2)
GENERATED ALWAYS AS (price * 1.1) STORED;
3. DML
INSERT + RETURNING (cực kỳ tiện)
INSERT INTO users (email, age)
VALUES ('alice@x.com', 25)
RETURNING id, email, created_at;
Postgres 18 thêm OLD và NEW trong RETURNING:
-- 18+ — UPDATE log diff
UPDATE users
SET age = age + 1
WHERE id = 1
RETURNING OLD.age AS old_age, NEW.age AS new_age;
RETURNING là tính năng cực mạnh — viết audit log, get ID vừa insert, biết row nào bị UPDATE/DELETE. Tương đương OUTPUT của SQL Server.Multi-row INSERT
INSERT INTO products (name, price) VALUES
('Laptop', 32000000),
('Mouse', 800000),
('Keyboard', 2200000);
UPSERT — ON CONFLICT
-- INSERT, nếu trùng email thì UPDATE
INSERT INTO users (email, age)
VALUES ('alice@x.com', 26)
ON CONFLICT (email) DO UPDATE
SET age = EXCLUDED.age,
updated_at = NOW();
-- Hoặc DO NOTHING
INSERT INTO users (email, age)
VALUES ('alice@x.com', 26)
ON CONFLICT DO NOTHING;
INSERT ... ON CONFLICT (key) DO UPDATE. Đáp án này luôn được điểm.EXCLUDED = row đang định INSERT (giúp UPDATE bằng giá trị mới).ON CONFLICT DO SELECT — hành động thứ ba cho phép atomic get-or-create không dead tuple, nhanh hơn ~4x so với no-op UPDATE. Chi tiết tại mục 8.1.UPDATE
UPDATE products
SET price = price * 1.1, updated_at = NOW()
WHERE category_id = 1;
-- UPDATE từ JOIN (Postgres dùng FROM)
UPDATE products p
SET category_name = c.name
FROM categories c
WHERE c.id = p.category_id;
DELETE
DELETE FROM products WHERE is_active = false;
-- DELETE từ JOIN (USING)
DELETE FROM orders o
USING users u
WHERE u.id = o.user_id AND u.banned = true;
TRUNCATE
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY; -- reset sequence
TRUNCATE TABLE logs RESTART IDENTITY CASCADE; -- + truncate bảng FK reference
4. NULL handling
-- IS NULL / IS NOT NULL
SELECT * FROM users WHERE email IS NULL;
-- COALESCE — first non-null
SELECT name, COALESCE(phone, email, 'no-contact') FROM users;
-- NULLIF — null nếu 2 giá trị bằng
SELECT amount / NULLIF(divisor, 0) FROM calc;
5. Function thường dùng
String
SELECT
LENGTH('Hello'), -- 5
LOWER('ABC'), UPPER('abc'),
SUBSTRING('Hello' FROM 2 FOR 3), -- 'ell'
LEFT('Hello', 3), RIGHT('Hello', 2),
TRIM(' abc '),
REPLACE('abc', 'b', 'X'),
CONCAT('Hello', ' ', 'World'),
'Hello' || ' ' || 'World', -- || operator
STRING_AGG(name, ', ' ORDER BY name); -- aggregate
Date
SELECT
NOW(), -- TIMESTAMPTZ
CURRENT_DATE, CURRENT_TIME,
AGE(TIMESTAMP '1990-01-01'), -- interval
NOW() + INTERVAL '7 days',
NOW() - INTERVAL '1 month',
EXTRACT(YEAR FROM NOW()),
DATE_TRUNC('month', NOW()), -- truncate đến month
TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI');
JSON / JSONB
-- Access
SELECT
payload->'user' AS user_json, -- trả JSONB
payload->>'user' AS user_text, -- trả TEXT
payload->'tags'->0 AS first_tag, -- index array
payload->'tags'->>0 AS first_tag_text;
-- Existence
SELECT * FROM events WHERE payload ? 'type'; -- key tồn tại
SELECT * FROM events WHERE payload ?| ARRAY['type', 'name']; -- bất kỳ key
SELECT * FROM events WHERE payload ?& ARRAY['type', 'name']; -- tất cả key
-- Containment (rất hay dùng + index GIN tốt)
SELECT * FROM events WHERE payload @> '{"type": "click"}';
-- Modify (18+ có thêm nhiều function)
UPDATE events
SET payload = jsonb_set(payload, '{processed}', 'true')
WHERE id = 1;
-- Path
SELECT jsonb_path_query(payload, '$.tags[*]') FROM events;
CREATE INDEX idx_events_payload ON events USING GIN (payload);
-- Hoặc cho jsonb_path_ops nếu chỉ dùng @> operator (nhỏ hơn, nhanh hơn)
CREATE INDEX idx_events_payload ON events USING GIN (payload jsonb_path_ops);
Array
SELECT
ARRAY[1, 2, 3, 4],
ARRAY_LENGTH(ARRAY[1, 2, 3], 1), -- 3
ARRAY[1, 2] || ARRAY[3, 4], -- {1,2,3,4}
1 = ANY(ARRAY[1, 2, 3]), -- TRUE
ARRAY_AGG(name ORDER BY name);
-- Array trong WHERE
SELECT * FROM posts WHERE tags && ARRAY['vue', 'react']; -- overlap
SELECT * FROM posts WHERE tags @> ARRAY['vue']; -- contains
6. Variable & block (PL/pgSQL)
Postgres dùng PL/pgSQL thay vì T-SQL. Cú pháp khác hẳn:
DO $$
DECLARE
user_count INT;
BEGIN
SELECT COUNT(*) INTO user_count FROM users;
RAISE NOTICE 'Total users: %', user_count;
END $$;
$$ ... $$ là dollar quoting — string không cần escape, có thể chứa '.
7. Function & Stored Procedure
Function (return value)
CREATE OR REPLACE FUNCTION full_name(first TEXT, last TEXT)
RETURNS TEXT
LANGUAGE SQL IMMUTABLE
AS $$
SELECT first || ' ' || last;
$$;
SELECT full_name('Alice', 'Nguyen');
PL/pgSQL function với logic
CREATE OR REPLACE FUNCTION get_user_tier(uid BIGINT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
spent NUMERIC;
BEGIN
SELECT COALESCE(SUM(total), 0) INTO spent
FROM orders WHERE user_id = uid;
IF spent >= 100000000 THEN RETURN 'gold';
ELSIF spent >= 10000000 THEN RETURN 'silver';
ELSE RETURN 'bronze';
END IF;
END $$;
Procedure (11+, có thể COMMIT trong thân hàm)
CREATE OR REPLACE PROCEDURE transfer(from_id BIGINT, to_id BIGINT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
COMMIT;
END $$;
CALL transfer(1, 2, 1000000);
- Function: return value, không COMMIT/ROLLBACK trong thân.
- Procedure (Postgres 11+): không return value, có thể COMMIT/ROLLBACK trong thân, gọi bằng
CALL.
8. Cải tiến DML trong PostgreSQL 19
PostgreSQL 19 (ra mắt 2026) mang đến ba cải tiến DML quan trọng giúp code ngắn hơn, nhanh hơn và chuẩn SQL hơn. Nếu đã quen thuộc với PG 16-18, đây là những tính năng bạn phải biết cho năm 2026.
8.1 ON CONFLICT DO SELECT — Atomic get-or-create
Đây là hành động thứ ba sau DO NOTHING và DO UPDATE, giúp thực hiện atomic get-or-create chỉ trong một câu lệnh duy nhất — không race condition, không dead tuple.
-- PG 19 — lấy row nếu tồn tại, insert nếu chưa, luôn trả về kết quả
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO SELECT
RETURNING *;
So sánh các pattern get-or-create:
| Cách | Ưu điểm | Nhược điểm |
|---|---|---|
DO NOTHING + SELECT riêng | Đơn giản | Cần 2 câu lệnh, 2 round-trip |
DO UPDATE SET col = EXCLUDED.col (no-op update) | 1 câu lệnh | Tạo dead tuple, bloat, chậm vacuum |
DO SELECT (PG 19) | 1 câu lệnh, atomic, không dead tuple | Cần PG 19+ |
-- Pattern cũ (PG 18 trở về trước) — vẫn chạy nhưng tạo dead tuple không cần thiết
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET email = EXCLUDED.email -- dirty no-op
RETURNING *;
-- PG 19 — sạch hơn, nhanh hơn ~4x
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO SELECT
RETURNING *;
"
DO NOTHINGchỉ insert nếu không conflict — nếu conflict thì không return gì cả, bạn buộc phải SELECT lại để lấy row đã tồn tại.DO SELECTtrả về luôn row bị conflict, giúp bạn có kết quả sau đúng 1 câu lệnh. Đây là pattern atomic get-or-create chuẩn chỉ: không race condition (không ai xen giữa SELECT và INSERT được), không dead tuple (không phải UPDATE vô nghĩa), và nhanh hơn ~4x so với cách no-op update cũ."
DO NOTHING— khi bạn không cần row trả về (VD: log, dedup silent)DO UPDATE SET— khi bạn thực sự muốn update dữ liệu (VD: counter, merge)DO SELECT— khi bạn muốn lấy row về mà không cần update gì (VD: ensure tồn tại rồi đọc)
8.2 GROUP BY ALL — Nhóm theo mọi cột non-aggregate
-- PG 19 — tự động GROUP BY mọi non-aggregate column trong SELECT
SELECT department, role, count(*)
FROM employees
GROUP BY ALL;
Không cần liệt kê từng cột trong GROUP BY nữa. Khi thêm hoặc xóa cột trong SELECT, GROUP BY ALL tự cập nhật — không còn lỗi quên sửa GROUP BY.
-- Thêm cột location mà không cần sửa GROUP BY
SELECT department, role, location, count(*)
FROM employees
GROUP BY ALL; -- tự hiểu là GROUP BY department, role, location
-- Xóa cột cũng vậy — GROUP BY ALL tự điều chỉnh
SELECT department, count(*)
FROM employees
GROUP BY ALL; -- tự hiểu là GROUP BY department
"Không.
GROUP BY ALLđược planner expand ra danh sách cột tường minh tại parse time, kế hoạch thực thi giống hệt như viết tay. Nó là syntactic sugar — tiện lợi mà không phải trả giá. Lưu ý duy nhất: nếu bạn có column trong SELECT là aggregate expression lồng ghép, hãy kiểm tra lại xem planner có hiểu đúng ý bạn không."
8.3 FOR PORTION OF — Cập nhật dữ liệu temporal
Khi làm việc với dữ liệu lịch sử (slowly changing dimension, bảng giá theo thời gian, lương nhân viên qua các kỳ), FOR PORTION OF tự động chia row để chỉ cập nhật đúng khoảng thời gian cần thiết:
-- Bảng giá sản phẩm theo thời gian (temporal table)
CREATE TABLE product_prices (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_id INT NOT NULL,
price NUMERIC(18, 2) NOT NULL,
valid_range TSRANGE NOT NULL,
EXCLUDE USING GIST (product_id WITH =, valid_range WITH &&)
);
-- Dữ liệu ban đầu: giá 29.99 áp dụng cả năm 2025
INSERT INTO product_prices (product_id, price, valid_range)
VALUES (1, 29.99, '[2025-01-01, 2025-12-31)');
-- PG 19 — cập nhật giá cho Q3/2025, Postgres tự chia row
UPDATE product_prices
FOR PORTION OF valid_range FROM '2025-07-01' TO '2025-10-01'
SET price = 34.99
WHERE product_id = 1;
Postgres tự động làm gì? Row gốc valid_range = [2025-01-01, 2025-12-31) được tự động chia thành 3 row:
[2025-01-01, 2025-07-01)— giá cũ 29.99[2025-07-01, 2025-10-01)— giá mới 34.99[2025-10-01, 2025-12-31)— giá cũ 29.99
Không cần viết 3-4 câu INSERT/DELETE/UPDATE thủ công như trước PG 19.
"Dùng khi bạn có dữ liệu phiên bản theo thời gian (temporal/bitemporal tables) — bảng giá sản phẩm, lương nhân viên, trạng thái hợp đồng. Trước PG 19, để update một khoảng thời gian bạn phải: (1) SELECT row cũ, (2) UPDATE row cũ thu hẹp range, (3) INSERT row mới cho khoảng update, (4) INSERT row mới cho phần còn lại.
FOR PORTION OFlàm tất cả trong 1 câu — tránh race condition và code đơn giản hơn nhiều. Đây là chuẩn SQL:2011 temporal — trước PG 19 chỉ có DB2 và Oracle hỗ trợ đầy đủ."
8.4 IGNORE NULLS cho Window Functions
Các window function lead(), lag(), first_value(), last_value(), nth_value() giờ hỗ trợ IGNORE NULLS để bỏ qua giá trị NULL khi duyệt qua partition:
-- PG 19 — forward-fill: điền giá trị sensor gần nhất không NULL
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;
So sánh hành vi:
RESPECT NULLS (mặc định) | IGNORE NULLS | |
|---|---|---|
| Gặp NULL | Trả về NULL | Bỏ qua, lấy giá trị non-NULL gần nhất |
| Use case | Giữ nguyên dữ liệu gốc | Forward-fill, backward-fill, gap-filling |
RESPECT NULLS (giữ nguyên NULL — tương đương hành vi cũ). Chi tiết về window functions xem tại bài Truy vấn nâng cao.1. Giới thiệu và Cài đặt
PostgreSQL 19 (Beta 1 — 04/06/2026) — landmark release với SQL/PGQ graph queries, ON CONFLICT DO SELECT, GROUP BY ALL, temporal data, cùng nền tảng vững chắc từ PG 18.
3. Truy vấn nâng cao
JOIN, LATERAL, CTE recursive, window function, MVCC, transaction, isolation, EXPLAIN — gốc Postgres.