2 นาที

เริ่มจากข้อจำกัดของ PostgreSQL: หยุดบั๊กมุมขอบของ AI ตั้งแต่แรก

ส่งมอบแอปที่สร้างโดย AI อย่างปลอดภัยขึ้นด้วยการพึ่งพาข้อจำกัดของ PostgreSQL สำหรับ NOT NULL, CHECK, UNIQUE และ FOREIGN KEY ก่อนโค้ดและการทดสอบ

เริ่มจากข้อจำกัดของ PostgreSQL: หยุดบั๊กมุมขอบของ AI ตั้งแต่แรก

ทำไมโค้ดที่เขียนโดย AI ถึงยังพังเมื่อเจอข้อมูลยุ่งเหยิงจริง\n\nโค้ดที่สร้างโดย AI มักดูถูกต้องเพราะมันจัดการกับเส้นทางที่เป็นปกติได้ดี แอปจริงมักพังในพื้นที่ตรงกลางที่ยุ่งเหยิง: ฟอร์มส่งสตริงว่างแทนที่จะเป็น null, งานพื้นหลัง retry แล้วสร้างเรคอร์ดเดียวกันสองครั้ง, หรือการลบพ่อแม่ทิ้งแล้วปล่อยลูกไว้ข้างหลัง ปัญหาเหล่านี้ไม่ใช่ข้อบกพร่องหายาก — มันปรากฏเป็นฟิลด์ที่ต้องกรอกเป็นค่าว่าง ค่าที่ควรจะ "unique" กลายเป็นซ้ำ และแถวที่อ้างอิงไปไหนไม่ได้\n\nปัญหาเหล่านี้ยังมักหลุดจากการรีวิวโค้ดและการทดสอบพื้นฐานด้วยเหตุผลง่าย ๆ: ผู้รีวิวอ่านเจตนา ไม่ใช่ทุกกรณีมุมขอบ การทดสอบมักครอบคลุมตัวอย่างทั่วไปไม่ใช่นิสัยผู้ใช้จริงเป็นสัปดาห์ การนำเข้าไฟล์ CSV เครือข่ายล้มเหลวซ้ำ ๆ หรือตัวขอพร้อมกัน โค้ดที่ผู้ช่วยสร้างอาจพลาดการเช็กเล็ก ๆ แต่สำคัญ เช่น การตัดช่องว่าง การตรวจสอบช่วงค่า หรือการป้องกันสภาวะการแข่งขัน\n\n"ข้อจำกัดก่อน โค้ดหลัง" หมายความว่าคุณใส่กฎที่ไม่ต่อรองได้ไว้ในฐานข้อมูลเพื่อให้ข้อมูลไม่ดีไม่สามารถถูกบันทึกได้ ไม่ว่าหนทางโค้ดใดจะพยายามเขียนมัน แอปของคุณยังควรตรวจสอบอินพุตเพื่อให้ข้อความผิดพลาดที่ชัดเจน แต่ฐานข้อมูลจะเป็นผู้บังคับความจริง นี่คือจุดที่ข้อจำกัดของ PostgreSQL โดดเด่น: มันปกป้องคุณจากหมวดข้อผิดพลาดทั้งหมวด\n\nตัวอย่างสั้น ๆ: ลองจินตนาการ CRM ขนาดเล็ก สคริปต์นำเข้าที่สร้างคอนแทคโดย AI มีแถวหนึ่งที่อีเมลเป็น "" (ว่าง), สองแถวใช้ที่อยู่อีเมลเดียวกันแต่ตัวพิมพ์ต่างกัน, และคอนแทคหนึ่งรายการอ้างอิง account_id ที่ไม่มีอยู่เพราะบัญชีถูกลบในกระบวนการอื่น หากไม่มีข้อจำกัด ข้อมูลทั้งหมดนั้นอาจขึ้นโปรดักชันและทำลายรายงานในภายหลัง\n\nด้วยกฎฐานข้อมูลที่เหมาะสม การเขียนเหล่านั้นจะล้มเหลวทันที ใกล้แหล่งที่มา ฟิลด์ที่จำเป็นจะไม่สามารถขาดได้ ค่าซ้ำจะไม่ลอบเข้ามาในช่วง retry ความสัมพันธ์จะไม่อ้างถึงแถวที่ถูกลบหรือไม่มีอยู่ และค่าจะไม่อยู่นอกช่วงที่อนุญาต\n\nข้อจำกัดไม่ได้ป้องกันบั๊กทุกอย่าง มันไม่แก้ UI ที่ทำให้สับสน การคำนวณส่วนลดที่ผิด หรือคิวรีที่ช้า แต่ข้อจำกัดหยุดการสะสมของข้อมูลเสียแบบเงียบ ๆ ซึ่งมักเป็นจุดที่ "บั๊กมุมขอบที่สร้างโดย AI" กลายเป็นเรื่องแพง\n\n## ทำไมฐานข้อมูลควรเป็นเส้นสุดท้ายของการป้องกัน\n\nแอปของคุณไม่ค่อยเป็นโค้ดเบสเดียวที่คุยกับผู้ใช้คนเดียว ผลิตภัณฑ์ทั่วไปมีเว็บ UI แอปมือถือ จอแอดมิน งานพื้นหลัง การนำเข้า CSV และบางครั้งการเชื่อมต่อบุคคลที่สาม แต่ละเส้นทางสามารถสร้างหรือเปลี่ยนข้อมูลได้ หากทุกเส้นทางต้องจำกฎเดิมทั้งหมด ทางใดทางหนึ่งจะลืม\n\nฐานข้อมูลเป็นที่เดียวที่ทุกเส้นทางแชร์ เมื่อคุณถือว่ามันเป็นผู้คัดกรองสุดท้าย กฎจะใช้กับทุกอย่างโดยอัตโนมัติ PostgreSQL constraints แปลง "เราคาดว่าสิ่งนี้เป็นจริงเสมอ" เป็น "สิ่งนี้ต้องเป็นจริง มิฉะนั้นการเขียนจะล้มเหลว"\n\nโค้ดที่สร้างโดย AI ยิ่งทำให้เรื่องนี้สำคัญขึ้น โมเดลอาจเพิ่มการตรวจสอบฟอร์มใน React UI แต่พลาดกรณีมุมขอบในงานพื้นหลัง หรือจัดการข้อมูลเส้นทางปกติได้ดีแล้วพังเมื่อผู้ใช้จริงใส่ข้อมูลที่ไม่คาดคิด ข้อจำกัดจับปัญหา ณ เวลาที่ข้อมูลไม่ดีพยายามเข้ามา ไม่ใช่สัปดาห์ต่อมาที่คุณต้องดีบักรายงานแปลก ๆ\n\nเมื่อคุณข้ามข้อจำกัด ข้อมูลเสียมักเงียบ การบันทึกสำเร็จ แอปเดินหน้าต่อ และปัญหาปรากฏเป็นตั๋วซัพพอร์ต ความคลาดเคลื่อนการเรียกเก็บเงิน หรือแดชบอร์ดที่ไม่มีใครเชื่อ การทำความสะอาดแพงเพราะคุณกำลังแก้ไขประวัติ ไม่ใช่คำขอเดียว\n\nข้อมูลเสียมักลอบเข้ามาจากสถานการณ์ประจำวัน: เวอร์ชันไคลเอ็นต์ส่งฟิลด์เป็นค่าว่างแทนที่จะขาดไป, retry สร้างข้อมูลซ้ำ, แก้ไขโดยแอดมินข้ามการตรวจสอบ UI, ไฟล์นำเข้าไม่สม่ำเสมอ, หรือสองผู้ใช้ปรับเรคอร์ดที่เกี่ยวข้องในเวลาเดียวกัน\n\nโมเดลจิตที่มีประโยชน์: รับข้อมูลก็ต่อเมื่อมันถูกต้องที่ขอบเขต ในทางปฏิบัติ ขอบเขตนั้นควรรวมถึงฐานข้อมูล เพราะฐานข้อมูลเห็นการเขียนทั้งหมด\n\n## NOT NULL: หยุดข้อมูลที่ต้องมีหายตั้งแต่ต้น\n\nNOT NULL เป็นข้อจำกัดพื้นฐานที่สุดของ PostgreSQL และมันป้องกันคลาสบั๊กที่น่าประหลาดใจได้มาก หากค่านั้นต้องมีเพื่อให้แถวมีความหมาย ให้ฐานข้อมูลบังคับมัน\n\nNOT NULL มักถูกต้องสำหรับตัวระบุ ชื่อที่จำเป็น และ timestamp หากคุณสร้างเรคอร์ดที่สมบูรณ์ไม่ได้โดยไม่มีค่านั้น อย่าอนุญาตให้มันว่าง ใน CRM เล็ก ๆ ผู้ที่มี lead โดยไม่มี owner หรือ created time ไม่ใช่ "lead บางส่วน" มันคือข้อมูลพังที่จะทำให้เกิดพฤติกรรมแปลก ๆ ในภายหลัง\n\nNULL มักสอดแทรกได้ง่ายกับโค้ดที่สร้างโดย AI เพราะสะดวกที่จะสร้างเส้นทาง "ไม่บังคับ" โดยไม่สังเกต ฟิลด์ฟอร์มอาจเป็นทางเลือกใน UI, API อาจรับคีย์ที่ขาดไป, และสาขาหนึ่งของฟังก์ชันสร้างอาจข้ามการกำหนดค่า ทุกอย่างยังคอมไพล์ผ่านและการทดสอบเส้นทางปกติผ่าน จากนั้นผู้ใช้จริงนำเข้า CSV ที่มีเซลล์ว่าง หรือไคลเอ็นต์มือถือส่ง payload ต่างกัน และ NULL ลงฐานข้อมูล\n\nรูปแบบที่ดีคือรวม NOT NULL กับค่าเริ่มต้นที่เหมาะสมสำหรับฟิลด์ที่ระบบเป็นเจ้าของ:\n\n- created_at TIMESTAMP NOT NULL DEFAULT now()\n- status TEXT NOT NULL DEFAULT 'new'\n- is_active BOOLEAN NOT NULL DEFAULT true\n\nค่าเริ่มต้นไม่ได้ดีเสมอไป อย่ากำหนดค่าเริ่มต้นให้ฟิลด์ที่ผู้ใช้กำหนด เช่น email หรือ company_name เพียงเพื่อให้ผ่าน NOT NULL สตริงว่างไม่ใช่ "ถูกต้องมากกว่า" เทียบกับ NULL มันแค่ซ่อนปัญหา\n\nเมื่อคุณไม่แน่ใจ ตัดสินใจว่าค่าดังกล่าวไม่ทราบจริงหรือว่ามันเป็นสถานะที่ต่างออกไป หาก "ยังไม่ได้ให้" มีความหมาย ให้พิจารณาคอลัมน์สถานะแยกต่างหากแทนการอนุญาต NULL ทั่วไป ตัวอย่างเช่น ให้ phone เป็น nullable แต่เพิ่ม phone_status เช่น missing, requested, หรือ verified จะช่วยรักษาความหมายให้สอดคล้องทั่วโค้ดของคุณ\n\n## CHECK: เข้ารหัสกฎธุรกิจให้ใกล้ข้อมูล\n\nCHECK constraint คือคำสัญญาที่ตารางของคุณให้: ทุกแถวต้องเป็นไปตามกฎ ทุกครั้ง มันเป็นหนึ่งในวิธีที่ง่ายที่สุดในการป้องกันกรณีมุมขอบจากการสร้างเรคอร์ดที่โค้ดดูเหมือนจะโอเคแต่ในความจริงไม่น่าเชื่อถือ\n\nCHECK ทำงานได้ดีที่สุดกับกฎที่ขึ้นกับค่าภายในแถวเดียวกันเท่านั้น: ช่วงตัวเลข ค่าที่อนุญาต และความสัมพันธ์ง่าย ๆ ระหว่างคอลัมน์\n\n```sql

-- 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);

\nCHECK ที่ดีอ่านเข้าใจได้ทันที ปฏิบัติกับมันเหมือนเอกสารของข้อมูลของคุณ ชอบนิพจน์สั้น ๆ ชื่อ constraint ชัดเจน และรูปแบบที่คาดเดาได้\n\nCHECK ไม่ใช่เครื่องมือสำหรับทุกอย่าง หากกฎต้องมองหาแถวอื่น รวบรวมข้อมูล หรือเปรียบเทียบข้ามตาราง (ตัวอย่างเช่น "บัญชีไม่ควรเกินขีดจำกัดแผน") เก็บตรรกะนั้นไว้ในโค้ดแอป ทริกเกอร์ หรืองานพื้นหลังที่ควบคุมได้\n\n## UNIQUE: ป้องกันการซ้ำที่คุณจะเสียดายในภายหลัง\n\nกฎ `UNIQUE` ง่ายมาก: ฐานข้อมูลปฏิเสธไม่ให้เก็บสองแถวที่มีค่าซ้ำในคอลัมน์ที่ถูกจำกัด (หรือการรวมค่าหลายคอลัมน์) นี่กำจัดทั้งคลาสของบั๊กที่เกิดเมื่อเส้นทาง "สร้าง" ทำงานสองครั้ง เกิด retry หรือสองผู้ใช้ส่งข้อมูลพร้อมกัน\n\nUNIQUE ยืนยันว่าไม่มีค่าซ้ำสำหรับค่าที่คุณกำหนด มันไม่ได้รับประกันว่าค่าจะมีอยู่ (`NOT NULL`) ว่ามีรูปแบบที่ถูกต้อง (`CHECK`) หรือว่ามันตรงกับนิยามความเท่าเทียมของคุณ (ตัวพิมพ์ ช่องว่าง เครื่องหมายวรรคตอน) เว้นแต่คุณจะกำหนดไว้\n\nที่ที่มักต้องการ uniqueness ได้แก่ อีเมลในตารางผู้ใช้ `external_id` จากระบบอื่น หรือชื่อที่ต้องไม่ซ้ำภายในบัญชีเช่น `(account_id, name)`\n\nข้อควรระวังหนึ่งข้อ: NULL กับ UNIQUE ใน PostgreSQL NULL ถูกมองว่าเป็น "ไม่ทราบ" ดังนั้นค่า NULL หลายค่าอนุญาตภายใต้ UNIQUE constraint หากคุณหมายความว่า "ค่าต้องมีอยู่และต้องไม่ซ้ำ" ให้รวม UNIQUE กับ `NOT NULL`\n\nรูปแบบที่ใช้ได้จริงสำหรับตัวระบุที่เห็นโดยผู้ใช้คือความไม่ซ้ำแบบไม่สนใจตัวพิมพ์ ผู้คนจะพิมพ์ "[email protected]" แล้วต่อมาพิมพ์ "[email protected]" และคาดหวังว่ามันคือคนเดียวกัน\n\n```sql
-- Case-insensitive unique email
CREATE UNIQUE INDEX users_email_unique_ci
ON users (lower(email));

