เริ่มจากข้อจำกัดของ PostgreSQL: หยุดบั๊กมุมขอบของ AI ตั้งแต่แรก | Koder.ai
NOT NULL
NOT NULL
NOT NULL
created_at TIMESTAMP NOT NULL DEFAULT now()
status TEXT NOT NULL DEFAULT 'new'
is_active BOOLEAN NOT NULL DEFAULT true
email
company_name
NOT NULL
phone
phone_status
missing
requested
verified
-- 1) Totals should never be negative
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total_cents >= 0);
-- 2) Enum-like allowed values without adding a custom type
ALTER TABLE tickets
ADD CONSTRAINT tickets_status_allowed
CHECK (status IN ('new', 'open', 'waiting', 'closed'));
-- 3) Date order rules
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_date_order
CHECK (end_date IS NULL OR end_date >= start_date);
\nกำหนดความหมายของ "ซ้ำ" สำหรับผู้ใช้ของคุณ (ตัวพิมพ์ ช่องว่าง ต่อบัญชีหรือทั่วระบบ) แล้วเข้ารหัสมันครั้งเดียวเพื่อให้ทุกเส้นทางโค้ดปฏิบัติตามกฎเดียวกัน\n\n## FOREIGN KEY: รักษาความสอดคล้องของความสัมพันธ์\n\nFOREIGN KEY พูดว่า "แถวนี้ต้องชี้ไปยังแถวจริงที่นู่น" หากไม่มีมัน โค้ดอาจสร้างเรคอร์ดกำพร้าที่ดูถูกต้องเมื่อแยกกันแต่ทำลายแอปในภายหลัง ตัวอย่างเช่น โน้ตที่อ้างถึงลูกค้าที่ถูกลบ หรือใบแจ้งหนี้ที่ชี้ไปยัง user ID ที่ไม่เคยมีอยู่\n\nคีย์ต่างประเทศสำคัญที่สุดเมื่อสองการกระทำเกิดขึ้นใกล้กัน: การลบและการสร้าง, การ retry หลัง timeout, หรือการทำงานของงานพื้นหลังด้วยข้อมูลล้าสมัย ฐานข้อมูลดีกว่าที่จะบังคับความสอดคล้องกว่าที่ทุกเส้นทางแอปจะต้องจำการตรวจสอบเอง\n\n### เลือกพฤติกรรม ON DELETE ที่เหมาะสม\n\nตัวเลือก ON DELETE ควรตรงกับความหมายในโลกจริงของความสัมพันธ์ ถามตัวเองว่า: "ถ้าพ่อแม่หายไป ลูกยังควรอยู่ไหม?"\n\n- RESTRICT (หรือ NO ACTION): บล็อกการลบพ่อเมือยังมีลูกอยู่\n- CASCADE: ลบพ่อแม่ก็ลบลูกด้วย\n- SET NULL: เก็บลูกไว้แต่เอาลิงก์ออก\n\nระวัง CASCADE มันอาจถูกต้อง แต่ก็อาจลบมากกว่าที่คาดเมื่อเกิดบั๊กหรือแอดมินเผลอลบเรคอร์ดพ่อแม่\n\n### สคีมาแบบ multi-tenant: แสดงความเป็นเจ้าของอย่างชัดเจน\n\nในแอป multi-tenant foreign key ไม่ได้เกี่ยวกับความถูกต้องเท่านั้น มันยังป้องกันการรั่วไหลข้ามบัญชี รูปแบบหนึ่งที่พบได้บ่อยคือรวม account_id ในทุกตารางที่เป็นของ tenant และเชื่อมความสัมพันธ์ผ่านมัน\n\n```sql
CREATE TABLE contacts (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
PRIMARY KEY (account_id, id)
);
CREATE TABLE notes (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
contact_id bigint NOT NULL,
body text NOT NULL,
PRIMARY KEY (account_id, id),
FOREIGN KEY (account_id, contact_id)
REFERENCES contacts (account_id, id)
ON DELETE RESTRICT
);
\nนี่บังคับว่า "ใครเป็นเจ้าของอะไร" ในสคีมา: โน้ตไม่สามารถชี้ไปยัง contact ในบัญชีอื่นได้ แม้ว่าโค้ดแอป (หรือ query ที่ LLM สร้าง) จะพยายามทำเช่นนั้น\n\n## ทีละขั้นตอน: เพิ่มข้อจำกัดโดยไม่ทำให้โปรดักชันพัง\n\nเริ่มด้วยการเขียนรายการ invariant สั้น ๆ: ข้อเท็จจริงที่ต้องเป็นจริงเสมอ เขียนให้ชัดเจน เช่น "ทุก contact ต้องมี email" "สถานะต้องเป็นหนึ่งในค่าที่อนุญาต" "ใบแจ้งหนี้ต้องเป็นของลูกค้าที่จริงอยู่" นี่คือกฎที่คุณต้องการให้ฐานข้อมูลบังคับทุกครั้ง\n\nค่อย ๆ เปิดตัวการเปลี่ยนแปลงด้วยมิเกรชันขนาดเล็กเพื่อไม่ให้โปรดักชันถูกเซอร์ไพรส์:\n\n- เพิ่มคอลัมน์หรือกฎใหม่ในวิธีที่ไม่ทำให้ระบบพังก่อน\n- เติมข้อมูลย้อนหลังในแบตช์\n- แก้ไขข้อมูลเสีย (dedupe, แก้ค่าที่ไม่ถูกต้อง) หรือกักมันไว้สำหรับทบทวน\n- บังคับกฎ (`NOT NULL`, `UNIQUE`, `CHECK`, `FOREIGN KEY`)\n- ทำให้พฤติกรรมแอปเข้มงวดขึ้นเพื่อให้จัดการข้อผิดพลาดชัดเจน\n\nส่วนที่ยุ่งคือข้อมูลเสียที่มีอยู่ วางแผนสำหรับมัน สำหรับค่าซ้ำ ให้เลือกแถวผู้ชนะ รวมที่เหลือไว้ และเก็บบันทึกเล็ก ๆ สำหรับการตรวจสอบ สำหรับฟิลด์ที่หาย ให้เลือกค่าเริ่มต้นที่ปลอดภัยเฉพาะเมื่อปลอดภัยจริง ๆ มิฉะนั้นกักข้อมูลไว้ สำหรับความสัมพันธ์ที่พัง ให้ระบุผู้ปกครองที่ถูกต้องให้กับแถวลูกหรือเอาแถวที่ผิดออก\n\nหลังการมิเกรชันแต่ละครั้ง ให้ตรวจสอบด้วยการเขียนไม่กี่รายการที่ควรล้มเหลว: แทรกแถวที่ขาดค่าที่จำเป็น, แทรกคีย์ซ้ำ, แทรกค่าที่อยู่นอกช่วง, และอ้างอิงแถวพ่อแม่ที่หายไป การเขียนที่ล้มเหลวเป็นสัญญาณที่มีประโยชน์ มันแสดงให้เห็นว่าโค้ดส่วนที่ผ่านมาพึ่งพาพฤติกรรม "พยายามอย่างดีที่สุด" อย่างเงียบ ๆ อยู่ที่ไหนบ้าง\n\n## ตัวอย่างที่สมจริง: CRM เล็ก ๆ ที่คงความสะอาด\n\nนึกภาพ CRM เล็ก ๆ: accounts (ลูกค้าของ SaaS คุณ), บริษัทที่พวกเขาทำงานด้วย, contacts ที่บริษัทเหล่านั้น และ deals ที่ผูกกับบริษัท\n\nนี่คือแอปประเภทที่คนมักสร้างอย่างรวดเร็วด้วยเครื่องมือแชท มันดูโอเคในการสาธิต แต่ข้อมูลจริงจะยุ่งเร็ว ปัญหาสองอย่างที่มักเกิดเร็วคือ: contact ซ้ำ (อีเมลเดิมถูกกรอกสองครั้งในรูปแบบต่างกันเล็กน้อย) และ deals ถูกสร้างโดยไม่มี company เพราะเส้นทางโค้ดบางทางลืมตั้ง `company_id` อีกเรื่องคลาสสิกคือค่า deal เป็นลบหลังรีแฟกเตอร์หรือความผิดพลาดในการ parse\n\nการแก้ไม่ใช่การเพิ่ม if-statement มากขึ้น แต่มันคือการเลือกข้อจำกัดไม่กี่ข้อที่ทำให้ข้อมูลเสียเป็นไปไม่ได้ที่จะเก็บ\n\n### ข้อจำกัดที่ช่วยให้ CRM สะอาด\n\n```sql
-- Contacts: prevent duplicates per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_email_uniq UNIQUE (account_id, email);
-- Deals: require a company and keep the relationship valid
ALTER TABLE deals
ALTER COLUMN company_id SET NOT NULL,
ADD CONSTRAINT deals_company_fk
FOREIGN KEY (company_id) REFERENCES companies(id);
-- Deals: deal value cannot be negative
ALTER TABLE deals
ADD CONSTRAINT deals_value_nonneg CHECK (deal_value >= 0);
-- A few obvious required fields
ALTER TABLE companies
ALTER COLUMN name SET NOT NULL;
ALTER TABLE contacts
ALTER COLUMN email SET NOT NULL;