ตรวจสอบสคีมา PostgreSQL ก่อนมิเกรชันแรก
การตรวจสอบสคีมา PostgreSQL ช่วยจับการแมปที่ผิด ข้อจำกัดที่อ่อนแอ ดัชนีที่ขาด และการเปลี่ยนแปลงที่ไม่ปลอดภัย ก่อนมิเกรชันแรกจะแตะต้องข้อมูล

AI สามารถสร้าง PostgreSQL ที่ใช้ได้ตามไวยากรณ์ แต่ยังอนุมานฐานข้อมูลผิดได้ ไวยากรณ์เป็นส่วนที่ง่าย ความผิดพลาดที่อันตรายมักดูสมเหตุสมผล เช่น ความสัมพันธ์ที่ไม่บังคับกลายเป็นบังคับ สตริงสถานะมี CHECK constraint ที่ไม่ครบถ้วน การลบลามไปยังระเบียนที่ควรรอดอยู่ หรือมิเกรชันสร้างตารางใหม่และทำคอลัมน์หายไปอย่างเงียบ ๆ
ดังนั้นการตรวจสอบสคีมา PostgreSQL ต้องทดสอบความหมาย พฤติกรรมของมิเกรชัน และการกู้คืน แยกเป็นคนละเรื่อง ผมจะอนุมัติสคีมาที่อนุมานมาเมื่อมันผ่านชุดข้อมูลที่ทราบแน่ชัด เงื่อนไขคงที่ที่ระบุชัด คิวรีตัวแทน การตรวจสอบการเปลี่ยนแปลงที่ทำลายข้อมูล และการซ้อมกู้คืนแล้วเท่านั้น หากขาดข้อใดข้อหนึ่ง มิเกรชันยังเป็นเพียงข้อเสนอ
มิเกรชันแรกสมควรถูกตรวจสอบเข้มงวด แม้ฐานข้อมูลจริงจะยังว่างอยู่ ความผิดพลาดของสคีมาตั้งแต่ต้นจะฝังแน่นอย่างรวดเร็ว เพราะโค้ดแอปพลิเคชัน ข้อมูลตั้งต้น รายงาน และมิเกรชันถัดไปเริ่มพึ่งพามัน การตรวจสอบ 15 นาทีก่อนรันครั้งแรกมักถูกกว่าการอธิบายหลังจากผ่านไปหกเดือนว่าทำไมแนวคิดสองอย่างจึงใช้คอลัมน์ข้อความที่รับค่าว่างคอลัมน์เดียวกัน
สคีมาที่อนุมานมาเป็นข้อกำหนดที่ยังเชื่อถือไม่ได้
ให้มองสคีมาที่อนุมานมาเป็นร่างข้อกำหนด ไม่ใช่ความจริงที่พร้อมรัน ตัวสร้างเห็นพรอมต์ หน้าจอตัวอย่าง ระเบียนที่นำเข้า หรือโค้ดแอปพลิเคชันที่สร้างขึ้น มันไม่ได้เห็นข้อยกเว้นทางธุรกิจทุกกรณี กฎการเก็บรักษาข้อมูล การนำเข้าจำนวนมาก การแก้ไขโดยทีมสนับสนุน และการชำระเงินที่ล้มเหลว ซึ่งฐานข้อมูลจะต้องเก็บในที่สุด
เริ่มจากแยกคำถามสามข้อที่ทีมมักปะปนกัน ความถูกต้องของสคีมาถามว่าตารางและข้อจำกัดจำลองโดเมนได้หรือไม่ ความปลอดภัยของมิเกรชันถามว่าการดำเนินการที่เสนอรักษาข้อมูลเดิมไว้และทำให้ฐานข้อมูลยังใช้งานได้ระหว่างรันหรือไม่ ความพร้อมกู้คืนถามว่าคุณกลับไปยังสถานะที่ทราบแน่ชัดได้หรือไม่หลังการเปลี่ยนแปลงที่ทำได้ไม่ครบหรือผิดความหมาย การผ่านข้อหนึ่งแทบไม่ได้บอกอะไรเกี่ยวกับอีกสองข้อ
คำสั่ง CREATE TABLE อาจบรรยายโครงสร้างปลายทางที่ตั้งใจไว้ และยังไปถึงด้วยการดำเนินการที่ไม่ปลอดภัย สมมติว่าตัวสร้างเปลี่ยน customer_name text เป็น customer_id bigint foreign key สุดท้ายอาจสมเหตุสมผล แต่มิเกรชันที่ลบคอลัมน์ชื่อก่อนจับคู่ชื่อในอดีตกับลูกค้าจะทำลายหลักฐานเดียวที่จำเป็นต่อการจับคู่นั้น การตรวจสอบสคีมาอนุมัติปลายทาง ส่วนการตรวจสอบมิเกรชันดูเส้นทาง
อ่านแบบจำลองที่เสนอออกเสียงด้วยภาษาของโดเมน พูดว่าใบแจ้งหนี้แต่ละใบเป็นของลูกค้านิติบุคคลเพียงรายเดียว แทนที่จะพูดว่า invoices.customer_id อ้างอิง customers.id ประโยคแรกชวนให้เกิดข้อโต้แย้งที่มีประโยชน์ เช่น อาจมีฉบับร่างก่อนเลือกลูกค้า ใบแจ้งหนี้ที่นำเข้าอาจอ้างถึงลูกค้าที่เก็บถาวรแล้ว และเอกสารทางกฎหมายอาจต้องตรึงชื่อลูกค้าไว้ ณ เวลาที่ออกเอกสาร คำศัพท์ SQL อาจปิดบังความเห็นต่างเหล่านี้
ผมกำหนดให้มีบันทึกสมมติฐานข้างทุกตารางที่อนุมานมา ควรระบุว่าแต่ละแถวหมายถึงอะไร ระบุตัวแถวอย่างไร ใครเป็นเจ้าของ มันอยู่ได้หรือไม่หากไม่มีแถวแม่ที่เห็นได้ชัด และการลบหมายถึงอะไร หากทีมตอบเรื่องเหล่านี้ไม่ได้ ตัวสร้างก็กำลังเดาฐานข้อมูลที่ทีมยังไม่ได้ออกแบบ
ระเบียนที่ทราบแน่ชัดเผยการแมปตารางที่ผิด
ชุดข้อมูลที่ทราบแน่ชัดควรมีระเบียนที่เลือกมาเพื่อครอบคลุมความหมาย เพราะตัวอย่างสุ่มขนาดใหญ่มักซ้ำกรณีง่ายแบบเดิม ระเบียนที่คัดสรรมาอย่างดีสิบรายการอาจเผยได้มากกว่าแถวที่เกือบเหมือนกันหนึ่งหมื่นแถวในกรณีที่ทุกอย่างราบรื่น
สร้างเมทริกซ์การแมปก่อนรัน DDL แต่ละแถวของเมทริกซ์ควรติดตามแนวคิดจากต้นทางไปยังปลายทางที่เสนอ และบันทึกจำนวนหรือค่าที่คาดหวัง สำหรับแอปพลิเคชันคำสั่งซื้อ สิ่งนี้อาจมีหน้าตาเช่นนี้:
| ข้อเท็จจริงที่ทราบ | ปลายทางที่เสนอ | ผลที่คาดหวัง |
|---|---|---|
| คำสั่งซื้อ A มีรายการสินค้า 2 รายการ | orders และ order_items | แถวคำสั่งซื้อ 1 แถวและแถวลูก 2 แถว |
| คำสั่งซื้อ B ยังไม่มีบัญชีที่กำหนด | orders.account_id | 1 แถวที่มีบัญชีเป็น NULL |
| คนสองคนใช้อีเมลเดียวกัน | contacts.email | ทั้งสองแถวอยู่รอด เว้นแต่ความไม่ซ้ำเป็นกฎที่ระบุไว้ |
| รหัสผลิตภัณฑ์มีเลขศูนย์นำหน้า | products.code | ค่าข้อความ 00417 ไม่เปลี่ยนแปลง |
| คำสั่งซื้อที่ยกเลิกยังเก็บรายการเรียกเก็บเงิน | orders และ charges | แถวรายการเรียกเก็บเงินคงอยู่หลังยกเลิก |
สิ่งนี้จับข้อผิดพลาดในการแมปตารางได้ก่อนที่รายละเอียดของข้อจำกัดจะดึงความสนใจไป AI มักทำให้ออบเจ็กต์ที่ซ้ำกันเป็นตารางแยก ซึ่งมักสมเหตุสมผล แต่การซ้ำไม่ได้พิสูจน์ว่าเป็นสิ่งเดียวกัน ที่อยู่จัดส่งสองรายการที่มีข้อความเหมือนกันอาจเป็นภาพบันทึกตามเวลา ไม่ใช่การอ้างอิงไปยังแถวที่อยู่เดียวซึ่งแก้ไขได้ การรวมเข้าด้วยกันหมายความว่าการแก้ไขที่อยู่ภายหลังจะเขียนประวัติใหม่
ข้อผิดพลาดในทางกลับกันก็เกิดขึ้น ตัวสร้างอาจคัดลอกฟิลด์ลูกค้าไว้ในทุกคำสั่งซื้อเพราะหน้าจอแสดงไว้ด้วยกัน บางค่าเป็นของลูกค้า ขณะที่ค่าอื่นต้องคงเป็นภาพบันทึกของคำสั่งซื้อ การออกแบบที่ถูกต้องอาจมีทั้ง customer_id และฟิลด์เอกสาร ณ เวลาที่ออก เช่น billing_name การเรียกสิ่งนี้ว่าข้อมูลซ้ำแล้วลบทิ้งฝั่งหนึ่ง จะทำให้สูญเสียตัวตนปัจจุบันหรือความจริงในอดีตอย่างใดอย่างหนึ่ง
โหลดชุดข้อมูลที่ทราบแน่ชัดลงฐานข้อมูลชั่วคราวโดยใช้เส้นทางนำเข้าหรือข้อมูลตั้งต้นเดียวกับที่แอปพลิเคชันจะใช้ แล้วเขียน assertion กับข้อเท็จจริง ไม่ใช่แค่นับจำนวนแถว:
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;
รูปร่างของผลลัพธ์ที่ผ่านควรถูกระบุให้ชัด:
order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
1 | 2 | t
อย่ายอมรับความต่างที่อธิบายไม่ได้เพียงเพราะแอปพลิเคชันที่สร้างยังแสดงผลได้ UI อาจซ่อนแถวแม่ที่ซ้ำ แถวลูกที่หาย รหัสที่ถูกตัด หรือค่าเริ่มต้นที่แต่งขึ้น กระทบยอดข้อมูลตัวอย่างทุกชุดที่ตั้งใจไว้ก่อนพูดถึงการนำขึ้นระบบจริง
ข้อจำกัดต้องเข้ารหัสความจริงของโดเมน
ข้อจำกัดของฐานข้อมูลควรปฏิเสธสถานะที่ผิดเสมอ ไม่ว่าจะมีหน้าจอ API การนำเข้า หรือสคริปต์ซ่อมแซมใดเขียนแถวนั้น หากกฎมีข้อยกเว้นหรือขึ้นกับข้อเท็จจริงภายนอกที่เปลี่ยนได้ การยัดมันลงในข้อจำกัดง่าย ๆ มักทำให้งานถูกบล็อกหรือข้อมูลไม่ตรงความจริง
Primary key ระบุแถว แต่ไม่ได้ให้ตัวตนทางธุรกิจที่มีความหมายโดยอัตโนมัติ ID bigint ภายในอยู่ร่วมกับเลขคำสั่งซื้อที่ไม่ซ้ำภายใต้ tenant ได้ หากธุรกิจบอกว่าเลขคำสั่งซื้อไม่ซ้ำต่อ tenant, UNIQUE (tenant_id, order_number) สื่อกฎนั้น ข้อจำกัดไม่ซ้ำทั้งระบบจะปฏิเสธระเบียนที่ถูกต้อง ขณะที่ไม่มีข้อจำกัดจะเปิดให้เกิดความกำกวมระหว่างการลองทำซ้ำ
CHECK constraint เหมาะกับข้อเท็จจริงของแถวที่คงที่ เช่น quantity > 0 หรือ finished_at >= started_at คู่มือ PostgreSQL อธิบายว่าฐานข้อมูลถือว่า expression ใน CHECK เปลี่ยนไม่ได้ตลอดอายุของข้อจำกัด นี่คือเหตุผลที่ CHECK ซึ่งเรียกฟังก์ชันแล้วพฤติกรรมฟังก์ชันเปลี่ยนภายหลัง อาจทำให้แถวเก่าละเมิดกฎที่เห็นอยู่ ใช้ expression คงที่กับความจริงที่คงที่ วางนโยบายที่เปลี่ยนได้ เช่น ชุดค่าที่อนุญาตในปัจจุบันซึ่งผู้ดูแลควบคุม ไว้ในตารางที่อ้างอิงหรือเวิร์กโฟลว์ของแอปพลิเคชัน
ข้อจำกัดสถานะที่สร้างขึ้นควรถูกตั้งข้อสงสัย ตัวสร้างอาจดูตัวอย่างปัจจุบันแล้วสร้าง:
status text NOT NULL
CHECK (status IN ('draft', 'active', 'closed'))
สิ่งนี้ถูกต้องก็ต่อเมื่อสถานะเหล่านั้นครบถ้วนและคงอยู่ตลอดไป ถามถึงระเบียนที่ล้มเหลว ยกเลิก ระงับ นำเข้า และข้อมูลเก่าที่ไม่ทราบสถานะ หาก state machine ยังเปลี่ยนอยู่ ตาราง lookup ทำให้การเพิ่มสถานะชัดเจนได้ แต่ไม่ได้แทนการตรวจสอบการเปลี่ยนสถานะ แถวที่อนุญาตให้เป็น closed ไม่ได้บอกว่ามันเปลี่ยนจาก draft เป็น closed โดยตรงได้หรือไม่
ใช้ความไม่ซ้ำอย่างตั้งใจ PostgreSQL ใช้ unique B-tree index รองรับ unique constraint แต่ partial unique index สื่อกฎคนละแบบ การลบแบบ soft delete มักต้องการความไม่ซ้ำเฉพาะแถวที่ยังใช้งาน:
CREATE UNIQUE INDEX users_tenant_email_live_uq
ON users (tenant_id, lower(email))
WHERE deleted_at IS NULL;
สิ่งนี้ใช้แทน UNIQUE (tenant_id, email, deleted_at) ไม่ได้ PostgreSQL จัดการค่า NULL ตามกฎความไม่ซ้ำของมัน และการเพิ่มเวลาในการลบเปลี่ยนตัวตนที่กำลังบังคับ ตรวจสอบกรณีข้อมูลซ้ำที่แน่ชัดด้วยข้อมูลตัวอย่าง แทนการอนุมานพฤติกรรมจากรายชื่อคอลัมน์
การอนุญาตให้เป็นค่าว่างคือการตัดสินใจทางธุรกิจ
กำหนดคอลัมน์เป็น NOT NULL เฉพาะเมื่อโดเมนต้องมีค่าสำหรับทุกแถวที่ถูกต้อง และทุกเส้นทางการเขียนให้ค่านั้นได้ การออกแบบหน้าจอเป็นหลักฐานที่อ่อนแอ ฟิลด์บังคับในฟอร์มปัจจุบันไม่ได้บอกอะไรเกี่ยวกับการนำเข้า ฉบับร่าง แถวที่ระบบสร้าง หรือระเบียนในอดีต
ทบทวนสี่สถานะแยกกัน: ต้นทางละฟิลด์ไว้ ต้นทางส่ง null อย่างชัดเจน ต้นทางส่งค่าว่าง และต้นทางส่งค่าที่มีความหมาย JSON API ฟอร์ม การนำเข้า CSV และ PostgreSQL อาจจัดการสถานะเหล่านี้ต่างกัน หากแอปพลิเคชันรวมทั้งสี่สถานะก่อนแทรกข้อมูล การตรวจสอบสคีมาควรเผยการตัดสินใจนั้น แทนทำเหมือนฐานข้อมูลแก้ปัญหาให้แล้ว
ค่าเริ่มต้นก็ต้องใส่ใจเช่นเดียวกัน ค่าเริ่มต้นจะให้ค่าเมื่อ INSERT ละคอลัมน์ไว้ มันไม่ซ่อม NULL ที่ระบุชัด และไม่ได้พิสูจน์ว่าค่านั้นจริง country_code DEFAULT 'US' อันตรายหากมีประเทศที่ไม่ทราบ แถวนั้นจะมีคำโกหกที่ดูมั่นใจ ซึ่งรายงานและตรรกะด้านการปฏิบัติตามข้อกำหนดอาจเชื่อ
มิเกรชันที่สร้างขึ้นบ่อยแบบหนึ่งเพิ่มคอลัมน์บังคับในคำสั่งเดียว:
ALTER TABLE customers
ADD COLUMN account_type text NOT NULL DEFAULT 'standard';
คำสั่งอาจรันได้ แต่ลูกค้าเก่าทุกรายจะกลายเป็น standard โดยไม่มีหลักฐาน ลำดับที่ปลอดภัยกว่าคือเพิ่มคอลัมน์ที่รับค่าว่าง ดึงค่าจากข้อมูลที่รู้ วัดแถวที่ยังหาค่าไม่ได้ ป้องกันการละค่าในงานเขียนใหม่ของแอปพลิเคชัน แล้วค่อยเพิ่ม NOT NULL หากโดเมนรองรับ หากไม่ทราบยังเป็นค่าที่ถูกต้อง ให้คง NULL ไว้และกำหนดว่าคิวรีกับหน้าจอจะแสดงอย่างไร
PostgreSQL แยกขั้นตอนที่เป็นประโยชน์สำหรับข้อจำกัดบางชนิด CHECK หรือ foreign key เพิ่มแบบ NOT VALID ได้ ซึ่งเลี่ยงการตรวจสอบแถวเก่าทั้งหมดระหว่างสร้าง แล้วค่อยตรวจภายหลังด้วย VALIDATE CONSTRAINT คู่มืออธิบายว่านี่เป็นวิธีเลื่อนการสแกนตารางครั้งแรกออกไป มันไม่ใช่อนุญาตให้เพิกเฉยต่อการละเมิดเก่า งานเขียนใหม่ยังถูกบังคับใช้ และขั้นตอนตรวจสอบต้องผ่านก่อนอนุมัติ
ก่อนบังคับการมีค่า ให้รันคิวรีการกระจายข้อมูลที่แสดงหมวดหมู่จริง:
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;
ค่าเริ่มต้นที่ AI สร้างอาจทำให้คิวรีนี้ดูสะอาดหลังมิเกรชัน รันมันก่อนเติมข้อมูลย้อนหลังด้วย และเก็บผลนั้นไว้ มิฉะนั้นคุณจะสูญเสียหลักฐานที่ใช้แยกค่าซึ่งอนุมานจากข้อมูลออกจากค่าที่แต่งขึ้น
ดัชนีควรตอบรูปแบบการเข้าถึงที่สังเกตได้
อนุมัติดัชนีเมื่อมันรองรับคิวรีที่รู้ บังคับกฎความไม่ซ้ำที่ระบุ หรือทำตามข้อกำหนดด้านการปฏิบัติการ การทำดัชนีทุกคอลัมน์ที่หน้าตาเหมือนตัวระบุจะสิ้นเปลืองพื้นที่และงานเขียน ขณะที่การขาด composite index สำคัญเพียงตัวเดียวอาจทำให้หน้ารายการปกติกลายเป็นการสแกนที่โตขึ้นเรื่อย ๆ
เริ่มจากคิวรีที่แอปพลิเคชันสร้างเรียกจริง บันทึกคอลัมน์กรอง ขอบเขต tenant คอลัมน์ join การเรียงลำดับ และขนาดผลลัพธ์ที่คาดหวัง สำหรับหน้าคำสั่งซื้อล่าสุด รูปร่างคิวรีนี้สำคัญกว่าภาพผังตาราง:
SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;
ดัชนีบน tenant_id เพียงอย่างเดียวอาจยังต้องตรวจหลายแถวของ tenant และเรียงลำดับ ดัชนีบน (tenant_id, status, created_at DESC) ตรงกับรูปแบบการเข้าถึงนี้มากกว่า ลำดับคอลัมน์ไม่ใช่การประกวดความนิยม แต่ตามเงื่อนไขเท่ากับ เงื่อนไขช่วง การเรียงลำดับ และ selectivity ของคิวรีจริง
รัน EXPLAIN (ANALYZE, BUFFERS) กับข้อมูลตัวแทน แต่อย่าถือว่าข้อมูลตัวอย่างขนาดเล็กชุดเดียวพิสูจน์ประสิทธิภาพ PostgreSQL อาจเลือก sequential scan กับตารางเล็กได้อย่างถูกต้อง การตรวจสอบควรยืนยันว่าดัชนีที่ตั้งใจมีอยู่ และการซ้อมด้วยขนาดใกล้ระบบจริงทำให้ planner มีตัวเลือกสมจริง อย่าปิด sequential scan เพื่อบังคับให้เกิด index scan สำหรับการอนุมัติ
Foreign key มีเรื่องที่มักทำให้แปลกใจ PostgreSQL ทำดัชนี primary key หรือคอลัมน์ unique ที่ถูกอ้างอิง แต่ไม่ได้สร้างดัชนีบนคอลัมน์ฝั่งลูกที่อ้างอิงโดยอัตโนมัติ การลบหรืออัปเดตแถวแม่จึงอาจสแกนตารางลูกเพื่อเช็กการอ้างอิง การ join จากลูกไปแม่อาจต้องใช้ดัชนีฝั่งลูกเช่นกัน ตรวจสอบทุกความสัมพันธ์ตามการอ่านที่คาดและการเปลี่ยนแปลงของแถวแม่
ปฏิเสธดัชนีซ้ำและไม่ได้ใช้ในข้อเสนอเริ่มต้น (tenant_id, status) อาจซ้ำซ้อนเมื่อมีดัชนี (tenant_id, status, created_at) ที่เหมาะสมอยู่แล้ว แม้รายละเอียดของ workload อาจทำให้คำตัดสินต่างไป เปรียบเทียบนิยาม ไม่ใช่ชื่อ AI มักสร้างดัชนีหนึ่งตัวต่อฟีเจอร์ และไม่เห็นว่าหลายฟีเจอร์ขอคอลัมน์นำเดียวกัน
สำหรับฐานข้อมูลเดิมที่มีงานหนาแน่น โปรดจำว่า CREATE INDEX CONCURRENTLY รันใน transaction block ไม่ได้ ใช้งานมากกว่า และอาจทิ้งดัชนีที่ใช้ไม่ได้เมื่อเกิดความล้มเหลว คู่มือ PostgreSQL ระบุความต่างเชิงปฏิบัติการเหล่านี้ไว้ เฟรมเวิร์กมิเกรชันที่ห่อทุกมิเกรชันใน transaction ต้องมีข้อยกเว้นและขั้นตอนเก็บกวาดอย่างชัดเจน ไม่ใช่แทนคำสำคัญด้วยความหวัง
Foreign key ต้องมีความเป็นเจ้าของและกฎการลบ
Foreign key จะถูกต้องก็ต่อเมื่อทีมตัดสินใจก่อนว่าความสัมพันธ์นั้นสื่อถึงความเป็นเจ้าของ การอ้างอิง บริบทที่ไม่บังคับ หรือการระบุที่มาในอดีต คอลัมน์ที่ดูคล้ายกันอาจต้องการพฤติกรรมการลบตรงข้ามกัน
พิจารณา projects.owner_user_id, invoices.customer_id และ audit_events.actor_user_id โครงการอาจโอนเจ้าของได้ ใบแจ้งหนี้อาจต้องอยู่รอดหลังปิดบัญชีลูกค้า เหตุการณ์ตรวจสอบอาจต้องเก็บตัวระบุเดิมของผู้กระทำ แม้ข้อมูลตัวตนถูกลบไปแล้ว การใส่ ON DELETE CASCADE ให้ทั้งสามเพราะอ้างถึง users จะเข้ารหัสเรื่องแต่งที่ทำลายข้อมูล
ใช้ CASCADE เมื่อแถวลูกไร้ความหมายหากไม่มีแถวแม่ และการลบแถวแม่หมายถึงการลบทั้ง aggregate จริง ๆ รายการสินค้าในคำสั่งซื้อมักเข้ากรณีนี้ แต่บันทึกการชำระเงิน เอกสารที่ออกแล้ว การนำเข้า บันทึกเหตุการณ์ และหลักฐานการดูแลเนื้อหา มักไม่ใช่ สำหรับกรณีเหล่านั้น การปฏิเสธ การเก็บถาวร การทำให้ไม่ระบุตัวตนอย่างควบคุม หรือการอ้างอิงที่รับค่าว่างพร้อมฟิลด์ภาพบันทึกที่เก็บไว้ อาจเหมาะกว่า
SET NULL ก็ต้องทบทวนเชิงความหมาย มันคงแถวลูกไว้แต่ลบความสัมพันธ์โดยตรง หากพนักงานต้องอธิบายภายหลังว่าบัญชีใดสร้างรายงาน การอ้างอิง null อาจไม่พอ การเก็บ token ในอดีตที่ไม่ระบุตัวบุคคลหรือภาพบันทึก อาจรักษาความรับผิดชอบโดยไม่เก็บข้อมูลส่วนบุคคลทั้งหมด แต่ทางเลือกการเก็บรักษาที่แน่นอนเป็นของนโยบายผลิตภัณฑ์ ไม่ใช่การเดาของ AI
ตรวจสอบ cardinality ทั้งสองทิศทาง ตัวสร้างอาจจำลอง one-to-one ด้วยการใส่ foreign key โดยไม่มี unique constraint แล้วเปิดให้มีแถวลูกหลายแถวอย่างเงียบ ๆ มันอาจบังคับ unique ทั้งที่ประวัติต้องมีหลายเวอร์ชัน เขียนข้อมูลตัวอย่างสำหรับแถวแม่ที่มีแถวลูกศูนย์ หนึ่ง และหลายแถว แล้วระบุว่า INSERT ใดควรผ่าน
Deferrable constraint ต้องมีเหตุผลเฉพาะ มันช่วยได้เมื่อ transaction ต้องละเมิดลำดับการอ้างอิงชั่วคราว หรืออัปเดตแถวที่พึ่งพากัน แต่การตั้ง foreign key ทุกตัวให้ deferred จะเลื่อนข้อผิดพลาดไปถึง commit และทำให้หาจุดล้มเหลวยากขึ้น คงการบังคับใช้ทันที เว้นแต่ลำดับ transaction จริงต้องเลื่อน
ตรวจสอบ catalog หลังใช้มิเกรชันกับฐานข้อมูลที่ใช้ซ้อม:
SELECT
conname,
contype,
convalidated,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;
ผลลัพธ์ตัวแทนมีหน้าตาเช่นนี้:
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))
เทียบนิยามที่ได้กับกฎความเป็นเจ้าของที่อนุมัติ ความสำเร็จของมิเกรชันเพียงอย่างเดียวไม่เผยการกระทำที่หายไป การเลื่อนที่ไม่คาดคิด หรือข้อจำกัดที่ยังไม่ได้ตรวจสอบ
การเปลี่ยนแปลงที่ทำลายข้อมูลซ่อนอยู่ใน SQL ที่ดูสมเหตุสมผล
ตรวจสอบมิเกรชันในฐานะการแปลงข้อมูล เพราะ DDL ที่ดูเรียบร้อยอาจทิ้งความหมายโดยไม่ใช้ DROP TABLE ที่เห็นชัด ค้นหาการทำลายโดยตรงก่อน จากนั้นตรวจ cast การเติมข้อมูลย้อนหลัง การเขียนใหม่ การเปลี่ยนชื่อ และการแทนที่ข้อจำกัด
การเปลี่ยนชื่อกับการลบต่างกันในเชิงปฏิบัติการ แม้สคีมาปลายทางดูเหมือนกัน หาก surname เปลี่ยนเป็น family_name การเปลี่ยนชื่อรักษาข้อมูลและ dependency ได้ตรงกว่า การลบคอลัมน์เก่าแล้วเพิ่มคอลัมน์ใหม่ให้ผังเดียวกัน แต่ทำให้ค่าทุกค่าหาย มิเกรชันที่สร้างขึ้นมักอนุมานสถานะสุดท้ายโดยไม่เข้าใจความต่อเนื่อง
การเปลี่ยนชนิดข้อมูลต้องมีตัวอย่างการแปลงและกรณีที่ควรถูกปฏิเสธ การเปลี่ยนตัวระบุข้อความเป็นจำนวนเต็มอาจลบเลขศูนย์นำหน้าหรือปฏิเสธตัวระบุผสม การลด precision ของตัวเลขอาจปัดค่า การแปลง timestamp ต้องมีสมมติฐานเขตเวลาที่ระบุไว้ ทดสอบ expression USING จริงกับค่าต่ำสุด สูงสุด null รูปแบบผิด และค่าประวัติที่แปลก ก่อนแก้คอลัมน์
ถือว่าการดำเนินการเหล่านี้ต้องมีเหตุผลเป็นลายลักษณ์อักษร: การลบตารางหรือคอลัมน์ การเปลี่ยนชนิดด้วย cast ที่ทำให้รายละเอียดหาย การแทนคอลัมน์ที่มีข้อมูล การเพิ่ม CASCADE การตั้ง NOT NULL หลังเติมข้อมูลย้อนหลังที่สร้างขึ้น และการสร้างความไม่ซ้ำใหม่ด้วยคอลัมน์ต่างไป ตรวจสอบ SQL ดิบที่ฝังในฟังก์ชันหรือ callback ของมิเกรชันด้วย การค้นหาข้อความเป็นตัวกรองเริ่มต้น ไม่ใช่การตรวจสอบทั้งหมด
รูปแบบความล้มเหลวหนึ่งเกิดซ้ำบ่อย ข้อมูลที่ทราบมีผู้ติดต่อที่บริษัทเป็นทางเลือก แต่หน้าจอตัวอย่างแสดงเฉพาะผู้ติดต่อทางธุรกิจ ตัวสร้างจึงตั้ง contacts.company_id เป็น NOT NULL และแทรกบริษัทที่สร้างขึ้นชื่อ Unknown สำหรับแถวที่จับคู่ไม่ได้ มิเกรชันผ่าน จำนวนแถวกระทบยอดได้ และ foreign key ทุกตัวตรวจผ่าน ข้อมูลยังผิดอยู่ ผู้ติดต่อบุคคลธรรมดาดูเหมือนสังกัดบริษัท รายงานจัดกลุ่มคนที่ไม่เกี่ยวข้องเข้าด้วยกัน และการลบตัวแทนอาจ cascade ไปยังผู้ติดต่อจริง
วิธีแก้ไม่ใช่ค่าเริ่มต้นอีกตัว กู้คืนสถานะต้นทาง ทำให้ความสัมพันธ์รับค่าว่าง ย้ายเฉพาะคู่ที่มีหลักฐานรองรับ และเพิ่ม assertion ว่าชุดที่จับคู่ไม่ได้เท่ากับผู้ติดต่อบุคคลธรรมดาที่ทราบ นี่คือเหตุผลที่ข้อมูลตัวอย่างเชิงความหมายต้องบันทึกความสัมพันธ์ที่คาดหวัง ไม่ใช่แค่จำนวนแถว
เครื่องมือ schema diff มีประโยชน์ แต่ผมไม่เห็นด้วยกับการอนุมัติจาก diff เพียงอย่างเดียว คำแนะนำนี้เป็นที่นิยมเพราะ diff กะทัดรัดและตรวจง่าย แต่มันผิดหากใช้เป็นด่านเดียว เพราะมันแสดงการเปลี่ยนโครงสร้าง ไม่ใช่ที่มาของค่าที่เติมย้อนหลัง ขอบเขต transaction พฤติกรรมล็อก หรือความจริงหลังมิเกรชัน
การซ้อมต้องพิสูจน์ผลลัพธ์และพฤติกรรมเมื่อผิดพลาด
รันมิเกรชันทั้งหมดบนการกู้คืนชุดข้อมูลที่ทราบในฐานข้อมูลชั่วคราว จากนั้นทดสอบทั้งผลที่ตั้งใจและเส้นทางที่ถูกขัดจังหวะหรือถูกปฏิเสธ ฐานข้อมูลว่างใหม่มีประโยชน์ในการจับข้อผิดพลาดด้านลำดับ แต่เผยการแปลงที่ทำให้ข้อมูลหาย แถวเก่าที่ไม่ถูกต้อง หรือการตรวจสอบที่ช้าไม่ได้
ใช้ลำดับการซ้อมนี้เป็นเอกสารประกอบการปล่อย:
- กู้คืนชุดข้อมูลก่อนเปลี่ยนแปลงลงฐานข้อมูลแยก และบันทึกจำนวนแถวพร้อม assertion เชิงความหมาย
- เก็บสคีมาปัจจุบัน ใช้มิเกรชันฉบับเดียวกับที่จะปล่อย และบันทึกผลลัพธ์ทั้งหมดพร้อมเวลาและขอบเขต transaction
- รันการตรวจ catalog assertion การแมป การทดสอบการปฏิเสธของข้อจำกัด และคิวรีแอปพลิเคชันตัวแทน
- เปรียบเทียบค่าที่สำคัญกับความคาดหวังที่บันทึกไว้ รวมถึงหมวดหมู่ที่จับคู่ไม่ได้และค่า null
- ทดลองวิธีกู้คืนที่บันทึกไว้ แล้วรัน assertion ก่อนมิเกรชันซ้ำกับฐานข้อมูลที่กู้คืน
เก็บ dump เฉพาะสคีมาก่อนและหลัง:
pg_dump --schema-only --no-owner --no-privileges \
--dbname "$DATABASE_URL" > schema.sql
ตรวจสอบตาราง sequence ดัชนี ข้อจำกัด ฟังก์ชัน trigger extension และสิทธิ์ที่เกี่ยวข้องกับแอปพลิเคชัน ORM model diff อาจละออบเจ็กต์ฐานข้อมูลที่แอปพลิเคชันไม่ได้จำลอง โดยเฉพาะ trigger expression index partial index และฟังก์ชันที่ติดตั้งเอง
เพิ่มการทดสอบเชิงลบที่พิสูจน์ว่าข้อจำกัดปฏิเสธสถานะผิด transaction สำหรับทดสอบอาจลอง INSERT ที่ไม่ถูกต้อง แล้ว rollback ไม่ว่าผลจะเป็นอย่างไร:
BEGIN;
INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);
ROLLBACK;
ผลลัพธ์ที่คาดควรระบุชื่อข้อจำกัดที่ละเมิด เช่น:
ERROR: new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL: Failing row contains (..., 0, 2500, ...).
อย่าเปรียบเทียบข้อความผิดพลาดทั้งหมดข้ามทุกสภาพแวดล้อม เพราะรายละเอียดอาจต่างกัน ให้ assertion SQLSTATE หรือตัวตนของข้อจำกัดในระบบทดสอบอัตโนมัติ และเก็บผลลัพธ์ที่อ่านได้ไว้ให้ผู้ตรวจทาน
วัดการล็อกและระยะเวลาบนชุดข้อมูลที่มีขนาดใกล้การใช้งานที่ตั้งใจไว้ งานที่เสร็จทันทีบน 50 แถวอาจบล็อกการเขียนเมื่อมันตรวจสอบหลายล้านแถว สำหรับฐานข้อมูลจริงเริ่มต้นที่ว่าง ความเสี่ยงทันทีต่ำกว่า แต่การซ้อมยังทดสอบข้อมูลตั้งต้นที่นำเข้า และสร้างฐานอ้างอิงสำหรับการเปลี่ยนแปลงในอนาคต
การกู้คืนต้องมากกว่ามิเกรชันย้อนกลับ
การกู้คืนจะน่าเชื่อถือก็ต่อเมื่อกู้ข้อมูลและความเข้ากันได้ของแอปพลิเคชันกลับมาได้ภายในเวลาที่บริการยอมรับได้ มิเกรชันย้อนกลับที่สร้างคอลัมน์ที่ลบไปใหม่ ไม่ได้กู้ค่าก่อนหน้าของคอลัมน์นั้น
เลือกหน่วยการกู้คืนก่อนดำเนินการ สำหรับฐานข้อมูลเริ่มต้นที่ว่าง การลบและสร้างฐานข้อมูลใหม่อาจยอมรับได้หากยังไม่มีผู้ใช้เขียนข้อมูล เมื่อมีการเขียนจริงแล้ว การกู้คืนอาจต้องใช้ database snapshot logical backup คอลัมน์เก่าที่ยังเก็บไว้ หรือการซ่อมไปข้างหน้า วิธีที่ถูกต้องขึ้นกับจำนวนข้อมูลใหม่ที่อาจเข้ามาระหว่างและหลังมิเกรชัน
ทดสอบคำสั่งกู้คืนและสิทธิ์เข้าถึงก่อนพึ่งพามัน backup ที่มีอยู่แต่ผู้ดำเนินการปล่อยระบบกู้คืนไม่ได้ ไม่ใช่แผนกู้คืน กู้คืนลงฐานข้อมูลแยก ตรวจสอบ ownership และ extension แล้วรัน assertion ที่ทราบชุดเดิมซึ่งใช้ก่อนมิเกรชัน
Snapshot และ transaction rollback แก้ความล้มเหลวคนละแบบ transaction สามารถย้อนคำสั่งเมื่อมิเกรชันล้มเหลวก่อน commit ได้ หากทุกการดำเนินการอยู่ใน transaction นั้น snapshot พาฐานข้อมูลทั้งก้อนกลับสู่สถานะก่อนหน้าได้ แต่การทำเช่นนั้นอาจทิ้งการเขียนที่ถูกต้องซึ่งเกิดหลัง snapshot ทั้งสองกลไกไม่ได้กระทบยอดการเขียนเหล่านั้นให้อัตโนมัติ
เลือกการเปลี่ยนแปลงแบบเพิ่มเมื่อยังมีความไม่แน่นอน เพิ่มคอลัมน์หรือตารางใหม่ คัดลอกข้อมูลด้วยกฎที่วัดผลได้ รันโค้ดทั้งสองเส้นทางในช่วงควบคุมหากจำเป็น แล้วลบโครงสร้างเก่าเมื่อยืนยันแล้ว วิธี expand and contract ใช้งานเพิ่ม แต่รักษาหลักฐาน การเก็บคอลัมน์เก่าที่เปลี่ยนชื่อไว้หนึ่งรุ่นมักถูกกว่าการสร้างคืนจาก log
เขียนเงื่อนไขกระตุ้นการกู้คืนล่วงหน้า ตัวอย่างคือ assertion เชิงความหมายล้มเหลว แถวที่จับคู่ไม่ได้เกินคาด ข้อจำกัดใช้ไม่ได้ มิเกรชันเกินช่วงเวลาล็อกที่อนุมัติ หรือแอปพลิเคชันผิดพลาดเพราะเวอร์ชันไม่ตรง ผู้ดำเนินการไม่ควรต้องคิดตัดสินใจในขณะที่ผู้ใช้กำลังรอ
บันทึกจุดหลังจากนั้นที่การกู้คืนฐานข้อมูลเก่าต้องกู้แอปพลิเคชันเก่าด้วย แอปพลิเคชันใหม่อาจพึ่งพาคอลัมน์ใหม่ ขณะที่แอปพลิเคชันเก่าอาจปฏิเสธค่า enum ใหม่หรือเขียนรูปแบบเก่า การกู้คืนฐานข้อมูลและแอปพลิเคชันต้องใช้เวอร์ชันที่เข้ากันได้
การอนุมัติต้องมีหลักฐาน ไม่ใช่ความมั่นใจ
อนุมัติมิเกรชันแรกก็ต่อเมื่อคนอื่นทำซ้ำและเห็นได้จากเอกสารที่เก็บไว้ว่าเหตุใดจึงปลอดภัย ความมั่นใจจาก code review ที่สะอาดหรืออินเทอร์เฟซที่สร้างขึ้นอย่างสวยงามอยู่ไม่รอดเมื่อพบข้อมูลคลาดเคลื่อนที่อธิบายไม่ได้ครั้งแรก
บันทึกการอนุมัติควรมีสมมติฐานที่อนุมาน เมทริกซ์การแมปตาราง ตัวตนของชุดข้อมูลที่ทราบ schema diff มิเกรชันฉบับจริง คิวรีและผลการตรวจสอบ การทดสอบข้อมูลที่ถูกปฏิเสธ เหตุผลของดัชนี เหตุผลของการดำเนินการที่ทำลายข้อมูล และขั้นตอนกู้คืนที่ผ่านการทดสอบ ระบุชื่อผู้ตรวจทาน และเก็บการตัดสินใจที่ยังไม่จบไว้เป็นตัวบล็อก แทนฝังไว้ในประวัติแชต
เมื่อสร้างแอปพลิเคชันใน Koder.ai ให้ใช้โหมดวางแผนบันทึกการตัดสินใจเรื่องสคีมาเหล่านี้ก่อนอนุญาตมิเกรชัน ส่งออกซอร์สเพื่อตรวจทาน และถือว่า snapshot กับ rollback เป็นเครื่องมือกู้คืนที่ยังต้องซ้อมกับชุดข้อมูลที่ทราบ
อย่าปล่อยให้ตัวสร้างอนุมัติการอนุมานของตัวเองด้วยการสร้างโค้ดซ้ำจนการทดสอบผ่าน วงจรนั้นอาจทำให้แอปพลิเคชันปรับตัวเข้ากับสคีมาที่ผิด แทนแก้แบบจำลอง มนุษย์ต้องตัดสินว่าฐานข้อมูลตรงกับโดเมนหรือไม่ โดยเฉพาะเรื่องตัวตน การลบ การเก็บรักษา และค่าที่ไม่ทราบ
คิวรีอนุมัติสุดท้ายควรเรียบง่าย ข้อเท็จจริงที่ทราบทุกข้อแมปไปยังผลที่คาดหวังหนึ่งเดียว ข้อจำกัดทุกตัวปฏิเสธตัวอย่างโต้แย้งที่ตั้งใจ การดำเนินการที่ทำลายข้อมูลทุกอย่างมีเหตุผล และการกู้คืนทำให้ assertion ก่อนมิเกรชันกลับมาได้ หากหลักฐานต้องใช้คำอธิบายชักจูงเพื่อแก้ตัวให้ความไม่ตรงกัน ให้หยุดมิเกรชัน PostgreSQL จะบังคับใช้สคีมาอย่างแม่นยำ รวมถึงส่วนที่ตัวสร้างเดาผิด
คำถามที่พบบ่อย
ควรตรวจสอบอะไรในสคีมา PostgreSQL ที่ AI สร้างขึ้น?
ตรวจสอบทั้ง DDL ที่สร้างขึ้น การดำเนินการในมิเกรชัน และสมมติฐานเบื้องหลังทั้งสองส่วน สคีมาปลายทางที่ถูกต้องอาจยังมาจากมิเกรชันที่ลบข้อมูล บล็อกการเขียนข้อมูล หรือสร้างค่าเริ่มต้นที่ทำให้เข้าใจผิดได้
ชุดข้อมูลสำหรับตรวจสอบสคีมาควรมีขนาดเท่าไร?
ใช้ชุดข้อมูลขนาดเล็กที่มีแถวปกติ ค่าขอบเขต ความสัมพันธ์ที่ขาดหาย ข้อมูลซ้ำ ค่า null สตริงว่าง และกรณีผิดปกติจากข้อมูลเก่า จุดประสงค์ไม่ใช่ปริมาณ แต่คือการหักล้างสมมติฐานของตัวสร้างก่อนที่ข้อมูลจริงจะทำเช่นนั้น
มิเกรชันทดสอบที่สำเร็จพิสูจน์ว่าสคีมาปลอดภัยหรือไม่?
ไม่ มิเกรชันที่สำเร็จพิสูจน์เพียงว่า PostgreSQL ยอมรับคำสั่งเหล่านั้นกับสถานะฐานข้อมูลนั้น มันไม่ได้พิสูจน์ว่าการแมปตารางถูกต้อง ข้อมูลยังคงความหมาย ดัชนีรองรับคิวรีจริง หรือการกู้คืนทำงานได้
คอลัมน์ PostgreSQL ควรเป็น NOT NULL เมื่อใด?
ฟิลด์ควรเป็น NOT NULL ก็ต่อเมื่อทุกระเบียนที่ถูกต้องมีค่า และแอปพลิเคชันสามารถให้ค่านั้นได้ในทุกเส้นทางการเขียน อย่าใช้ค่าเริ่มต้นที่แต่งขึ้นเพียงเพื่อให้ผ่านข้อจำกัด เพราะมันแทนที่ข้อมูลที่ขาดหายซึ่งมองเห็นได้ด้วยข้อมูลเท็จที่ดูน่าเชื่อถือ
ควรใช้ข้อจำกัด unique หรือ unique index?
ข้อจำกัด unique ใช้สื่อกฎที่ออบเจ็กต์ฐานข้อมูลอื่นอ้างอิงได้ และ PostgreSQL รองรับด้วยดัชนี ส่วน unique index มีประโยชน์เมื่อความไม่ซ้ำใช้กับแถวหรือ expression บางส่วนเท่านั้น เช่น ระเบียนที่ยังไม่ถูกลบ หรืออีเมลที่ผ่านการปรับรูปแบบแล้ว
PostgreSQL สร้างดัชนีให้ foreign key โดยอัตโนมัติหรือไม่?
สร้างดัชนีสำหรับคอลัมน์ที่ใช้ค้นหาแถวแม่ กรองคิวรีที่ใช้บ่อย เชื่อมตารางขนาดใหญ่ หรือบังคับความไม่ซ้ำ PostgreSQL ไม่ได้สร้างดัชนีให้ฝั่งที่อ้างอิงของ foreign key โดยอัตโนมัติ จึงต้องตรวจสอบคอลัมน์ฝั่งลูกแยกต่างหาก
ON DELETE CASCADE ปลอดภัยเมื่อใด?
ใช้ CASCADE เฉพาะเมื่อแถวลูกไม่มีความหมายอิสระหลังแถวแม่หายไป หากการลบเป็นการตัดสินใจทางธุรกิจ หรือแถวลูกเป็นหลักฐาน เช่น ใบแจ้งหนี้หรือบันทึกตรวจสอบ ควรปฏิเสธหรือจัดการการลบอย่างชัดเจนแทน
จะตรวจพบการเปลี่ยนแปลงที่ทำลายข้อมูลในมิเกรชันได้อย่างไร?
ถือว่า DROP ทุกแบบ การแปลงชนิดข้อมูลที่ทำให้รายละเอียดหาย การเขียนตารางใหม่ คอลัมน์บังคับใหม่ และข้อจำกัดที่ถูกแทนที่ อาจทำลายข้อมูลได้ ค้นหาในข้อความมิเกรชัน และตรวจสอบฟังก์ชันที่สร้างขึ้นกับ SQL ดิบด้วย เพราะพฤติกรรมทำลายข้อมูลอาจซ่อนอยู่ในนั้น
วิธีที่ปลอดภัยที่สุดในการทดสอบการกู้คืนมิเกรชันคืออะไร?
กู้คืนฐานข้อมูลก่อนเปลี่ยนแปลงไปยังตำแหน่งแยกต่างหาก รันมิเกรชันที่นั่น ตรวจสอบด้วยคิวรีเชิงความหมาย แล้วเทียบผลกับสิ่งที่บันทึกไว้ การทดสอบเฉพาะมิเกรชันย้อนกลับมองไม่เห็นข้อมูลที่ถูกลบและอาจทำให้มั่นใจผิด
ควรเก็บหลักฐานอะไรหลังอนุมัติสคีมา?
เก็บ DDL ที่สร้างขึ้น ข้อความมิเกรชัน schema diff คิวรีและผลการตรวจสอบ ขั้นตอนกู้คืน และตัวตนของผู้ตรวจทานไว้ด้วยกัน บันทึกนี้อธิบายสิ่งที่อนุมัติ และช่วยให้การตรวจทานมิเกรชันครั้งถัดไปพบสมมติฐานที่เปลี่ยนไป