Kiểu dữ liệu chuyên sâu

← Ràng buộc - PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, NOT NULL, DEFAULT | → Biểu thức điều kiện - CASE, COALESCE, NULLIF, CAST

Bạn sẽ học được

Chọn đúng kiểu dữ liệu cho từng bài toán — quyết định ảnh hưởng tới độ chính xác, dung lượng và hiệu năng suốt vòng đời ứng dụng.


1. Bảng tra cứu nhanh

Nhu cầuDùngKhông dùng
Khóa chínhBIGINT IDENTITY hoặc UUIDINT (dễ tràn)
Tiền tệNUMERIC(15,2)FLOAT, MONEY
Chuỗi văn bảnTEXT hoặc VARCHAR(n)CHAR(n)
Thời điểmTIMESTAMPTZTIMESTAMP
Ngày (không giờ)DATETIMESTAMP
Đúng/saiBOOLEANCHAR(1), INT
Dữ liệu bán cấu trúcJSONBJSON, TEXT
Tập giá trị cố địnhTEXT + CHECK hoặc bảng tra cứuENUM (khó sửa)
Đo lường khoa họcDOUBLE PRECISIONNUMERIC (chậm)

2. Kiểu số

2.1. Số nguyên

KiểuBytePhạm viBí danh
SMALLINT2−32.768 … 32.767INT2
INTEGER4±2,1 tỷINT, INT4
BIGINT8±9,2 × 10¹⁸INT8

Chia số nguyên bị cắt cụt

SELECT 7 / 2;              -- 3   (không phải 3.5)
SELECT 7 / 2.0;            -- 3.5
SELECT 7::numeric / 2;     -- 3.5
SELECT 7 * 1.0 / 2;        -- 3.5

Đây là nguyên nhân số 1 gây sai số trong báo cáo thống kê.

2.2. NUMERIC / DECIMAL — chính xác tuyệt đối

NUMERIC(precision, scale)
-- precision: tổng số chữ số
-- scale: số chữ số sau dấu thập phân
NUMERIC(15, 2)   -- tối đa 13 chữ số phần nguyên + 2 chữ số thập phân
NUMERIC(5, 2)    -- 999.99 là lớn nhất
NUMERIC          -- không giới hạn (lưu chính xác mọi giá trị nhập vào)

Luôn dùng NUMERIC cho tiền tệ:

SELECT 0.1::float + 0.2::float;        -- 0.30000000000000004  ❌
SELECT 0.1::numeric + 0.2::numeric;    -- 0.3                  ✅
 
SELECT 0.1::float + 0.2::float = 0.3;  -- false  😱

Số thực dấu phẩy động không biểu diễn được chính xác 0.1 trong hệ nhị phân. Với tiền bạc, sai số tích lũy sẽ thành vấn đề pháp lý.

2.3. Số thực dấu phẩy động

KiểuByteĐộ chính xácBí danh
REAL4~6 chữ sốFLOAT4
DOUBLE PRECISION8~15 chữ sốFLOAT8, FLOAT

Nhanh hơn NUMERIC nhiều lần vì dùng phần cứng FPU. Dùng cho: tọa độ, nhiệt độ, kết quả đo lường, tính toán khoa học — nơi sai số nhỏ chấp nhận được.

Giá trị đặc biệt:

SELECT 'Infinity'::float8, '-Infinity'::float8, 'NaN'::float8;
SELECT 'NaN'::float8 = 'NaN'::float8;   -- true trong PostgreSQL

2.4. Kiểu MONEY — đừng dùng

MONEY phụ thuộc lc_monetary của phiên, không chuyển đổi tiền tệ được, và độ chính xác cố định. Tài liệu chính thức PostgreSQL cũng khuyến cáo tránh. Dùng NUMERIC(15,2) + cột currency VARCHAR(3).


3. Kiểu chuỗi

KiểuMô tả
CHAR(n)Độ dài cố định, đệm khoảng trắng
VARCHAR(n)Giới hạn n ký tự
VARCHARKhông giới hạn
TEXTKhông giới hạn