-- Unique contact name per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_name_unique UNIQUE (account_id, name);

\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;

\nนี่ไม่ใช่การเข้มงวดโดยไม่มีเหตุผล คุณกำลังเปลี่ยนความคาดหวังที่คลุมเครือให้เป็นกฎที่ฐานข้อมูลสามารถบังคับได้ทุกครั้ง ไม่ว่าโค้ดส่วนใดของแอปจะเขียนข้อมูล\n\n### อะไรเปลี่ยนในแอปหลังจากนั้น\n\nเมื่อข้อจำกัดเหล่านี้อยู่ แอปจะเรียบง่ายขึ้น คุณสามารถเอาการตรวจสอบป้องกันหลายอย่างที่พยายามตรวจจับซ้ำทีหลังออกได้ ความล้มเหลวจะชัดเจนและจัดการได้ (เช่น "email มีอยู่แล้วในบัญชีนี้" แทนพฤติกรรมแปลก ๆ ในภายหลัง) และเมื่อ route ที่สร้างโดยเครื่องมือหลงลืมฟิลด์หรือจัดการค่าผิด การเขียนจะล้มเหลวทันทีแทนที่จะทำให้ฐานข้อมูลเสียแบบเงียบ ๆ\n\n## ข้อผิดพลาดทั่วไปที่ทำให้ข้อจำกัดเจ็บปวด\n\nข้อจำกัดทำงานได้ดีที่สุดเมื่อมันสอดคล้องกับการทำงานจริงของธุรกิจ ความเจ็บปวดส่วนใหญ่เกิดจากการเพิ่มกฎที่ดู "ปลอดภัย" ตอนนั้น แต่กลายเป็นความประหลาดใจในภายหลัง\n\nกับดักทั่วไปคือการใช้ ON DELETE CASCADE ทุกที่ มันดูเรียบร้อยจนกว่าจะมีคนลบแถวพ่อแม่และฐานข้อมูลลบครึ่งระบบไปด้วย Cascades อาจถูกต้องสำหรับข้อมูลที่เป็นของจริง ๆ (เช่น รายการบรรทัดร่างที่ไม่ควรมีอยู่คนเดียว) แต่เสี่ยงกับบันทึกสำคัญ (ลูกค้า ใบแจ้งหนี้ ตั๋ว) หากไม่แน่ใจ ให้เลือก RESTRICT แล้วจัดการการลบอย่างตั้งใจ\n\nปัญหาอีกอย่างคือเขียน CHECK ที่แคบเกินไป "สถานะต้องเป็น 'new', 'won', หรือ 'lost'" ฟังดูโอเคจนกว่าคุณต้องการ "paused" หรือ "archived" ข้อจำกัด CHECK ที่ดีอธิบายความจริงที่คงที่ ไม่ใช่ตัวเลือก UI ชั่วคราว "amount >= 0" อยู่ได้ดี ส่วน "country in (...)" มักจะไม่อยู่ยาว\n\nปัญหาบ่อยเมื่อทีมเพิ่มข้อจำกัดหลังจากโค้ดที่สร้างแล้วเริ่มใช้งานคือ:\n\n- ใช้ CASCADE เป็นเครื่องมือทำความสะอาดแล้วลบข้อมูลมากกว่าที่ตั้งใจ\n- เขียน CHECK ที่แคบจนบล็อกกรณีอนาคตที่ถูกต้อง\n- คิดว่า UNIQUE หยุดการซ้ำเมื่อเกี่ยวกับ NULL\n- เปลี่ยนกฎโดยไม่มีแผนแก้แถวที่ผิดอยู่แล้ว\n\nเรื่องประสิทธิภาพ: PostgreSQL จะสร้าง index ให้กับ UNIQUE โดยอัตโนมัติ แต่ foreign keys จะไม่สร้างดัชนีให้คอลัมน์ที่อ้างอิงโดยอัตโนมัติ หากไม่มีดัชนีนั้น การอัปเดตและการลบบนพ่อแม่อาจช้าเพราะ Postgres ต้องสแกนตารางลูกเพื่อตรวจสอบการอ้างอิง\n\nก่อนจะเข้มงวดกับกฎ ใส่ใจหาแถวที่มีอยู่แล้วที่จะล้มเหลวกับกฎนั้น ตัดสินใจแก้หรือกักไว้ และปล่อยการเปลี่ยนแปลงเป็นขั้นตอน\n\n## เช็คลิสต์ด่วนและขั้นตอนถัดไปสำหรับงานถัดไปของคุณ\n\nก่อนปล่อย ให้ใช้เวลาห้านาทีต่อตารางแล้วเขียนลงว่าต้องเป็นจริงอย่างไร หากคุณพูดเป็นภาษาอังกฤษชัดเจน มันมักบังคับได้ด้วยข้อจำกัด\n\nถามคำถามเหล่านี้สำหรับแต่ละตาราง:\n\n- อะไรที่ไม่ควรเป็น NULL?\n- อะไรที่ไม่ควรถูกทำซ้ำ?\n- อะไรที่ไม่ควรเป็นค่าลบหรืออยู่นอกช่วง?\n- อะไรที่ไม่ควรมีอยู่หากไม่มีพ่อแม่ (ไม่มีแถวกำพร้า)?\n- อะไรที่ควรปฏิบัติตามกฎง่าย ๆ เสมอ (สถานะที่อนุญาต เริ่มก่อนจบ)?\n\nหากคุณใช้เครื่องมือสร้างด้วยแชท ให้ถือ invariant เหล่านั้นเป็นเกณฑ์การรับงานสำหรับข้อมูล ไม่ใช่โน้ตทางเลือก ตัวอย่าง: "จำนวนดีลต้องไม่เป็นลบ" "อีเมลคอนแทคไม่ซ้ำต่อ workspace" "งานต้องอ้างอิง contact จริง" ยิ่งกฎชัดเจนเท่าไร โอกาสเกิดมุมขอบโดยไม่ได้ตั้งใจก็น้อยลงเท่านั้น\n\nKoder.ai (koder.ai) รวมฟีเจอร์เช่น โหมดวางแผน snapshots และการย้อนกลับ รวมถึงการส่งออกซอร์สโค้ด ซึ่งช่วยให้คุณวน iterate การเปลี่ยนสคีมาอย่างปลอดภัยเมื่อค่อย ๆ เข้มงวดกับข้อจำกัดตามกาลเวลา\n\nรูปแบบการเปิดตัวที่ง่ายและใช้ได้จริงกับทีมจริง: เลือกตารางที่ให้คุณค่ามากที่สุด (users, orders, invoices, contacts), เพิ่ม 1-2 ข้อจำกัดที่ป้องกันความล้มเหลวร้ายแรงที่สุด (มักเป็น NOT NULL และ UNIQUE), แก้การเขียนที่ล้มเหลว แล้วทำซ้ำ การค่อย ๆ เข้มงวดกว่ามิเกรชันใหญ่อันเสี่ยง\n

