Phi chuẩn hoá trong cơ sở dữ liệu: Khi nào và cách sử dụng
Tìm hiểu cách phi chuẩn hoá tăng tốc hiệu năng đọc bằng cách giảm join và đơn giản hoá truy vấn. Hiểu các đánh đổi, kỹ thuật và trường hợp sử dụng khiến nó trở thành công cụ mạnh cho hệ thống phân tích và báo cáo.
Tôi đã viết rất nhiều về chuẩn hoá và vì sao đó là nền tảng đáng tin cậy cho tính toàn vẹn dữ liệu. Tôi không tự mâu thuẫn với mình ở đây. Tôi vẫn tin rằng một lược đồ được chuẩn hoá tốt là điểm khởi đầu đúng cho hầu hết hệ thống giao dịch. Bài viết này nói về lựa chọn có chủ đích để đi ngược lại sự “thuần khiết” đó trong những tình huống rất cụ thể, nơi hiệu năng đọc quan trọng hơn việc tuân thủ chặt chẽ các dạng chuẩn.
Phi chuẩn hoá không phải là bỏ qua chuẩn hoá. Đây là một tối ưu hiệu năng bạn áp dụng lên mô hình đã chuẩn hoá khi các truy vấn thực tế, người dùng thực tế và SLA thực tế cho thấy join và tính toán tức thời là quá chậm hoặc quá tốn kém. Trên thực tế, bạn đánh đổi đọc nhanh hơn và truy vấn đơn giản hơn lấy nhiều lưu trữ hơn, ghi phức tạp hơn và thêm công việc đảm bảo nhất quán.
Trong bài viết này, tôi sẽ chỉ cho bạn nơi phi chuẩn hoá hữu ích và khi nào thì không, cũng như cách triển khai an toàn trong cơ sở dữ liệu SQL. Mục tiêu không phải bỏ rơi thiết kế tốt, mà là chồng thêm “đường tắt” lên trên khi khối lượng công việc biện minh cho nó.
Phi chuẩn hoá trong cơ sở dữ liệu là gì?
Nếu bạn quen thuộc với chuẩn hoá và thiết kế cơ sở dữ liệu, đây là câu trả lời ngắn:
Phi chuẩn hoá là hành động có chủ đích bổ sung dữ liệu dư thừa vào một lược đồ đã chuẩn hoá để tăng tốc đọc và đơn giản hoá truy vấn. Đây là tối ưu hiệu năng có mục tiêu, không phải cái cớ để bỏ qua mô hình hoá tốt!
Nếu bạn còn mới với thiết kế cơ sở dữ liệu, đáng để tách bạch ý nghĩa của “được chuẩn hoá”, “phi chuẩn hoá” và “chưa chuẩn hoá”. Bạn thường nghe 3 thuật ngữ này trên mạng, và điều quan trọng là đừng nhầm lẫn.
Được chuẩn hoá: Dữ liệu được tách thành các bảng có cấu trúc tốt nhằm tối thiểu hoá dư thừa và bảo vệ tính toàn vẹn (xem 3NF/BCNF).
Phi chuẩn hoá: Bạn đưa trở lại sự dư thừa có chọn lọc bằng các cột bổ sung, giá trị tính sẵn hoặc bảng đã join sẵn, dựa trên mô hình chuẩn hoá đó để làm nhanh các lượt đọc phổ biến. Với phi chuẩn hoá, bạn giữ một nguồn sự thật duy nhất (các bảng chuẩn hoá), rồi duy trì một hoặc nhiều biểu diễn nhanh hơn cho các đường truy cập nóng, như dashboard, danh sách sản phẩm, tìm kiếm, v.v. Bạn đánh đổi lưu trữ + độ phức tạp ghi lấy tốc độ đọc + truy vấn đơn giản.
Chưa chuẩn hoá: Dữ liệu thô, tuỳ tiện hoặc lộn xộn nơi cấu trúc và ràng buộc chưa từng được thiết kế đúng. Đó không phải điều chúng ta làm ở đây.
Ví dụ trong SQL
Đây là một ví dụ SQL nhỏ giúp bạn hình dung sự khác nhau giữa mô hình hoá chuẩn hoá và phi chuẩn hoá.
Mô hình hoá chuẩn hoá
-- Source of truthCREATETABLE customers (
customer_id BIGINTPRIMARYKEY,
name TEXTNOTNULL,
tier TEXTNOTNULL);CREATETABLE orders (
order_id BIGINTPRIMARYKEY,
customer_id BIGINTNOTNULLREFERENCES customers(customer_id),
order_total NUMERIC(12,2)NOTNULL,
created_at TIMESTAMPNOTNULLDEFAULTnow());-- Typical report needs a joinSELECT c.name, c.tier,SUM(o.order_total)AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUPBY c.name, c.tier;Được hỗ trợ bởi
Phi chuẩn hoá cho luồng báo cáo
-- Add redundant fields for faster readsALTERTABLE orders
ADDCOLUMN customer_name TEXT,ADDCOLUMN customer_tier TEXT;-- Now most reports avoid the joinSELECT customer_name, customer_tier,SUM(order_total)AS revenue
FROM orders
GROUPBY customer_name, customer_tier;Được hỗ trợ bởi
Trong ví dụ này, bạn sẽ giữ bảng customers làm nguồn sự thật và đảm bảo orders.customer_name / orders.customer_tierluôn đồng bộ (ví dụ trigger, job CDC, hoặc backfill theo lịch). Cùng một sự thật, hai biểu diễn, mỗi cái tối ưu cho một việc khác nhau.
Phi chuẩn hoá có phải thiết kế tệ?
Có một hiểu lầm phổ biến rằng phi chuẩn hoá chỉ là sản phẩm của thiết kế tệ. Không phải vậy nếu nó được đo lường, giới hạn phạm vi và được duy trì. Đúng là phi chuẩn hoá thường vi phạm các dạng chuẩn cao hơn (đó là mục đích), nhưng nó là có chủ đích và có kế hoạch để bảo toàn tính nhất quán. Thiết kế tệ là bỏ qua chuẩn hoá hoàn toàn hoặc rải rác dư thừa mà không có chiến lược đồng bộ.
Nếu bạn muốn tìm hiểu thêm về thiết kế cơ sở dữ liệu, khóa học cho người mới bắt đầu của chúng tôi về chủ đề này là điểm khởi đầu tốt!
Chuẩn hoá và Phi chuẩn hoá
Dưới đây là so sánh song song. Không đầy đủ nhưng sẽ giúp bạn hiểu các đánh đổi của mỗi cách tiếp cận.
Khía cạnh
Chuẩn hoá
Phi chuẩn hoá
Mục tiêu chính
Toàn vẹn, tối thiểu dư thừa, cập nhật dễ & đúng
Đọc nhanh hơn, truy vấn đơn giản hơn trên đường nóng
Phù hợp nhất cho
Hệ thống OLTP với ghi/cập nhật thường xuyên
Dashboard nặng đọc, trang tìm kiếm/danh sách, báo cáo/phân tích
Hiệu năng đọc
Thường cần join, tốt nếu có chỉ mục phù hợp
Ít join hơn, có thể nhanh hơn và dự đoán được
Độ phức tạp ghi
Đơn giản: một nguồn sự thật
Cao hơn: phải cập nhật/đồng bộ bản sao hoặc số tổng hợp
Lưu trữ
Gọn nhẹ
Lớn hơn (cột/bảng bổ sung, view tính sẵn)
Tính toàn vẹn dữ liệu
Mạnh do thiết kế (ràng buộc 3NF/BCNF)
Cần cơ chế ngăn trôi lệch (trigger, CDC, job)
Tốc độ thay đổi
Đổi tên/cập nhật cột mang tính cục bộ
Thay đổi có thể lan toả qua dữ liệu trùng lặp
Chi phí vận hành
Thấp: ít thành phần động
Cao: chính sách làm mới, backfill, giám sát
Kiểu lỗi
Truy vấn N+1, join chậm, thiếu chỉ mục
Dữ liệu lỗi thời, không nhất quán, khuếch đại ghi
Tiến hoá lược đồ
Dễ dự đoán, thân thiện tái cấu trúc
Cần kế hoạch migrate cho các biểu diễn dư thừa
Ví dụ điển hình
Đơn hàng, khách hàng, giao dịch
Danh sách sản phẩm đã join sẵn; bảng tổng hợp doanh số; view vật hoá/đánh chỉ mục
Vì sao và khi nào nên phi chuẩn hoá
Phi chuẩn hoá thực sự hữu ích khi người dùng và truy vấn thực bị chặn bởi join, tổng hợp hoặc tra cứu lặp lại. Khi bạn đã xác nhận rằng đánh chỉ mục, tinh chỉnh truy vấn và cache là chưa đủ, bạn có thể chuyển sang phi chuẩn hoá để tối ưu cho đọc nhanh trên các mẫu truy cập có thể dự đoán.
Khi phi chuẩn hoá có thể giúp
Tải công việc nặng đọc:độ trễ p95/p99 bị chi phối bởi join hoặc tổng hợp, CPU tiêu tốn cho hash/merge join, bộ đệm cache thay đổi liên tục.
Hình dạng truy vấn ổn định:Các dashboard/endpoint giống nhau chạy suốt ngày với bộ lọc tương tự (ví dụ doanh số hôm qua theo danh mục).
Join fan-out:Một bảng nóng join với 3–5 bảng khác chỉ để render thẻ hoặc danh sách.
Điểm nóng tổng hợp:Bạn liên tục tính tổng, đếm hoặc giá trị mới nhất trên phạm vi lớn.
Áp lực SLA:Sản phẩm cần phản hồi dưới 200 ms trong khi kế hoạch hiện tại tràn ra đĩa hoặc quét quá nhiều.
Trường hợp sử dụng kinh điển
Dashboard & báo cáo BI (OLAP/analytics):Tính trước các tổng hợp theo ngày/tháng, giữ materialized view cho các group-by đắt đỏ, hoặc lưu bảng fact phi chuẩn hoá cho các lát cắt phổ biến.
E-commerce/danh mục & trang tìm kiếm/danh sách:Nhân đôi category_name, brand_name, price_with_tax, hoặc một phép chiếu sản phẩm đã join sẵn để có danh sách và bộ lọc nhanh.
CMS/blog/bảng tin:Lưu author_name, primary_topic, hoặc rendered_excerpt ngay trên bảng bài viết để tránh join hoặc chuyển đổi lúc chạy.
Bảng hoạt động & bộ đếm:Giữ like_count, follower_count, hoặc latest_comment_at như thuộc tính dẫn xuất thay vì tính lại.
Phân tích sự kiện/log ở quy mô lớn (OLAP/NoSQL):Làm phẳng dữ liệu lồng nhau cho kho dạng cột và giữ các hàng rộng thân thiện với phân vùng để việc quét có thể dự đoán.
Khi không nên phi chuẩn hoá và nên dùng thứ khác trước
OLTP nặng ghi với yêu cầu nhất quán nghiêm ngặt (đơn hàng, thanh toán, điều chỉnh tồn kho).
Nút thắt là chỉ mục thiếu/kém, truy vấn N+1, hoặc ORM nói chuyện quá nhiều. Hãy sửa những thứ đó trước.
Các trường biến động cao (ví dụ tình trạng sẵn có của sản phẩm thay đổi từng giây) nơi việc nhân đôi khuếch đại dao động.
Nhóm không có kế hoạch sở hữu và đồng bộ rõ ràng (trigger, CDC/job, chính sách làm mới, giám sát trôi lệch). Thiếu những điều này, bạn có nguy cơ gây hại nhiều hơn lợi!
Tập dữ liệu đủ nhỏ để một chỉ mục bao phủ hoặc cache đã khiến nó nhanh.
Bạn thực sự muốn dùng phi chuẩn hoá nơi nó cho bạn tốc độ đọc lớn nhất với gánh nặng vận hành bổ sung ít nhất, và chỉ sau khi bạn đã loại trừ các cách sửa rẻ hơn.
Nếu bạn nhận ra rằng phi chuẩn hoá chưa hẳn là điều bạn cần lúc này, đừng vội! Chúng ta sẽ khám phá các lựa chọn thay thế trong phần tiếp theo.
Các lựa chọn thay thế cho Phi chuẩn hoá
Phi chuẩn hoá không phải lời giải cho mọi vấn đề. Trước khi thêm dư thừa, bạn cần vắt kiệt sức mạnh từ engine và ứng dụng của mình. Những cách sửa này rẻ hơn để duy trì và thường mang lại hiệu quả tương tự.
1) Đánh chỉ mục
Chỉ mục tổng hợp/bao phủ: Trong truy vấn SQL, đặt các cột lọc trước, rồi đến các cột GROUP BY/ORDER BY. Tôi đưa cả các cột trong danh sách chọn vào để engine có thể phục vụ truy vấn chỉ từ chỉ mục.
Chỉ mục lọc/một phần: Chỉ đánh chỉ mục lát cắt nóng (ví dụ status = 'ACTIVE'), giữ chỉ mục nhỏ và nhanh.
Chỉ mục biểu thức/hàm: Đánh chỉ mục trên LOWER(email) hoặc date_trunc('day', created_at) để tránh quét tính toán.
2) Tinh chỉnh truy vấn & phân trang
Tránh SELECT *. Chỉ lấy những gì bạn hiển thị.
Thay join không cần thiết bằng EXISTS/SEMI khi bạn chỉ cần kiểm tra sự hiện diện.
Đẩy điều kiện xuống: lọc sớm, tổng hợp muộn.
Dùng phân trang theo khoá (WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) để cuộn nhanh, ổn định.
3) Caching
Dùng cache ứng dụng (ví dụ Redis) cho truy vấn nóng và các mảnh đã render.
Dùng cache HTTP (ETag/Last-Modified) cho trang công khai và dashboard.
Cache ngắn hạn (30–120 s) thường loại bỏ nhu cầu đổi lược đồ.
4) Bản sao chỉ đọc
Chuyển tải đọc nặng sang replica. Tuyệt vời cho dashboard và xuất dữ liệu.
5) Phân vùng & cắt tỉa
Phân vùng dải/băm các bảng lớn để các lượt quét chỉ chạm phân vùng liên quan (ví dụ 30 ngày gần nhất). Nhân tiện, đây không phải là phi chuẩn hoá, chỉ là giảm lượng dữ liệu bị quét.
6) Kho dạng cột/OLAP
Chuyển phân tích nặng sang Snowflake/BigQuery/ClickHouse (qua ELT/dbt). Giữ OLTP ở trạng thái chuẩn hoá và để warehouse xử lý các dạng rộng, thân thiện quét.
7) Vệ sinh ORM
Loại bỏ truy vấn N+1 (tải sớm hoặc theo lô), đặt danh sách chọn hợp lý, và giới hạn kích thước trang. Một lớp ORM gọn gàng có thể loại bỏ nhu cầu phi chuẩn hoá.
8) Cột tính toán/phát sinh (DB duy trì)
Để DB duy trì các giá trị dẫn xuất (ví dụ price_with_tax) dưới dạng cột phát sinh/tính toán hoặc qua chỉ mục biểu thức. Điều đó cho phép đọc nhanh mà không cần logic đồng bộ ở cấp ứng dụng.
Kỹ thuật Phi chuẩn hoá
Dưới đây là những cách phổ biến nhất để thêm dư thừa có kiểm soát vào mô hình đã chuẩn hoá. Với mỗi cách, tôi sẽ chỉ ra nó làm gì, khi nào dùng, và cách giữ đồng bộ.
Thiết lập: giả sử một lõi chuẩn hoá với customers, orders, order_items, products.
1) Bảng “phép chiếu” làm phẳng/đã join sẵn
Là gì: Tạo một bảng join sẵn các cột bạn cần cho đọc nhanh (ví dụ danh sách sản phẩm hoặc dashboard đơn hàng).
Khi nào: Đường nóng của bạn join 3 đến 5 bảng theo cách có thể dự đoán.
Cách làm (PostgreSQL):
-- Read-optimised projection for a typical orders listCREATETABLE orders_projection (
order_id BIGINTPRIMARYKEY,
created_day DATENOTNULL,
customer_id BIGINTNOTNULL,
customer_name TEXTNOTNULL,
total_amount NUMERIC(12,2)NOTNULL);-- Initial backfillINSERTINTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)SELECT o.order_id,
o.created_at::dateAS created_day,
c.customer_id,
c.name AS customer_name,SUM(oi.quantity * oi.unit_price)AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUPBY o.order_id, created_day, c.customer_id, c.name;-- Index for fast filtering by day/customerCREATEINDEXON orders_projection (created_day, customer_id);Được hỗ trợ bởi
Tuỳ chọn đồng bộ:
Job sau khi ghi (enqueue “order.updated” → tính lại hàng)
Job theo lô hàng đêm/mỗi 15 phút (dbt/cron)
2) Cột dư thừa (sao chép vài thuộc tính)
Là gì: Nhân đôi một vài thuộc tính được đọc thường xuyên sang bảng khác để tránh join (ví dụ orders.customer_name).
Khi nào: Bạn chỉ cần 1–2 giá trị và không muốn một phép chiếu đầy đủ.
Cách làm (ví dụ trigger PostgreSQL):
ALTERTABLE orders
ADDCOLUMN customer_name TEXT,ADDCOLUMN customer_tier TEXT;-- Keep the redundant fields correct on insert/updateCREATEORREPLACEFUNCTION sync_order_customer_fields()RETURNSTRIGGERAS $
BEGINSELECT name, tier INTO NEW.customer_name, NEW.customer_tier
FROM customers WHERE customer_id = NEW.customer_id;RETURN NEW;END;
$ LANGUAGE plpgsql;CREATETRIGGER trg_orders_sync_customer
BEFORE INSERTORUPDATEOF customer_id ON orders
FOR EACH ROWEXECUTEFUNCTION sync_order_customer_fields();-- Propagate customer name/tier changes to existing ordersCREATEORREPLACEFUNCTION propagate_customer_changes()RETURNSTRIGGERAS $
BEGINUPDATE orders
SET customer_name = NEW.name,
customer_tier = NEW.tier
WHERE customer_id = NEW.customer_id;RETURN NEW;END;
$ LANGUAGE plpgsql;CREATETRIGGER trg_customer_propagate
AFTERUPDATEOF name, tier ON customers
FOR EACH ROWEXECUTEFUNCTION propagate_customer_changes();Được hỗ trợ bởi
Đánh đổi: Đọc đơn giản, nhưng ghi sẽ lan toả khi khách hàng được cập nhật.
3) Thuộc tính dẫn xuất (lưu giá trị tính toán)
Là gì: Lưu các giá trị vốn dĩ bạn tính tức thời (ví dụ price_with_tax, latest_comment_at, item_count).
Khi nào: Phép tính được thực hiện liên tục và có tính quyết định.
Hai mẫu phổ biến:
Cột phát sinh (DB tính lại khi ghi: bạn ít kiểm soát, không tham chiếu chéo bảng)
Cột lưu trữ + trigger/job (bạn kiểm soát khi nào/cách nó thay đổi)
-- Generated column example (Postgres 12+; same-table expressions)ALTERTABLE products
ADDCOLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS(price *1.20) STORED;-- Counter maintained by triggers (likes per post)ALTERTABLE posts ADDCOLUMN like_count INTEGERNOTNULLDEFAULT0;CREATEORREPLACEFUNCTION inc_like_count()RETURNSTRIGGERAS $
BEGINUPDATE posts SET like_count = like_count +1WHERE post_id = NEW.post_id;RETURN NEW;END; $ LANGUAGE plpgsql;CREATETRIGGER trg_like_insert
AFTERINSERTON post_likes
FOR EACH ROWEXECUTEFUNCTION inc_like_count();CREATEORREPLACEFUNCTION dec_like_count()RETURNSTRIGGERAS $
BEGINUPDATE posts SET like_count = GREATEST(like_count -1,0)WHERE post_id = OLD.post_id;RETURN OLD;END; $ LANGUAGE plpgsql;CREATETRIGGER trg_like_delete
AFTERDELETEON post_likes
FOR EACH ROWEXECUTEFUNCTION dec_like_count();Được hỗ trợ bởi
4) Bảng tổng hợp/tóm tắt
Là gì: Tính sẵn các rollup như doanh thu hàng ngày, người dùng hoạt động mỗi ngày, đơn hàng theo danh mục.
Khi nào: Dashboard lặp lại cùng group-by và quét thô thì tốn kém.
CREATETABLE daily_sales (
sales_day DATEPRIMARYKEY,
gross_amount NUMERIC(14,2)NOTNULL,
order_count INTEGERNOTNULL);-- Incremental upsert for "yesterday" (run hourly/15-min)INSERTINTO daily_sales (sales_day, gross_amount, order_count)SELECT(o.created_at AT TIME ZONE 'UTC')::dateAS sales_day,SUM(oi.quantity * oi.unit_price)AS gross_amount,COUNT(DISTINCT o.order_id)AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day',now()-interval'1 day')AND o.created_at < date_trunc('day',now())GROUPBY sales_day
ON CONFLICT (sales_day)DOUPDATESET gross_amount = EXCLUDED.gross_amount,
order_count = EXCLUDED.order_count;Được hỗ trợ bởi
Tuỳ chọn đồng bộ: job theo lịch (cron/dbt), cập nhật streaming (CDC), hoặc bộ tổng hợp theo sự kiện.
5) Materialized view (kết quả truy vấn được lưu)
Là gì: Lưu đầu ra của một truy vấn đắt đỏ dưới dạng bảng vật lý mà bạn có thể đánh chỉ mục.
Khi nào: Tập kết quả đắt đỏ, tương đối ổn định, và bạn chấp nhận được khoảng làm mới.
-- Expensive reportCREATE MATERIALIZED VIEW mv_category_sales ASSELECT p.category_id,
date_trunc('day', o.created_at)ASday,SUM(oi.quantity * oi.unit_price)AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUPBY p.category_id, date_trunc('day', o.created_at);-- Make it fast to queryCREATEINDEXON mv_category_sales (category_id,day);-- Refresh patterns-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;-- Non-blocking reads (requires unique index on the MV):-- 1) ensure a unique index exists (e.g., (category_id, day))-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;Được hỗ trợ bởi
Đánh đổi: Bạn sẽ phải quản lý nhịp làm mới vì Postgres không hỗ trợ làm mới gia tăng nguyên bản. Dù vậy, khá dễ khi dùng dbt hoặc các mẫu “chỉ-append + rebuild theo cửa sổ”.
6) Phép chiếu “tài liệu” JSON tính sẵn
Là gì: Lưu một blob JSON phi chuẩn hoá khớp nhu cầu API/UI của bạn (ví dụ thẻ sản phẩm), kèm các chỉ mục JSON mục tiêu.
Khi nào: API của bạn phục vụ cùng một hình dạng lặp đi lặp lại, và bạn muốn một lần đọc.
CREATETABLE product_cards (
product_id BIGINTPRIMARYKEY,
card_json JSONB NOTNULL);-- Build or rebuild card documentsINSERTINTO product_cards (product_id, card_json)SELECT p.product_id,
jsonb_build_object('id', p.product_id,'name', p.name,'brand', b.name,'price_with_tax', p.price *1.20,'category', c.name
)FROM products p
JOIN brands b ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id)DOUPDATESET card_json = EXCLUDED.card_json;-- GIN index for JSON queries if neededCREATEINDEX idx_product_cards_gin ON product_cards USING GIN (card_json);Được hỗ trợ bởi
Tuỳ chọn đồng bộ: rebuild theo sự kiện khi sản phẩm/nhãn hiệu/danh mục thay đổi, hoặc làm mới theo lô thường xuyên cho các sản phẩm cập nhật gần đây.
Chọn chiến lược đồng bộ (hướng dẫn nhanh)
Trigger: Tức thời và đảm bảo nhất quán giao dịch. Nói chung rất tốt cho fan-out nhỏ và khối lượng ghi thấp nhưng có thể làm chậm độ trễ ghi nếu lạm dụng.
Ghi kép ở ứng dụng: Ứng dụng ghi cả nguồn + phần phi chuẩn hoá. Đơn giản nhưng rủi ro hơn. Bạn có thể giảm rủi ro bằng retry idempotent và mẫu outbox/CDC.
CDC → worker: Chiến lược đồng bộ này đáng tin cậy và mở rộng tốt vì nó truyền thay đổi bất đồng bộ. Lý tưởng khi chấp nhận nhất quán cuối cùng.
Job theo lịch: Tuỳ chọn đơn giản nhất cho số tổng hợp và materialized view. Chỉ cần chọn khung làm mới phù hợp với dung sai UX.
Tôi khuyên bạn luôn xây dựng khả năng quan sát vào giải pháp. Không gì tệ hơn việc không chắc các thay đổi đã được xử lý hoặc sao chép đúng chưa. Nó cũng giúp bạn tính toán kiểm tra trôi lệch và các script backfill.
Cách triển khai Phi chuẩn hoá theo từng bước
Như mọi khi, can thiệp vào cơ sở dữ liệu khá rủi ro. Tôi rất khuyến nghị bạn nhân bản cơ sở dữ liệu và thử triển khai phi chuẩn hoá trên bản sao trước, để chắc chắn mọi thứ vận hành như kỳ vọng. Nếu có môi trường dev, càng tốt!
Quy trình khá đơn giản: đo lường → thay đổi ít nhất có thể → giữ cho đúng → xác minh → giám sát → lặp lại.
1) Profiling và đặt mục tiêu thành công
Ghi lại chính xác truy vấn (văn bản + tham số) và tần suất của nó.
Ghi đường cơ sở: EXPLAIN (ANALYZE, BUFFERS), độ trễ p95, CPU/IO, số hàng quét.
Đồng thuận tiêu chí thành công (ví dụ “p95 < 120 ms với chi phí ghi ≤1,2×”).
Xác nhận mức dung sai nhất quán (ví dụ “analytics có thể trễ 5 phút”).
2) Chọn mức phi chuẩn hoá nhỏ nhất hiệu quả
Lý tưởng là thử một cột dư thừa hoặc một bảng tóm tắt trước khi làm một phép chiếu lớn.
Phác thảo hình dạng mới:
Cột dư thừa? (cột nào, vì sao)
Bảng tổng hợp hay materialized view? (cấp hạt, khoá, khung làm mới)
Quyết định mô hình nhất quán:
Mạnh (trigger/giao dịch) so với cuối cùng (CDC/job/làm mới materialized view).
Ghi ngân sách khuếch đại ghi (chấp nhận bao nhiêu ghi bổ sung cho mỗi sự kiện).
3) Thiết kế đường đồng bộ
Trigger (nhất quán mạnh, fan-out nhỏ).
CDC/outbox → worker (mở rộng tốt, nhất quán cuối cùng):
Ứng dụng ghi vào bảng nguồn và một hàng outbox trong cùng giao dịch.
Worker đọc outbox và cập nhật đích phi chuẩn hoá một cách idempotent.
Job theo lịch / làm mới MV (tốt cho tổng hợp):
Xác định nhịp làm mới, cửa sổ và chiến lược backfill.
Idempotency là bất khả thương lượng. Các cập nhật phải có thể lặp lại an toàn (ví dụ UPSERT với tính toán quyết định). Tin tôi đi, sau này bạn sẽ cảm ơn chính mình.
4) Tạo, backfill và xác thực
Tạo cấu trúc (bảng, cột, chỉ mục, trigger/job) nhưng giữ ứng dụng không đổi.
Backfill từ nguồn sự thật.
Xác thực tính tương đương bằng các bất biến:
-- Example parity check: orders_projection vs live joinSELECTCOUNT(*)AS mismatches
FROM orders_projection p
JOIN orders o ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
OR p.total_amount <>(SELECTSUM(oi.quantity * oi.unit_price)FROM order_items oi WHERE oi.order_id = o.order_id
);Được hỗ trợ bởi
4. Sửa các sai lệch; chạy lại cho đến khi bằng 0 (hoặc trong ngân sách sai số đã thống nhất).
5) Chuyển đổi an toàn
Triển khai thay đổi ứng dụng sau cờ tính năng hoặc canary (ví dụ 10% lưu lượng đọc từ đường phi chuẩn hoá).
Chạy đọc “bóng”: tính kết quả cũ ở nền và so sánh băm/tổng hợp cho một phần yêu cầu.
Giữ phương án rollback nhanh (chuyển ngay về đường chuẩn hoá).
6) Giám sát đúng thứ
Tạo một dashboard nhỏ trong công cụ quan sát bạn chọn:
Đọc: độ trễ p50/p95, số hàng đọc, hit bộ đệm, độ ổn định kế hoạch truy vấn.
Ghi: thời gian ghi bổ sung, lỗi trigger/job, độ trễ hàng đợi (CDC), timestamp làm mới MV lần cuối.
Chất lượng dữ liệu: bộ đếm trôi lệch (kiểm tra tương đương hàng ngày), số hàng đã backfill, % sai lệch.
Chi phí/dấu chân: kích thước bảng, phình to chỉ mục, tăng trưởng kích thước MV.
7) Vận hành & lặp lại
Phân công sở hữu (nhóm/hệ thống trực) và runbook (cách rebuild, backfill, sửa chữa). Điều này thường bị bỏ qua, nhưng bạn sẽ muốn phản hồi nhanh nhất có thể khi sự cố xảy ra.
Đánh giá lại hàng quý: còn cần không? Vẫn đúng hình dạng chứ? Có thể đơn giản hoá không? Ghi lại quyết định và liên kết tới các dashboard.
Ưu và nhược điểm của Phi chuẩn hoá
Chúng ta đã đề cập chi tiết trong bảng Chuẩn hoá vs Phi chuẩn hoá ở phần trước, nên đây là tóm tắt nhanh.
Bạn được gì
Đọc nhanh hơn và độ trễ p95/p99 dễ dự đoán hơn
Truy vấn đơn giản hơn trên đường nóng (ít join/tổng hợp hơn)
CPU/IO thấp hơn mỗi lần đọc và truy vấn phân tích rẻ hơn
Bạn phải trả giá gì
Khuếch đại ghi (cập nhật/chèn bổ sung)
Rủi ro nhất quán (dữ liệu cũ/trôi lệch trừ khi bạn đồng bộ tốt)
Tăng trưởng lưu trữ (cột/bảng/view trùng lặp)
Chi phí vận hành (làm mới, backfill, giám sát)
Lan toả thay đổi (cập nhật lược đồ/logic phải được phản chiếu)
Công cụ và công nghệ hỗ trợ Phi chuẩn hoá
Không phải mọi CSDL đều hỗ trợ phi chuẩn hoá theo cùng một cách. Một số cung cấp tính năng hạng nhất để lưu kết quả truy vấn đắt đỏ, số khác kỳ vọng bạn tự xây bằng job, trigger hoặc pipeline.
PostgreSQL
Postgres cho bạn vài cách để “vật hoá” các hình dạng tối ưu đọc.
Materialized view lưu kết quả truy vấn, nên đọc là tức thời. Bạn chọn khi nào làm mới (theo lịch hoặc ad hoc), và thậm chí có thể làm mới đồng thời để không chặn người đọc.
Với sao chép nhẹ nhàng, cột phát sinh và chỉ mục biểu thức cho phép DB duy trì giá trị dẫn xuất và tăng tốc các bộ lọc phổ biến mà không cần viết logic đồng bộ bổ sung.
Khi bạn cần nhất quán mạnh giữa nguồn và bản sao phi chuẩn hoá, bạn có thể dùng trigger để giữ đồng bộ (nhưng nhớ rằng, chúng tăng tải lên đường ghi!). Với cập nhật theo sự kiện hoặc theo lô, bạn có thể ghép Postgres với bộ lập lịch (ví dụ pg_cron hoặc job runner ngoài) hoặc với sao chép logic/CDC để stream thay đổi vào worker cập nhật các bảng phi chuẩn hoá một cách bất đồng bộ.
Nếu bạn muốn thực hành và tự tạo CSDL để thử phi chuẩn hoá, hãy xem khóa học PostgreSQL của chúng tôi.
SQL Server
SQL Server tận dụng mạnh mẽ indexed view, về bản chất là materialized view “luôn bật”, nơi engine giữ view đồng bộ trên mọi insert/update/delete, nên đọc siêu nhanh. Mặt trái là đường ghi giờ phải duy trì cả bảng gốc và view, nên khối lượng OLTP nặng có thể chậm lại.
Ngoài ra, bạn có thể dùng cột tính toán persisted để bao phủ các thuộc tính dẫn xuất theo từng hàng.
Với báo cáo ở quy mô lớn, columnstore index là lựa chọn thay thế cho phi chuẩn hoá: chúng nén và quét bộ dữ liệu lớn rất hiệu quả, có thể loại bỏ nhu cầu nhân đôi dữ liệu ngay từ đầu.
Oracle
Materialized View của Oracle là lựa chọn trưởng thành với làm mới FAST dùng nhật ký thay đổi để cập nhật phần đã đổi, hoặc làm mới COMPLETE khi bạn chấp nhận rebuild toàn bộ.
Với Query Rewrite, bộ tối ưu có thể tự động dùng materialized view của bạn khi ai đó truy vấn bảng nền, nên đội ngũ được tăng tốc mà không cần đổi SQL ứng dụng. Như mọi khi, đọc nhanh hơn đồng nghĩa có chỗ khác làm thêm việc: duy trì log và làm mới view làm tăng chi phí ghi và vận hành.
MySQL / MariaDB
MySQL không có materialized view gốc, nên phi chuẩn hoá thường được xây bằng các bảng vật lý cộng với job theo lịch hoặc trigger để cập nhật.
Cột phát sinh giúp với các giá trị dẫn xuất đơn giản. Cách này hiệu quả cho dashboard và tóm tắt có thể dự đoán, nhưng một lần nữa, đáng để cân nhắc nhịp làm mới và độ phức tạp của trigger để không vô tình đánh thuế đường ghi quan trọng. Nhiều đội kết hợp với replica chỉ đọc để chuyển tải truy vấn báo cáo hoàn toàn.
Snowflake / BigQuery (lớp phân tích)
Kho dạng cột sinh ra để phục vụ dữ liệu rộng, thân thiện quét, phi chuẩn hoá. Bạn thường mô hình bảng fact rộng với các thuộc tính hữu ích được “nướng sẵn”, rồi dựa vào phân vùng/clustering để cắt tỉa phạm vi quét.
Cả hai nền tảng hỗ trợ materialized view và tác vụ/truy vấn theo lịch để giữ số tổng hợp tươi mới mà không đụng tới hệ OLTP. Bạn đánh đổi chi phí lưu trữ và làm mới lấy các lượt đọc rất dự đoán được, rẻ ở quy mô lớn. Rất phù hợp cho dashboard và BI.
dbt được coi là lớp “hạ tầng như mã” cho phân tích phi chuẩn hoá. Bạn định nghĩa model một lần, chọn cách vật hoá (bảng, view, incremental), và để dbt xử lý build, phụ thuộc và test. Model incremental đặc biệt hữu ích cho bảng tóm tắt chỉ cần append và merge dữ liệu mới. Đây là một trong những công cụ tôi yêu thích nhất!
CDC & pipeline dữ liệu
Khi bạn muốn thay đổi từ ứng dụng chảy vào các cấu trúc phi chuẩn hoá một cách đáng tin cậy, Change Data Capture là lựa chọn hàng đầu. Công cụ như Debezium (tự vận hành) hoặc các connector managed (Fivetran, Airbyte) stream thay đổi cấp hàng từ OLTP vào worker hoặc warehouse để cập nhật bảng phép chiếu, bộ đếm, hoặc tổng hợp. Thường là nhất quán cuối cùng (tuyệt cho dashboard và feed), và mở rộng tốt hơn nhiều so với nhồi nhét mọi thứ vào trigger. Hãy làm cập nhật idempotent, giám sát độ trễ, và giữ đường backfill cho sự kiện trễ hoặc bỏ lỡ.
Điểm mấu chốt & Lời kết
Phi chuẩn hoá là tối ưu hoá thực dụng được chồng lên thiết kế chuẩn hoá, không phải phủ nhận nó. Bạn đang đánh đổi lưu trữ và độ phức tạp ghi để có các lượt đọc nhanh hơn, đơn giản hơn trên những đường đi cụ thể và được hiểu rõ.
Hãy nhớ:
Bắt đầu bằng chuẩn hoá. Chỉ phi chuẩn hoá nơi một truy vấn đã đặt tên, đã đo lường cần.
Dùng hết các cách sửa rẻ hơn trước (chỉ mục, viết lại truy vấn, cache, replica, OLAP).
Chọn mức phi chuẩn hoá nhỏ nhất mà hiệu quả (một cột dư thừa, một tổng hợp nhỏ, hoặc một materialized view).
Rõ ràng về độ tươi (nhất quán mạnh so với cuối cùng) và xây đồng bộ idempotent.
Đo trước/sau, giám sát trôi lệch và giữ công tắc rollback trong tầm tay.
Xem các bảng chuẩn hoá là nguồn sự thật! Các phần phi chuẩn hoá là bản sao tối ưu đọc có thể rebuild.
Tác giả
Marie Fayard
Tôi là một trưởng nhóm kỹ thuật định hướng sản phẩm, chuyên giúp các startup giai đoạn đầu phát triển từ nguyên mẫu đầu tiên đến khi đạt được sự phù hợp sản phẩm - thị trường và xa hơn nữa. Tôi luôn tò mò về cách con người sử dụng công nghệ, và tôi thích làm việc sát sao với nhà sáng lập và các nhóm liên chức năng để biến những ý tưởng táo bạo thành hiện thực. Khi không xây dựng sản phẩm, tôi tìm cảm hứng ở những miền đất mới hoặc xả căng thẳng tại phòng tập yoga.
Tôi đã viết rất nhiều về chuẩn hoá và vì sao đó là nền tảng đáng tin cậy cho tính toàn vẹn dữ liệu. Tôi không tự mâu thuẫn với mình ở đây. Tôi vẫn tin rằng một lược đồ được chuẩn hoá tốt là điểm khởi đầu đúng cho hầu hết hệ thống giao dịch. Bài viết này nói về lựa chọn có chủ đích để đi ngược lại sự “thuần khiết” đó trong những tình huống rất cụ thể, nơi hiệu năng đọc quan trọng hơn việc tuân thủ chặt chẽ các dạng chuẩn.
Phi chuẩn hoá không phải là bỏ qua chuẩn hoá. Đây là một tối ưu hiệu năng bạn áp dụng lên mô hình đã chuẩn hoá khi các truy vấn thực tế, người dùng thực tế và SLA thực tế cho thấy join và tính toán tức thời là quá chậm hoặc quá tốn kém. Trên thực tế, bạn đánh đổi đọc nhanh hơn và truy vấn đơn giản hơn lấy nhiều lưu trữ hơn, ghi phức tạp hơn và thêm công việc đảm bảo nhất quán.
Trong bài viết này, tôi sẽ chỉ cho bạn nơi phi chuẩn hoá hữu ích và khi nào thì không, cũng như cách triển khai an toàn trong cơ sở dữ liệu SQL. Mục tiêu không phải bỏ rơi thiết kế tốt, mà là chồng thêm “đường tắt” lên trên khi khối lượng công việc biện minh cho nó.
Phi chuẩn hoá trong cơ sở dữ liệu là gì?
Nếu bạn quen thuộc với chuẩn hoá và thiết kế cơ sở dữ liệu, đây là câu trả lời ngắn:
Phi chuẩn hoá là hành động có chủ đích bổ sung dữ liệu dư thừa vào một lược đồ đã chuẩn hoá để tăng tốc đọc và đơn giản hoá truy vấn. Đây là tối ưu hiệu năng có mục tiêu, không phải cái cớ để bỏ qua mô hình hoá tốt!
Nếu bạn còn mới với thiết kế cơ sở dữ liệu, đáng để tách bạch ý nghĩa của “được chuẩn hoá”, “phi chuẩn hoá” và “chưa chuẩn hoá”. Bạn thường nghe 3 thuật ngữ này trên mạng, và điều quan trọng là đừng nhầm lẫn.
Được chuẩn hoá: Dữ liệu được tách thành các bảng có cấu trúc tốt nhằm tối thiểu hoá dư thừa và bảo vệ tính toàn vẹn (xem 3NF/BCNF).
Phi chuẩn hoá: Bạn đưa trở lại sự dư thừa có chọn lọc bằng các cột bổ sung, giá trị tính sẵn hoặc bảng đã join sẵn, dựa trên mô hình chuẩn hoá đó để làm nhanh các lượt đọc phổ biến. Với phi chuẩn hoá, bạn giữ một nguồn sự thật duy nhất (các bảng chuẩn hoá), rồi duy trì một hoặc nhiều biểu diễn nhanh hơn cho các đường truy cập nóng, như dashboard, danh sách sản phẩm, tìm kiếm, v.v. Bạn đánh đổi lưu trữ + độ phức tạp ghi lấy tốc độ đọc + truy vấn đơn giản.
Chưa chuẩn hoá: Dữ liệu thô, tuỳ tiện hoặc lộn xộn nơi cấu trúc và ràng buộc chưa từng được thiết kế đúng. Đó không phải điều chúng ta làm ở đây.
Ví dụ trong SQL
Đây là một ví dụ SQL nhỏ giúp bạn hình dung sự khác nhau giữa mô hình hoá chuẩn hoá và phi chuẩn hoá.
Mô hình hoá chuẩn hoá
Phi chuẩn hoá cho luồng báo cáo
Trong ví dụ này, bạn sẽ giữ bảng customers làm nguồn sự thật và đảm bảo
orders.customer_name / orders.customer_tierluôn đồng bộ (ví dụ trigger, job CDC, hoặc backfill theo lịch). Cùng một sự thật, hai biểu diễn, mỗi cái tối ưu cho một việc khác nhau.Phi chuẩn hoá có phải thiết kế tệ?
Có một hiểu lầm phổ biến rằng phi chuẩn hoá chỉ là sản phẩm của thiết kế tệ. Không phải vậy nếu nó được đo lường, giới hạn phạm vi và được duy trì. Đúng là phi chuẩn hoá thường vi phạm các dạng chuẩn cao hơn (đó là mục đích), nhưng nó là có chủ đích và có kế hoạch để bảo toàn tính nhất quán. Thiết kế tệ là bỏ qua chuẩn hoá hoàn toàn hoặc rải rác dư thừa mà không có chiến lược đồng bộ.
Nếu bạn muốn tìm hiểu thêm về thiết kế cơ sở dữ liệu, khóa học cho người mới bắt đầu của chúng tôi về chủ đề này là điểm khởi đầu tốt!
Chuẩn hoá và Phi chuẩn hoá
Dưới đây là so sánh song song. Không đầy đủ nhưng sẽ giúp bạn hiểu các đánh đổi của mỗi cách tiếp cận.
Khía cạnh
Chuẩn hoá
Phi chuẩn hoá
Mục tiêu chính
Toàn vẹn, tối thiểu dư thừa, cập nhật dễ & đúng
Đọc nhanh hơn, truy vấn đơn giản hơn trên đường nóng
Phù hợp nhất cho
Hệ thống OLTP với ghi/cập nhật thường xuyên
Dashboard nặng đọc, trang tìm kiếm/danh sách, báo cáo/phân tích
Hiệu năng đọc
Thường cần join, tốt nếu có chỉ mục phù hợp
Ít join hơn, có thể nhanh hơn và dự đoán được
Độ phức tạp ghi
Đơn giản: một nguồn sự thật
Cao hơn: phải cập nhật/đồng bộ bản sao hoặc số tổng hợp
Lưu trữ
Gọn nhẹ
Lớn hơn (cột/bảng bổ sung, view tính sẵn)
Tính toàn vẹn dữ liệu
Mạnh do thiết kế (ràng buộc 3NF/BCNF)
Cần cơ chế ngăn trôi lệch (trigger, CDC, job)
Tốc độ thay đổi
Đổi tên/cập nhật cột mang tính cục bộ
Thay đổi có thể lan toả qua dữ liệu trùng lặp
Chi phí vận hành
Thấp: ít thành phần động
Cao: chính sách làm mới, backfill, giám sát
Kiểu lỗi
Truy vấn N+1, join chậm, thiếu chỉ mục
Dữ liệu lỗi thời, không nhất quán, khuếch đại ghi
Tiến hoá lược đồ
Dễ dự đoán, thân thiện tái cấu trúc
Cần kế hoạch migrate cho các biểu diễn dư thừa
Ví dụ điển hình
Đơn hàng, khách hàng, giao dịch
Danh sách sản phẩm đã join sẵn; bảng tổng hợp doanh số; view vật hoá/đánh chỉ mục
Vì sao và khi nào nên phi chuẩn hoá
Phi chuẩn hoá thực sự hữu ích khi người dùng và truy vấn thực bị chặn bởi join, tổng hợp hoặc tra cứu lặp lại. Khi bạn đã xác nhận rằng đánh chỉ mục, tinh chỉnh truy vấn và cache là chưa đủ, bạn có thể chuyển sang phi chuẩn hoá để tối ưu cho đọc nhanh trên các mẫu truy cập có thể dự đoán.
Khi phi chuẩn hoá có thể giúp
Trường hợp sử dụng kinh điển
Khi không nên phi chuẩn hoá và nên dùng thứ khác trước
Bạn thực sự muốn dùng phi chuẩn hoá nơi nó cho bạn tốc độ đọc lớn nhất với gánh nặng vận hành bổ sung ít nhất, và chỉ sau khi bạn đã loại trừ các cách sửa rẻ hơn.
Nếu bạn nhận ra rằng phi chuẩn hoá chưa hẳn là điều bạn cần lúc này, đừng vội! Chúng ta sẽ khám phá các lựa chọn thay thế trong phần tiếp theo.
Các lựa chọn thay thế cho Phi chuẩn hoá
Phi chuẩn hoá không phải lời giải cho mọi vấn đề. Trước khi thêm dư thừa, bạn cần vắt kiệt sức mạnh từ engine và ứng dụng của mình. Những cách sửa này rẻ hơn để duy trì và thường mang lại hiệu quả tương tự.
1) Đánh chỉ mục
Chỉ mục tổng hợp/bao phủ: Trong truy vấn SQL, đặt các cột lọc trước, rồi đến các cột
GROUP BY/ORDER BY. Tôi đưa cả các cột trong danh sách chọn vào để engine có thể phục vụ truy vấn chỉ từ chỉ mục.Chỉ mục lọc/một phần: Chỉ đánh chỉ mục lát cắt nóng (ví dụ
status = 'ACTIVE'), giữ chỉ mục nhỏ và nhanh.Chỉ mục biểu thức/hàm: Đánh chỉ mục trên
LOWER(email)hoặcdate_trunc('day', created_at)để tránh quét tính toán.2) Tinh chỉnh truy vấn & phân trang
Tránh
SELECT *. Chỉ lấy những gì bạn hiển thị.Thay join không cần thiết bằng
EXISTS/SEMIkhi bạn chỉ cần kiểm tra sự hiện diện.Đẩy điều kiện xuống: lọc sớm, tổng hợp muộn.
Dùng phân trang theo khoá (
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) để cuộn nhanh, ổn định.3) Caching
4) Bản sao chỉ đọc
5) Phân vùng & cắt tỉa
6) Kho dạng cột/OLAP
7) Vệ sinh ORM
8) Cột tính toán/phát sinh (DB duy trì)
Để DB duy trì các giá trị dẫn xuất (ví dụ
price_with_tax) dưới dạng cột phát sinh/tính toán hoặc qua chỉ mục biểu thức. Điều đó cho phép đọc nhanh mà không cần logic đồng bộ ở cấp ứng dụng.Kỹ thuật Phi chuẩn hoá
Dưới đây là những cách phổ biến nhất để thêm dư thừa có kiểm soát vào mô hình đã chuẩn hoá. Với mỗi cách, tôi sẽ chỉ ra nó làm gì, khi nào dùng, và cách giữ đồng bộ.
Thiết lập: giả sử một lõi chuẩn hoá với customers, orders, order_items, products.
1) Bảng “phép chiếu” làm phẳng/đã join sẵn
Là gì: Tạo một bảng join sẵn các cột bạn cần cho đọc nhanh (ví dụ danh sách sản phẩm hoặc dashboard đơn hàng).
Khi nào: Đường nóng của bạn join 3 đến 5 bảng theo cách có thể dự đoán.
Cách làm (PostgreSQL):
Tuỳ chọn đồng bộ:
2) Cột dư thừa (sao chép vài thuộc tính)
Là gì: Nhân đôi một vài thuộc tính được đọc thường xuyên sang bảng khác để tránh join (ví dụ orders.customer_name).
Khi nào: Bạn chỉ cần 1–2 giá trị và không muốn một phép chiếu đầy đủ.
Cách làm (ví dụ trigger PostgreSQL):
Đánh đổi: Đọc đơn giản, nhưng ghi sẽ lan toả khi khách hàng được cập nhật.
3) Thuộc tính dẫn xuất (lưu giá trị tính toán)
Là gì: Lưu các giá trị vốn dĩ bạn tính tức thời (ví dụ
price_with_tax,latest_comment_at,item_count).Khi nào: Phép tính được thực hiện liên tục và có tính quyết định.
Hai mẫu phổ biến:
4) Bảng tổng hợp/tóm tắt
Là gì: Tính sẵn các rollup như doanh thu hàng ngày, người dùng hoạt động mỗi ngày, đơn hàng theo danh mục.
Khi nào: Dashboard lặp lại cùng group-by và quét thô thì tốn kém.
Tuỳ chọn đồng bộ: job theo lịch (cron/dbt), cập nhật streaming (CDC), hoặc bộ tổng hợp theo sự kiện.
5) Materialized view (kết quả truy vấn được lưu)
Là gì: Lưu đầu ra của một truy vấn đắt đỏ dưới dạng bảng vật lý mà bạn có thể đánh chỉ mục.
Khi nào: Tập kết quả đắt đỏ, tương đối ổn định, và bạn chấp nhận được khoảng làm mới.
Đánh đổi: Bạn sẽ phải quản lý nhịp làm mới vì Postgres không hỗ trợ làm mới gia tăng nguyên bản. Dù vậy, khá dễ khi dùng dbt hoặc các mẫu “chỉ-append + rebuild theo cửa sổ”.
6) Phép chiếu “tài liệu” JSON tính sẵn
Là gì: Lưu một blob JSON phi chuẩn hoá khớp nhu cầu API/UI của bạn (ví dụ thẻ sản phẩm), kèm các chỉ mục JSON mục tiêu.
Khi nào: API của bạn phục vụ cùng một hình dạng lặp đi lặp lại, và bạn muốn một lần đọc.
Tuỳ chọn đồng bộ: rebuild theo sự kiện khi sản phẩm/nhãn hiệu/danh mục thay đổi, hoặc làm mới theo lô thường xuyên cho các sản phẩm cập nhật gần đây.
Chọn chiến lược đồng bộ (hướng dẫn nhanh)
Tôi khuyên bạn luôn xây dựng khả năng quan sát vào giải pháp. Không gì tệ hơn việc không chắc các thay đổi đã được xử lý hoặc sao chép đúng chưa. Nó cũng giúp bạn tính toán kiểm tra trôi lệch và các script backfill.
Cách triển khai Phi chuẩn hoá theo từng bước
Như mọi khi, can thiệp vào cơ sở dữ liệu khá rủi ro. Tôi rất khuyến nghị bạn nhân bản cơ sở dữ liệu và thử triển khai phi chuẩn hoá trên bản sao trước, để chắc chắn mọi thứ vận hành như kỳ vọng. Nếu có môi trường dev, càng tốt!
Quy trình khá đơn giản: đo lường → thay đổi ít nhất có thể → giữ cho đúng → xác minh → giám sát → lặp lại.
1) Profiling và đặt mục tiêu thành công
2) Chọn mức phi chuẩn hoá nhỏ nhất hiệu quả
3) Thiết kế đường đồng bộ
Idempotency là bất khả thương lượng. Các cập nhật phải có thể lặp lại an toàn (ví dụ
UPSERTvới tính toán quyết định). Tin tôi đi, sau này bạn sẽ cảm ơn chính mình.4) Tạo, backfill và xác thực
4. Sửa các sai lệch; chạy lại cho đến khi bằng 0 (hoặc trong ngân sách sai số đã thống nhất).
5) Chuyển đổi an toàn
6) Giám sát đúng thứ
Tạo một dashboard nhỏ trong công cụ quan sát bạn chọn:
7) Vận hành & lặp lại
Ưu và nhược điểm của Phi chuẩn hoá
Chúng ta đã đề cập chi tiết trong bảng Chuẩn hoá vs Phi chuẩn hoá ở phần trước, nên đây là tóm tắt nhanh.
Bạn được gì
Bạn phải trả giá gì
Công cụ và công nghệ hỗ trợ Phi chuẩn hoá
Không phải mọi CSDL đều hỗ trợ phi chuẩn hoá theo cùng một cách. Một số cung cấp tính năng hạng nhất để lưu kết quả truy vấn đắt đỏ, số khác kỳ vọng bạn tự xây bằng job, trigger hoặc pipeline.
PostgreSQL
Postgres cho bạn vài cách để “vật hoá” các hình dạng tối ưu đọc.
Materialized view lưu kết quả truy vấn, nên đọc là tức thời. Bạn chọn khi nào làm mới (theo lịch hoặc ad hoc), và thậm chí có thể làm mới đồng thời để không chặn người đọc.
Với sao chép nhẹ nhàng, cột phát sinh và chỉ mục biểu thức cho phép DB duy trì giá trị dẫn xuất và tăng tốc các bộ lọc phổ biến mà không cần viết logic đồng bộ bổ sung.
Khi bạn cần nhất quán mạnh giữa nguồn và bản sao phi chuẩn hoá, bạn có thể dùng trigger để giữ đồng bộ (nhưng nhớ rằng, chúng tăng tải lên đường ghi!). Với cập nhật theo sự kiện hoặc theo lô, bạn có thể ghép Postgres với bộ lập lịch (ví dụ pg_cron hoặc job runner ngoài) hoặc với sao chép logic/CDC để stream thay đổi vào worker cập nhật các bảng phi chuẩn hoá một cách bất đồng bộ.
Nếu bạn muốn thực hành và tự tạo CSDL để thử phi chuẩn hoá, hãy xem khóa học PostgreSQL của chúng tôi.
SQL Server
SQL Server tận dụng mạnh mẽ indexed view, về bản chất là materialized view “luôn bật”, nơi engine giữ view đồng bộ trên mọi insert/update/delete, nên đọc siêu nhanh. Mặt trái là đường ghi giờ phải duy trì cả bảng gốc và view, nên khối lượng OLTP nặng có thể chậm lại.
Ngoài ra, bạn có thể dùng cột tính toán persisted để bao phủ các thuộc tính dẫn xuất theo từng hàng.
Với báo cáo ở quy mô lớn, columnstore index là lựa chọn thay thế cho phi chuẩn hoá: chúng nén và quét bộ dữ liệu lớn rất hiệu quả, có thể loại bỏ nhu cầu nhân đôi dữ liệu ngay từ đầu.
Oracle
Materialized View của Oracle là lựa chọn trưởng thành với làm mới
FASTdùng nhật ký thay đổi để cập nhật phần đã đổi, hoặc làm mớiCOMPLETEkhi bạn chấp nhận rebuild toàn bộ.Với Query Rewrite, bộ tối ưu có thể tự động dùng materialized view của bạn khi ai đó truy vấn bảng nền, nên đội ngũ được tăng tốc mà không cần đổi SQL ứng dụng. Như mọi khi, đọc nhanh hơn đồng nghĩa có chỗ khác làm thêm việc: duy trì log và làm mới view làm tăng chi phí ghi và vận hành.
MySQL / MariaDB
MySQL không có materialized view gốc, nên phi chuẩn hoá thường được xây bằng các bảng vật lý cộng với job theo lịch hoặc trigger để cập nhật.
Cột phát sinh giúp với các giá trị dẫn xuất đơn giản. Cách này hiệu quả cho dashboard và tóm tắt có thể dự đoán, nhưng một lần nữa, đáng để cân nhắc nhịp làm mới và độ phức tạp của trigger để không vô tình đánh thuế đường ghi quan trọng. Nhiều đội kết hợp với replica chỉ đọc để chuyển tải truy vấn báo cáo hoàn toàn.
Snowflake / BigQuery (lớp phân tích)
Kho dạng cột sinh ra để phục vụ dữ liệu rộng, thân thiện quét, phi chuẩn hoá. Bạn thường mô hình bảng fact rộng với các thuộc tính hữu ích được “nướng sẵn”, rồi dựa vào phân vùng/clustering để cắt tỉa phạm vi quét.
Cả hai nền tảng hỗ trợ materialized view và tác vụ/truy vấn theo lịch để giữ số tổng hợp tươi mới mà không đụng tới hệ OLTP. Bạn đánh đổi chi phí lưu trữ và làm mới lấy các lượt đọc rất dự đoán được, rẻ ở quy mô lớn. Rất phù hợp cho dashboard và BI.
Điều này nâng cao hơn một chút so với khóa PostgreSQL của chúng tôi, nên nếu bạn đã thoải mái với CSDL, cứ thử khóa nhập môn mô hình hoá dữ liệu với Snowflake.
dbt (mô hình hoá và điều phối)
dbt được coi là lớp “hạ tầng như mã” cho phân tích phi chuẩn hoá. Bạn định nghĩa model một lần, chọn cách vật hoá (bảng, view, incremental), và để dbt xử lý build, phụ thuộc và test. Model incremental đặc biệt hữu ích cho bảng tóm tắt chỉ cần append và merge dữ liệu mới. Đây là một trong những công cụ tôi yêu thích nhất!
CDC & pipeline dữ liệu
Khi bạn muốn thay đổi từ ứng dụng chảy vào các cấu trúc phi chuẩn hoá một cách đáng tin cậy, Change Data Capture là lựa chọn hàng đầu. Công cụ như Debezium (tự vận hành) hoặc các connector managed (Fivetran, Airbyte) stream thay đổi cấp hàng từ OLTP vào worker hoặc warehouse để cập nhật bảng phép chiếu, bộ đếm, hoặc tổng hợp. Thường là nhất quán cuối cùng (tuyệt cho dashboard và feed), và mở rộng tốt hơn nhiều so với nhồi nhét mọi thứ vào trigger. Hãy làm cập nhật idempotent, giám sát độ trễ, và giữ đường backfill cho sự kiện trễ hoặc bỏ lỡ.
Điểm mấu chốt & Lời kết
Phi chuẩn hoá là tối ưu hoá thực dụng được chồng lên thiết kế chuẩn hoá, không phải phủ nhận nó. Bạn đang đánh đổi lưu trữ và độ phức tạp ghi để có các lượt đọc nhanh hơn, đơn giản hơn trên những đường đi cụ thể và được hiểu rõ.
Hãy nhớ: