Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 21 — Self-Referencing & Hierarchical Data

Data Modeling and ERD Mastery Bài 21/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang mô hình hóa dữ liệu nhân sự cho một công ty. Bạn có entity NHAN_VIEN, và mỗi nhân viên đều có một người quản lý. Câu hỏi đặt ra: người quản lý đó lưu ở đâu? Nếu bạn tạo thêm một entity QUAN_LY riêng, bạn sẽ rơi vào một cái bẫy kinh điển — vì bản thân người quản lý cũng là một nhân viên, cũng có lương, cũng có phòng ban, cũng có một quản lý cấp trên nữa. Tạo entity riêng cho quản lý nghĩa là bạn nhân đôi toàn bộ cấu trúc, và khi giám đốc lên chức, dữ liệu của ông ấy bị mắc kẹt giữa hai bảng.

Lời giải đúng là cho entity NHAN_VIEN quan hệ với chính nó. Đây gọi là self-referencing relationship (quan hệ tự tham chiếu), hay còn gọi là recursive relationship (quan hệ đệ quy). Nó là một trong những kỹ thuật quan trọng nhất nhưng cũng dễ gây bối rối nhất với người mới làm mô hình dữ liệu.

Vì sao bài này quan trọng với một Business Analyst (BA) hay người làm data modeling? Bởi vì dữ liệu phân cấp (hierarchical data) xuất hiện ở khắp mọi nơi: sơ đồ tổ chức công ty, cây danh mục sản phẩm, cấu trúc tài khoản kế toán, các bình luận trả lời lồng nhau trên mạng xã hội, đơn vị hành chính (tỉnh chứa huyện, huyện chứa xã), thậm chí cả cấu trúc BOM (Bill of Materials) trong sản xuất. Nếu không nắm vững self-referencing, bạn sẽ thiết kế ra những schema cồng kềnh, lặp lại, không thể mở rộng độ sâu phân cấp. Nắm vững nó, bạn giải quyết được cả một lớp bài toán chỉ với một bảng duy nhất.

Khái niệm cốt lõi

Self-referencing relationship là gì

Một quan hệ tự tham chiếu xảy ra khi một entity có quan hệ với chính nó. Trong sơ đồ ERD, đường quan hệ không nối hai bảng khác nhau mà uốn cong quay về chính bảng đó — người ta hay gọi vui là "đường vòng" (loop) hoặc "pig's ear" trong ký pháp Crow's Foot.

Về mặt kỹ thuật, ta thực hiện điều này bằng một foreign key (khóa ngoại) trỏ về primary key của chính bảng đó. Ví dụ kinh điển:

NHAN_VIEN
---------
nhan_vien_id    PK
ho_ten
phong_ban_id    FK -> PHONG_BAN
quan_ly_id      FK -> NHAN_VIEN(nhan_vien_id)   <-- tự tham chiếu

Cột quan_ly_id của một nhân viên chứa chính nhan_vien_id của người quản lý — và người quản lý đó cũng nằm trong cùng bảng. Tổng giám đốc, người không có cấp trên, sẽ có quan_ly_id = NULL. Đó chính là gốc (root) của cây phân cấp.

Phân biệt theo cardinality

Quan hệ tự tham chiếu cũng tuân theo các loại cardinality như quan hệ thông thường (mà bạn đã học ở Bài 7 và 8), chỉ khác là cả hai đầu đều trỏ về một entity:

  • One-to-Many tự tham chiếu: phổ biến nhất. Một nhân viên quản lý nhiều nhân viên, nhưng mỗi nhân viên chỉ có một quản lý trực tiếp. Một danh mục cha chứa nhiều danh mục con, mỗi danh mục con có một cha duy nhất. Đây chính là cấu trúc cây (tree).
  • One-to-One tự tham chiếu: hiếm hơn, ví dụ "người kế nhiệm" — mỗi nhân viên có tối đa một người thay thế, và mỗi người chỉ thay thế cho một người.
  • Many-to-Many tự tham chiếu: ví dụ "sản phẩm liên quan" (một sản phẩm gợi ý nhiều sản phẩm khác và ngược lại), hoặc quan hệ bạn bè trên mạng xã hội (một người kết bạn với nhiều người). Trường hợp này không thể giải bằng một FK đơn lẻ; bạn phải tạo một bảng trung gian (junction/bridge table) với hai cột cùng trỏ về bảng gốc.

Cây (tree) so với đồ thị (graph)

Hãy ghi nhớ sự phân biệt quan trọng này. Quan hệ One-to-Many tự tham chiếu tạo ra một cây: mỗi nút có đúng một cha, không có vòng lặp. Đây là cấu trúc của sơ đồ tổ chức, danh mục sản phẩm, đơn vị hành chính.