คำถามที่พบบ่อย

ทำไมควรกำหนดกฎข้อมูลไว้ใน PostgreSQL แทนที่จะไว้แค่ในแอป

กำหนดกฎที่ต้องเป็นจริงเสมอไว้ใน PostgreSQL แล้วให้ทุกเส้นทางของแอปปฏิบัติตาม การตรวจสอบใน UI และ API ช่วยแสดงข้อความที่เข้าใจง่ายได้ ส่วนข้อจำกัดจะป้องกันการเขียนข้อมูลที่ไม่ถูกต้องจากการนำเข้า งานเบื้องหลัง การลองซ้ำ และเครื่องมือผู้ดูแลระบบ

ควรใช้ NOT NULL เมื่อไร

ใช้ NOT NULL เมื่อแถวนั้นไม่มีความหมายหากไม่มีค่า เช่น ID ของเจ้าของ ชื่อ หรือเวลาที่สร้าง อย่าใช้สตริงว่างแทนข้อมูลที่ผู้ใช้ไม่ได้กรอก เพราะจะซ่อนข้อผิดพลาดไว้

ข้อจำกัด CHECK เหมาะกับอะไร

ใช้ CHECK กับกฎภายในแถวเดียว เช่น amount >= 0 สถานะที่อนุญาต หรือวันที่สิ้นสุดที่ต้องอยู่หลังวันที่เริ่มต้น กฎควรสั้นและคงที่ เพื่อไม่ให้การเปลี่ยนแปลงผลิตภัณฑ์ในอนาคตทำให้ข้อมูลที่ถูกต้องกลายเป็นข้อผิดพลาด

