กลับไปหน้าบทความ
#PostgreSQL#Schema Design#Database#Node.js#Backend

ออกแบบ Schema PostgreSQL ให้เป็นสัญญาที่ชัดเจนระหว่างแอปกับฐานข้อมูล

การออกแบบ schema ใน PostgreSQL ไม่ได้มีไว้เพื่อความสวยงามของชื่อตารางเท่านั้น แต่เป็นการกำหนดกติกาว่าข้อมูลแบบใดควรถูกยอมรับและแบบใดต้องถูกปฏิเสธตั้งแต่ต้นทาง เพื่อให้ระบบ production ปลอดภัยและดูแลง่ายขึ้นในระยะยาว

26 กรกฎาคม 2569อ่านประมาณ 3 นาที

แชร์บทความ

ออกแบบ Schema PostgreSQL ให้เป็นสัญญาที่ชัดเจนระหว่างแอปกับฐานข้อมูล

ออกแบบ Schema PostgreSQL ให้เป็นสัญญาที่ชัดเจนระหว่างแอปกับฐานข้อมูล

Schema design ใน PostgreSQL ไม่ใช่เพียงเรื่องของการตั้งชื่อตารางให้ดูดี แต่คือการกำหนด “สัญญา” ระหว่างแอปพลิเคชันกับฐานข้อมูลอย่างชัดเจน ว่าข้อมูลประเภทใดสามารถเข้ามาได้ และข้อมูลแบบใดต้องถูกปฏิเสธตั้งแต่วันแรก

สำหรับระบบ backend โดยเฉพาะฝั่ง Node.js ที่เริ่มใช้งานจริงใน production ปัญหาที่ทำให้ระบบพังบ่อยครั้งไม่ใช่ query ที่ซับซ้อนที่สุด แต่เป็น schema ที่เปิดช่องให้ข้อมูลผิดรูปแบบไหลเข้ามาแบบเงียบ ๆ เช่น user ไม่มี email, order ไม่มี owner, status สะกดไม่ตรงกัน หรือคอลัมน์เดียวเก็บข้อมูลหลายรูปแบบปะปนกันโดยไม่มีขอบเขตที่ชัดเจน

บทความนี้จะสรุปแนวคิดสำคัญในการออกแบบ schema PostgreSQL ให้แข็งแรง อ่านง่าย และพร้อมเติบโตไปกับระบบ

ตั้งชื่อตารางและคอลัมน์ให้สื่อความหมาย

จุดเริ่มต้นของ schema ที่ดีคือการตั้งชื่อให้สม่ำเสมอและเข้าใจได้ทันที

สำหรับการตั้งชื่อตาราง ควรเลือก convention ให้ชัดเจนตั้งแต่แรกว่าจะใช้แบบพหูพจน์หรือเอกพจน์ เช่น

  • แบบพหูพจน์: users, orders, order_items
  • แบบเอกพจน์: user, order, order_item

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

นอกจากนี้ ชื่อตารางควรอ้างอิงถึง business entity มากกว่าชื่อหน้าจอหรือ API route ตัวอย่างเช่น ถ้าข้อมูลนั้นคือคำสั่งซื้อจริง ๆ ก็ควรใช้ชื่อ orders แทน checkout_logs เว้นแต่มันจะเป็น log โดยธรรมชาติจริง ๆ เพราะเมื่อระบบเติบโตขึ้น flow ของ checkout อาจเปลี่ยนได้หลายแบบ แต่แนวคิดเรื่อง order ยังเป็นแกนหลักของธุรกิจอยู่เสมอ

ส่วนการตั้งชื่อคอลัมน์ ควรใช้ snake_case ให้สอดคล้องกับธรรมชาติของ PostgreSQL เช่น

  • created_at
  • updated_at
  • user_id
  • billing_address_id

ควรหลีกเลี่ยงชื่อกว้างหรือกำกวม เช่น data, info, value เพราะชื่อเหล่านี้ไม่ช่วยให้เข้าใจเจตนาของข้อมูล หากเปิด schema แล้วต้องหยุดคิดว่า “คอลัมน์นี้ไว้เก็บอะไร” แปลว่าชื่อนั้นยังไม่ดีพอ

ออกแบบ Primary Key ให้มั่นคง

ทุกตารางควรมี primary key อย่างชัดเจน สำหรับระบบเว็บทั่วไป ค่าที่นิยมใช้คือ id ซึ่งอาจเป็น bigint generated identity หรือ uuid

การเลือกใช้ขึ้นอยู่กับบริบทของระบบ

  • ถ้าระบบมีหลาย service ต้องสร้าง id จากหลายจุด หรือมีการเปิดเผย id ผ่าน public URL จำนวนมาก การใช้ uuid จะช่วยลดโอกาสการเดาค่า id และลดการชนกันข้ามระบบ
  • ถ้าระบบยังเป็น service เดียว ใช้ database เดียว bigint มักเรียบง่ายกว่า ทำงานเร็วกว่า และ debug ได้สะดวกกว่า

สิ่งที่ไม่ควรทำคือเอาค่าทางธุรกิจ เช่น email หรือ username มาเป็น primary key เพราะข้อมูลประเภทนี้เปลี่ยนแปลงได้เสมอ ผู้ใช้เปลี่ยน email ได้ บริษัทเปลี่ยนรหัสลูกค้าได้ แต่ primary key ควรเป็นค่าที่นิ่งที่สุดในระบบ

ทางเลือกที่ปลอดภัยกว่าคือใช้ id เป็น primary key แล้วกำหนด email หรือ username เป็น UNIQUE แทน

ใช้ Foreign Key เป็นกฎกลางของระบบ

Foreign key คือ guardrail สำคัญที่ไม่ควรถูกละเลย หาก orders.user_id อ้างถึง users.id ก็ควรประกาศ FOREIGN KEY ไว้ใน database ให้ชัดเจน

หลายทีมมักปล่อยให้แอปฝั่ง Node.js ตรวจสอบความสัมพันธ์เองทุกครั้ง แต่ในความเป็นจริง ข้อมูลไม่ได้เข้าฐานข้อมูลผ่าน API เพียงทางเดียวเสมอไป ยังมีทั้ง

  • migration
  • script
  • admin tool
  • import job
  • background worker

เมื่อมีหลายช่องทางเข้าระบบ กฎเรื่องความสัมพันธ์ของข้อมูลควรถูกบังคับใช้ที่ database เพื่อให้ทุกทางอยู่ภายใต้กติกาเดียวกัน ลดปัญหาข้อมูลกำพร้าและข้อมูลอ้างอิงผิดตาราง

ใช้ NOT NULL, UNIQUE และ CHECK ให้สะท้อนความจริงของธุรกิจ

Constraint คือหัวใจของ schema ที่ดี เพราะมันทำหน้าที่บอกฐานข้อมูลว่า “ความจริงของระบบ” คืออะไร

NOT NULL

ใช้กับ field ที่ธุรกิจยืนยันว่าต้องมีแน่นอน เช่น

  • users.email
  • orders.user_id
  • order_items.quantity

หากปล่อยให้ nullable ไปก่อนเพราะคิดว่า “เดี๋ยวค่อยจัดการ” สุดท้ายโค้ดทุกจุดจะต้องตามล้างหนี้ด้วย COALESCE หรือเงื่อนไข if else จำนวนมาก

อย่างไรก็ตาม ไม่ควรใส่ NOT NULL ให้ทุกคอลัมน์แบบไม่คิด เพราะบางค่ามีเหตุผลชัดเจนที่ควรเป็น NULL เช่น

  • delivered_at อาจยังไม่มีค่าจนกว่าจะส่งสินค้าสำเร็จ
  • deleted_at อาจเป็น NULL หากยังไม่ถูกลบในระบบที่ใช้ soft delete

ดังนั้น nullable ไม่ใช่สิ่งผิด ถ้ามันสื่อความหมายที่ชัดเจน

UNIQUE

ใช้ล็อกข้อมูลที่ห้ามซ้ำ เช่น

  • email
  • slug
  • payment transaction id

บางกรณีอาจต้องใช้ composite unique เช่น

  • user_id + provider สำหรับการล็อกอินผ่านผู้ให้บริการภายนอก
  • order_id + product_id หากใน order เดียวกันไม่ควรมีสินค้าเดิมซ้ำ

CHECK

CHECK constraint เป็นเครื่องมือที่มีประโยชน์มาก แต่หลายทีมมองข้าม ทั้งที่ช่วยกัน bug ได้ตั้งแต่ระดับฐานข้อมูล เช่น

  • quantity > 0
  • price_cents >= 0
  • status IN ('draft', 'paid', 'cancelled')

ข้อดีของ CHECK คือช่วยป้องกันสถานะหรือค่าที่หลุดจาก logic ฝั่ง backend ได้ก่อนที่ข้อมูลผิดจะถูกบันทึกลงจริง

เลือก Data Type ให้ตรงกับลักษณะข้อมูล

การเลือกชนิดข้อมูลไม่ควรใช้ text ครอบจักรวาลเพียงเพราะสะดวก เพราะจะทำให้ schema ขาดความชัดเจนและเปิดทางให้ข้อมูลผิดรูปแบบเข้ามาง่ายขึ้น

ตัวอย่างแนวคิดที่ควรพิจารณา ได้แก่

  • ข้อมูลเกี่ยวกับเงิน ควรตัดสินใจให้ชัดว่าจะเก็บเป็นจำนวนเต็มหน่วยย่อย เช่น cents หรือใช้ numeric
  • เวลาที่เกี่ยวกับเหตุการณ์จริงควรใช้ timestamptz เพื่อจัดการ timezone อย่างเหมาะสม
  • boolean ควรใช้เมื่อมีเพียงสองสถานะจริง ๆ เท่านั้น

