Học bằng cách đọc theo thứ tự

Thiết kế bảng và chuẩn hóa (Schema 3 tầng)

Dùng orders_flat với các cột lặp item1/item2 để cảm nhận trực tiếp các bất thường update, insert và delete, rồi tách bảng theo từng bước qua 1NF / 2NF / 3NF thành schema 3 tầng customer / order_record / order_line.

Bộ dữ liệu cho bài này — orders_flat (ví dụ chưa chuẩn hóa)

Chuẩn hóa (kỹ thuật thiết kế tách bảng để giảm dữ liệu trùng lặp và không nhất quán) là ý tưởng phá vỡ một bảng khổng lồ chứa mọi thứ thành nhiều bảng dễ giữ nhất quán hơn.

Trong bài này bạn sẽ thấy các vấn đề xảy ra trong một bảng chưa chuẩn hóa với các cột lặp, rồi xây sự hiểu biết về 1NF / 2NF / 3NF và schema 3 tầng qua các bài tập thiết kế thực hành.

Bộ dữ liệu là orders_flat (2 dòng), nhồi dữ liệu đơn hàng vào một bảng duy nhất.

Đó là một bảng chưa chuẩn hóa kinh điển nơi tên khách, số điện thoại, sản phẩm 1 và sản phẩm 2 đều xếp hàng trong một dòng — và bạn sẽ chuẩn hóa nó thành ba bảng customer / order_record / order_line.

Trước khi vào bài tập, hãy xem định nghĩa cộtdữ liệu mẫu của bảng orders_flat.

① Chạy PRAGMA table_info(orders_flat); để kiểm tra tên và kiểu cột. Để ý các cột sản phẩm xuất hiện dưới dạng các cột lặp như item1 / item2.

② Chạy SELECT * FROM orders_flat; để xem trước tất cả các dòng. Cũng quan sát các dòng chỉ có một sản phẩm có chuỗi rỗng hoặc NULL ở item2.

SQL Editor

Chạy truy vấn để xem kết quả

Ba bất thường trong bảng chưa chuẩn hóa

Thiết kế như orders_flat nhồi mọi thứ vào một bảng duy nhất dễ dàng sinh ra mâu thuẫn khi bạn thêm, sửa, hoặc xóa dữ liệu.

Ví dụ kinh điển là ba: bất thường update / bất thường insert / bất thường delete.

Bất thường update xảy ra khi cùng một sự thật bị trùng lặp qua nhiều dòng — ví dụ, nếu bạn muốn đổi số điện thoại, bạn phải update mọi dòng mà không sót.

Bất thường insert là khi bạn không thể đăng ký một khách hàng chưa đặt đơn nào, vì không có dòng đơn để gắn thông tin khách vào.

Bất thường delete là khi xóa một đơn cũng xóa luôn thông tin bạn muốn giữ, như số điện thoại của khách.

Những điều này xảy ra vì thiết kế không giữ "một sự thật ở một nơi".

Ba bất thường trong bảng chưa chuẩn hóa
orders_flat(khách + đơn + sản phẩm trong một)Bất thường updateBất thường insertBất thường deleteSố điện thoại trùng qua các dòngSót một update gây sai khớpKhông đăng ký được kháchchưa có đơnXóa một đơn cũngxóa luôn thông tin khách
Nhồi khách hàng, đơn hàng và sản phẩm vào một bảng gây ra mâu thuẫn hoặc mất thông tin khi update, insert và delete. Đó là động lực cho việc tách bảng qua chuẩn hóa.
-- Ví dụ bất thường delete: xóa đơn 2 cũng xóa thông tin liên hệ của Bob
-- (quan sát hành vi trên bảng tạm flat_demo để không ảnh hưởng orders_flat thật)
DROP TABLE IF EXISTS flat_demo;
CREATE TABLE flat_demo(
  order_id INTEGER, customer TEXT, customer_tel TEXT, product TEXT
);
INSERT INTO flat_demo VALUES
  (1,'Alice','03-1111','Pen'),
  (2,'Bob','03-2222','Clip');