3.1. TEXT vs VARCHAR(n) — hiệu năng giống hệt nhau

Trong PostgreSQL, TEXT và VARCHAR(n) lưu trữ y hệt nhau

Không có khác biệt về tốc độ hay dung lượng. VARCHAR(n) chỉ thêm một phép kiểm tra độ dài.

Điều này khác với SQL Server hay Oracle, nơi TEXT/CLOB là kiểu đặc biệt chậm hơn.

Nên dùng gì?

  • TEXT khi không có giới hạn nghiệp vụ rõ ràng.
  • VARCHAR(n) khi giới hạn là một quy tắc thật (mã bưu chính 5 ký tự, ISBN 13 ký tự).
  • Nếu chỉ muốn “phòng xa”, hãy dùng TEXT CHECK (length(x) <= 255) — sửa CHECK dễ hơn nhiều so với ALTER TYPE.

3.2. CHAR(n) — tránh dùng

CREATE TABLE t (ma CHAR(10));
INSERT INTO t VALUES ('ABC');
SELECT length(ma), ma || '|' FROM t;
-- 3 | 'ABC       |'   ← có 7 khoảng trắng đệm khi nối chuỗi

CHAR đệm khoảng trắng, gây bất ngờ khi so sánh và nối chuỗi, mà không tiết kiệm dung lượng.

3.3. Hàm chuỗi hay dùng

length('xin chào')              -- 8 (số ký tự)
octet_length('xin chào')        -- 10 (số byte, tiếng Việt 2-3 byte/ký tự)
upper('abc') / lower('ABC')
initcap('nguyễn văn an')        -- 'Nguyễn Văn An'
trim(' abc ') / ltrim / rtrim
lpad('5', 3, '0')               -- '005'
substring('PostgreSQL', 1, 4)   -- 'Post'
replace('a-b-c', '-', '_')      -- 'a_b_c'
split_part('a,b,c', ',', 2)     -- 'b'
string_to_array('a,b,c', ',')   -- {a,b,c}
position('gre' IN 'PostgreSQL') -- 5
left('PostgreSQL', 4)           -- 'Post'
right('PostgreSQL', 3)          -- 'SQL'
repeat('ab', 3)                 -- 'ababab'
reverse('abc')                  -- 'cba'
format('Xin chào %s, bạn %s tuổi', 'An', 25)

3.4. Tiếng Việt và Unicode

-- Bỏ dấu tiếng Việt (cần extension unaccent)
CREATE EXTENSION IF NOT EXISTS unaccent;
SELECT unaccent('Nguyễn Văn Ánh');   -- 'Nguyen Van Anh'
 
-- Tìm kiếm không dấu, không phân biệt hoa thường
CREATE INDEX idx_ten_unaccent ON nguoi_dung (lower(unaccent(ho_ten)));
 
SELECT * FROM nguoi_dung
WHERE lower(unaccent(ho_ten)) LIKE '%nguyen%';

unaccent không phải IMMUTABLE mặc định

Để dùng trong index, cần bọc trong hàm IMMUTABLE:

CREATE FUNCTION f_unaccent(text) RETURNS text AS
$$ SELECT unaccent('unaccent', $1) $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
 
CREATE INDEX idx_ten ON nguoi_dung (lower(f_unaccent(ho_ten)));

4. BOOLEAN

CREATE TABLE t (dang_hoat_dong BOOLEAN NOT NULL DEFAULT true);

Giá trị chấp nhận:

TRUEFALSE
TRUE, true, 't', 'true', 'y', 'yes', 'on', '1'FALSE, false, 'f', 'false', 'n', 'no', 'off', '0'
SELECT * FROM t WHERE dang_hoat_dong;         -- gọn hơn = true
SELECT * FROM t WHERE NOT dang_hoat_dong;
SELECT * FROM t WHERE dang_hoat_dong IS NOT TRUE;   -- gồm cả NULL

BOOLEAN có 3 trạng thái: true, false, NULL. Thêm NOT NULL nếu chỉ muốn 2.