ถ้าธุรกิจมีสถานะมากกว่าสองแบบ เช่น pending, approved, rejected ก็ไม่ควรฝืนยุบเหลือแค่ boolean เพราะจะทำให้ความหมายของข้อมูลพร่าเลือน

Normalize เท่าที่จำเป็น อย่าซับซ้อนเกินเหตุ

Normalization คือการแยกข้อมูลออกเป็นโครงสร้างที่ลดความซ้ำซ้อน เช่น

  • แยก users ออกจาก orders
  • แยก products ออกจาก order_items

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

แต่ในอีกด้านหนึ่ง การออกแบบเกินความจำเป็นก็สร้างภาระได้เช่นกัน ระบบขนาดเล็กไม่จำเป็นต้องแยกทุก enum เป็นตาราง หรือสร้างโครงสร้างยิบย่อยตั้งแต่วันแรกโดยยังไม่มี use case ชัดเจน

คำถามง่าย ๆ ที่ควรถามก่อนแยกข้อมูลคือ

  • ข้อมูลนี้เปลี่ยนบ่อยหรือไม่
  • ข้อมูลนี้ถูกใช้ร่วมกันหลายจุดหรือไม่
  • ข้อมูลนี้จำเป็นต้อง query แยกต่างหากหรือไม่

หากคำตอบยังไม่ชัด การเริ่มจากโครงสร้างที่เรียบง่ายมักเป็นทางเลือกที่ดีกว่า

ออกแบบ Schema Change ให้ปลอดภัยใน Production

Schema ที่ดีไม่ใช่ schema ที่เดาอนาคตได้ถูกทั้งหมด แต่คือ schema ที่สามารถเปลี่ยนแปลงได้โดยไม่ทำให้ production เจ็บหนัก

แนวทางที่ปลอดภัยมีดังนี้

  • เพิ่มคอลัมน์ใหม่แบบ nullable ก่อน
  • deploy โค้ดที่เริ่มเขียนค่าให้คอลัมน์นั้น
  • backfill ข้อมูลเดิมเป็น batch
  • ตรวจสอบว่าข้อมูลครบถ้วน
  • ค่อยเพิ่ม NOT NULL หรือ constraint อื่นในภายหลัง

วิธีนี้ปลอดภัยกว่าการเพิ่ม required column แล้วบังคับ NOT NULL ทันที โดยเฉพาะในตารางขนาดใหญ่ที่มีข้อมูลจำนวนมากอยู่แล้ว

อีกจุดที่ควรระวังคือการ rename column แบบใจร้อน หากแอปหลาย instance ยังรันโค้ดเวอร์ชันเก่าอยู่ การเปลี่ยนชื่อคอลัมน์ทันทีอาจทำให้ production พังได้

วิธีที่ปลอดภัยกว่าคือ

  1. เพิ่มคอลัมน์ใหม่
  2. เขียนข้อมูลลงทั้งคอลัมน์เก่าและใหม่ชั่วคราว
  3. ย้ายฝั่งอ่านข้อมูลไปใช้คอลัมน์ใหม่
  4. backfill ข้อมูลให้ครบ
  5. ลบคอลัมน์เก่าทีหลัง

แนวคิดนี้ช่วยให้การเปลี่ยน schema เกิดขึ้นแบบค่อยเป็นค่อยไป และลดความเสี่ยงจาก deployment ที่ไม่พร้อมกันทุก instance

มุมมองที่ควรมีต่อ PostgreSQL ในงานจริง

PostgreSQL ที่ดีไม่ใช่แค่ฐานข้อมูลที่ query ได้เร็ว แต่คือฐานข้อมูลที่ช่วยทีมป้องกันข้อมูลผิดตั้งแต่ต้นทาง

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

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

สรุป

การออกแบบ schema ใน PostgreSQL ที่ดีควรเริ่มจากชื่อ table และ column ที่อ่านแล้วเข้าใจทันที เลือก primary key ที่นิ่ง ใช้ foreign key เพื่อป้องกันข้อมูลกำพร้า และใช้ NOT NULL, UNIQUE, CHECK เพื่อสะท้อนความจริงของธุรกิจให้ชัดเจน

ขณะเดียวกันก็ควร normalize เท่าที่ช่วยลดปัญหา ไม่ใช่เพื่อเพิ่มความซับซ้อนโดยไม่จำเป็น และทุก schema change ควรถูกมองเหมือนการเปลี่ยนล้อรถขณะรถยังวิ่งอยู่ นั่นคือทำอย่างระมัดระวัง ค่อยเป็นค่อยไป และรองรับช่วง transition เสมอ

เมื่อออกแบบ schema ด้วยแนวคิดนี้ ฐานข้อมูลจะไม่ใช่แค่ที่เก็บข้อมูล แต่จะกลายเป็นแนวป้องกันสำคัญที่ช่วยให้ทีมไม่ใส่งานผิดเข้า production ได้ง่ายเกินไป