Data types — vì sao TIMESTAMP có timezone cứu deploy multi-region
Kiểu chuẩn SQL: INTEGER/BIGINT, VARCHAR, DECIMAL/NUMERIC, DATE/TIMESTAMP, BOOLEAN. 4 type pitfall production. Dialect: auto-increment, timezone, JSON, array.
TL;DR: Type là contract giữa schema và application — chọn đúng từ đầu loại bỏ cả một lớp bug runtime. Kiểu chuẩn SQL cốt lõi: INTEGER/BIGINT cho ID và counter, DECIMAL/NUMERIC cho tiền (KHÔNG FLOAT), VARCHAR/CHAR cho chuỗi, DATE/TIME/TIMESTAMP cho thời gian, BOOLEAN cho true/false. 4 pitfall lặp lại ở mọi engine: timestamp không lưu timezone (bug multi-region), float cho tiền (sai số tích lũy), độ dài chuỗi cap quá sớm, lưu số dưới dạng text. Chi tiết auto-increment, timezone, JSON, array là dialect — mỗi engine làm khác nhau.
Team TaskFlow vừa deploy lên hai region: server EU ở Frankfurt, server US ở Virginia. User EU nhìn thấy task due 2026-05-04 00:00, user US nhìn thấy cùng row đó với due 2026-05-03 17:00. Cùng một task — hai người thấy hai giá trị khác nhau. Bug ticket đầu tiên: "task chưa due hay đã due?" Mọi thứ chạy tốt trên local (một server, một timezone).
Root cause: column due_at khai báo là TIMESTAMP không kèm timezone. Server Frankfurt store thời gian local của server, không convert khi user US đọc. Fix một dòng DDL: đổi sang TIMESTAMP WITH TIME ZONE. Đây là 1 trong 4 type pitfall phổ biến nhất production. Bài này map type catalog theo chuẩn SQL và giải thích tại sao mỗi type tồn tại — không phải syntax dump cho một engine.
1. Analogy — Type là contract dữ liệu
Type là cam kết giữa schema và application: "column này chứa đúng loại dữ liệu này, không có ngoại lệ". Hình dung ổ cắm điện — ổ 2 chân và ổ 3 chân không cắm lẫn nhau được, không phải vì kỹ thuật không cho phép mà vì contract về an toàn. Type constraint trong database hoạt động tương tự: ngăn dữ liệu sai loại vào từ đầu thay vì để bug xảy ra runtime.
| Ổ cắm điện | SQL type |
|---|---|
| Ổ 2 chân / 3 chân | Loại type (TEXT/VARCHAR, INTEGER, TIMESTAMP...) |
| Ổ không nhận phích không đúng chuẩn | Database reject INSERT sai type |
| Cắm sai loại điện → hỏng thiết bị | Store sai type → silent bug (float tiền, no-TZ timestamp) |
| Một ổ chỉ nhận một chuẩn | Một column chỉ nhận đúng một type |
| Adapter chuyển đổi chuẩn | Type cast trong SQL (CAST(x AS INTEGER)) |
| Adapter kém chất lượng → mất điện | Cast không an toàn → runtime error hoặc data loss |
Chọn type đúng từ đầu = ít bug nhất, không cần validate runtime. Mismatch type không luôn gây error ngay — đôi khi là silent bug (float tiền, timestamp no-TZ) mà bạn chỉ phát hiện khi production chạy thật.
2. Type catalog chuẩn SQL — tổng quan
Chuẩn SQL (ISO/IEC 9075) định nghĩa một tập type cốt lõi mà gần như mọi RDBMS đều hỗ trợ. Bảng dưới nhóm các type production matter theo chuẩn — bài này đi sâu vào những type hay gặp nhất. Tên type cụ thể có thể khác nhẹ giữa engine (nêu trong cột "Note dialect").
| Group | Type chuẩn SQL | Use case | Note dialect |
|---|---|---|---|
| Số nguyên | SMALLINT, INTEGER, BIGINT | Counter, ID, measurement | Hầu hết engine giống nhau |
| Số thập phân chính xác | DECIMAL(p,s) / NUMERIC(p,s) | Money, số cần exact | DECIMAL = NUMERIC (đồng nghĩa chuẩn SQL) |
| Số dấu phẩy động | REAL, DOUBLE PRECISION | Đo lường khoa học (KHÔNG tiền) | FLOAT cũng có nhưng ngữ nghĩa precision khác nhau theo engine |
| Chuỗi | VARCHAR(n), CHAR(n) | String độ dài thay đổi / cố định | Một số engine thêm TEXT (không cap) — xem Section 4 |
| Thời gian | DATE, TIME, TIMESTAMP, TIMESTAMP WITH TIME ZONE | Time-related | Hỗ trợ TZ và độ chính xác khác nhau — xem Section 5 |
| Boolean | BOOLEAN | True/false (không hack 0/1) | Vài engine cũ không có BOOLEAN native — dùng SMALLINT/BIT |
| Định danh | (xem Section 6) | Distributed ID, random PK | UUID/auto-increment là dialect — xem Section 6 |
| Semi-structured | (JSON) | Cấu trúc lỏng | Nhiều engine có kiểu JSON nhưng cú pháp/index khác nhau |
| Nhị phân | BLOB / BINARY | File content, hash | Tên type khác nhau theo engine |
Bài này dạy kiểu chuẩn SQL và nguyên lý chọn type, không gắn engine cụ thể. Khi một tính năng là dialect (auto-increment, timezone behavior, JSON, array), bài chỉ nêu khái niệm và vì sao — cú pháp chính xác tra tài liệu engine bạn dùng. Mục tiêu: hiểu để chọn type đúng ở bất kỳ RDBMS nào.
Cây quyết định chọn type (áp dụng cho mọi RDBMS):
flowchart TD
START["Du lieu can luu"] --> Q1{"Du lieu gi?"}
Q1 -->|"So nguyen"| INT["INTEGER / BIGINT<br/>(PK lon -> BIGINT)"]
Q1 -->|"Tien / so can chinh xac"| DEC["DECIMAL(p,s)<br/>KHONG dung float"]
Q1 -->|"Do luong khoa hoc"| FLT["REAL / DOUBLE PRECISION"]
Q1 -->|"Chuoi"| STR["VARCHAR(n) rong + CHECK<br/>(CHAR(n) neu do dai co dinh)"]
Q1 -->|"Thoi diem"| TS{"Cross-region?"}
TS -->|"Co"| TZ["TIMESTAMP WITH TIME ZONE"]
TS -->|"Khong / chi ngay"| DATE["TIMESTAMP / DATE / TIME"]
Q1 -->|"True-false"| BOOL["BOOLEAN"]
Q1 -->|"Dinh danh random"| UUID["UUID (sinh la dialect)<br/>v7 > v4 cho index"]3. Số — số nguyên và kiểu tiền
3.1 Chọn kiểu số nguyên đúng
-- Cac kieu so nguyen chuan SQL - chon dung de tranh overflow + tiet kiem storage
SMALLINT -- thuong 2 byte, range nho (counter nho, flag)
INTEGER -- thuong 4 byte, range khoang -2.1 ti den 2.1 ti (default pho bien)
BIGINT -- thuong 8 byte, range rat lon (PK lon, counter global)
Quy tắc thực chiến: PK của bảng có thể lớn dùng BIGINT (tránh overflow khi vượt khoảng 2 tỉ row), counter nhỏ như view_count trong một bài viết dùng INTEGER là đủ, SMALLINT chỉ hợp lý khi biết chắc range nhỏ và cần tối ưu storage (vd: bảng có hàng tỉ row).
Cách sinh ID tự tăng là dialect: chuẩn SQL hiện đại có cú pháp GENERATED ALWAYS AS IDENTITY (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY). Các engine còn cung cấp shorthand riêng: một số dùng kiểu giả SERIAL/BIGSERIAL, một số dùng thuộc tính AUTO_INCREMENT trên cột, một số dùng SEQUENCE tách rời. Bản chất giống nhau: một bộ đếm sinh giá trị tăng dần. Trong bài, ta viết theo chuẩn GENERATED ... AS IDENTITY cho portable.
3.2 Money — KHÔNG dùng float
-- Float KHONG exact - tranh cho money
REAL -- float chinh xac thap, KHONG exact
DOUBLE PRECISION -- float chinh xac cao hon, van KHONG exact
-- Minh hoa: float khong bieu dien chinh xac phan so thap phan
-- 0.1 + 0.2 trong float = 0.30000000000000004 (IEEE 754)
SELECT CAST(0.1 AS DOUBLE PRECISION) + CAST(0.2 AS DOUBLE PRECISION);
-- Ket qua: 0.30000000000000004 -- so sanh = 0.3 se fail
-- DECIMAL(p, s) / NUMERIC(p, s) - exact, dung cho tien
-- p = tong so chu so, s = so chu so sau dau thap phan
balance DECIMAL(15, 2) -- max 9999999999999.99 (15 digit, 2 decimal)
-- So sanh: float vs decimal
SELECT CAST(0.1 AS DOUBLE PRECISION) + CAST(0.2 AS DOUBLE PRECISION); -- 0.30000000000000004
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)); -- 0.30 (exact)
-- Pattern alternative: store cents as bigint (tranh float hoan toan)
balance_cents BIGINT NOT NULL DEFAULT 0
-- Store $10.50 -> 1050 cents
-- App code: display = balance_cents / 100.0
-- Trao doi: app phai convert, nhung zero float bug
IEEE 754 float không thể biểu diễn chính xác nhiều phân số thập phân (0.1, 0.2, 0.3...). Tích lũy sai số qua nhiều phép tính — báo cáo tài chính cuối tháng sai vài cent. Dùng DECIMAL(p,s) (đồng nghĩa NUMERIC(p,s)) hoặc store cents dưới dạng integer. Không bao giờ dùng REAL hay DOUBLE PRECISION cho cột tiền — nguyên tắc này đúng ở mọi RDBMS.
4. Chuỗi — VARCHAR, CHAR, và độ dài
-- Cac kieu chuoi chuan SQL
VARCHAR(n) -- variable length voi gioi han n ky tu (pho bien nhat)
CHAR(n) -- FIXED length, padded space (chi dung khi do dai that su co dinh)
CHAR(n) luôn pad đủ n ký tự bằng dấu cách — chỉ hợp lý khi giá trị thực sự có độ dài cố định (mã quốc gia ISO CHAR(2), mã tiền tệ CHAR(3)). Còn lại dùng VARCHAR(n).
-- Pitfall: VARCHAR(n) cap qua som
title VARCHAR(50) -- spec ban dau OK
-- 6 thang sau marketing muon title 200 char
ALTER TABLE tasks ALTER COLUMN title TYPE VARCHAR(200);
-- Tuy engine: co the rewrite bang hoac chi doi metadata
-- Tren bang lon: nhieu engine phai hold lock trong thoi gian ALTER -> downtime
-- Better: cap rong rai hon + CHECK constraint cho business rule
title VARCHAR(500) NOT NULL CHECK (CHAR_LENGTH(title) BETWEEN 1 AND 200)
-- ALTER CHECK de hon ALTER TYPE: chi thay doi constraint, khong rewrite du lieu
Một số engine cung cấp kiểu TEXT (chuỗi không cap độ dài). Trong một engine, TEXT và VARCHAR(n) dùng cùng cơ chế lưu trữ nên không khác performance; engine khác lại tối ưu VARCHAR riêng. Vì TEXT không phải kiểu chuẩn SQL portable, bài này dùng VARCHAR(n) với n đủ rộng + CHECK cho business rule. Khi bạn biết chắc engine đích, có thể chọn TEXT nếu nó portable trong môi trường đó.
Khi nào nên cap chặt VARCHAR(n): khi business rule thực sự rang buộc length và không bao giờ thay đổi (vd: mã quốc gia ISO CHAR(2), mã SWIFT VARCHAR(11)). Còn lại — cap rộng rãi + CHECK để dễ alter sau.
5. Thời gian — TIMESTAMP WITH TIME ZONE luôn thắng
Đây là type pitfall nghiêm trọng nhất vì lỗi chỉ lộ ra khi deploy multi-region hoặc khi server đổi timezone.
-- 2 type thoi diem chinh trong chuan SQL:
TIMESTAMP -- "wall clock time" thuan tuy, KHONG luu timezone
TIMESTAMP WITH TIME ZONE -- gan timezone, cho phep convert dung giua cac mui gio
-- Demo bug: TIMESTAMP khong co TZ
-- Server EU (UTC+2) insert:
INSERT INTO events(event_at) VALUES (TIMESTAMP '2026-05-04 14:00');
-- Store: 2026-05-04 14:00 (con so tuong trung, khong co TZ context)
-- User US (UTC-4) doc cung row:
SELECT event_at FROM events;
-- Tra ve: 2026-05-04 14:00 (KHONG convert, van giu nguyen con so)
-- -> User US hieu la 14:00 gio dia phuong cua ho, lech 6 tieng so voi y dinh!
-- Fix: TIMESTAMP WITH TIME ZONE
-- Server EU (UTC+2) insert:
INSERT INTO events(event_at)
VALUES (TIMESTAMP WITH TIME ZONE '2026-05-04 14:00+02:00');
-- Store chuan hoa ve thoi diem tuyet doi (tuong duong 12:00 UTC)
-- User US (UTC-4) doc voi session timezone cua ho:
SELECT event_at FROM events;
-- Tra ve: 2026-05-04 08:00-04:00 (cung mot thoi diem, hien thi theo mui gio US)
-- -> Dung! Cung mot moment in time, khac nhau cach hien thi per timezone
TIMESTAMP (không TZ) store "con số đồng hồ treo tường" — không có ngữ cảnh timezone. Khi session/server timezone thay đổi (server chuyển DC, user ở timezone khác), cùng value bị interpret khác nhau. TIMESTAMP WITH TIME ZONE gắn thông tin múi giờ và chuẩn hoá về một thời điểm tuyệt đối khi store, hiển thị theo session timezone khi đọc — cùng một moment in time, mọi user đều thấy đúng local time của họ.
Nguyên lý "lưu thời điểm tuyệt đối, hiển thị theo múi giờ" là phổ quát, nhưng cách hiện thực là dialect: một số engine chuẩn hoá toàn bộ về UTC khi store rồi convert lúc đọc; engine khác lưu kèm offset; tên kiểu cũng có thể viết tắt (vd một số engine có alias cho TIMESTAMP WITH TIME ZONE). Hàm lấy "thời điểm hiện tại có timezone" (CURRENT_TIMESTAMP) là chuẩn SQL và an toàn. Tránh các hàm trả về timestamp không TZ cho dữ liệu cross-region.
Các type thời gian khác trong chuẩn SQL:
| Type | Lưu gì | Khi nào dùng |
|---|---|---|
DATE | Ngày (không giờ) | Birthday, anniversary, ngày kế toán |
TIME | Giờ (không ngày, không TZ) | Giờ mở cửa cố định (07:00 mỗi ngày) |
INTERVAL | Khoảng thời gian | due_at + INTERVAL '7' DAY, duration |
6. Định danh, JSON, array — đều là dialect
Ba nhóm dưới đây không phải kiểu chuẩn SQL thống nhất — mỗi engine làm khác nhau. Học khái niệm và khi nào dùng, tra cú pháp ở tài liệu engine.
6.1 UUID làm ID
UUID (Universally Unique Identifier) là chuỗi 128-bit, dùng làm ID random/distributed thay cho số tăng dần.
-- UUID: 128-bit, format 8-4-4-4-12 hex, vd:
-- a1b2c3d4-e5f6-7890-abcd-ef1234567890
-- Cach sinh UUID la DIALECT:
-- Mot so engine: ham gen_random_uuid()
-- Mot so engine: ham UUID()
-- ULID la dinh dang/thu vien ngoai (sortable theo thoi gian), thuong sinh o app layer
-- Kieu luu cung khac nhau: engine co kieu UUID native;
-- engine khac luu nhu CHAR(36) hoac BINARY(16)
UUID v4 (random) phân tán đều trên index có thứ tự, gây fragmentation; UUID v7 (timestamp-prefix) sequential nên tốt hơn cho index. Bài 05 — Surrogate vs natural key deep dive UUID v4/v7, ULID, và tradeoff page split.
6.2 JSON
Nhiều engine có kiểu JSON để lưu dữ liệu semi-structured trong một column. Đây không phải mặc định cho mọi field — chỉ hợp khi cấu trúc thực sự lỏng (config tuỳ user, payload webhook). Cú pháp truy vấn JSON và khả năng index khác nhau hoàn toàn giữa các engine.
-- Khai niem: mot column luu document JSON (cu phap minh hoa, dialect khac nhau)
-- settings JSON
-- Query path trong JSON: moi engine mot cu phap rieng
-- Index tren field JSON: co/khong va cach lam tuy engine
Nếu field có cấu trúc cố định, dùng column riêng (normalize) thay vì nhồi vào JSON — query nhanh hơn, có type-safety. Vòng đời và encoding dữ liệu semi-structured được bàn ở các bài sau của khoá.
6.3 Array
Một số engine cho phép column kiểu array (INTEGER[], VARCHAR[]). Về mặt lý thuyết, array trong một cell vi phạm 1NF (mỗi cell một giá trị scalar) — và phần lớn trường hợp nên normalize ra bảng riêng.
-- ARRAY column (dialect - khong phai moi engine ho tro)
-- tags VARCHAR[] -- vd: ['urgent', 'bug', 'backend']
-- Anti-pattern thuong: vi pham 1NF
-- Kho query, kho index, kho aggregate
-- Bai 03 cua module nay: normalize tags ra bang rieng
Bài 03 — Normalization 1NF→3NF giải thích vì sao multi-value trong một cell gây vấn đề và cách refactor ra junction table.
7. Pitfall — 4 type bug thường gặp production
Bốn pitfall dưới lặp lại ở mọi engine — không phụ thuộc dialect.
-- BUG 1: TIMESTAMP (no TZ) -> multi-region conflict
-- Da demo o Section 5 - chi ghi lai de nho
due_at TIMESTAMP -- KHONG dung cho du lieu cross-region
due_at TIMESTAMP WITH TIME ZONE -- DUNG
-- BUG 2: float cho money -> accumulated rounding error
balance REAL NOT NULL DEFAULT 0
-- Cong don nhieu lan + 0.1 -> ket qua 0.9999999... (khong phai 1.0)
-- Fix: DECIMAL(15,2) hoac balance_cents BIGINT
-- BUG 3: do dai chuoi cap qua nho - phat hien muon
title VARCHAR(50) -- OK khi ra mat
-- 6 thang sau: user nhap title 120 char -> ERROR: value too long
-- ALTER TYPE tren bang lon -> co the lock, downtime
-- Fix: VARCHAR(500) buffer + CHECK, hoac cap rong tu dau
-- BUG 4: luu so duoi dang text
age VARCHAR(10) -- sai! khong the SUM/AVG, sort theo string ('9' > '10')
phone VARCHAR(20) -- OK - so dien thoai khong can so hoc, co the co dau '+'
user_count VARCHAR(10) -- sai! '100' < '99' khi sort string
8. Type cast và CHECK constraint
-- Explicit cast chuan SQL: an toan, ro rang (dung CAST, khong dung cu phap :: rieng engine)
SELECT CAST('123' AS INTEGER); -- 123
SELECT CAST('2026-05-04' AS DATE); -- 2026-05-04
SELECT CAST('abc' AS INTEGER); -- ERROR: invalid input
-- CHECK constraint - rang buoc linh hoat hon type-level limit
title VARCHAR(500) NOT NULL CHECK (CHAR_LENGTH(title) BETWEEN 1 AND 200)
-- Co the ALTER CHECK de dang:
ALTER TABLE tasks DROP CONSTRAINT tasks_title_check;
ALTER TABLE tasks ADD CONSTRAINT tasks_title_check
CHECK (CHAR_LENGTH(title) BETWEEN 1 AND 500); -- tang gioi han, khong rewrite du lieu
Dùng CAST(x AS type) (chuẩn SQL) thay cho cú pháp cast riêng của một engine — code portable hơn. CHECK constraint linh hoạt hơn giới hạn type vì alter constraint không cần rewrite dữ liệu như alter type.
9. Applied — TaskFlow type rationale
Schema TaskFlow đã tour đầy đủ DDL ở Module 1 (tour TaskFlow schema). Bài này giải thích "tại sao" cho từng type choice, viết bằng kiểu chuẩn SQL:
-- users.id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
-- Chon BIGINT thay INTEGER: tranh overflow khi user base vuot 2 ti (scale safety)
-- IDENTITY thay UUID random: sequential -> index co thu tu tot hon (xem bai 05)
-- users.email VARCHAR(254) UNIQUE NOT NULL
-- 254 = do dai email toi da theo RFC; cap dung mot lan, on dinh
-- UNIQUE constraint dam bao khong duplicate
-- users.created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-- Tour TaskFlow dung TIMESTAMP (keyword pho quat); nen nang len
-- TIMESTAMP WITH TIME ZONE cho he da vung (xem muc 5) -> hien thi dung per session tz
-- DEFAULT CURRENT_TIMESTAMP: server-side default, app khong can set
-- tasks.due_at TIMESTAMP
-- Nullable: khong phai task nao cung co deadline (khac created_at)
-- Da vung -> nang len TIMESTAMP WITH TIME ZONE de user EU va US thay cung moment
-- tasks.status VARCHAR(20) CHECK (status IN ('todo', 'doing', 'done', 'archived'))
-- VARCHAR + CHECK: de ALTER danh sach gia tri (them value chi can ADD CONSTRAINT moi)
-- Alternative la kieu ENUM cua engine (dialect): type-safe hon nhung kho alter
-- Bai 02 cua module nay: rationale CHECK vs domain/enum
-- tasks.title VARCHAR(500) NOT NULL CHECK (CHAR_LENGTH(title) <= 200)
-- Cap rong + CHECK: marketing co the doi gioi han length ma chi alter CHECK
10. Deep Dive — Data types
- Use The Index, Luke — "Data Types" — góc nhìn agnostic về cách kiểu dữ liệu ảnh hưởng index và performance, không gắn engine.
- Markus Winand — "Modern SQL" (modern-sql.com) — cross-vendor data type pitfall: khi nào các engine khác nhau về type behavior (timezone, precision, string). Search "Modern SQL data types".
- Wikipedia — SQL data types (ISO/IEC 9075) — danh sách kiểu chuẩn SQL và mapping sang các engine phổ biến.
Ghi chú: Modern SQL cho góc nhìn so sánh multi-vendor — hữu ích nhất khi migrate giữa engine hoặc viết query cần portable. Khi triển khai trên một engine cụ thể, đối chiếu tài liệu chính thức của engine đó cho tên type và behavior chính xác.
11. Liên hệ các bài khác
- Bài 02 — Constraints: type là tầng phòng thủ đầu (đúng loại dữ liệu), constraint là tầng tiếp theo (đúng quy tắc nghiệp vụ) — đọc để thấy chúng bổ sung nhau.
- Bài 03 — Normalization 1NF→3NF: vì sao array/JSON nhồi nhiều giá trị vào một cell vi phạm 1NF, và cách normalize.
- Bài 05 — Surrogate vs natural key: UUID v4/v7, ULID, auto-increment ảnh hưởng index có thứ tự thế nào.
- Module 6 — Storage & indexing: kích thước type ảnh hưởng số row mỗi page và hiệu năng index — cơ chế index có thứ tự được mổ ở đó.
12. Tóm tắt
- Số nguyên:
BIGINTcho PK lớn (tránh overflow),INTEGERcho counter thường,SMALLINTchỉ khi range nhỏ + cần tiết kiệm storage. - Money:
DECIMAL(p,s)/NUMERIC(p,s)hoặc store cents dưới dạngBIGINT— KHÔNG bao giờREAL/DOUBLE PRECISION. - Chuỗi:
VARCHAR(n)vớinrộng rãi +CHECKcho business rule;CHAR(n)chỉ khi độ dài thực sự cố định. - Thời gian:
TIMESTAMP WITH TIME ZONEluôn cho dữ liệu cross-region —TIMESTAMPthuần là bug magnet multi-region. - Định danh / JSON / array là dialect: học khái niệm (UUID v4 vs v7, JSON khi cấu trúc lỏng, array vi phạm 1NF), tra cú pháp ở tài liệu engine.
- CAST chuẩn SQL (
CAST(x AS type)) thay cú pháp cast riêng engine; CHECK linh hoạt hơn giới hạn type vì alter không rewrite dữ liệu. - 4 pitfall production ở mọi engine: timestamp no-TZ, float cho money, độ dài chuỗi cap sớm, lưu số dưới dạng text.
13. Tự kiểm tra
Q1Vì sao DECIMAL/NUMERIC dùng cho tiền còn REAL/DOUBLE PRECISION thì không? Nguyên lý này có phụ thuộc engine không?▸
REAL và DOUBLE PRECISION là floating point theo chuẩn IEEE 754 — biểu diễn số trong hệ nhị phân. Nhiều phân số thập phân quen thuộc (0.1, 0.2) là chuỗi nhị phân vô hạn tuần hoàn, bị truncate khi store. Qua nhiều phép tính, sai số tích lũy — báo cáo tài chính lệch vài cent.
DECIMAL(p,s) (đồng nghĩa NUMERIC(p,s)) là arbitrary-precision decimal — lưu chính xác từng chữ số thập phân, không qua biểu diễn nhị phân gần đúng. Cùng input luôn cho cùng output exact.
Nguyên lý này không phụ thuộc engine — nó là hệ quả của cách float được định nghĩa (IEEE 754) so với decimal arbitrary-precision. Mọi RDBMS đều có cùng vấn đề với float và cùng lời giải với decimal. Alternative phổ biến: store cents dưới dạng integer.
Q2TIMESTAMP (no TZ) chạy tốt trên dev local. Production multi-region phát sinh bug. Cơ chế gì, và TIMESTAMP WITH TIME ZONE fix thế nào?▸
Dev local thường một server, một timezone. TIMESTAMP store "con số đồng hồ treo tường" — không có metadata timezone. Khi tất cả server và user cùng timezone, không có vấn đề vì mọi người interpret cùng một cách.
Production multi-region: server Frankfurt UTC+2, server Virginia UTC-4. User EU insert due_at = 2026-05-04 09:00 từ Frankfurt. TIMESTAMP store nguyên con số đó — không convert. Khi user US đọc, họ thấy 09:00 và hiểu là 09:00 giờ địa phương của họ — lệch 6 tiếng so với ý định ban đầu.
TIMESTAMP WITH TIME ZONE fix: nó gắn thông tin múi giờ và lưu một thời điểm tuyệt đối. Khi đọc, engine convert sang session timezone của người đọc. Cùng một moment in time, cách hiển thị khác nhau per timezone, nhưng semantics nhất quán. Cơ chế lưu chính xác (UTC vs offset) là dialect, nhưng kết quả là phổ quát.
Q3Vì sao auto-increment, UUID generation, JSON, array được xếp vào 'dialect' chứ không phải kiểu chuẩn SQL? Hệ quả khi viết code portable?▸
Chuẩn SQL (ISO/IEC 9075) định nghĩa một tập type cốt lõi (số, chuỗi, thời gian, boolean) mà mọi engine tuân theo. Nhưng nhiều tính năng tiện dụng nằm ngoài chuẩn hoặc được mỗi engine hiện thực khác nhau: cách sinh ID tự tăng (kiểu giả như SERIAL, thuộc tính AUTO_INCREMENT, hay SEQUENCE), hàm sinh UUID (gen_random_uuid() vs UUID()), cú pháp truy vấn JSON, và sự tồn tại của kiểu array.
Hệ quả khi viết code portable: ưu tiên cú pháp chuẩn (GENERATED ALWAYS AS IDENTITY, CAST(x AS t), TIMESTAMP WITH TIME ZONE). Khi buộc dùng tính năng dialect, cô lập nó (vd sinh UUID/ULID ở app layer thay vì DB function) để giảm phụ thuộc engine. Luôn đối chiếu tài liệu engine đích cho cú pháp chính xác.
Q4varchar(50) cap title — 6 tháng sau marketing yêu cầu 200 char. ALTER TYPE vs cap rộng + CHECK ngay từ đầu — tradeoff thực chiến?▸
ALTER TYPE trên bảng lớn: tuỳ engine, mở rộng giới hạn VARCHAR(n) có thể chỉ đổi metadata (instant) hoặc phải rewrite toàn bộ row. Trên bảng triệu row đang có traffic, nhiều engine cần giữ lock trong khi alter — dù ngắn cũng có thể gây drop connection.
Cap rộng + CHECK từ đầu: khai báo VARCHAR(500) với CHECK (CHAR_LENGTH(title) <= 200). Khi cần nới lên 500, chỉ DROP/ADD constraint — đổi constraint definition, thường không rewrite dữ liệu, nhanh và ít rủi ro hơn alter type.
Zero-downtime pattern (khi buộc đổi type): thêm column mới rộng hơn, backfill, double-write trong app, swap, drop column cũ — phức tạp nhưng không lock production. Thực chiến: cap rộng + CHECK ngay từ đầu để hầu như không bao giờ phải đụng đến vấn đề này.
Q5Khi nào nên lưu một giá trị dưới dạng số (INTEGER/DECIMAL) thay vì chuỗi (VARCHAR)? Cho ví dụ đúng và sai.▸
Lưu dưới dạng số khi cần tính toán hoặc sắp xếp theo giá trị số: age, user_count, price. Nếu lưu các field này dạng VARCHAR, ta mất khả năng SUM/AVG, và sort sai vì so sánh từ điển: chuỗi '100' đứng trước '99' (so '1' với '9'), gây bug âm thầm trong report.
Lưu dưới dạng chuỗi khi giá trị chỉ là định danh, không tham gia số học, kể cả khi trông giống số: số điện thoại (có thể có tiền tố '+', số 0 đầu), mã bưu chính, mã sản phẩm. Ép các giá trị này thành số sẽ mất số 0 đầu và ký tự đặc biệt.
Quy tắc: hỏi "tôi có bao giờ cộng/trung bình/so sánh thứ tự số trên field này không?". Có → kiểu số. Không, nó chỉ là nhãn → chuỗi.
Bài tiếp theo: Constraints — DB là last line of defense
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên