Xác minh schema PostgreSQL trước migration đầu tiên
Xác minh schema PostgreSQL giúp phát hiện ánh xạ sai, ràng buộc yếu, index thiếu và thay đổi không an toàn trước khi migration đầu tiên chạm vào dữ liệu.

Một công cụ xây dựng bằng AI có thể tạo PostgreSQL hợp lệ nhưng lại suy luận sai cơ sở dữ liệu. Cú pháp là phần dễ. Những lỗi nguy hiểm thường rất có vẻ hợp lý: quan hệ tùy chọn bị biến thành bắt buộc, chuỗi trạng thái có ràng buộc CHECK không đầy đủ, thao tác xóa lan truyền qua các bản ghi đáng lẽ phải còn lại, hoặc migration tạo lại bảng rồi âm thầm làm mất một cột.
Vì vậy, việc xác minh schema PostgreSQL cần kiểm tra riêng ý nghĩa, hành vi migration và khả năng khôi phục. Tôi chỉ duyệt schema được suy luận khi nó vượt qua một bộ dữ liệu đã biết, các bất biến được nêu rõ, truy vấn đại diện, việc xem xét thay đổi phá hủy và buổi diễn tập khôi phục. Thiếu bất kỳ phần nào, migration vẫn chỉ là một đề xuất.
Migration đầu tiên cũng cần được xem xét kỹ như vậy ngay cả khi cơ sở dữ liệu production còn trống. Lỗi schema ban đầu nhanh chóng trở nên cố định vì mã ứng dụng, dữ liệu mẫu, báo cáo và các migration sau đều bắt đầu phụ thuộc vào chúng. Mười lăm phút xem xét trước lần chạy đầu thường rẻ hơn nhiều so với sáu tháng sau phải giải thích vì sao hai khái niệm khác nhau lại dùng chung một cột văn bản có thể null.
Schema được suy luận là một đặc tả chưa đáng tin
Hãy coi schema được suy luận là bản nháp đặc tả, không phải sự thật có thể thực thi. Công cụ đã thấy prompt, màn hình mẫu, bản ghi nhập vào hoặc mã ứng dụng được tạo. Nó không chứng kiến mọi ngoại lệ nghiệp vụ, quy tắc lưu giữ, lần nhập hàng loạt, lần sửa lỗi của bộ phận hỗ trợ và khoản thanh toán thất bại mà cơ sở dữ liệu rồi sẽ chứa.
Hãy bắt đầu bằng cách tách ba câu hỏi mà các nhóm thường gộp lẫn. Tính đúng đắn của schema hỏi liệu bảng và ràng buộc có mô hình hóa đúng lĩnh vực không. Độ an toàn của migration hỏi liệu các thao tác đề xuất có bảo toàn dữ liệu hiện có và giữ cơ sở dữ liệu hoạt động được trong lúc chạy không. Mức độ sẵn sàng khôi phục hỏi liệu bạn có thể trở về trạng thái đã biết sau một thay đổi dang dở hoặc sai về ngữ nghĩa không. Việc vượt qua một câu hỏi gần như không nói gì về hai câu còn lại.
Một câu lệnh CREATE TABLE có thể mô tả đúng cấu trúc cuối cùng nhưng lại đi đến đó bằng thao tác không an toàn. Giả sử công cụ đổi customer_name text thành customer_id bigint. Foreign key cuối cùng có thể hợp lý, nhưng migration xóa cột tên trước khi đối chiếu tên lịch sử với khách hàng sẽ hủy bằng chứng duy nhất cần cho việc đối chiếu đó. Xem xét schema duyệt đích đến, còn xem xét migration kiểm tra hành trình.
Hãy đọc to mô hình đề xuất bằng ngôn ngữ nghiệp vụ. Hãy nói mỗi hóa đơn thuộc về đúng một khách hàng pháp lý, thay vì nói invoices.customer_id tham chiếu customers.id. Câu đầu mở ra những phản biện hữu ích: bản nháp có thể tồn tại trước khi chọn khách hàng, hóa đơn nhập vào có thể tham chiếu khách hàng đã lưu trữ, và hồ sơ pháp lý có thể cần đóng băng tên khách hàng tại thời điểm phát hành. Từ vựng SQL có thể che lấp các bất đồng đó.
Tôi yêu cầu có ghi chú giả định cạnh mỗi bảng được suy luận. Ghi chú cần nêu một hàng có ý nghĩa gì, cách nhận diện hàng, ai sở hữu nó, liệu nó có thể tồn tại khi không có hàng cha tưởng như tương ứng hay không, và xóa có nghĩa gì. Nếu nhóm không trả lời được các điểm này, công cụ đã đoán về một cơ sở dữ liệu mà nhóm chưa thiết kế.
Bản ghi đã biết làm lộ ánh xạ bảng sai
Một bộ dữ liệu đã biết nên có các bản ghi được chọn để bao phủ ý nghĩa, vì mẫu ngẫu nhiên lớn thường lặp lại cùng một trường hợp dễ. Mười bản ghi được chọn kỹ có thể cho thấy nhiều hơn mười nghìn hàng gần như giống hệt nhau ở luồng thuận lợi.
Hãy lập ma trận ánh xạ trước khi chạy DDL. Mỗi hàng trong ma trận cần theo dõi một khái niệm nguồn qua đích đề xuất và ghi lại số lượng hoặc giá trị dự kiến. Với ứng dụng đơn hàng, tài liệu có thể như sau:
| Dữ kiện đã biết | Đích đề xuất | Kết quả dự kiến |
|---|---|---|
| Đơn A có hai dòng hàng | orders và order_items | Một hàng đơn và hai hàng con |
| Đơn B chưa được gán tài khoản | orders.account_id | Một hàng có tài khoản NULL |
| Hai người dùng chung email | contacts.email | Cả hai hàng vẫn còn, trừ khi tính duy nhất là quy tắc đã nêu |
| Mã sản phẩm có số 0 ở đầu | products.code | Giá trị văn bản 00417 không đổi |
| Đơn đã hủy vẫn giữ các khoản phí | orders và charges | Các hàng phí vẫn còn sau khi hủy |
Cách này bắt được lỗi ánh xạ bảng trước khi chi tiết ràng buộc làm phân tán việc xem xét. Công cụ AI thường chuẩn hóa các đối tượng lặp lại thành bảng riêng, điều này thường hợp lý, nhưng sự lặp lại không chứng minh cùng một danh tính. Hai địa chỉ giao hàng có cùng văn bản có thể là ảnh chụp lịch sử chứ không phải tham chiếu đến một hàng địa chỉ có thể sửa. Gộp chúng lại nghĩa là lần sửa địa chỉ sau sẽ viết lại lịch sử.
Lỗi ngược lại cũng xảy ra. Công cụ có thể sao chép trường khách hàng vào mọi đơn hàng vì màn hình hiển thị chúng cùng nhau. Một số giá trị thuộc về khách hàng, còn giá trị khác phải lưu lại như ảnh chụp của đơn. Thiết kế đúng có thể chứa cả customer_id lẫn các trường tài liệu đã phát hành như billing_name. Gọi đó là trùng lặp rồi xóa một bên sẽ làm mất danh tính hiện tại hoặc sự thật lịch sử.
Hãy nạp bộ dữ liệu đã biết vào cơ sở dữ liệu dùng một lần bằng cùng đường nhập hoặc tạo dữ liệu mẫu mà ứng dụng sẽ dùng. Sau đó viết các assertion theo dữ kiện, không chỉ theo số hàng:
SELECT
(SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
(SELECT count(*) FROM order_items i
JOIN orders o ON o.id = i.order_id
WHERE o.external_id = 'ORDER-A') AS order_a_items,
(SELECT account_id IS NULL FROM orders
WHERE external_id = 'ORDER-B') AS order_b_unassigned;
Kết quả đạt yêu cầu cần được nêu rõ:
order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
1 | 2 | t
Đừng chấp nhận khác biệt chưa được giải thích chỉ vì ứng dụng được tạo vẫn hiển thị. Giao diện có thể che hàng cha trùng lặp, hàng con bị mất, mã bị cắt ngắn và giá trị mặc định bịa ra. Hãy đối chiếu mọi fixture có chủ đích trước khi bàn đến việc triển khai production.
Ràng buộc phải mã hóa sự thật nghiệp vụ
Ràng buộc cơ sở dữ liệu cần từ chối một trạng thái luôn không hợp lệ, bất kể hàng được ghi từ màn hình, API, lần nhập hay script sửa lỗi nào. Nếu quy tắc có ngoại lệ hoặc phụ thuộc vào dữ kiện bên ngoài có thể thay đổi, ép nó vào một ràng buộc đơn giản thường dẫn đến công việc bị chặn hoặc dữ liệu không trung thực.
Khóa chính nhận diện hàng, nhưng không tự tạo ra danh tính nghiệp vụ có ý nghĩa. ID bigint nội bộ có thể tồn tại cùng số đơn hàng duy nhất trong phạm vi tenant. Nếu nghiệp vụ quy định số đơn hàng là duy nhất theo tenant, UNIQUE (tenant_id, order_number) diễn đạt quy tắc đó. Ràng buộc unique toàn cục sẽ từ chối bản ghi hợp lệ, còn không có ràng buộc sẽ cho phép mơ hồ khi thử lại.
Ràng buộc CHECK phù hợp với dữ kiện ổn định trên hàng như quantity > 0 hoặc finished_at >= started_at. Tài liệu PostgreSQL giải thích rằng cơ sở dữ liệu giả định biểu thức CHECK không thay đổi trong suốt vòng đời của ràng buộc. Vì vậy, CHECK gọi một function có hành vi thay đổi sau này có thể khiến các hàng cũ vi phạm quy tắc có vẻ đang áp dụng. Hãy dùng biểu thức cố định cho sự thật cố định. Với chính sách thay đổi, chẳng hạn tập giá trị đang được quản trị viên cho phép, hãy đưa vào bảng tham chiếu hoặc luồng xử lý ứng dụng.
Ràng buộc trạng thái được tạo cần được nghi ngờ. Công cụ có thể xem các ví dụ hiện tại rồi tạo:
status text NOT NULL
CHECK (status IN ('draft', 'active', 'closed'))
Điều này chỉ đúng nếu đó là toàn bộ các trạng thái bền vững. Hãy hỏi về các bản ghi thất bại, đã hủy, tạm ngưng, đã nhập và bản ghi cũ chưa rõ trạng thái. Nếu máy trạng thái vẫn thay đổi, bảng tra cứu có thể làm rõ việc thêm trạng thái, nhưng nó không thay thế việc xác thực chuyển trạng thái. Một hàng được phép có closed không nói gì về việc nó có được chuyển thẳng từ draft sang closed hay không.
Hãy dùng tính duy nhất có chủ đích. PostgreSQL triển khai unique constraint bằng unique B-tree index, nhưng partial unique index diễn đạt quy tắc khác. Xóa mềm thường chỉ cần duy nhất trong các hàng còn hoạt động:
CREATE UNIQUE INDEX users_tenant_email_live_uq
ON users (tenant_id, lower(email))
WHERE deleted_at IS NULL;
Điều này không tương đương với UNIQUE (tenant_id, email, deleted_at). PostgreSQL xử lý giá trị NULL theo quy tắc về tính duy nhất của nó, và việc thêm thời điểm xóa làm thay đổi danh tính đang được thực thi. Hãy xem xét các trường hợp trùng lặp chính xác bằng fixture thay vì suy ra hành vi từ danh sách cột.
Khả năng null là quyết định nghiệp vụ
Chỉ đặt một cột thành NOT NULL khi nghiệp vụ yêu cầu giá trị cho mọi hàng hợp lệ và mọi đường ghi đều có thể cung cấp giá trị đó. Thiết kế màn hình là bằng chứng yếu. Một trường bắt buộc trong biểu mẫu hiện tại không nói gì về lần nhập, bản nháp, hàng do hệ thống tạo hay bản ghi lịch sử.
Hãy xem xét riêng bốn trạng thái: nguồn bỏ qua trường, nguồn gửi rõ null, nguồn gửi giá trị rỗng và nguồn cung cấp giá trị có ý nghĩa. JSON API, biểu mẫu, nhập CSV và PostgreSQL có thể xử lý các trạng thái này khác nhau. Nếu ứng dụng gộp cả bốn trước khi insert, việc xem xét schema cần phơi bày quyết định đó thay vì giả vờ cơ sở dữ liệu đã giải quyết nó.
Giá trị mặc định cũng cần được chú ý tương tự. Giá trị mặc định cung cấp giá trị khi INSERT bỏ qua cột, không sửa được NULL được gửi rõ ràng và không chứng minh giá trị là đúng. country_code DEFAULT 'US' là nguy hiểm nếu có thể không biết quốc gia. Hàng lúc này chứa một lời nói dối đầy tự tin mà báo cáo và logic tuân thủ có thể tin.
Một migration được tạo thường thêm cột bắt buộc trong một câu lệnh:
ALTER TABLE customers
ADD COLUMN account_type text NOT NULL DEFAULT 'standard';
Câu lệnh có thể chạy, nhưng mọi khách hàng lịch sử đều thành standard mà không có bằng chứng. Trình tự an toàn hơn là thêm cột có thể null, suy ra giá trị từ dữ liệu đã biết, đo các hàng chưa xác định, chặn việc bỏ sót mới trong các lần ghi của ứng dụng, rồi mới thêm NOT NULL nếu nghiệp vụ cho phép. Nếu chưa biết vẫn là hợp lệ, hãy giữ NULL và xác định truy vấn cùng giao diện sẽ hiển thị nó ra sao.
PostgreSQL cho phép tách bước hữu ích với một số ràng buộc. CHECK hoặc foreign key có thể được thêm dưới dạng NOT VALID, tránh xác thực toàn bộ hàng hiện có khi tạo, rồi sau đó kiểm tra bằng VALIDATE CONSTRAINT. Tài liệu mô tả đây là cách hoãn lần quét bảng ban đầu. Đây không phải quyền bỏ qua vi phạm cũ: các lần ghi mới vẫn bị thực thi ràng buộc, còn bước xác thực vẫn phải thành công trước khi duyệt.
Trước khi siết chặt khả năng null, hãy chạy truy vấn phân bố cho thấy các nhóm thực tế:
SELECT
count(*) AS total,
count(*) FILTER (WHERE account_type IS NULL) AS nulls,
count(*) FILTER (WHERE account_type = '') AS empty_strings,
count(*) FILTER (WHERE account_type NOT IN
('standard', 'partner', 'internal')) AS unexpected
FROM customers;
Giá trị mặc định do AI tạo có thể khiến truy vấn này trông sạch sau migration. Hãy chạy nó trước khi điền dữ liệu và lưu lại kết quả đó. Nếu không, bạn mất bằng chứng cần thiết để phân biệt giá trị suy ra với giá trị bịa ra.
Index nên đáp ứng mẫu truy cập đã quan sát
Chỉ duyệt index khi nó hỗ trợ một truy vấn đã biết, thực thi quy tắc duy nhất đã nêu hoặc đáp ứng yêu cầu vận hành. Đánh index mọi cột trông như định danh sẽ tốn dung lượng và công sức ghi, trong khi thiếu một composite index quan trọng có thể biến trang danh sách thông thường thành lần quét ngày càng lớn.
Hãy bắt đầu bằng các truy vấn mà ứng dụng được tạo thực sự gửi. Ghi lại cột lọc, ranh giới tenant, cột join, thứ tự sắp xếp và kích thước kết quả dự kiến. Với trang đơn hàng gần đây, hình dạng truy vấn này quan trọng hơn sơ đồ bảng:
SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Index chỉ trên tenant_id vẫn có thể phải xem nhiều hàng của tenant rồi sắp xếp. Index trên (tenant_id, status, created_at DESC) phù hợp mẫu truy cập này hơn. Thứ tự cột không phải cuộc thi độ phổ biến, nó tuân theo điều kiện bằng, điều kiện phạm vi, thứ tự sắp xếp và độ chọn lọc trong truy vấn thực tế.
Hãy chạy EXPLAIN (ANALYZE, BUFFERS) với dữ liệu đại diện, nhưng đừng coi một fixture nhỏ là bằng chứng hiệu năng. PostgreSQL có thể đúng khi ưu tiên quét tuần tự trên bảng nhỏ. Việc xác minh cần xác nhận index dự định tồn tại và buổi diễn tập ở quy mô production cho planner lựa chọn thực tế. Đừng tắt quét tuần tự để tạo ra một lần quét index chỉ nhằm được duyệt.
Foreign key còn tạo một bất ngờ quen thuộc: PostgreSQL đánh index các cột khóa chính hoặc unique được tham chiếu, nhưng không tự tạo index cho cột con tham chiếu. Vì vậy, xóa hoặc cập nhật hàng cha có thể phải quét bảng con để kiểm tra tham chiếu. Join từ con sang cha cũng có thể cần index ở phía con. Hãy xem từng quan hệ dựa trên lượt đọc dự kiến và thay đổi ở hàng cha.
Hãy từ chối index trùng lặp và không dùng trong đề xuất ban đầu. (tenant_id, status) có thể dư nếu đã có index (tenant_id, status, created_at) phù hợp, dù chi tiết tải công việc có thể thay đổi nhận định này. Hãy so sánh định nghĩa, không phải tên. Công cụ AI thường tạo một index cho mỗi tính năng và không nhận ra nhiều tính năng yêu cầu cùng các cột đầu.
Với cơ sở dữ liệu hiện có đang bận, hãy nhớ CREATE INDEX CONCURRENTLY không thể chạy trong transaction block, tốn nhiều công hơn và có thể để lại index không hợp lệ sau lỗi. Tài liệu PostgreSQL nêu rõ các khác biệt vận hành đó. Framework migration bọc mọi migration trong transaction cần có ngoại lệ rõ ràng và quy trình dọn dẹp, không phải thay thế từ khóa đầy hy vọng.
Foreign key cần có quyền sở hữu và quy tắc xóa
Foreign key chỉ đúng sau khi nhóm quyết định quan hệ đó thể hiện quyền sở hữu, tham chiếu, ngữ cảnh tùy chọn hay gán nguồn gốc lịch sử. Các cột trông giống nhau có thể cần hành vi xóa trái ngược.
Hãy xét projects.owner_user_id, invoices.customer_id và audit_events.actor_user_id. Dự án có thể chuyển quyền sở hữu. Hóa đơn có thể phải tồn tại sau khi tài khoản khách hàng bị đóng. Sự kiện kiểm toán có thể giữ mã định danh cũ của tác nhân ngay cả sau khi dữ liệu danh tính bị xóa. Áp ON DELETE CASCADE cho cả ba chỉ vì chúng tham chiếu users sẽ mã hóa một điều hư cấu mang tính phá hủy.
Dùng CASCADE khi hàng con không có ý nghĩa nếu thiếu hàng cha và việc xóa hàng cha thực sự nghĩa là xóa toàn bộ aggregate. Dòng hàng đơn thường phù hợp. Bản ghi thanh toán, tài liệu đã phát hành, lần nhập, log và bằng chứng kiểm duyệt thường không phù hợp. Với chúng, từ chối, lưu trữ, ẩn danh có kiểm soát hoặc tham chiếu có thể null đi cùng trường ảnh chụp được giữ lại có thể phù hợp hơn.
SET NULL cũng cần được xem xét về ý nghĩa. Nó giữ hàng con nhưng xóa quan hệ trực tiếp. Nếu nhân viên sau này cần giải thích tài khoản nào đã tạo báo cáo, tham chiếu null có thể không đủ. Giữ một token lịch sử không định danh hoặc ảnh chụp có thể duy trì trách nhiệm giải trình mà không giữ toàn bộ dữ liệu cá nhân, nhưng lựa chọn lưu giữ chính xác phải thuộc chính sách sản phẩm, không phải phỏng đoán của AI.
Hãy kiểm tra lực lượng theo cả hai hướng. Công cụ có thể mô hình hóa một-một bằng foreign key mà không có unique constraint, âm thầm cho phép nhiều hàng con. Nó cũng có thể bắt buộc tính duy nhất trong khi lịch sử cần nhiều phiên bản. Viết các trường hợp fixture cho một hàng cha có không, một và nhiều hàng con, rồi nêu rõ insert nào phải thành công.
Ràng buộc có thể trì hoãn cần một lý do cụ thể. Chúng có thể hữu ích khi transaction tạm thời phải vi phạm thứ tự tham chiếu hoặc cập nhật các hàng phụ thuộc lẫn nhau, nhưng đặt mọi foreign key thành deferred sẽ dời lỗi đến lúc commit và làm lỗi khó xác định hơn. Hãy giữ thực thi ngay trừ khi một trình tự transaction thực sự cần hoãn.
Hãy kiểm tra catalog sau khi áp migration vào cơ sở dữ liệu diễn tập:
SELECT
conname,
contype,
convalidated,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;
Kết quả đại diện có dạng như sau:
conname | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
orders_pkey | p | t | PRIMARY KEY (id)
orders_customer_fk | f | t | FOREIGN KEY (customer_id) REFERENCES customers(id)
orders_total_check | c | t | CHECK ((total_cents >= 0))
Hãy so sánh các định nghĩa trả về với quy tắc quyền sở hữu đã duyệt. Migration chạy thành công không cho thấy hành động bị thiếu, việc trì hoãn không mong muốn hay ràng buộc chưa xác thực.
Thay đổi phá hủy ẩn trong SQL có vẻ hợp lý
Hãy xem migration như một phép biến đổi dữ liệu, vì DDL trông gọn gàng có thể làm mất ý nghĩa mà không dùng DROP TABLE rõ ràng. Trước tiên tìm sự phá hủy trực tiếp, sau đó kiểm tra ép kiểu, điền dữ liệu, viết lại, đổi tên và thay ràng buộc.
Đổi tên và xóa khác nhau về vận hành dù schema cuối cùng trông y hệt. Nếu surname đổi thành family_name, đổi tên bảo toàn dữ liệu và phụ thuộc trung thực hơn. Xóa cột cũ rồi thêm cột mới cho ra cùng sơ đồ nhưng làm trống mọi giá trị. Migration được tạo thường suy ra trạng thái cuối mà không hiểu tính liên tục.
Thay đổi kiểu cần có mẫu chuyển đổi và trường hợp bị từ chối. Đổi mã định danh văn bản sang số nguyên có thể làm mất số 0 đầu hoặc từ chối mã lẫn chữ số. Giảm độ chính xác số có thể làm tròn giá trị. Chuyển đổi timestamp cần giả định múi giờ rõ ràng. Hãy kiểm tra biểu thức USING thực tế với giá trị nhỏ nhất, lớn nhất, null, không hợp lệ và các giá trị lịch sử khác thường trước khi thay cột.
Hãy xem các thao tác sau là cần có lý do bằng văn bản: xóa bảng hoặc cột, đổi kiểu qua ép kiểu làm mất dữ liệu, thay cột đã có dữ liệu, thêm CASCADE, đặt NOT NULL sau khi điền dữ liệu do máy tạo, và xây lại tính duy nhất với cột khác. Cũng hãy kiểm tra SQL thô nhúng trong function được tạo hoặc callback migration. Tìm kiếm văn bản là bộ lọc khởi đầu, không phải toàn bộ việc xem xét.
Một kiểu lỗi xuất hiện lặp lại. Dữ liệu đã biết có các liên hệ với công ty tùy chọn, nhưng màn hình mẫu chỉ cho thấy liên hệ doanh nghiệp. Công cụ đặt contacts.company_id thành NOT NULL và chèn một công ty do máy tạo tên Unknown cho các hàng không khớp. Migration thành công, số lượng khớp và mọi foreign key đều xác thực. Dữ liệu vẫn sai: liên hệ cá nhân giờ trông như thuộc một công ty, báo cáo gom những người không liên quan vào cùng nhóm và việc xóa chỗ giữ chỗ có thể cascade sang liên hệ thật.
Cách sửa không phải một giá trị mặc định khác. Hãy khôi phục trạng thái nguồn, để quan hệ có thể null, chỉ migrate những cặp khớp có bằng chứng và thêm assertion rằng tập không khớp đúng bằng các liên hệ cá nhân đã biết. Đây là lý do fixture ngữ nghĩa phải ghi lại quan hệ kỳ vọng, không chỉ số hàng kỳ vọng.
Công cụ diff schema hữu ích, nhưng tôi không ủng hộ việc duyệt chỉ từ diff. Khuyến nghị này phổ biến vì diff gọn và dễ xem. Nó sai nếu là cổng duy nhất vì nó cho thấy thay đổi cấu trúc, không cho thấy nguồn gốc của giá trị đã điền, ranh giới transaction, hành vi khóa hoặc sự thật sau migration.
Diễn tập phải chứng minh kết quả và hành vi khi lỗi
Hãy chạy toàn bộ migration trên bản khôi phục dùng một lần của bộ dữ liệu đã biết, sau đó kiểm tra cả kết quả dự kiến lẫn đường đi bị ngắt hoặc bị từ chối. Cơ sở dữ liệu mới trống hữu ích để bắt lỗi thứ tự, nhưng không thể làm lộ chuyển đổi mất dữ liệu, hàng lịch sử không hợp lệ hay việc xác thực chậm.
Hãy dùng chuỗi diễn tập này làm tài liệu phát hành:
- Khôi phục bộ dữ liệu trước thay đổi vào cơ sở dữ liệu tách biệt, ghi số hàng cùng các assertion ngữ nghĩa.
- Chụp schema hiện tại, áp dụng đúng artifact migration và lưu toàn bộ đầu ra cùng thời gian và ranh giới transaction.
- Chạy kiểm tra catalog, assertion ánh xạ, kiểm tra ràng buộc từ chối và truy vấn ứng dụng đại diện.
- So sánh giá trị quan trọng với kỳ vọng đã ghi, bao gồm các nhóm không khớp và null.
- Thực hiện phương án khôi phục đã ghi, rồi chạy lại các assertion trước migration trên cơ sở dữ liệu đã khôi phục.
Hãy tạo bản dump chỉ schema trước và sau:
pg_dump --schema-only --no-owner --no-privileges \
--dbname "$DATABASE_URL" > schema.sql
Hãy xem xét bảng, sequence, index, ràng buộc, function, trigger, extension và quyền liên quan đến ứng dụng. ORM model diff có thể bỏ sót đối tượng cơ sở dữ liệu mà ứng dụng không mô hình hóa, đặc biệt là trigger, expression index, partial index và function được cài thủ công.
Hãy thêm kiểm tra âm để chứng minh ràng buộc từ chối trạng thái sai. Một transaction kiểm thử có thể thử insert không hợp lệ và rollback dù kết quả ra sao:
BEGIN;
INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);
ROLLBACK;
Đầu ra dự kiến nên nêu tên ràng buộc bị vi phạm, ví dụ:
ERROR: new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL: Failing row contains (..., 0, 2500, ...).
Đừng so sánh toàn bộ nội dung lỗi ở mọi môi trường vì chi tiết có thể khác. Hãy kiểm tra SQLSTATE hoặc danh tính ràng buộc trong kiểm thử tự động và giữ đầu ra dễ đọc cho người duyệt.
Hãy đo thời lượng và khóa trên bộ dữ liệu đủ lớn để giống triển khai dự kiến. Một thao tác hoàn thành tức thì trên năm mươi hàng có thể chặn ghi khi phải xác thực hàng triệu hàng. Với cơ sở dữ liệu production ban đầu còn trống, rủi ro tức thời thấp hơn, nhưng diễn tập vẫn kiểm tra dữ liệu mẫu đã nhập và tạo đường cơ sở cho thay đổi sau này.
Khôi phục cần nhiều hơn migration đảo ngược
Khôi phục chỉ đáng tin khi nó khôi phục dữ liệu và tính tương thích của ứng dụng trong thời gian dịch vụ có thể chịu được. Migration đảo ngược tạo lại cột đã xóa không khôi phục giá trị cũ của chúng.
Hãy chọn đơn vị khôi phục trước khi chạy. Với cơ sở dữ liệu ban đầu trống, xóa và tạo lại cơ sở dữ liệu có thể chấp nhận được nếu chưa có ghi dữ liệu từ người dùng. Khi đã có ghi thực, khôi phục có thể cần snapshot cơ sở dữ liệu, backup logic, giữ cột cũ hoặc sửa theo hướng tiến. Phương pháp đúng tùy thuộc lượng dữ liệu mới có thể đến trong và sau migration.
Hãy kiểm tra lệnh khôi phục và thông tin xác thực trước khi dựa vào chúng. Backup tồn tại nhưng người vận hành triển khai không thể khôi phục thì không phải kế hoạch khôi phục. Hãy khôi phục vào cơ sở dữ liệu riêng, kiểm tra quyền sở hữu và extension, rồi chạy các assertion đã biết như trước migration.
Snapshot và rollback transaction giải quyết các lỗi khác nhau. Transaction có thể hoàn tác câu lệnh khi migration lỗi trước commit, miễn mọi thao tác đều tham gia transaction đó. Snapshot có thể đưa cả cơ sở dữ liệu về trạng thái cũ hơn, nhưng có thể làm mất các lần ghi hợp lệ sau snapshot. Không cơ chế nào tự động đối chiếu các lần ghi đó.
Hãy ưu tiên thay đổi bổ sung khi còn bất định. Thêm cột hoặc bảng mới, sao chép dữ liệu bằng quy tắc có thể đo lường, chạy cả hai đường mã trong giai đoạn được kiểm soát nếu cần, và chỉ xóa cấu trúc cũ sau khi xác minh. Cách mở rộng rồi thu gọn tốn thêm công sức nhưng giữ lại bằng chứng. Giữ cột cũ đã đổi tên trong một bản phát hành thường rẻ hơn dựng lại nó từ log.
Hãy viết sẵn điều kiện kích hoạt khôi phục. Ví dụ gồm assertion ngữ nghĩa thất bại, bản ghi không khớp bất ngờ, ràng buộc không hợp lệ, migration vượt cửa sổ khóa được duyệt hoặc lỗi ứng dụng do lệch phiên bản. Người vận hành không nên tự nghĩ ra quyết định khi người dùng đang chờ.
Hãy ghi lại thời điểm mà việc khôi phục cơ sở dữ liệu cũ cũng đòi hỏi khôi phục ứng dụng cũ. Ứng dụng mới có thể phụ thuộc vào cột mới, còn ứng dụng cũ có thể từ chối giá trị enum mới hoặc ghi theo hình dạng cũ. Khôi phục cơ sở dữ liệu và ứng dụng phải dùng các phiên bản tương thích.
Duyệt cần bằng chứng, không phải tự tin
Chỉ duyệt migration đầu tiên khi một người khác có thể tái tạo lý do nó an toàn từ các artifact đã lưu. Sự tự tin từ lần xem xét mã sạch hoặc giao diện được tạo trau chuốt không đứng vững trước sai lệch dữ liệu đầu tiên chưa được giải thích.
Hồ sơ duyệt nên chứa các giả định suy luận, ma trận ánh xạ bảng, danh tính bộ dữ liệu đã biết, schema diff, migration chính xác, truy vấn và kết quả xác thực, kiểm tra đầu vào bị từ chối, lý do về index, lý do cho thao tác phá hủy và quy trình khôi phục đã kiểm thử. Hãy nêu tên người duyệt và giữ các quyết định chưa giải quyết làm blocker thay vì chôn chúng trong lịch sử trò chuyện.
Khi ứng dụng được tạo trong Koder.ai, hãy dùng chế độ lập kế hoạch để ghi lại các quyết định schema này trước khi cho phép migration, xuất mã nguồn để xem xét và coi snapshot cùng rollback là công cụ khôi phục vẫn cần được diễn tập trên bộ dữ liệu đã biết.
Đừng để công cụ tự duyệt suy luận của mình bằng cách tạo lại mã cho đến khi kiểm thử đạt. Vòng lặp đó có thể khiến ứng dụng tuân theo schema sai thay vì sửa mô hình. Con người phải quyết định cơ sở dữ liệu có khớp với lĩnh vực không, đặc biệt ở danh tính, xóa, lưu giữ và các giá trị chưa biết.
Truy vấn duyệt cuối cùng nên nhàm chán. Mọi dữ kiện đã biết ánh xạ tới đúng một kết quả dự kiến, mọi ràng buộc từ chối phản ví dụ đã định, mọi thao tác phá hủy có lý do và quá trình khôi phục tái tạo các assertion trước migration. Nếu bằng chứng cần một lời giải thích thuyết phục để biện minh cho sự không khớp, hãy dừng migration. PostgreSQL sẽ thực thi schema một cách chính xác, bao gồm cả những phần mà công cụ đã đoán sai.
Câu hỏi thường gặp
Tôi nên kiểm tra gì trong schema PostgreSQL do AI tạo?
Hãy kiểm tra DDL được tạo, các thao tác migration và những giả định đứng sau cả hai. Schema cuối cùng có thể đúng nhưng vẫn được tạo ra bằng một migration làm mất dữ liệu, chặn ghi dữ liệu hoặc tự đặt các giá trị mặc định gây hiểu lầm.
Bộ dữ liệu để xác minh schema nên lớn đến mức nào?
Hãy dùng một bộ dữ liệu nhỏ có các hàng thông thường, giá trị biên, quan hệ bị thiếu, dữ liệu trùng lặp, null, chuỗi rỗng và những trường hợp lịch sử bất thường. Mục đích không phải số lượng mà là bác bỏ các giả định của công cụ trước khi dữ liệu production làm điều đó.
Migration thử nghiệm thành công có chứng minh schema an toàn không?
Không. Migration thành công chỉ chứng minh PostgreSQL chấp nhận các câu lệnh với trạng thái cơ sở dữ liệu đó. Nó không chứng minh bảng được ánh xạ đúng, dữ liệu vẫn giữ nguyên ý nghĩa, index hỗ trợ truy vấn thực tế hay khả năng khôi phục hoạt động.
Khi nào một cột PostgreSQL nên là NOT NULL?
Một trường chỉ nên là NOT NULL khi mọi bản ghi hợp lệ đều có giá trị và ứng dụng có thể cung cấp giá trị đó trên mọi đường ghi dữ liệu. Đừng dùng giá trị mặc định bịa ra chỉ để thỏa ràng buộc, vì nó biến dữ liệu thiếu rõ ràng thành dữ liệu sai nhưng có vẻ đáng tin.
Tôi nên dùng unique constraint hay unique index?
Ràng buộc unique thể hiện một quy tắc mà các đối tượng cơ sở dữ liệu khác có thể tham chiếu và PostgreSQL hỗ trợ bằng index. Unique index hữu ích khi tính duy nhất chỉ áp dụng cho các hàng hoặc biểu thức chọn lọc, như bản ghi chưa bị xóa hoặc địa chỉ email đã chuẩn hóa.
Foreign key PostgreSQL có tự động tạo index không?
Hãy đánh index cho các cột dùng để tìm hàng cha, lọc các truy vấn phổ biến, join bảng lớn hoặc thực thi tính duy nhất. PostgreSQL không tự động đánh index ở phía tham chiếu của foreign key, vì vậy hãy kiểm tra riêng các cột con thay vì cho rằng ràng buộc đã xử lý việc đó.
Khi nào ON DELETE CASCADE an toàn?
Chỉ dùng CASCADE khi hàng con không còn ý nghĩa độc lập sau khi hàng cha biến mất. Nếu xóa là một quyết định nghiệp vụ hoặc hàng con là bằng chứng như hóa đơn hay nhật ký kiểm toán, hãy từ chối hoặc xử lý việc xóa một cách rõ ràng.
Làm sao phát hiện thay đổi phá hủy dữ liệu trong migration?
Hãy coi mọi DROP, ép kiểu làm hẹp dữ liệu, viết lại bảng, cột bắt buộc mới và ràng buộc thay thế đều có khả năng phá hủy dữ liệu. Tìm trong nội dung migration, đồng thời kiểm tra các function được tạo và SQL thô vì hành vi phá hủy có thể ẩn trong đó.
Cách an toàn nhất để kiểm tra khả năng khôi phục migration là gì?
Khôi phục cơ sở dữ liệu trước thay đổi sang một nơi riêng, chạy migration tại đó, thực hiện các truy vấn xác minh ngữ nghĩa rồi so sánh kết quả với kỳ vọng đã ghi lại. Chỉ kiểm tra migration đảo ngược sẽ bỏ sót dữ liệu đã mất và có thể tạo cảm giác an toàn sai lầm.
Tôi nên lưu bằng chứng gì sau khi duyệt schema?
Hãy lưu DDL được tạo, nội dung migration, schema diff, các truy vấn và kết quả xác minh, quy trình khôi phục và danh tính người duyệt cùng một chỗ. Hồ sơ này giải thích điều đã được duyệt và giúp lần xem xét migration sau phát hiện những giả định đã thay đổi.