UNIQUE ป้องกันค่าที่หายไปด้วยหรือไม่

เพิ่ม UNIQUE เมื่อข้อมูลซ้ำจะสร้างปัญหาจริง เช่น ID ภายนอกที่ซ้ำกัน หรือผู้ติดต่อสองรายที่มีอีเมลเดียวกันในบัญชีเดียว จับคู่กับ NOT NULL หากทุกระเบียนต้องมีค่านั้น เพราะ PostgreSQL อนุญาตให้มี NULL ได้หลายค่าในข้อจำกัด unique มาตรฐาน

คีย์นอกป้องกันระเบียนกำพร้าได้อย่างไร

คีย์นอกทำให้ฐานข้อมูลปฏิเสธแถวลูกที่อ้างถึงแถวแม่ซึ่งไม่มีอยู่จริง จึงป้องกันระเบียนกำพร้า เช่น ดีลที่ไม่มีบริษัทที่ถูกต้อง หรือโน้ตที่เชื่อมกับลูกค้าที่ถูกลบไปแล้ว

ควรใช้ ON DELETE CASCADE ทุกที่หรือไม่

เริ่มด้วย RESTRICT หากการลบแถวแม่ควรต้องมีการตัดสินใจอย่างรอบคอบ เลือก CASCADE เฉพาะเมื่อระเบียนลูกไม่มีคุณค่าหากไม่มีแถวแม่นั้นจริง ๆ เช่น รายการร่างชั่วคราว