Quan hệ Many-to-Many tự tham chiếu tạo ra một đồ thị (graph): một nút có thể liên kết với nhiều nút khác theo nhiều hướng. Đây là cấu trúc của mạng xã hội, sản phẩm liên quan, mạng lưới giao thông.

Việc nhận diện đúng bạn đang làm việc với cây hay đồ thị quyết định bạn dùng một FK hay một bảng trung gian.

Tình huống thực tế

Ví dụ 1 — Sơ đồ tổ chức tại FPT Software

Giả sử bạn là BA cho dự án xây dựng hệ thống HR nội bộ của FPT Software, công ty có khoảng 30.000 nhân viên trải dài nhiều cấp: từ thực tập sinh → leader → manager → giám đốc trung tâm → tổng giám đốc. Độ sâu phân cấp có thể lên tới 6–7 cấp và thay đổi theo từng đơn vị — bộ phận này 4 cấp, bộ phận kia 7 cấp.

Nếu bạn cố mô hình hóa bằng cách tạo bảng riêng cho mỗi cấp (THUC_TAP_SINH, LEADER, MANAGER...), bạn sẽ chết ngạt: mỗi lần ai đó thăng chức là phải chuyển dữ liệu sang bảng khác, và bạn không bao giờ biết cần bao nhiêu bảng. Giải pháp đúng chỉ cần một bảng NHAN_VIEN với cột quan_ly_id tự tham chiếu. Bạn Tổng (TGĐ) có quan_ly_id = NULL. Mỗi nhân viên còn lại trỏ về quản lý trực tiếp của mình.

Khi cần trả lời câu hỏi nghiệp vụ "Liệt kê toàn bộ nhân viên thuộc quyền quản lý của anh A (kể cả gián tiếp qua nhiều cấp)", ta dùng truy vấn đệ quy (recursive CTE — Common Table Expression):

WITH RECURSIVE cap_duoi AS (
  SELECT nhan_vien_id, ho_ten, quan_ly_id
  FROM NHAN_VIEN WHERE nhan_vien_id = :id_anh_A
  UNION ALL
  SELECT nv.nhan_vien_id, nv.ho_ten, nv.quan_ly_id
  FROM NHAN_VIEN nv
  JOIN cap_duoi cd ON nv.quan_ly_id = cd.nhan_vien_id
)
SELECT * FROM cap_duoi;

Bài học rút ra: dữ liệu phân cấp với độ sâu không cố định và thay đổi theo thời gian là dấu hiệu rõ ràng nhất để chọn self-referencing. Đừng cố "trải phẳng" cây thành các bảng theo cấp.

Ví dụ 2 — Cây danh mục sản phẩm tại Tiki

Trên sàn thương mại điện tử như Tiki, cây danh mục có thể là: "Điện tử" → "Điện thoại & Phụ kiện" → "Điện thoại" → "iPhone". Mỗi danh mục con thuộc về đúng một danh mục cha, và độ sâu rất khác nhau giữa các ngành hàng. Ngành "Sách" có thể chỉ 2 cấp, ngành "Điện tử" tới 4–5 cấp.

Ta dùng một bảng DANH_MUC duy nhất:

DANH_MUC
--------
danh_muc_id      PK
ten
danh_muc_cha_id  FK -> DANH_MUC(danh_muc_id)

Danh mục gốc "Trang chủ" có danh_muc_cha_id = NULL. Khi người dùng bấm vào "iPhone", hệ thống dựng breadcrumb ("Điện tử / Điện thoại / iPhone") bằng cách đi ngược lên cha cho tới khi gặp NULL. Khi cần đếm tổng số sản phẩm trong "Điện tử" (gồm mọi danh mục con cháu), ta lại dùng truy vấn đệ quy đi xuống.

Một điểm thực tế đáng lưu ý: vì truy vấn đệ quy có thể chậm khi cây sâu và lượng truy cập lớn, nhiều sàn TMĐT bổ sung một cột duong_dan (materialized path) dạng "1/5/23/108" để tăng tốc — nhưng đó là chiến lược tối ưu hiệu năng, vẫn dựa trên nền tảng self-referencing.

Bài học rút ra: cùng một cấu trúc tự tham chiếu phục vụ được cả hai chiều truy vấn — đi lên (breadcrumb) và đi xuống (đếm con cháu). Đó là sức mạnh của thiết kế này.

Ví dụ 3 — Đơn vị hành chính Việt Nam

Đây là ví dụ rất gần gũi. Cấu trúc hành chính VN: Tỉnh/Thành phố → Quận/Huyện → Phường/Xã. Bạn có thể mô hình hóa bằng ba bảng riêng (TINH, HUYEN, XA) — và thực tế nhiều hệ thống VN làm vậy vì mỗi cấp có thuộc tính riêng và độ sâu cố định đúng 3 cấp.

