SQL 11 — Hàm ngày tháng, chuỗi & số

13 thg 7, 2026 3 lượt xem
#sql
#ham
#ngay-thang
#chuoi

Mở đầu: vì sao cần hàm tích hợp?

Dữ liệu thô gần như không bao giờ ở đúng hình dạng bạn cần để báo cáo. created_at là một dấu thời gian chính xác đến micro-giây, nhưng sếp chỉ muốn biết "mỗi tháng có bao nhiêu giao dịch". balance là một số thập phân dài dằng dặc, nhưng người dùng muốn nhìn thấy 1.250.000 VND. Tên khách hàng nhập tay đầy khoảng trắng thừa và chữ hoa lộn xộn, nhưng bạn cần so khớp chính xác.

Đây chính là lúc các hàm tích hợp (built-in functions) phát huy tác dụng. Bài này đi sâu vào ba nhóm hàm bạn dùng hằng ngày — ngày tháng/thời gian, chuỗi, số — cộng thêm hai kỹ năng nền tảng: ép kiểu (CAST) và xử lý NULL. Tất cả ví dụ dùng cú pháp PostgreSQL và chạy được ngay trong SQL Builder.

Schema sandbox dùng xuyên suốt:

  • customers(id, full_name, city, created_at)
  • accounts(id, customer_id, account_no, balance, currency)
  • transactions(id, account_id, amount, kind ['credit'|'debit'], created_at)
  • employees(id, name, department_id, salary, hired_at)
  • departments(id, name)

Hàm ngày tháng & thời gian

Lấy thời điểm hiện tại

Trước khi xử lý ngày trong cột, bạn cần biết "bây giờ" là khi nào. PostgreSQL có vài hàm trả về thời điểm hiện tại với mức chi tiết khác nhau:

HàmTrả vềGhi chú
CURRENT_DATENgày hôm nay (không có giờ)Kiểu date
CURRENT_TIMEGiờ hiện tạiKiểu time
NOW() / CURRENT_TIMESTAMPNgày + giờ + timezoneKiểu timestamptz
LOCALTIMESTAMPNgày + giờ (không timezone)Kiểu timestamp

Điểm quan trọng: trong cùng một câu lệnh, NOW() luôn trả về một giá trị duy nhất — thời điểm bắt đầu transaction — chứ không phải "tick" thay đổi từng dòng. Điều này giúp tính toán nhất quán.

▶ Chạy được trong SQL Builder

SELECT
  CURRENT_DATE        AS hom_nay,
  NOW()               AS bay_gio,
  CURRENT_TIMESTAMP   AS dau_transaction;

date_trunc — cắt cụt về mốc thời gian

date_trunc(field, source) "cắt cụt" một dấu thời gian về đầu của đơn vị bạn chỉ định. Ví dụ date_trunc('month', '2026-06-30 14:25:00') trả về 2026-06-01 00:00:00. Đây là công cụ số một để gom nhóm theo tháng/quý/năm.

Các mốc thường dùng: 'hour', 'day', 'week', 'month', 'quarter', 'year'.

Ví dụ kinh điển: đếm số giao dịch theo từng tháng.

▶ Chạy được trong SQL Builder

SELECT
  date_trunc('month', created_at) AS thang,
  COUNT(*)                        AS so_giao_dich,
  SUM(amount)                     AS tong_tien
FROM transactions
GROUP BY date_trunc('month', created_at)
ORDER BY thang;

Vì ta GROUP BY chính biểu thức date_trunc, mọi giao dịch trong cùng một tháng đều "rơi" về cùng một mốc đầu tháng và được gom lại.

EXTRACT & DATE_PART — moi ra một thành phần

Khi bạn chỉ cần một con số — năm, tháng, ngày trong tuần — dùng EXTRACT(field FROM source). Hàm DATE_PART('field', source) cho kết quả tương đương, chỉ khác cú pháp.

FieldÝ nghĩaVí dụ kết quả
yearNăm2026
monthTháng (1–12)6
dayNgày trong tháng30
dowNgày trong tuần (0=CN)2
quarterQuý (1–4)2
hourGiờ14

Ví dụ: tìm năm tuyển dụng của từng nhân viên.

▶ Chạy được trong SQL Builder

SELECT
  name,
  hired_at,
  EXTRACT(year FROM hired_at) AS nam_tuyen_dung
FROM employees
ORDER BY nam_tuyen_dung;

Đếm nhân viên được tuyển theo từng năm (kết hợp EXTRACT với GROUP BY):

▶ Chạy được trong SQL Builder

SELECT
  EXTRACT(year FROM hired_at) AS nam,
  COUNT(*)                    AS so_nhan_vien