5. Ngày và giờ

KiểuByteMô tả
DATE4Chỉ ngày
TIME8Chỉ giờ
TIMESTAMP8Ngày + giờ, không múi giờ
TIMESTAMPTZ8Ngày + giờ có múi giờ
INTERVAL16Khoảng thời gian

5.1. TIMESTAMPTZ — gần như luôn là lựa chọn đúng

Tên gọi gây hiểu nhầm

TIMESTAMPTZ không lưu múi giờ. Nó lưu thời điểm tuyệt đối (UTC) và tự chuyển đổi sang múi giờ của phiên khi hiển thị.

TIMESTAMP (không TZ) lưu “chữ số trên đồng hồ” mà không biết đó là đồng hồ ở đâu — hầu như luôn là nguồn gốc của bug.

SET timezone = 'Asia/Ho_Chi_Minh';
SELECT now();                    -- 2026-07-30 14:30:00+07
 
SET timezone = 'UTC';
SELECT now();                    -- 2026-07-30 07:30:00+00
-- Cùng một thời điểm, hiển thị khác nhau

Chuyển đổi múi giờ:

SELECT now() AT TIME ZONE 'Asia/Ho_Chi_Minh';
SELECT '2026-07-30 10:00:00'::timestamp AT TIME ZONE 'UTC';
 
-- Xem danh sách múi giờ
SELECT * FROM pg_timezone_names WHERE name LIKE 'Asia/H%';

Khi nào dùng TIMESTAMP không TZ? Chỉ khi thời gian thực sự không gắn với thời điểm tuyệt đối: giờ mở cửa cửa hàng (“9:00 sáng giờ địa phương, ở đâu cũng vậy”), lịch phát sóng truyền hình.

5.2. Hàm ngày giờ hay dùng

now()                     -- thời điểm hiện tại (đầu giao dịch)
current_date              -- ngày hôm nay
current_time              -- giờ hiện tại
clock_timestamp()         -- thời gian thực, thay đổi trong cùng câu lệnh
 
-- Trích xuất thành phần
EXTRACT(YEAR    FROM ngay)
EXTRACT(MONTH   FROM ngay)
EXTRACT(DAY     FROM ngay)
EXTRACT(DOW     FROM ngay)    -- 0 = Chủ nhật … 6 = Thứ bảy
EXTRACT(ISODOW  FROM ngay)    -- 1 = Thứ hai … 7 = Chủ nhật
EXTRACT(QUARTER FROM ngay)
EXTRACT(EPOCH   FROM ngay)    -- Unix timestamp
 
-- Cắt về đầu kỳ (rất hữu ích khi nhóm)
date_trunc('month', ngay)     -- 2026-07-01 00:00:00
date_trunc('week',  ngay)     -- đầu tuần (thứ Hai)
date_trunc('day',   ngay)
date_trunc('hour',  ngay)
 
-- Định dạng
to_char(now(), 'DD/MM/YYYY')            -- '30/07/2026'
to_char(now(), 'YYYY-MM-DD HH24:MI:SS')
to_char(now(), 'Day, DD Month YYYY')
 
-- Phân tích chuỗi
to_date('30/07/2026', 'DD/MM/YYYY')
to_timestamp('30/07/2026 14:30', 'DD/MM/YYYY HH24:MI')
 
-- Tuổi
age(timestamp '1990-05-15')        -- '36 years 2 mons 15 days'
age('2026-07-30', '1990-05-15')

5.3. INTERVAL

SELECT now() + INTERVAL '1 day';
SELECT now() - INTERVAL '2 hours 30 minutes';
SELECT now() + INTERVAL '3 months';
SELECT INTERVAL '1 year 2 months 3 days';
 
-- Cộng số ngày trực tiếp với DATE
SELECT current_date + 7;              -- 7 ngày sau
SELECT current_date - DATE '2026-01-01';   -- số ngày chênh lệch (integer)
 