-- Bob chỉ có một đơn. Xóa nó cũng mất số điện thoại 03-2222 của Bob
DELETE FROM flat_demo WHERE order_id = 2;
SELECT * FROM flat_demo;
-- -> Thông tin liên hệ của Bob giờ biến mất khỏi cơ sở dữ liệu (bất thường delete)

Tái hiện trên một bảng tạm tình huống "số điện thoại của Alice đã đổi và bạn muốn cập nhật, nhưng cùng số điện thoại trùng lặp qua nhiều dòng". Bài tập này tự đứng vững — bạn sẽ tạo bảng riêng flat_anomaly để giữ nó độc lập. (Nếu bạn chạy đúng, phần giải thích sẽ xuất hiện.)

DROP TABLE IF EXISTS bảng flat_anomaly, rồi CREATE nó với 4 cột: order_id / customer / customer_tel / product.

② Chèn 2 dòng cho các đơn của Alice (cả hai cùng số điện thoại 03-1111; sản phẩm là Pen và Note).

③ Chạy một UPDATE đổi số điện thoại của Alice thành 03-9999 nhưng chỉ chạm vào một dòng (ví dụ với điều kiện như WHERE order_id = 1), rồi chạy SELECT * FROM flat_anomaly; và xác nhận số điện thoại của hai dòng không khớp.

SQL Editor

Chạy truy vấn để xem kết quả

1NF đến 3NF — tách theo từng bước

Bảng được tổ chức qua chuẩn hóa theo các giai đoạn.

1NF — loại bỏ các cột lặp

Bỏ các cột lặp đánh số như item1 / item2.

Trước — các cột lặp item1 / item2

order_idcustomertelitem1item1_qtyitem2item2_qty
1Alice03-1111Pen2Note1
2Bob03-2222Clip5(rỗng)NULL

Sau — một dòng cho mỗi sản phẩm, với khóa chính composite `(order_id, product)`

order_id (PK)customertelproduct (PK)qty
1Alice03-1111Pen2
1Alice03-1111Note1
2Bob03-2222Clip5

2NF — chuyển các cột phụ thuộc vào chỉ một phần khóa chính vào bảng khác

Bảng từ bước trước có khóa chính composite (order_id, product). Nhưng nhìn kỹ: customertel được xác định chỉ bởi `order_id` (cùng order_id luôn có cùng khách).

Chuyển các cột phụ thuộc vào chỉ một phần khóa chính (phụ thuộc một phần) vào một bảng riêng là 2NF. Bạn tách nó thành header đơn hàng order_header và chi tiết đơn order_line.

Trước — customer và tel trùng trên mọi dòng

order_id (PK)customertelproduct (PK)qty
1Alice03-1111Pen2
1Alice03-1111Note1
2Bob03-2222Clip5

Sau — tách thành 2 bảng

order_header (một dòng cho mỗi đơn)

order_id (PK)customertel
1Alice03-1111
2Bob03-2222

order_line (khóa chính composite (order_id, product), trong đó order_id là khóa ngoại tham chiếu order_header)

order_id (PK,FK)product (PK)qty
1Pen2
1Note1
2Clip5

3NF — chuyển các cột được xác định bởi cột không khóa vào một bảng khác

Nhìn vào order_header từ bước trước, tel không được xác định bởi order_idbởi `customer` (cùng Alice luôn có 03-1111).

Loại bỏ các phụ thuộc đi qua một cột không khóa khác thay vì trực tiếp từ khóa chính (phụ thuộc bắc cầu: order_id -> customer -> tel) là điều 3NF nói tới.

Bạn trích xuất một bảng master khách hàng customer và để order_record chỉ giữ khóa ngoại đến khách.

Trước — tel phụ thuộc customer