Nhưng nếu bạn cần tính linh hoạt — chẳng hạn để xử lý cả những đặc khu, hay khi cấu trúc thay đổi sau sáp nhập đơn vị hành chính — một bảng DON_VI_HANH_CHINH tự tham chiếu với cột cap (tỉnh/huyện/xã) và don_vi_cha_id sẽ chịu được mọi biến động mà không cần đổi schema:

DON_VI_HANH_CHINH
-----------------
don_vi_id      PK
ten
cap            -- 'TINH' | 'HUYEN' | 'XA'
don_vi_cha_id  FK -> DON_VI_HANH_CHINH(don_vi_id)

Bài học rút ra: khi độ sâu cố định và mỗi cấp khác biệt nhiều, nhiều bảng riêng có thể rõ ràng hơn. Khi độ sâu biến động hoặc cấu trúc dễ thay đổi, self-referencing linh hoạt hơn. BA giỏi là người cân nhắc được đánh đổi này, không máy móc.

Hướng dẫn từng bước

Khi gặp một yêu cầu nghiệp vụ, đây là quy trình để quyết định và thiết kế quan hệ tự tham chiếu:

Bước 1 — Nhận diện tín hiệu phân cấp. Lắng nghe các từ khóa trong yêu cầu: "quản lý", "thuộc về", "cha/con", "cấp trên/cấp dưới", "danh mục con", "trả lời bình luận", "linh kiện cấu thành". Nếu một thực thể có quan hệ với những thực thể cùng loại với nó, đó là dấu hiệu self-referencing.

Bước 2 — Xác định cardinality. Hỏi: "Một phần tử có thể có bao nhiêu cha?" Nếu tối đa một cha → cây → dùng một FK tự tham chiếu. Nếu nhiều cha / quan hệ nhiều-nhiều (như bạn bè, sản phẩm liên quan) → đồ thị → dùng bảng trung gian.

Bước 3 — Thiết kế khóa. Với cây: thêm một cột FK (ví dụ quan_ly_id, danh_muc_cha_id) cho phép NULL, trỏ về PK của chính bảng. Cột này phải cho phép NULL để biểu diễn nút gốc. Với đồ thị: tạo bảng trung gian hai cột, ví dụ:

QUAN_HE_BAN_BE
--------------
nguoi_a_id  FK -> NGUOI_DUNG(id)
nguoi_b_id  FK -> NGUOI_DUNG(id)
PRIMARY KEY (nguoi_a_id, nguoi_b_id)

Bước 4 — Đặt ràng buộc chống vòng lặp. Một nhân viên không được là quản lý của chính mình (quan_ly_id <> nhan_vien_id), và rộng hơn, không được tạo vòng (A quản lý B, B quản lý A). Ràng buộc đơn giản có thể đặt bằng CHECK; ràng buộc chống vòng nhiều cấp thường phải xử lý ở tầng ứng dụng hoặc trigger.

Bước 5 — Xác định kịch bản truy vấn. Bạn cần đi lên (tìm tổ tiên) hay đi xuống (tìm con cháu)? Câu trả lời quyết định bạn có cần tối ưu thêm (materialized path, closure table) hay chỉ recursive CTE là đủ.

Bước 6 — Vẽ trên ERD. Trong Crow's Foot, vẽ đường quan hệ uốn từ entity quay về chính nó, ghi rõ cardinality ở hai đầu (ví dụ đầu "quản lý" là "one and optional" — biểu thị bằng vòng tròn + một gạch, vì gốc không có cha).

Lỗi thường gặp & mẹo

Lỗi 1 — Tạo bảng riêng cho từng cấp. Như đã nói, tạo QUAN_LY tách khỏi NHAN_VIEN gây trùng lặp cấu trúc và khóa cứng độ sâu phân cấp. Mẹo: nếu các "cấp" có cùng tập thuộc tính, gần như chắc chắn bạn nên gộp về một bảng tự tham chiếu.

Lỗi 2 — Quên cho FK nhận NULL. Nếu cột quan_ly_id đặt NOT NULL, bạn không thể lưu nút gốc (TGĐ, danh mục Trang chủ). Luôn để cột tự tham chiếu cho phép NULL.

Lỗi 3 — Tạo vòng lặp dữ liệu. Người dùng vô tình gán A là quản lý của B và B là quản lý của A, hoặc nặng hơn là một chuỗi vòng. Khi đó truy vấn đệ quy sẽ chạy vô tận (hoặc tràn giới hạn đệ quy). Mẹo: luôn thêm điều kiện chống vòng ở tầng ứng dụng, và đặt giới hạn độ sâu đệ quy trong CTE để an toàn.