FROM employees
GROUP BY EXTRACT(year FROM hired_at)
ORDER BY nam;

AGE — khoảng cách giữa hai mốc

AGE(end, start) trả về khoảng thời gian dạng "năm tháng ngày" giữa hai mốc. Nếu chỉ truyền một tham số, PostgreSQL ngầm so với CURRENT_DATE: AGE(hired_at) = thâm niên tính đến hôm nay.

▶ Chạy được trong SQL Builder

SELECT
  name,
  hired_at,
  AGE(CURRENT_DATE, hired_at)            AS tham_nien,
  EXTRACT(year FROM AGE(CURRENT_DATE, hired_at)) AS so_nam_lam
FROM employees
ORDER BY so_nam_lam DESC;

Mẹo: bọc AGE(...) trong EXTRACT(year FROM ...) để lấy đúng số năm tròn — hữu ích khi xét điều kiện thâm niên.

Cộng trừ INTERVAL

Bạn cộng/trừ thời gian bằng cách thêm một INTERVAL. Cú pháp: INTERVAL '30 days', INTERVAL '1 month', INTERVAL '2 years'. Phép toán trả về cùng kiểu với toán hạng ngày.

Ví dụ: lọc các giao dịch trong 30 ngày gần nhất.

▶ Chạy được trong SQL Builder

SELECT
  id,
  account_id,
  amount,
  created_at
FROM transactions
WHERE created_at >= NOW() - INTERVAL '30 days'
ORDER BY created_at DESC;

Tìm nhân viên đã làm trên 5 năm bằng cách so hired_at với hôm nay trừ đi 5 năm:

▶ Chạy được trong SQL Builder

SELECT
  name,
  hired_at
FROM employees
WHERE hired_at <= CURRENT_DATE - INTERVAL '5 years'
ORDER BY hired_at;

to_char — định dạng ngày thành chuỗi đẹp

to_char(value, format) biến ngày (hoặc số) thành chuỗi theo mẫu. Với ngày, các mẫu hay dùng: YYYY (năm 4 chữ số), MM (tháng), DD (ngày), HH24:MI (giờ:phút), Mon/Month (tên tháng).

▶ Chạy được trong SQL Builder

SELECT
  date_trunc('month', created_at)                 AS moc_thang,
  to_char(date_trunc('month', created_at), 'YYYY-MM') AS nhan_thang,
  COUNT(*)                                        AS so_gd
FROM transactions
GROUP BY date_trunc('month', created_at)
ORDER BY moc_thang;

Cột nhan_thang cho ra chuỗi gọn gàng kiểu 2026-06 rất tiện hiển thị trên dashboard.


Hàm chuỗi

Chuỗi do người dùng nhập gần như luôn "bẩn": khoảng trắng thừa, chữ hoa chữ thường lẫn lộn, dấu phân cách không nhất quán. Nhóm hàm chuỗi giúp bạn làm sạch và biến đổi.

HàmCông dụngVí dụ
LENGTH(s)Độ dài chuỗiLENGTH('Hà Nội') → 6
LOWER(s) / UPPER(s)Chuyển hoa/thườngLOWER('ABc')abc
TRIM(s)Bỏ khoảng trắng đầu/cuốiTRIM(' x ')x
SUBSTRING(s FROM a FOR b)Lấy đoạn conxem dưới
POSITION(sub IN s)Vị trí xuất hiệnPOSITION('@' IN email)
REPLACE(s, from, to)Thay thếREPLACE('a-b','-','_')a_b
LEFT(s, n) / RIGHT(s, n)Lấy n ký tự trái/phảiLEFT('12345', 2)12
CONCAT(a, b, ...) / ||Nối chuỗi'a' || 'b'ab
SPLIT_PART(s, delim, n)Tách & lấy phần thứ nxem dưới

Làm sạch & chuẩn hoá

Khi so khớp tên hoặc thành phố, hãy chuẩn hoá trước: bỏ khoảng trắng thừa và đưa về chữ thường.

▶ Chạy được trong SQL Builder

SELECT
  full_name,
  LOWER(TRIM(full_name))      AS ten_chuan_hoa,
  LENGTH(TRIM(full_name))     AS do_dai
FROM customers
ORDER BY do_dai DESC;

Tách & lấy phần — SPLIT_PART, LEFT/RIGHT

SPLIT_PART(s, delim, n) chia chuỗi theo dấu phân cách rồi lấy mảnh thứ n (đếm từ 1). Cực hữu ích để tách họ/tên, hoặc lấy phần tên miền của email.

Giả sử account_no có dạng VN-001234. Lấy mã chi nhánh (phần đầu) và số tài khoản (phần sau):

▶ Chạy được trong SQL Builder

SELECT
  account_no,
  SPLIT_PART(account_no, '-', 1) AS ma_chi_nhanh,
  SPLIT_PART(account_no, '-', 2) AS so_tk,
  RIGHT(account_no, 4)           AS bon_so_cuoi
FROM accounts;

Nối chuỗi & lấy chữ cái đầu của tên

Toán tử || nối chuỗi, còn CONCAT() an toàn hơn vì coi NULL như chuỗi rỗng (trong khi || gặp NULL sẽ ra NULL). Kết hợp LEFT để lấy chữ cái đầu mỗi từ:

▶ Chạy được trong SQL Builder

SELECT
  full_name,
  city,
  CONCAT(full_name, ' (', city, ')') AS hien_thi,
  UPPER(LEFT(full_name, 1))          AS chu_cai_dau
FROM customers
ORDER BY chu_cai_dau;

SUBSTRING & POSITION

Lấy phần "tên đăng nhập" trước dấu @ của một email giả định bằng cách dùng POSITION tìm vị trí @ rồi SUBSTRING cắt:

▶ Chạy được trong SQL Builder

SELECT
  full_name,
  SUBSTRING(full_name FROM 1 FOR 3) AS ba_ky_tu_dau,
  POSITION(' ' IN full_name)        AS vi_tri_dau_cach
FROM customers;

Hàm số & làm tròn

Số tiền cần làm tròn, lãi suất cần ép về 2 chữ số thập phân, mã số cần lấy phần dư. Các hàm số xử lý tất cả.

HàmCông dụngVí dụ
ROUND(n, d)Làm tròn đến d chữ sốROUND(1234.567, 2) → 1234.57
CEIL(n)Làm tròn lênCEIL(1.1) → 2
FLOOR(n)Làm tròn xuốngFLOOR(1.9) → 1
ABS(n)Giá trị tuyệt đốiABS(-50) → 50
MOD(a, b)Phần dưMOD(10, 3) → 1
POWER(a, b)Luỹ thừaPOWER(2, 10) → 1024

Ví dụ: tính số dư trung bình mỗi tài khoản, làm tròn 0 chữ số, và phân loại chẵn/lẻ theo id:

▶ Chạy được trong SQL Builder

SELECT
  account_no,
  balance,
  ROUND(balance, 0)        AS so_du_lam_tron,
  ABS(balance)             AS gia_tri_tuyet_doi,
  MOD(id, 2)               AS chan_le
FROM accounts
ORDER BY balance DESC;

debit thường lưu dưới dạng số âm; ABS() giúp lấy độ lớn giao dịch bất kể chiều.


Ép kiểu: CAST và ::

Đôi khi kiểu dữ liệu không khớp với phép toán bạn muốn. Ví dụ chia hai số nguyên trong PostgreSQL cho ra số nguyên (cắt cụt phần thập phân) — bạn phải ép một vế về numeric để có kết quả thập phân.

Có hai cú pháp tương đương:

  • Chuẩn SQL: CAST(value AS type)
  • Rút gọn của PostgreSQL: value::type

▶ Chạy được trong SQL Builder

SELECT
  COUNT(*) FILTER (WHERE kind = 'credit')                                   AS so_credit,
  COUNT(*)                                                                  AS tong,
  ROUND(
    COUNT(*) FILTER (WHERE kind = 'credit')::numeric / COUNT(*) * 100,
    1
  )                                                                         AS phan_tram_credit
FROM transactions;

Nếu bỏ ::numeric, phép chia hai số nguyên sẽ ra 0 hoặc 1 — một lỗi rất hay gặp. Ép kiểu cũng dùng để chuyển chuỗi '2026-06-30' thành date: '2026-06-30'::date.


Xử lý NULL: COALESCE, NULLIF, IS DISTINCT FROM

NULL nghĩa là "không biết". Bất kỳ phép toán nào chạm vào NULL đều cho NULL, và so sánh = NULL luôn ra "unknown" chứ không phải TRUE/FALSE. Ba công cụ sau giúp bạn kiểm soát:

  • COALESCE(a, b, c, ...) — trả về giá trị không NULL đầu tiên. Dùng để đặt giá trị mặc định.
  • NULLIF(a, b) — trả về NULL nếu a = b, ngược lại trả về a. Mẹo kinh điển: chống chia cho 0.
  • IS DISTINCT FROM — so sánh "an toàn với NULL", coi NULL như một giá trị bình thường (NULL IS DISTINCT FROM 5 → TRUE).

Ví dụ: hiển thị city, thay NULL bằng chữ "Chưa rõ":

▶ Chạy được trong SQL Builder

SELECT
  full_name,
  COALESCE(city, 'Chưa rõ') AS thanh_pho
FROM customers
ORDER BY full_name;