-- Tính khoảng thời gian
SELECT return_date - rental_date AS thoi_gian_thue
FROM rental WHERE return_date IS NOT NULL LIMIT 5;
 
-- Chuyển interval sang số
SELECT EXTRACT(EPOCH FROM INTERVAL '2 hours') / 3600;    -- 2
SELECT EXTRACT(DAY FROM (return_date - rental_date)) FROM rental LIMIT 1;

5.4. So sánh khoảng thời gian — nhắc lại

Không dùng BETWEEN với TIMESTAMP

-- ❌ Mất gần trọn ngày 31
WHERE payment_date BETWEEN '2007-03-01' AND '2007-03-31'
 
-- ✅ Nửa khoảng mở
WHERE payment_date >= '2007-03-01' AND payment_date < '2007-04-01'
 
-- ✅ Hoặc dùng date_trunc
WHERE date_trunc('month', payment_date) = '2007-03-01'

Lưu ý: date_trunc trong WHERE sẽ vô hiệu hóa index trên payment_date. Với bảng lớn, dạng nửa khoảng mở nhanh hơn nhiều.

→ Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL


6. UUID

CREATE TABLE t (
   id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);

gen_random_uuid() có sẵn từ PostgreSQL 13 (trước đó cần extension pgcrypto).

6.1. UUID vs BIGINT làm khóa chính

BIGINT IDENTITYUUID
Dung lượng8 byte16 byte
Sinh ở client được❌✅
Lộ số lượng bản ghi⚠️ Có (id=1000 → biết có ~1000 dòng)❌ Không
Hiệu năng indexTốt (tuần tự)⚠️ Kém — ngẫu nhiên gây phân mảnh B-tree
Gộp dữ liệu nhiều nguồnXung độtKhông xung đột

UUID v7 — giải pháp cho vấn đề phân mảnh

UUID v7 sắp xếp được theo thời gian, nên chèn tuần tự vào index như BIGINT, đồng thời giữ tính duy nhất toàn cục. PostgreSQL 18 có uuidv7() sẵn; phiên bản cũ hơn có thể sinh từ ứng dụng.


7. ARRAY

PostgreSQL cho phép mọi kiểu dữ liệu tạo thành mảng.

CREATE TABLE bai_viet (
   id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
   tieu_de TEXT NOT NULL,
   the TEXT[],
   diem INT[]
);
 
INSERT INTO bai_viet (tieu_de, the, diem)
VALUES ('Học PostgreSQL', ARRAY['sql','database','postgres'], ARRAY[9,8,10]),
       ('Học Python',     '{"python","lap-trinh"}',           '{7,9}');

7.1. Truy cập và toán tử

SELECT the[1] FROM bai_viet;              -- ⚠️ mảng đánh số từ 1
SELECT the[1:2] FROM bai_viet;            -- lát cắt
SELECT array_length(the, 1) FROM bai_viet;
SELECT cardinality(the) FROM bai_viet;    -- tổng số phần tử
 
-- Tìm kiếm
WHERE 'sql' = ANY(the)                    -- chứa 'sql'
WHERE the @> ARRAY['sql','database']      -- chứa TẤT CẢ
WHERE the && ARRAY['sql','python']        -- chứa ÍT NHẤT MỘT
WHERE NOT ('sql' = ANY(the))              -- không chứa
 
-- Thao tác
SELECT array_append(the, 'moi') FROM bai_viet;
SELECT array_remove(the, 'sql') FROM bai_viet;
SELECT array_to_string(the, ', ') FROM bai_viet;
SELECT array_position(the, 'database') FROM bai_viet;

7.2. Khai triển mảng thành dòng

SELECT id, tieu_de, unnest(the) AS tung_the FROM bai_viet;
 
-- Cách chuẩn hơn (kiểm soát được join)
SELECT b.id, b.tieu_de, t
FROM bai_viet b, unnest(b.the) AS t;

7.3. Index cho mảng

CREATE INDEX idx_bai_viet_the ON bai_viet USING GIN (the);
-- Bây giờ @>, &&, = ANY() dùng được index

Khi nào không dùng mảng

Mảng tiện nhưng phá vỡ chuẩn hóa. Đừng dùng khi:

  • Cần khóa ngoại tới từng phần tử.
  • Cần thêm thuộc tính cho từng phần tử (thời gian gắn thẻ, người gắn).
  • Cần cập nhật từng phần tử thường xuyên (phải ghi lại cả mảng).

Trong những trường hợp đó, bảng quan hệ nhiều-nhiều vẫn tốt hơn.


8. JSON và JSONB

PostgreSQL có hai kiểu JSON:

Đặc điểmJSONJSONB
Lưu trữVăn bản nguyên vănNhị phân đã phân tích
Dung lượngLớn hơn (giữ khoảng trắng)Nhỏ hơn
Tốc độ ghiNhanh hơnChậm hơn chút
Tốc độ đọc/truy vấnChậm (phân tích mỗi lần)Nhanh
IndexChỉ full-textGIN — mạnh
Giữ thứ tự khóa✅❌
Khóa trùngGiữ giá trị cuốiLoại bỏ
Toán tử phong phúHạn chếĐầy đủ

Gần như luôn dùng JSONB

Chỉ dùng JSON khi bạn cần lưu nguyên văn tài liệu gốc (ví dụ để kiểm tra chữ ký số) hoặc thứ tự khóa quan trọng.

8.1. Lưu đối tượng JSON

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    properties JSONB
);
 
INSERT INTO products(name, properties)
VALUES ('Ink Fusion T-Shirt',    '{"color": "white", "size": ["S","M","L","XL"]}'),
       ('ThreadVerse T-Shirt',   '{"color": "black", "size": ["S","M","L","XL"]}'),
       ('Design Dynamo T-Shirt', '{"color": "blue",  "size": ["S","M","L","XL"]}')
RETURNING *;
 id |         name          |                    properties
----+-----------------------+---------------------------------------------------
  1 | Ink Fusion T-Shirt    | {"size": ["S", "M", "L", "XL"], "color": "white"}
  2 | ThreadVerse T-Shirt   | {"size": ["S", "M", "L", "XL"], "color": "black"}
  3 | Design Dynamo T-Shirt | {"size": ["S", "M", "L", "XL"], "color": "blue"}

Chú ý: JSONB đã sắp xếp lại thứ tự khóa — size đứng trước color dù ta nhập ngược lại.

8.2. Toán tử trích xuất

-- -> trả về JSON (giữ dấu nháy kép)
SELECT id, name, properties -> 'color' AS color FROM products;
 id |         name          |  color
----+-----------------------+---------
  1 | Ink Fusion T-Shirt    | "white"
  2 | ThreadVerse T-Shirt   | "black"
  3 | Design Dynamo T-Shirt | "blue"
-- ->> trả về TEXT (bỏ dấu nháy kép)
SELECT id, name, properties ->> 'color' AS color FROM products;
 id |         name          | color
----+-----------------------+-------
  1 | Ink Fusion T-Shirt    | white
  2 | ThreadVerse T-Shirt   | black
  3 | Design Dynamo T-Shirt | blue

Lọc theo giá trị JSON:

SELECT id, name, properties ->> 'color' AS color
FROM products
WHERE properties ->> 'color' IN ('black', 'white');
 id |        name         | color
----+---------------------+-------
  1 | Ink Fusion T-Shirt  | white
  2 | ThreadVerse T-Shirt | black

8.3. Bảng toán tử đầy đủ

Toán tửÝ nghĩaVí dụ
->Lấy trường, trả về JSONdata -> 'ten'
->>Lấy trường, trả về textdata ->> 'ten'
#>Lấy theo đường dẫn, trả JSONdata #> '{dia_chi,thanh_pho}'
#>>Lấy theo đường dẫn, trả textdata #>> '{dia_chi,thanh_pho}'
@>Chứadata @> '{"mau":"do"}'
<@Bị chứa trong
?Có khóa này khôngdata ? 'email'
?|Có ít nhất một khóadata ?| ARRAY['a','b']
?&Có tất cả các khóadata ?& ARRAY['a','b']
||Nối/gộpdata || '{"moi":1}'
-Xóa khóadata - 'khoa'
#-Xóa theo đường dẫndata #- '{a,b}'
@?JSONPath tồn tạidata @? '$.items[*] ? (@.gia > 100)'