จะป้องกันอีเมลซ้ำที่ต่างกันแค่ตัวพิมพ์เล็กใหญ่ได้อย่างไร

ใช้ดัชนี unique กับค่าที่ทำให้เป็นรูปแบบมาตรฐานแล้ว เช่น lower(email) เมื่อผู้ใช้คาดหวังว่าอักษรพิมพ์เล็กและพิมพ์ใหญ่จะหมายถึงอีเมลเดียวกัน ตัดช่องว่างและทำให้ข้อมูลนำเข้าเป็นมาตรฐานในแอปด้วย เพื่อให้ผู้ใช้ได้รับข้อความข้อผิดพลาดที่ชัดเจนขึ้น

จะเพิ่มข้อจำกัดให้ฐานข้อมูล production อย่างปลอดภัยได้อย่างไร

เริ่มจากค้นหาแถวที่ละเมิดกฎใหม่ แก้ไข รวม หรือแยกกักข้อมูลเหล่านั้นเป็นชุด ๆ จากนั้นเพิ่มข้อจำกัดและอัปเดตแอปให้แสดงข้อผิดพลาดที่มีประโยชน์เมื่อการเขียนข้อมูลล้มเหลว

คีย์นอกจำเป็นต้องมีดัชนีหรือไม่

ข้อจำกัด unique สร้างดัชนีของตัวเอง แต่คีย์นอกจะไม่สร้างดัชนีให้คอลัมน์ลูกโดยอัตโนมัติ เพิ่มดัชนีในคอลัมน์ที่อ้างอิงเมื่อมีตารางขนาดใหญ่ หรือเมื่อมีการอัปเดตหรือลบแถวแม่บ่อยครั้ง

ควรเพิ่มข้อจำกัดใดก่อน

เริ่มจากตารางหนึ่งที่เกี่ยวข้องกับเงิน ลูกค้า หรืออัตลักษณ์ เพิ่มกฎหนึ่งหรือสองข้อที่มีประโยชน์ชัดเจน เช่น ต้องมีเจ้าของ อีเมลไม่ซ้ำกันภายในบัญชีเดียว หรือจำนวนเงินต้องไม่ติดลบ แล้วทดสอบการเขียนข้อมูลที่ควรล้มเหลว

Related posts