Chống chia cho 0 bằng NULLIF — nếu mẫu số bằng 0, biến nó thành NULL để phép chia trả NULL thay vì lỗi:

▶ Chạy được trong SQL Builder

SELECT
  d.name                                              AS phong_ban,
  SUM(e.salary)                                       AS tong_luong,
  ROUND(
    SUM(e.salary)::numeric / NULLIF(COUNT(e.id), 0),
    0
  )                                                   AS luong_tb
FROM departments d
LEFT JOIN employees e ON e.department_id = d.id
GROUP BY d.name
ORDER BY tong_luong DESC NULLS LAST;

Phòng ban không có nhân viên có COUNT(e.id) = 0; NULLIF biến nó thành NULL, tránh lỗi "division by zero", và luong_tb ra NULL một cách hợp lý.


Ví dụ tổng hợp: báo cáo giao dịch theo tháng có định dạng tiền tệ

Gộp tất cả kỹ năng: nhóm theo tháng bằng date_trunc, định dạng nhãn tháng bằng to_char, tính tổng tiền và định dạng theo kiểu tiền tệ, xử lý chiều credit/debit bằng ABS.

▶ Chạy được trong SQL Builder

SELECT
  to_char(date_trunc('month', created_at), 'YYYY-MM')        AS thang,
  COUNT(*)                                                   AS so_gd,
  SUM(amount) FILTER (WHERE kind = 'credit')                 AS tong_credit,
  ABS(COALESCE(SUM(amount) FILTER (WHERE kind = 'debit'), 0)) AS tong_debit,
  to_char(SUM(amount), 'FM999G999G999D00')                   AS rong_dinh_dang
FROM transactions
GROUP BY date_trunc('month', created_at)
ORDER BY thang;

Mẫu 'FM999G999G999D00' của to_char: 9 là chữ số (ẩn nếu không có), G là dấu phân cách hàng nghìn, D là dấu thập phân, FM bỏ khoảng trắng đệm. Kết quả ra chuỗi kiểu 1.250.000,00 — sẵn sàng hiển thị.


Sơ đồ: luồng xử lý một cột thời gian


Tóm tắt

  • Ngày tháng: CURRENT_DATE/NOW() lấy hiện tại; date_trunc('month', col) để gom nhóm theo mốc; EXTRACT/DATE_PART moi một thành phần; AGE tính khoảng cách; cộng/trừ INTERVAL '...' để dịch chuyển thời gian; to_char định dạng hiển thị.
  • Chuỗi: TRIM + LOWER để chuẩn hoá; SPLIT_PART, LEFT/RIGHT, SUBSTRING để tách; ||CONCAT để nối (CONCAT an toàn với NULL).
  • Số: ROUND, CEIL, FLOOR, ABS, MOD, POWER cho mọi nhu cầu tính toán và làm tròn.
  • Ép kiểu: CAST(x AS type) hoặc x::type — nhớ ép ::numeric trước khi chia số nguyên.
  • NULL: COALESCE đặt mặc định, NULLIF chống chia 0, IS DISTINCT FROM so sánh an toàn.

Tự kiểm tra

  1. Khác biệt giữa date_trunc('month', created_at)EXTRACT(month FROM created_at) là gì? Khi nào dùng cái nào?
  2. Vì sao COUNT(*) / COUNT(*) (hai số nguyên) có thể cho kết quả sai khi tính phần trăm, và ::numeric sửa nó thế nào?
  3. Viết một điều kiện WHERE lọc các giao dịch trong 7 ngày gần nhất bằng INTERVAL.
  4. COALESCE(city, 'Chưa rõ')NULLIF(count, 0) mỗi cái giải quyết vấn đề gì với NULL?
  5. Cho account_no = 'VN-001234', làm sao lấy ra 001234 bằng SPLIT_PART? Còn 1234 bằng RIGHT?
  6. Tại sao 'a' || NULL cho NULL nhưng CONCAT('a', NULL) cho 'a'?

Đọc tiếp

SQL 12 — JSON trong SQL

Bài viết liên quan

Cách index hoạt động (B-Tree), đọc EXPLAIN, seq scan vs index scan và mẫu tối ưu truy vấn.

13 thg 7, 2026 3

Khoá, ràng buộc, quan hệ và chuẩn hoá 1NF/2NF/3NF — thiết kế lược đồ đúng từ đầu.

13 thg 7, 2026 3

Kết nhiều bảng đúng cách: các loại JOIN, bẫy thường gặp và phép hợp tập.

13 thg 7, 2026 2

Truy vấn lồng, CTE (WITH), CTE đệ quy và hàm cửa sổ — vũ khí cho phân tích nâng cao.

13 thg 7, 2026 2