8.4. Lưu mảng JSON

CREATE TABLE contacts (
   id SERIAL PRIMARY KEY,
   name VARCHAR(255) NOT NULL,
   phones JSONB
);
 
INSERT INTO contacts(name, phones)
VALUES ('John Doe','["408-111-2222", "408-111-2223"]'),
       ('Jane Doe','["212-111-2222", "212-111-2223"]');
 
-- Lấy phần tử theo chỉ số (mảng JSON đánh số từ 0!)
SELECT name, phones ->> 0 AS "so_chinh" FROM contacts;
   name   |  so_chinh
----------+--------------
 John Doe | 408-111-2222
 Jane Doe | 212-111-2222

Mảng JSON đánh số từ 0, mảng PostgreSQL đánh số từ 1

Nguồn nhầm lẫn thường gặp khi trộn hai loại.

8.5. Hàm JSON hữu ích

-- Khai triển thành dòng
SELECT jsonb_array_elements('[1,2,3]'::jsonb);
SELECT * FROM jsonb_each('{"a":1,"b":2}'::jsonb);
SELECT jsonb_object_keys('{"a":1,"b":2}'::jsonb);
 
-- Tạo JSON từ dữ liệu quan hệ
SELECT jsonb_build_object('id', film_id, 'ten', title) FROM film LIMIT 3;
SELECT jsonb_agg(jsonb_build_object('id', film_id, 'ten', title))
FROM film LIMIT 3;
SELECT row_to_json(f) FROM (SELECT film_id, title FROM film LIMIT 3) f;
 
-- Cập nhật một trường
UPDATE products
SET properties = jsonb_set(properties, '{color}', '"red"')
WHERE id = 1;
 
-- Thêm trường (gộp)
UPDATE products
SET properties = properties || '{"gia": 199000}'::jsonb
WHERE id = 1;
 
-- Xóa trường
UPDATE products SET properties = properties - 'gia' WHERE id = 1;
 
-- Chuyển JSON thành bảng
SELECT * FROM jsonb_to_recordset('[{"a":1,"b":"x"},{"a":2,"b":"y"}]'::jsonb)
   AS t(a INT, b TEXT);

8.6. Index cho JSONB

-- GIN mặc định: hỗ trợ @>, ?, ?|, ?&
CREATE INDEX idx_props ON products USING GIN (properties);
 
-- GIN jsonb_path_ops: nhỏ hơn, nhanh hơn, chỉ hỗ trợ @>
CREATE INDEX idx_props_path ON products USING GIN (properties jsonb_path_ops);
 
-- B-tree trên một trường cụ thể (nhanh nhất cho trường đó)
CREATE INDEX idx_color ON products ((properties ->> 'color'));

->> không dùng được index GIN

WHERE properties ->> 'color' = 'red'    -- ❌ không dùng GIN
WHERE properties @> '{"color":"red"}'   -- ✅ dùng được GIN

Nếu bạn thường lọc bằng ->>, hãy tạo B-tree trên biểu thức đó.

8.7. Khi nào dùng JSONB?

Nên dùng khi:

  • Thuộc tính thay đổi theo từng bản ghi (thông số kỹ thuật sản phẩm).
  • Lưu payload từ API bên ngoài.
  • Cấu hình, metadata, log.
  • Schema chưa ổn định trong giai đoạn phát triển.

Không nên khi:

  • Dữ liệu có cấu trúc rõ ràng, ổn định → dùng cột thường.
  • Cần khóa ngoại, ràng buộc, hoặc join thường xuyên trên trường đó.
  • Cần cập nhật một trường nhỏ liên tục — mỗi lần update ghi lại toàn bộ JSONB.