Lỗi 4 — Lạm dụng self-referencing cho quan hệ nhiều-nhiều. Cố nhồi quan hệ "sản phẩm liên quan" vào một cột FK đơn là sai — một sản phẩm liên quan tới nhiều sản phẩm. Trường hợp nhiều-nhiều bắt buộc dùng bảng trung gian.

Lỗi 5 — Bỏ qua hiệu năng khi cây sâu. Recursive CTE rất gọn nhưng có thể chậm với hàng triệu dòng và cây sâu. Mẹo: với hệ thống đọc nhiều, cân nhắc materialized path hoặc closure table. Nhưng đừng tối ưu sớm — bắt đầu bằng FK đơn giản, chỉ tối ưu khi đo được vấn đề thật.

Mẹo đặt tên: đặt tên cột FK tự tham chiếu rõ vai trò, như quan_ly_id, danh_muc_cha_id, binh_luan_goc_id — tránh tên mơ hồ như parent_id chung chung khi bảng có nhiều quan hệ.

Bài tập thực hành

Bài tập 1 — Thiết kế cơ bản. Một công ty logistics ở TP.HCM muốn lưu sơ đồ tổ chức với độ sâu không cố định. Hãy vẽ ERD (Crow's Foot) cho entity NHAN_VIEN với quan hệ tự tham chiếu, ghi rõ cardinality hai đầu và chỉ ra giá trị của cột FK đối với Tổng giám đốc.

Bài tập 2 — Phân loại cây hay đồ thị. Với mỗi tình huống sau, xác định nên dùng FK tự tham chiếu (cây) hay bảng trung gian (đồ thị): (a) Bình luận và bình luận trả lời trên một bài blog; (b) "Khách hàng đã mua sản phẩm này cũng xem các sản phẩm khác"; (c) Cây tài khoản kế toán theo Thông tư 200; (d) Mạng lưới người giới thiệu (referral) — A giới thiệu B, B giới thiệu C, mỗi người có một người giới thiệu duy nhất.

Bài tập 3 — Viết truy vấn đệ quy. Cho bảng DANH_MUC(danh_muc_id, ten, danh_muc_cha_id). Hãy viết một recursive CTE để liệt kê toàn bộ danh mục con cháu của danh mục "Điện tử", kèm theo cấp độ (level) của mỗi danh mục.

Bài tập 4 — Tư duy nghiệp vụ. Một dự án quản lý chuỗi nhà thuốc có cấu trúc đúng 3 cấp cố định: Vùng → Khu vực → Cửa hàng, mỗi cấp có thuộc tính rất khác nhau. Hãy lập luận: nên dùng một bảng tự tham chiếu hay ba bảng riêng? Nêu ít nhất hai ưu điểm cho lựa chọn của bạn.

Gợi ý đáp án Bài tập 2: (a) cây, (b) đồ thị — bảng trung gian, (c) cây, (d) cây (mỗi người một người giới thiệu duy nhất).

Tóm tắt

Quan hệ tự tham chiếu (self-referencing / recursive relationship) là khi một entity quan hệ với chính nó, được hiện thực bằng một foreign key trỏ về primary key của cùng bảng. Đây là công cụ chuẩn để mô hình hóa dữ liệu phân cấp: sơ đồ tổ chức, cây danh mục, đơn vị hành chính, bình luận lồng nhau, cấu trúc linh kiện.

Những điều cốt lõi cần nhớ:

  • Quan hệ One-to-Many tự tham chiếu tạo ra cây — dùng một cột FK cho phép NULL; nút gốc có FK = NULL.
  • Quan hệ Many-to-Many tự tham chiếu tạo ra đồ thị — bắt buộc dùng bảng trung gian hai cột.
  • Truy vấn dữ liệu phân cấp dùng recursive CTE, đi lên (tìm tổ tiên) hoặc đi xuống (tìm con cháu).
  • Luôn chống vòng lặp và giới hạn độ sâu đệ quy.
  • Cân nhắc đánh đổi giữa self-referencing (linh hoạt, độ sâu biến động) và nhiều bảng riêng (rõ ràng khi độ sâu cố định, mỗi cấp khác biệt).
Khi bạn thấy bất kỳ thực thể nào "quan hệ với những thực thể cùng loại với chính nó", hãy nghĩ ngay đến self-referencing. Đây là một trong những mẫu thiết kế thanh lịch nhất giúp bạn giải quyết cả một lớp bài toán phân cấp chỉ với một bảng duy nhất. Ở bài tiếp theo, chúng ta sẽ chuyển sang một chủ đề liên quan mật thiết tới việc theo dõi sự thay đổi dữ liệu theo thời gian — bảng audit và dữ liệu temporal.