ทำความเข้าใจความสัมพันธ์ในฐานข้อมูลก่อนเริ่มต้น
เวลาเราออกแบบฐานข้อมูล (แหล่งเก็บข้อมูลที่จัดระเบียบไว้) เรามักจะมีตารางสองตารางที่เกี่ยวข้องกันเสมอ เช่น ตารางผู้ใช้งาน และตารางออเดอร์สินค้า ความสัมพันธ์นี้เรียกว่า Foreign Key (กุญแจเชื่อมโยงข้อมูลระหว่างสองตาราง) ซึ่งเป็นหัวใจสำคัญที่ทำให้ข้อมูลในระบบของเราไม่มั่วซั่วและมีความถูกต้องแม่นยำอยู่เสมอ
ปัญหาจะเกิดขึ้นเมื่อเราต้องการลบข้อมูลฝั่งหนึ่งออกไป เช่น ถ้าเราลบชื่อผู้ใช้งานทิ้ง แล้วออเดอร์ที่ผู้คนคนนั้นเคยสั่งไว้จะทำอย่างไรต่อ นี่คือจุดที่ ON DELETE (คำสั่งจัดการข้อมูลเมื่อข้อมูลต้นทางถูกลบ) เข้ามามีบทบาทสำคัญมากในการตัดสินใจว่าเราจะจัดการกับข้อมูลที่เหลืออยู่ (Child row) อย่างไรให้เหมาะสมกับธุรกิจ
ถ้าเปรียบเทียบให้เห็นภาพ ลองนึกถึงสมุดรายชื่อเพื่อน ถ้าเพื่อนคนนี้ลาออกไปจากบริษัท เราควรจะฉีกหน้ากระดาษที่จดเบอร์โทรเขาทิ้งไปเลย หรือจะแค่ลบชื่อออกแล้วเหลือเบอร์ทิ้งไว้ หรือจะเก็บชื่อไว้แต่เปลี่ยนสถานะว่าเขาไม่อยู่แล้ว การเลือกวิธีที่ถูกต้องจะช่วยป้องกันไม่ให้ข้อมูลในระบบของเรากลายเป็น ข้อมูลขยะ (ข้อมูลที่ไม่มีประโยชน์และหาความเชื่อมโยงไม่ได้) ในอนาคต
CASCADE: ลบให้เหี้ยนเมื่อต้นทางหายไป
CASCADE (การลบแบบลูกโซ่) เป็นวิธีที่ตรงไปตรงมาที่สุด หากคุณเลือกว่าเมื่อข้อมูลหลักถูกลบไปแล้ว ข้อมูลลูกที่เกี่ยวข้องก็ไม่มีความหมายอีกต่อไป วิธีนี้จะจัดการลบข้อมูลที่เหลือทิ้งให้โดยอัตโนมัติทันทีที่คำสั่งลบทำงาน ทำให้เราไม่ต้องมาคอยนั่งไล่ลบข้อมูลย่อยด้วยตัวเองให้เสียเวลา
วิธีนี้เหมาะมากกับสถานการณ์ที่ข้อมูลลูกไม่สามารถอยู่ได้หากไม่มีข้อมูลหลัก เช่น รายการสินค้าในออเดอร์หนึ่งๆ ถ้าเราลบออเดอร์นั้นทิ้งไป รายการสินค้าเหล่านั้นก็ไม่มีความจำเป็นต้องเก็บไว้ในฐานข้อมูลอีกต่อไป เพราะมันไม่ได้ระบุตัวตนได้ด้วยตัวเอง การใช้ CASCADE จึงช่วยให้ฐานข้อมูลสะอาดอยู่เสมอ
อย่างไรก็ตาม ต้องระวังให้ดีเพราะวิธีนี้เป็นการลบข้อมูลแบบถาวรและรวดเร็วมาก หากเผลอลบข้อมูลหลักผิดพลาด ข้อมูลลูกทั้งหมดจะหายวับไปพร้อมกันทันทีโดยไม่มีการแจ้งเตือน ดังนั้นควรใช้กับข้อมูลที่แน่ใจแล้วว่าไม่มีประโยชน์อีกต่อไปหากขาดตัวหลักไปแล้ว
-- สร้างตารางออเดอร์และรายการสินค้าแบบ CASCADE
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
product_name TEXT NOT NULL
);
ในตัวอย่างนี้ บรรทัด REFERENCES orders(id) ON DELETE CASCADE คือการบอก PostgreSQL (ระบบจัดการฐานข้อมูลยอดนิยม) ว่าถ้าลบแถวในตาราง orders ให้ลบข้อมูลใน order_items ที่มี order_id ตรงกันทิ้งทั้งหมดทันที
ผลลัพธ์ที่ควรเห็นคือ เมื่อคุณรันคำสั่ง DELETE FROM orders WHERE id = 1; ข้อมูลสินค้าทั้งหมดที่เคยผูกกับออเดอร์หมายเลข 1 จะถูกลบออกจากฐานข้อมูลไปโดยอัตโนมัติโดยที่คุณไม่ต้องเขียนคำสั่งลบแยกต่างหาก
SET NULL: เก็บข้อมูลลูกไว้แต่ตัดการเชื่อมต่อ
ในบางกรณี ข้อมูลลูกอาจจะยังมีความสำคัญแม้ว่าข้อมูลหลักจะถูกลบไปแล้ว SET NULL (การเปลี่ยนค่าเป็นความว่างเปล่า) จะเข้ามาช่วยในจังหวะนี้ โดยมันจะเข้าไปเปลี่ยนค่าในช่องที่เชื่อมโยงอยู่ให้กลายเป็นค่าว่าง (NULL) แทนที่จะลบแถวนั้นทิ้งไปทั้งแถว
ลองนึกถึงระบบจัดการตั๋วงานบริการ ถ้าพนักงานที่ดูแลตั๋วลาออกไป แต่เรายังต้องเก็บข้อมูลตั๋วใบนั้นไว้เพื่อตรวจสอบย้อนหลัง การใช้ SET NULL จะทำให้ช่อง "ผู้ดูแล" กลายเป็นค่าว่าง ซึ่งสื่อความหมายได้ว่า "ตั๋วใบนี้ยังไม่มีคนรับผิดชอบ" แทนที่จะทำลายข้อมูลตั๋วใบนั้นทิ้งไปทั้งหมด
การใช้คำสั่งนี้ต้องระวังเรื่องการตั้งค่าคอลัมน์ในฐานข้อมูลด้วย เพราะถ้าคุณตั้งค่าคอลัมน์นั้นให้เป็น NOT NULL (ห้ามเป็นค่าว่าง) ตั้งแต่แรก ระบบจะเกิดข้อผิดพลาดทันทีเมื่อมีการลบข้อมูลหลัก เพราะมันพยายามจะใส่ค่าว่างลงไปในช่องที่ห้ามว่างนั่นเอง
-- สร้างตารางตั๋วและพนักงานแบบ SET NULL
CREATE TABLE agents (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE tickets (
id SERIAL PRIMARY KEY,
assignee_id INTEGER REFERENCES agents(id) ON DELETE SET NULL,
title TEXT NOT NULL
);
โค้ดด้านบนกำหนดให้ assignee_id เป็น ON DELETE SET NULL ซึ่งหมายความว่าเมื่อ agents ถูกลบ ค่าใน assignee_id ของตาราง tickets จะเปลี่ยนเป็นค่าว่างแทนที่จะหายไป
ผลลัพธ์ที่ได้คือ ตั๋วที่พนักงานคนนั้นเคยดูแลจะยังคงอยู่ในระบบ แต่ในช่อง assignee_id จะกลายเป็นค่า NULL ทำให้แอดมินระบบสามารถมองเห็นได้ว่าตั๋วใบไหนบ้างที่ยังไม่มีพนักงานรับผิดชอบและต้องนำไปจัดสรรใหม่
RESTRICT และ NO ACTION: ป้องกันการลบเพื่อความปลอดภัย
ถ้าข้อมูลลูกของคุณมีความสำคัญระดับที่ว่า "ห้ามหายไปเด็ดขาด" หรือ "ต้องให้คนมาจัดการก่อนลบ" คุณควรใช้ RESTRICT (การจำกัดการกระทำ) หรือ NO ACTION (ไม่ทำอะไรเลย) เพื่อหยุดยั้งการลบข้อมูลหลักไว้ก่อนที่ความเสียหายจะเกิดขึ้น
สมมติว่าคุณมีข้อมูลใบเสร็จรับเงินที่ผูกกับชื่อลูกค้า หากมีใครพยายามลบชื่อลูกค้านั้นออก ระบบที่ใช้ RESTRICT จะปฏิเสธการลบนั้นทันทีและแจ้งเตือนว่า "คุณทำไม่ได้นะ เพราะยังมีใบเสร็จที่อ้างอิงถึงลูกค้าคนนี้อยู่" วิธีนี้ดีมากสำหรับการป้องกันความผิดพลาดจากฝั่งผู้ใช้งาน
ความแตกต่างเล็กน้อยระหว่างสองคำสั่งนี้คือ RESTRICT จะทำงานทันทีและไม่ยอมให้เลื่อนการตรวจสอบไปทีหลัง ส่วน NO ACTION ในบางกรณีอาจจะยอมให้มีการตรวจสอบล่าช้าได้ถ้ามีการตั้งค่าการทำธุรกรรมแบบพิเศษ แต่โดยทั่วไปสำหรับมือใหม่ถือว่าทั้งคู่ทำงานคล้ายกันคือ "หยุดการลบ" เหมือนกันครับ
-- ใช้ RESTRICT เพื่อป้องกันการลบข้อมูลหลัก
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id) ON DELETE RESTRICT,
amount DECIMAL NOT NULL
);
ในตัวอย่างนี้ การใส่ ON DELETE RESTRICT จะทำให้คำสั่ง DELETE FROM customers WHERE id = 1; ล้มเหลวทันทีหากยังมีข้อมูลในตาราง invoices ที่อ้างอิงถึงลูกค้าหมายเลข 1 อยู่
ผลลัพธ์ที่ควรเห็นคือระบบจะส่ง Error (ข้อความแจ้งเตือนเมื่อเกิดความผิดพลาด) ขึ้นมาว่าการลบไม่สามารถทำได้เนื่องจากติดข้อจำกัดของ Foreign Key ทำให้คุณต้องกลับไปลบหรือย้ายข้อมูลใบเสร็จก่อนถึงจะลบชื่อลูกค้าได้สำเร็จ
จุดที่มือใหม่มักพลาดและข้อควรระวัง
หนึ่งในจุดที่มือใหม่มักจะพลาดบ่อยที่สุดคือการลืมสร้าง Index (ดัชนีช่วยค้นหาข้อมูล) ให้กับคอลัมน์ที่เป็น Foreign Key หลายคนเข้าใจว่าฐานข้อมูลจะทำให้เองโดยอัตโนมัติ แต่จริงๆ แล้ว PostgreSQL ไม่ได้สร้างให้ ซึ่งจะส่งผลเสียอย่างมากต่อความเร็วในการลบข้อมูลเมื่อตารางมีขนาดใหญ่ขึ้น
เมื่อเราสั่งลบข้อมูลหลัก ระบบต้องไปไล่ค้นหาว่าข้อมูลลูกแถวไหนบ้างที่เกี่ยวข้อง ถ้าไม่มี Index ระบบจะต้องสแกนดูข้อมูลทั้งตาราง ทำให้การลบช้าลงอย่างมหาศาล สมมติว่ามีตารางเป็นล้านแถว การลบข้อมูลเพียงหนึ่งแถวอาจใช้เวลานานจนแอปพลิเคชันค้างไปเลยก็ได้
อีกเรื่องคือการเปลี่ยนค่า ON DELETE หลังจากสร้างตารางไปแล้ว คุณไม่สามารถแก้ค่านี้ได้ด้วยคำสั่ง ALTER TABLE แบบง่ายๆ คุณต้องลบเงื่อนไขเก่าออกแล้วสร้างใหม่ ซึ่งหากตารางมีข้อมูลเยอะ ขั้นตอนนี้อาจทำให้ฐานข้อมูลหยุดชะงักชั่วคราวได้ ดังนั้นวางแผนให้ดีตั้งแต่ตอนออกแบบจะดีที่สุดครับ
สรุป: เลือกใช้อะไรในงานจริง?
การเลือก ON DELETE ที่เหมาะสมไม่ใช่เรื่องของเทคนิคที่ซับซ้อน แต่เป็นเรื่องของความเข้าใจใน "ความหมาย" ของข้อมูล ให้ลองถามตัวเองว่า "ถ้าข้อมูลหลักหายไป ข้อมูลลูกยังมีความหมายอยู่ไหม" ถ้าคำตอบคือไม่ ก็ใช้ CASCADE เพื่อความสะอาดของระบบ แต่ถ้าคำตอบคือยังมีค่าอยู่ ให้พิจารณา SET NULL หรือ RESTRICT แทน
ตัวอย่างการนำไปใช้จริง: หากคุณกำลังสร้างระบบบล็อกส่วนตัว สำหรับตาราง posts (บทความ) และ comments (ความคิดเห็น) การลบบทความทิ้งควรใช้ CASCADE เพราะคอมเมนต์ใต้บทความไม่มีความหมายหากบทความต้นทางหายไป แต่ถ้าเป็นระบบจัดการสมาชิก users และ posts คุณอาจใช้ RESTRICT เพื่อป้องกันไม่ให้ลบสมาชิกที่ยังมีบทความเขียนทิ้งไว้อยู่ เพื่อป้องกันข้อมูลหายโดยไม่ตั้งใจ
สุดท้ายจำไว้ว่า SET NULL นั้นสะดวกแต่ต้องระวังเรื่องข้อจำกัดของคอลัมน์ และการทำ Index เป็นสิ่งจำเป็นที่ห้ามมองข้ามเด็ดขาด หากคุณเริ่มต้นฝึกฝนด้วยการออกแบบฐานข้อมูลให้รัดกุมตั้งแต่แรก คุณจะประหยัดเวลาในการแก้ปัญหาหน้างานไปได้มหาศาลเมื่อต้องทำโปรเจกต์ขนาดใหญ่ขึ้นครับ
ที่มา: ON DELETE SET NULL vs CASCADE vs RESTRICT in PostgreSQL: Which to Use — DEV Community