Mẫu lai thường tốt nhất

CREATE TABLE san_pham (
   id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
   ten TEXT NOT NULL,              -- cột thường: hay dùng, cần index
   gia NUMERIC(12,2) NOT NULL,     -- cột thường: cần tính toán
   thuoc_tinh JSONB                -- JSONB: thông số linh hoạt
);

9. ENUM

CREATE TYPE trang_thai_don AS ENUM ('cho', 'dang_giao', 'hoan_thanh', 'huy');
 
CREATE TABLE don_hang (
   id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
   trang_thai trang_thai_don NOT NULL DEFAULT 'cho'
);

Ưu điểm: lưu trữ nhỏ (4 byte), sắp xếp theo thứ tự khai báo (không phải bảng chữ cái).

SELECT * FROM don_hang ORDER BY trang_thai;
-- cho → dang_giao → hoan_thanh → huy

9.1. Sửa đổi enum

ALTER TYPE trang_thai_don ADD VALUE 'hoan_tien';
ALTER TYPE trang_thai_don ADD VALUE 'dang_dong_goi' BEFORE 'dang_giao';
ALTER TYPE trang_thai_don RENAME VALUE 'cho' TO 'cho_xu_ly';

Không thể xóa giá trị khỏi enum

Muốn xóa, bạn phải tạo type mới, chuyển đổi cột, xóa type cũ — thao tác đòi hỏi khóa bảng:

ALTER TYPE trang_thai_don RENAME TO trang_thai_don_cu;
CREATE TYPE trang_thai_don AS ENUM ('cho', 'hoan_thanh');
ALTER TABLE don_hang ALTER COLUMN trang_thai
   TYPE trang_thai_don USING trang_thai::text::trang_thai_don;
DROP TYPE trang_thai_don_cu;

9.2. Ba lựa chọn thay thế

CáchƯuNhược
ENUMNhỏ gọn, thứ tự tùy chỉnhKhó xóa giá trị, khó thêm metadata
TEXT + CHECKDễ sửa (DROP/ADD CONSTRAINT)Tốn dung lượng hơn
Bảng tra cứu + FKLinh hoạt nhất, thêm được mô tả/thứ tự/màu sắcCần join

Thực tế: dùng bảng tra cứu khi danh sách có thể thay đổi hoặc cần thuộc tính kèm theo; dùng TEXT + CHECK cho danh sách nhỏ, ổn định.


10. Các kiểu khác

10.1. hstore — key-value phẳng

CREATE EXTENSION IF NOT EXISTS hstore;
 
CREATE TABLE t (thuoc_tinh HSTORE);
INSERT INTO t VALUES ('"mau" => "do", "size" => "L"');
 
SELECT thuoc_tinh -> 'mau' FROM t;
SELECT * FROM t WHERE thuoc_tinh ? 'mau';

Về cơ bản JSONB đã thay thế hstore (chỉ hỗ trợ chuỗi phẳng, không lồng nhau). Dùng hstore chỉ khi bảo trì code cũ.

10.2. Kiểu khoảng (range)

CREATE TABLE gia_khuyen_mai (
   san_pham_id INT,
   khoang_ngay DATERANGE,
   gia NUMERIC(12,2)
);
 
INSERT INTO gia_khuyen_mai VALUES (1, '[2026-08-01, 2026-08-31]', 99000);
 
-- Kiểm tra chứa
SELECT * FROM gia_khuyen_mai WHERE khoang_ngay @> current_date;
 
-- Kiểm tra giao nhau
SELECT * FROM gia_khuyen_mai WHERE khoang_ngay && '[2026-08-15,2026-09-15]'::daterange;

Các kiểu: INT4RANGE, INT8RANGE, NUMRANGE, TSRANGE, TSTZRANGE, DATERANGE.

Kết hợp với EXCLUDE để chống chồng lấn → Ràng buộc - PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, NOT NULL, DEFAULT

10.3. BYTEA — dữ liệu nhị phân