order_id (PK)customertel
1Alice03-1111
2Bob03-2222

Sau — tách thành 3 bảng

customer (master khách hàng)

customer_id (PK)customer_namecustomer_tel
1Alice03-1111
2Bob03-2222

order_record (header đơn hàng: tham chiếu khách qua khóa ngoại)

order_id (PK)customer_id (FK)
11
22

order_line (chi tiết đơn: giống bước trước)

order_id (PK,FK)product (PK)qty
1Pen2
1Note1
2Clip5

Khi đã tách đến mức này, mỗi sự thật (số điện thoại của một khách, liên kết giữa đơn và khách, số lượng của một sản phẩm trong một đơn) sống ở đúng một nơi. Điều đó loại bỏ các bất thường update, insert và delete.

Cấu trúc bảng trước chuẩn hóa so với sau 3NF
Trước chuẩn hóa1 bảngSau 3NF3 bảngorders_flatorder_id / customer / telitem1 / item1_qtyitem2 / item2_qtycustomercustomer_id (PK)customer_namecustomer_telorder_recordorder_id (PK)customer_id (FK)order_lineorder_id (FK), product (PK)qtyPK(order_id, product)táchFKFK
Bảng orders_flat nhồi vào một được phá vỡ thành ba bảng: master khách, header đơn, và chi tiết đơn. Thông tin gốc có thể được dựng lại bằng JOIN qua các khóa ngoại.

Schema 3 tầng và dạng cuối — customer / order_record / order_line

Schema sau chuẩn hóa có thể được tổ chức thành ba tầng với các vai trò khác nhau.

  • Tầng master: các thực thể ít thay đổi và được tham chiếu bởi các bảng khác, như customer
  • Tầng transaction: các bảng ghi nhận sự kiện kinh doanh, như order_record
  • Tầng detail: các bảng giữ các dòng chi tiết của một sự kiện, như order_line

Bằng cách trỏ phía tham chiếu vào master bằng khóa ngoại, mỗi sự thật (như số điện thoại của một khách) nằm ở một nơi.

Dạng cuối là ba bảng này:

customer(customer_id khóa chính, customer_name, customer_tel), order_record(order_id khóa chính, customer_id -> customer), order_line(order_id -> order_record, product, qty, khóa chính composite (order_id, product)).

Số điện thoại nằm chỉ ở một dòng trong customer, nên một cập nhật là một dòng đơn; một khách không có đơn vẫn có thể được thêm vào customer (bất thường insert được giải quyết); và xóa một đơn vẫn để khách trong customer (bất thường delete được giải quyết).

Schema 3 tầng — master / transaction / detail
Tầng mastercustomer(customer_id PK, name, tel)Tầng transactionorder_record(order_id PK, customer_id FK)Tầng detailorder_line(order_id FK, product, qty, PK(order_id,product))order_record tham chiếucustomerorder_line tham chiếuorder_recordMột sự thật ở một nơikhắc phục cả 3 bất thường
Khách hàng sống ở tầng master, đơn hàng ở tầng transaction, và chi tiết đơn ở tầng detail, liên kết với nhau bằng khóa ngoại. Mỗi sự thật nằm ở một nơi và cả ba bất thường được giải quyết.
-- Lợi ích của chuẩn hóa (chỉ đọc): số điện thoại của một khách nằm ở một dòng của customer
-- Trong dạng orders_flat bạn phải sửa mọi dòng đơn của Alice = bất thường update
UPDATE customer SET customer_tel = '03-9999'
WHERE customer_name = 'Alice';

-- Sửa một chỗ, và số mới chảy vào mọi đơn khi JOIN
SELECT o.order_id, c.customer_name, c.customer_tel
FROM order_record o
JOIN customer c ON o.customer_id = c.customer_id
ORDER BY o.order_id;

Hãy tưởng tượng yêu cầu: "Chuẩn hóa orders_flat thành ba bảng — khách hàng, đơn hàng, và chi tiết đơn." Thiết kế schema cuối với khóa ngoại liên kết chúng, di chuyển dữ liệu sang, và dựng lại danh sách gốc bằng JOIN.

① Bật kiểm tra khóa ngoại, sau đó DROP TABLE IF EXISTS theo thứ tự order_line -> order_record -> customer (xóa con trước an toàn hơn).

CREATE bảng customer (customer_id INTEGER khóa chính / customer_name TEXT NOT NULL / customer_tel TEXT).

CREATE bảng order_record (order_id INTEGER khóa chính / customer_id INTEGER NOT NULL, khóa ngoại đến customer).

CREATE bảng order_line (order_id INTEGER, khóa ngoại đến order_record / product TEXT NOT NULL / qty INTEGER NOT NULL / khóa chính composite (order_id, product)).

⑤ Chèn Alice(1, 03-1111) và Bob(2, 03-2222) vào customer; đơn 1 -> khách 1 và đơn 2 -> khách 2 vào order_record; Pen(đơn 1, 2) / Note(đơn 1, 1) / Clip(đơn 2, 5) vào order_line.

⑥ Kết hợp 3 bảng theo order_idcustomer_id và chọn order_id / customer_name / customer_tel / product / qty theo thứ tự tăng dần của order_idproduct để xác nhận bạn có thể dựng lại danh sách gốc.

SQL Editor

Chạy truy vấn để xem kết quả

Dùng các bảng customer / order_record / order_line từ Bài tập 2, xác nhận từng cái rằng các bất thường update, insert và delete từng làm phiền bảng chưa chuẩn hóa không còn xảy ra. Đây là bài tập cuối cùng của bài viết.

Bất thường update được giải quyết: Đổi số điện thoại của Alice thành '03-9999'. Xác nhận đó là UPDATE một dòng đơn trên bảng customer.

Bất thường insert được giải quyết: INSERT một khách mới Charlie (customer_id 3 / customer_name 'Charlie' / customer_tel '03-3333') vào customer. Xác nhận bạn có thể đăng ký một khách không có đơn.

Bất thường delete được giải quyết: Xóa đơn 1. Xóa từ con order_line trước, rồi từ order_record. Chạy SELECT * FROM customer ORDER BY customer_id; và xác nhận dòng của Alice vẫn ở trong customer.

SQL Editor

Chạy truy vấn để xem kết quả

Tips — Luôn đặt khóa chính trên mọi bảng

Dù bạn có chuẩn hóa hay không, hãy thiết kế mọi bảng với một khóa chính (`PRIMARY KEY`). Một khóa chính cho bạn ba điều:

- Định danh dòng duy nhất: không có hai dòng nào có cùng khóa, nên UPDATE / DELETE nhắm chính xác một dòng qua WHERE pk = ...

- Có thể tham chiếu từ bảng khác: một khóa ngoại cần một khóa chính (hoặc UNIQUE) ở phía kia. Đó là nền tảng để liên kết các bảng bạn tách qua chuẩn hóa

- Lập chỉ mục tự động: các cột khóa chính tự động nhận chỉ mục B-Tree, tăng tốc tìm kiếm bằng và JOIN

Nếu không có cột đơn nào duy nhất tự thân, dùng khóa chính composite như PRIMARY KEY (order_id, product). Bỏ qua với "để sau thêm" làm các dòng trùng không phân biệt được và mất cả ba lợi ích trên.

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Số điện thoại của một khách trùng lặp qua nhiều dòng đơn, nên đổi số điện thoại yêu cầu cập nhật mọi dòng không sót — nếu không các giá trị không khớp. Vấn đề này được gọi là gì?

Câu 2Thao tác nào đưa một bảng vào 1NF?

Câu 3Sau chuẩn hóa, orders_flat được tách thành ba bảng customer / order_record / order_line. Làm thế nào để lấy lại danh sách gộp gốc?