Mở đầu — vì sao bài này quan trọng
Trong suốt gần 40 bài học vừa qua, bạn đã học cách làm đúng: chuẩn hóa dữ liệu, chọn khóa chính, vẽ quan hệ, đặt ràng buộc. Nhưng trong thực tế nghề nghiệp của một Business Analyst hay Data Analyst, bạn sẽ dành phần lớn thời gian không phải để thiết kế mô hình mới từ con số 0, mà để tiếp nhận, sửa chữa và vá víu những mô hình đã tồn tại từ nhiều năm trước. Và những mô hình đó thường mắc phải những sai lầm lặp đi lặp lại đến mức chúng có tên riêng: anti-patterns — những "mô típ phản diện" trong thiết kế dữ liệu.
Anti-pattern không giống một lỗi cú pháp. Nó là một cách làm thoạt nhìn có vẻ hợp lý, thậm chí thông minh, giải quyết được vấn đề trước mắt, nhưng lại gieo mầm cho một loạt vấn đề nghiêm trọng về sau: truy vấn chậm, dữ liệu bẩn, khó bảo trì, và cực kỳ khó thay đổi. Điều nguy hiểm là người thiết kế thường tự hào về giải pháp của mình cho đến ngày hệ thống sập dưới sức nặng của chính nó.
Bài này giúp bạn nhận diện những anti-pattern phổ biến nhất, hiểu vì sao chúng độc hại, và quan trọng nhất — biết cách chỉ ra chúng trong buổi review với đội kỹ thuật một cách thuyết phục. Một BA giỏi không chỉ biết vẽ ERD đẹp; họ còn là người thổi còi khi thấy đội dev đang lao vào một cái bẫy quen thuộc.
Khái niệm cốt lõi
EAV — Entity-Attribute-Value (kẻ thù số một)
EAV là anti-pattern kinh điển và cũng gây tranh cãi nhất. Ý tưởng của nó: thay vì tạo cột cho từng thuộc tính, bạn lưu mọi thuộc tính dưới dạng cặp key-value trong một bảng chung.
CREATE TABLE entity_attribute (
entity_id INT,
attribute VARCHAR(100),
value VARCHAR(255)
);
Với thiết kế này, một sản phẩm điện thoại có thể được lưu thành nhiều dòng: (1, 'màu', 'đen'), (1, 'RAM', '8GB'), (1, 'giá', '5000000'). Nghe rất linh hoạt — bạn có thể thêm thuộc tính mới mà không cần đổi cấu trúc bảng. Đó chính là cái bẫy: sự linh hoạt giả tạo.
Vấn đề của EAV rất nặng nề. Thứ nhất, bạn mất hoàn toàn kiểm soát kiểu dữ liệu — cột value là VARCHAR nên giá bằng "5000000" nằm chung kiểu với "đen". Không có ràng buộc nào ngăn ai đó nhập "năm triệu" vào ô giá. Thứ hai, mọi truy vấn đơn giản đều trở thành ác mộng. Muốn tìm điện thoại màu đen, RAM 8GB, giá dưới 6 triệu, bạn phải self-join bảng này ba lần và ép kiểu chuỗi thành số. Thứ ba, bạn không thể đặt khóa ngoại, không thể index hiệu quả, không thể dùng NOT NULL. Về bản chất, EAV là việc bạn tự tay xây một cơ sở dữ liệu bên trong một cơ sở dữ liệu — và làm nó tệ hơn nhiều.
Khi nào EAV thực sự chấp nhận được? Chỉ khi thuộc tính thật sự không dự đoán được và cực kỳ thưa thớt — ví dụ hồ sơ bệnh án với hàng nghìn xét nghiệm có thể có mà mỗi bệnh nhân chỉ làm vài cái. Nhưng ngay cả khi đó, ngày nay giải pháp cột JSON (Bài 29) thường tốt hơn EAV rất nhiều.
Cột đa giá trị — nhồi nhiều giá trị vào một ô
Anti-pattern này vi phạm trực tiếp 1NF (Bài 12). Đội dev muốn lưu danh sách nên nhét chúng vào một chuỗi ngăn cách bằng dấu phẩy:
tags VARCHAR(255) -- "khuyến_mãi,hot,ban_chay"
Hậu quả: bạn không thể index để tìm nhanh, muốn tìm sản phẩm có tag "hot" phải dùng LIKE '%hot%' — vừa chậm vừa sai (nó khớp cả "hotline"). Không đếm được, không đảm bảo tính toàn vẹn, không xóa một tag riêng lẻ mà không đọc-sửa-ghi lại cả chuỗi. Giải pháp đúng luôn là một bảng liên kết many-to-many.
Khóa chính thông minh (Smart Key / Intelligent Key)
Đây là khi khóa chính mang ý nghĩa nghiệp vụ được mã hóa bên trong. Ví dụ mã sản phẩm HN-2024-DTDD-0001 trong đó HN là kho Hà Nội, 2024 là năm, DTDD là ngành hàng điện thoại. Nghe tiện, nhưng đây là quả bom nổ chậm: ngày sản phẩm được chuyển sang kho khác hoặc đổi ngành hàng, khóa chính của bạn nói dối. Mà khóa chính thì không được đổi vì hàng loạt bản ghi khác đang tham chiếu tới nó. Nguyên tắc: khóa nên vô nghĩa và bất biến (surrogate key — Bài 6); còn thông tin nghiệp vụ như kho, năm, ngành hàng phải nằm ở các cột riêng.
Bảng "God Table" và cột dùng chung mơ hồ
Là bảng phình to với 80-100 cột chứa mọi thứ về "khách hàng", trong đó nhiều cột chỉ dùng cho một số ít bản ghi (khách cá nhân dùng cột này, khách doanh nghiệp dùng cột kia, phần lớn để NULL). Kèm theo đó thường là các cột "vạn năng" như note1, extra_field_5, hoặc một cột status mà ý nghĩa thay đổi tùy loại bản ghi. Đây là dấu hiệu thiếu tách bảng theo subtype (Bài 20).
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử "ChợViệt" và cái bẫy EAV
ChợViệt là một startup TMĐT giả định tại TP.HCM, gọi vốn vòng seed 2 triệu USD, bán đủ loại hàng từ điện thoại đến thời trang đến thực phẩm. Đội kỹ thuật ban đầu lập luận rất thuyết phục: "Mỗi ngành hàng có thuộc tính khác nhau — điện thoại có RAM, áo có size, gạo có khối lượng. Chúng ta không thể tạo cột riêng cho tất cả, nên hãy dùng EAV để linh hoạt."
Họ xây bảng product_attributes theo đúng mô hình key-value. Trong sáu tháng đầu với 5.000 sản phẩm, mọi thứ chạy tốt. Nhưng khi catalog lên 400.000 sản phẩm và họ ra mắt bộ lọc tìm kiếm (filter theo giá, hãng, size), thảm họa ập đến. Một truy vấn lọc điện thoại Samsung, RAM 8GB, giá 5-10 triệu, còn hàng — phải join bảng attributes bốn lần, mỗi lần quét hàng chục triệu dòng. Trang danh mục tải mất 8-12 giây. Tệ hơn, bộ phận kinh doanh phát hiện dữ liệu giá bị bẩn: có bản ghi ghi "5.000.000", có bản ghi ghi "5000000đ", có bản ghi ghi "5 triệu" — vì cột value là chuỗi tự do không ràng buộc được.
Bài học rút ra: Đội BA được mời vào để cứu vãn. Giải pháp không phải là "sửa EAV" mà là thoát khỏi nó. Họ chuyển các thuộc tính lõi và hay-được-lọc (giá, tồn kho, hãng, trạng thái) thành cột thật có kiểu dữ liệu và index rõ ràng; các thuộc tính đặc thù, ít được lọc thì gom vào một cột JSON theo ngành hàng. Thời gian tải trang giảm còn dưới 1 giây. Bài học: "linh hoạt" không miễn phí — nó đánh đổi bằng hiệu năng và chất lượng dữ liệu, và cái giá đó tăng theo cấp số nhân khi dữ liệu lớn lên.
Ví dụ 2 — Ngân hàng và mã tài khoản "biết nói"
Một ngân hàng thương mại cỡ trung ở Việt Nam (bối cảnh giả định) thiết kế mã khách hàng dạng smart key: ký tự đầu là loại khách (C cá nhân, D doanh nghiệp), ba ký tự tiếp là mã chi nhánh mở tài khoản, phần còn lại là số thứ tự. Ví dụ C-HCM-0000482. Dùng mã này làm khóa chính trong bảng customers và mọi bảng con tham chiếu tới.
Vấn đề bùng nổ khi ngân hàng triển khai chương trình sáp nhập chi nhánh và cho phép khách hàng "chuyển hộ khẩu" tài khoản giữa các chi nhánh. Bỗng nhiên mã C-HCM-0000482 của một khách đã chuyển ra Hà Nội trở thành sai lệch. Đội vận hành muốn đổi mã cho đúng, nhưng khóa chính này đang được 14 bảng khác nhau tham chiếu — từ giao dịch, khoản vay, đến lịch sử đăng nhập. Đổi một mã nghĩa là cập nhật hàng triệu dòng và rủi ro mất toàn vẹn dữ liệu cực cao. Ngoài ra, khi một khách cá nhân đăng ký thêm dịch vụ doanh nghiệp, ký tự C/D không còn đủ mô tả.
Bài học rút ra: Đội BA đề xuất tách bạch hai khái niệm. Khóa chính chuyển sang một surrogate key thuần số vô nghĩa (customer_id BIGINT), bất biến vĩnh viễn. Thông tin loại khách, chi nhánh hiện tại trở thành các cột riêng có thể cập nhật tự do, có khóa ngoại tới bảng branches. Mã hiển thị C-HCM-0000482 vẫn được giữ như một cột "customer_code" cho con người đọc, nhưng không còn gánh vai trò khóa. Bài học kinh điển: đừng bao giờ để khóa chính mang thông tin có thể thay đổi.
Ví dụ 3 — Chuỗi phòng khám và cột đa giá trị
Một chuỗi phòng khám tư nhân ở Đà Nẵng lưu danh sách dịch vụ mà mỗi lần khám sử dụng trong một cột duy nhất: services VARCHAR(500) chứa chuỗi kiểu "khám tổng quát; xét nghiệm máu; siêu âm". Ban đầu chỉ để in ra hóa đơn nên không ai thấy vấn đề.
Rắc rối đến khi ban giám đốc yêu cầu báo cáo: "Dịch vụ nào được dùng nhiều nhất quý này? Doanh thu theo từng dịch vụ?". Không thể trả lời được, vì dữ liệu bị khóa trong chuỗi. Muốn đếm số lần "siêu âm" phải dùng LIKE '%siêu âm%', nhưng điều đó vừa chậm vừa không tính được doanh thu riêng của từng dịch vụ, và còn dính lỗi khi có dịch vụ "siêu âm tim" bị đếm nhầm. Việc đổi giá một dịch vụ cũng bất khả thi vì giá không được lưu tách rời.
Bài học rút ra: Đội cải tổ tách thành bảng visit_services (một dòng cho mỗi cặp lần-khám và dịch vụ), với khóa ngoại tới bảng services chứa tên và giá chuẩn. Ngay lập tức mọi câu hỏi báo cáo đều trả lời được bằng một câu GROUP BY. Bài học: bất cứ khi nào bạn thấy một cột chứa danh sách ngăn cách bằng dấu phẩy hay chấm phẩy, đó là dấu hiệu 1NF bị vi phạm và một bảng liên kết đang bị thiếu.
Hướng dẫn từng bước
Khi review một schema để săn anti-pattern, hãy làm theo trình tự này:
- Quét các bảng key-value. Tìm bảng nào có cột tên kiểu
attributevàvalue, hoặc bảng chỉ có 3-4 cột nhưng chứa cực nhiều dòng cho mỗi thực thể. Đó là dấu hiệu EAV. Hỏi: những thuộc tính này có thực sự không dự đoán được không, hay chỉ là lười tạo cột?
- Soi từng cột chuỗi khả nghi. Với mỗi cột VARCHAR/TEXT, tự hỏi "cột này có bao giờ chứa nhiều hơn một giá trị không?". Nếu có, bạn vừa tìm thấy vi phạm 1NF cần tách bảng.
- Kiểm tra ý nghĩa của khóa chính. Đọc vài giá trị khóa chính thật. Nếu bạn có thể "giải mã" thông tin nghiệp vụ từ chúng (năm, kho, loại), đó là smart key. Hỏi: thông tin đó có bao giờ thay đổi không? Nếu có, đề xuất tách surrogate key.
- Đếm số cột và tỷ lệ NULL. Bảng nào có trên 40-50 cột, hoặc có nhiều cột luôn NULL với một nhóm bản ghi, thường cần tách theo subtype hoặc tách bảng con.
- Tìm các cột "vạn năng".
note,extra,field1,custom_data, hay cộtstatusmang ý nghĩa khác nhau tùy dòng — đều là mùi khét của thiết kế thiếu rõ ràng.
- Đề xuất giải pháp kèm chi phí di trú. Đừng chỉ chỉ trích. Với mỗi anti-pattern, đưa ra thiết kế thay thế và ước lượng công sức chuyển đổi để đội có cơ sở ra quyết định.
Lỗi thường gặp & mẹo
- Nhầm "chuẩn hóa quá mức" là anti-pattern nên phá bỏ tất cả. Có người sau khi nghe về denormalization (Bài 15) thì gom hết mọi thứ vào một bảng "cho nhanh". Đó lại là một anti-pattern khác. Denormalization là quyết định có chủ đích, có đo lường, không phải cái cớ để lười chuẩn hóa.
- Cho rằng JSON luôn tốt hơn EAV nên nhét mọi thứ vào JSON. JSON hợp cho dữ liệu thưa, ít truy vấn lọc. Nhưng nếu bạn thường xuyên lọc, sắp xếp, join theo một thuộc tính, nó phải là cột thật. JSON không phải thùng rác để tránh thiết kế.
- Sợ đụng vào schema cũ nên cứ vá thêm cột. Mỗi lần thêm
phone2,phone3,address_backuplà bạn đang bồi đắp nợ kỹ thuật. Dấu hiệu lặp cột (phone1,phone2,phone3) gần như luôn có nghĩa là thiếu một bảng con one-to-many.
- Mẹo diễn đạt với đội kỹ thuật: Đừng nói "thiết kế này sai". Hãy đóng khung theo rủi ro nghiệp vụ: "Khi dữ liệu đạt 500 nghìn dòng, truy vấn lọc sẽ chậm 10 giây và ảnh hưởng tỷ lệ chuyển đổi" — con số và hệ quả kinh doanh thuyết phục hơn lý thuyết chuẩn hóa.
- Mẹo nhận diện nhanh EAV trá hình: cột JSON chứa vài chục key cố định giống hệt nhau ở mọi bản ghi thực chất chỉ là EAV mặc áo mới. Nếu cấu trúc ổn định, hãy dùng cột thật.
Bài tập thực hành
- Săn anti-pattern. Cho bảng sau, hãy chỉ ra ít nhất ba anti-pattern và đề xuất thiết kế thay thế:
CREATE TABLE orders (
order_code VARCHAR(30) PRIMARY KEY, -- 'HN-2024-000123'
customer VARCHAR(200),
products TEXT, -- 'SP001:2, SP045:1, SP103:5'
notes TEXT,
extra_data TEXT -- JSON đủ loại thứ
);
- Ra quyết định EAV. Bạn đang thiết kế hệ thống quản lý bất động sản cho thuê. Mỗi loại tài sản (căn hộ, nhà phố, văn phòng, kho) có một số thuộc tính chung và một số riêng. Hãy lập luận: khi nào bạn dùng cột thật, khi nào dùng JSON, và tại sao bạn sẽ (hoặc sẽ không) dùng EAV. Viết khoảng 150 từ.
- Viết kịch bản review. Giả sử bạn phát hiện đội dev dùng smart key làm khóa chính. Soạn 3-4 câu bạn sẽ nói trong buổi họp để thuyết phục họ chuyển sang surrogate key, tập trung vào rủi ro nghiệp vụ chứ không phải lý thuyết.
- Đo lường tác động. Với ví dụ ChợViệt, hãy liệt kê ba chỉ số bạn sẽ đo trước và sau khi bỏ EAV để chứng minh giá trị của việc tái thiết kế cho ban lãnh đạo.
Tóm tắt
Anti-pattern là những cách thiết kế thoạt nhìn hợp lý nhưng gây hại lâu dài. Năm kẻ nguy hiểm nhất bạn cần thuộc lòng: EAV (nhồi thuộc tính vào bảng key-value, đánh mất kiểu dữ liệu và hiệu năng), cột đa giá trị (vi phạm 1NF, phá vỡ khả năng truy vấn và báo cáo), smart key (khóa chính mang thông tin nghiệp vụ có thể thay đổi), God Table (bảng phình to thiếu tách subtype), và cột vạn năng (note, extra mơ hồ). Điểm chung của chúng là đánh đổi sự tiện lợi trước mắt lấy nợ kỹ thuật khổng lồ về sau, và cái giá luôn tăng theo quy mô dữ liệu. Vai trò của bạn với tư cách BA không chỉ là nhận ra chúng, mà là chỉ ra chúng bằng ngôn ngữ rủi ro nghiệp vụ, kèm giải pháp thay thế và ước lượng chi phí di trú — để đội có thể ra quyết định sáng suốt thay vì tiếp tục lao vào cái bẫy quen thuộc.