CREATE TABLE tep (id INT, noi_dung BYTEA);
INSERT INTO tep VALUES (1, '\x48656c6c6f');
SELECT encode(noi_dung, 'hex'), encode(noi_dung, 'base64') FROM tep;

Đừng lưu file lớn trong CSDL

Lưu file trên object storage (S3, MinIO) và chỉ giữ đường dẫn trong CSDL. Lưu ảnh/video trong BYTEA làm phình backup, chậm replication, tốn bộ nhớ đệm.

10.4. XML

CREATE TABLE t (du_lieu XML);
INSERT INTO t VALUES ('<sach><ten>PostgreSQL</ten></sach>');
SELECT xpath('/sach/ten/text()', du_lieu) FROM t;

Nếu bạn có lựa chọn, JSONB tốt hơn về mọi mặt.

10.5. Kiểu tự định nghĩa

DOMAIN — kiểu có sẵn + ràng buộc:

CREATE DOMAIN email_vn AS VARCHAR(255)
   CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
 
CREATE DOMAIN so_duong AS NUMERIC CHECK (VALUE > 0);
 
CREATE TABLE t (
   email email_vn NOT NULL,
   gia so_duong NOT NULL
);

Ràng buộc được tái sử dụng ở mọi bảng — nhất quán hơn nhiều so với chép CHECK khắp nơi.

TYPE — kiểu tổng hợp:

CREATE TYPE dia_chi AS (
   duong TEXT,
   phuong TEXT,
   quan TEXT,
   thanh_pho TEXT
);
 
CREATE TABLE khach (id INT, noi_o dia_chi);
 
INSERT INTO khach VALUES (1, ROW('123 Lê Lợi','Bến Nghé','Quận 1','TP.HCM'));
SELECT (noi_o).thanh_pho FROM khach;

Dấu ngoặc quanh (noi_o) là bắt buộc khi truy cập trường.


11. Bài tập

  1. Thiết kế bảng giao_dich lưu tiền tệ chính xác, thời điểm có múi giờ, và mã tiền tệ.
  2. Chứng minh sự khác biệt giữa NUMERIC và FLOAT khi cộng 0.1 + 0.2 mười lần.
  3. Tạo bảng bai_viet với cột thẻ dạng mảng; tìm bài viết chứa cả 'sql' và 'postgres'.
  4. Tạo bảng sản phẩm với JSONB; tìm sản phẩm có color = 'blue' bằng cả ->> và @>.
  5. So sánh EXPLAIN của hai cách lọc ở bài 4 khi đã tạo index GIN.
  6. Tạo DOMAIN cho số điện thoại Việt Nam và dùng nó trong 2 bảng khác nhau.
  7. Nhóm doanh thu payment theo tuần bằng date_trunc.
  8. Cho biết sự khác nhau giữa now() và clock_timestamp() trong cùng một câu lệnh.

12. Tóm tắt

  • Tiền tệ dùng NUMERIC, không bao giờ dùng FLOAT hay MONEY.
  • Chia số nguyên bị cắt cụt — nhớ ::numeric.
  • TEXT và VARCHAR(n) lưu trữ giống hệt nhau trong PostgreSQL; tránh CHAR(n).
  • Dùng TIMESTAMPTZ mặc định; nó lưu UTC và tự chuyển theo múi giờ phiên.
  • Không dùng BETWEEN với TIMESTAMP — dùng >= đầu AND < đầu_kỳ_sau.
  • Mảng PostgreSQL đánh số từ 1, mảng JSON từ 0.
  • Dùng JSONB thay JSON; @> dùng được index GIN, ->> thì không.
  • ENUM khó xóa giá trị — cân nhắc TEXT + CHECK hoặc bảng tra cứu.
  • DOMAIN giúp tái sử dụng ràng buộc nhất quán trên nhiều bảng.
  • Đừng lưu file lớn trong BYTEA — lưu đường dẫn tới object storage.

← Ràng buộc - PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, NOT NULL, DEFAULT | → Biểu thức điều kiện - CASE, COALESCE, NULLIF, CAST