กลับไปหน้าบทความ
#PostgreSQL#Database Migration#Expand/Contract#Zero Downtime#Production

เปลี่ยน PostgreSQL Schema บน Production อย่างปลอดภัยด้วย Expand/Contract

แนวทางเปลี่ยน PostgreSQL schema บนระบบ production โดยลดผลกระทบต่อผู้ใช้ด้วย Expand/Contract, batch backfill และ concurrent index พร้อมวางแผน rollback อย่างปลอดภัย

4 สิงหาคม 2569อ่านประมาณ 3 นาที

แชร์บทความ

เปลี่ยน PostgreSQL Schema บน Production อย่างปลอดภัยด้วย Expand/Contract

การเปลี่ยน database schema บน production ไม่ควรเป็นการกดคำสั่ง migrate แล้วรอดูว่าระบบจะรอดหรือไม่ แม้ PostgreSQL จะจัดการงานหนักได้ดี แต่บางคำสั่งอาจล็อกตาราง สแกนข้อมูลจำนวนมาก หรือเพิ่มภาระให้ replication จน request ของผู้ใช้ค้างได้

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

เหตุใด Migration เล็กจึงสร้างผลกระทบใหญ่ได้

คำสั่ง SQL ที่ดูสั้นอาจมีต้นทุนเบื้องหลังมากกว่าที่เห็น เช่น การรอ lock การตรวจข้อมูลทั้งตาราง หรือการสร้าง index จากข้อมูลหลายล้านแถว หาก migration ถือ lock นานเกินไป request อื่นจะเริ่มต่อคิวและทำให้ปัญหาลุกลามไปยัง connection pool, API และบริการที่พึ่งพาฐานข้อมูลเดียวกัน

ความเสี่ยงไม่ได้ขึ้นอยู่กับคำสั่งเพียงอย่างเดียว แต่ยังขึ้นอยู่กับขนาดข้อมูล ปริมาณ read/write เวอร์ชันของ PostgreSQL และพฤติกรรมของ migration tool ด้วย ดังนั้นจึงควรตรวจ SQL ที่เครื่องมือจะรันจริงและทดสอบกับฐานข้อมูลที่มีขนาดใกล้เคียง production เสมอ

หลัก Expand/Contract

Expand/Contract คือการแยกการเปลี่ยน schema ออกเป็นหลายช่วง แทนที่จะเพิ่มของใหม่ ย้ายระบบ และลบของเก่าภายใน deployment เดียว แนวทางนี้ช่วยให้แอปพลิเคชันเวอร์ชันเก่าและใหม่ทำงานร่วมกับ schema เดียวกันได้ในช่วงเปลี่ยนผ่าน

กระบวนการแบ่งเป็นสามช่วงหลัก:

  1. Expand: เพิ่ม column, index หรือโครงสร้างใหม่โดยยังไม่ทำลายของเดิม
  2. Transition: deploy แอปให้รองรับ schema ทั้งเก่าและใหม่ จากนั้นย้ายหรือเติมข้อมูลให้ครบ
  3. Contract: ลบ column, constraint หรือโค้ดเก่าเมื่อยืนยันแล้วว่าไม่มีส่วนใดใช้งานอยู่

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

ตัวอย่างการเพิ่ม Column full_name

สมมติว่าตาราง users มีข้อมูลอยู่แล้วและต้องการเพิ่ม column ชื่อ full_name การเพิ่มเป็น NOT NULL ทันทีอาจล้มเหลวเพราะข้อมูลเดิมยังไม่มีค่า และบางวิธีอาจสร้างภาระจากการตรวจหรือเขียนข้อมูลจำนวนมากในครั้งเดียว

ขั้นแรกควรเพิ่ม column แบบ nullable ก่อน:

ALTER TABLE users ADD COLUMN full_name text;

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

ระหว่าง deploy อาจให้แอปเขียนทั้ง field เดิมและ full_name ชั่วคราว หรือใช้วิธีอ่านค่าใหม่ก่อนแล้ว fallback ไปยังข้อมูลเดิม สิ่งสำคัญคือทุก instance ต้องรองรับช่วงเปลี่ยนผ่าน ไม่ว่าจะเป็น instance ที่ยังใช้โค้ดเก่าหรือ instance ที่เพิ่งได้รับ deployment ใหม่

Backfill ข้อมูลเป็น Batch

หลังจากแอปเริ่มเขียนค่าใหม่แล้ว ขั้นต่อไปคือ backfill ข้อมูลเก่า แต่ไม่ควรใช้ UPDATE ครั้งเดียวกับทั้งตารางขนาดใหญ่ เพราะอาจสร้าง transaction ขนาดใหญ่ ถือ row lock จำนวนมาก เพิ่ม WAL และทำให้ replication lag สูงขึ้น

ควรแบ่งงานเป็น batch ตาม primary key หรือช่วงข้อมูล เช่นครั้งละ 1,000 หรือ 10,000 rows แล้ว commit แยกแต่ละรอบ ขนาด batch ที่เหมาะสมไม่มีค่าตายตัว ต้องปรับตามความเร็วของฐานข้อมูล ปริมาณ traffic และความสามารถของ replica

ระหว่าง backfill ควรติดตามตัวชี้วัดต่อไปนี้:

  • Latency ของ query และ API ที่สำคัญ
  • จำนวน lock และระยะเวลาที่ transaction รอ lock
  • CPU, disk I/O และจำนวน active connections
  • ปริมาณ WAL และ replication lag
  • อัตราความสำเร็จและเวลาที่ใช้ต่อ batch

หากระบบเริ่มรับภาระสูง สามารถลดขนาด batch หรือเว้นช่วงสั้น ๆ ระหว่างรอบได้ งาน backfill ควรถูกออกแบบให้หยุดและทำต่อได้โดยไม่สร้างข้อมูลซ้ำหรือทำให้สถานะเสียหาย

เพิ่ม Constraint หลังข้อมูลพร้อม

เมื่อ backfill ครบแล้วจึงค่อยบังคับคุณภาพข้อมูล เช่น NOT NULL หรือ constraint อื่น อย่างไรก็ตาม คำสั่งตรวจสอบข้อมูลอาจต้องสแกนตารางและสร้างภาระสูง จึงไม่ควรรันโดยประเมินจากชื่อ migration เพียงอย่างเดียว

ในหลายกรณีสามารถเพิ่ม CHECK constraint แบบ NOT VALID ก่อน แล้วจึง validate ข้อมูลเดิมภายหลัง:

ALTER TABLE users
  ADD CONSTRAINT users_full_name_not_null
  CHECK (full_name IS NOT NULL) NOT VALID;

ALTER TABLE users
  VALIDATE CONSTRAINT users_full_name_not_null;

แนวทางนี้แยกช่วงเพิ่มกฎออกจากช่วงตรวจข้อมูลเดิม ทำให้ควบคุมเวลาสแกนและติดตามผลกระทบได้ง่ายขึ้น แม้การ validate จะลดการปิดกั้น write ในหลายกรณี แต่ยังคงใช้ทรัพยากร จึงควรรันในช่วงที่เหมาะสมและเฝ้าดูระบบตลอดกระบวนการ

สร้าง Index โดยไม่หยุดการเขียนข้อมูล

การใช้ CREATE INDEX แบบปกติบนตาราง production อาจ block การเขียนข้อมูลระหว่างสร้าง index หากตารางใหญ่ ผลกระทบอาจกินเวลานานกว่าที่คาด แม้ตัว migration จะมี SQL เพียงบรรทัดเดียว

เมื่อเหมาะสมควรใช้ CREATE INDEX CONCURRENTLY เพื่อให้ read และ write ส่วนใหญ่ดำเนินต่อได้ระหว่างสร้าง index:

CREATE INDEX CONCURRENTLY idx_users_full_name
  ON users (full_name);

วิธีนี้อาจใช้เวลานานและใช้ทรัพยากรมากกว่าการสร้าง index ปกติ แต่ช่วยลดช่วงที่ write traffic ถูกปิดกั้น ข้อควรระวังคือคำสั่ง CONCURRENTLY ห้ามทำงานภายใน transaction block ดังนั้น migration tool ที่ครอบทุกคำสั่งด้วย transaction อัตโนมัติต้องถูกตั้งค่าให้รันขั้นตอนนี้แยกต่างหาก

หากคำสั่งล้มเหลวควรตรวจว่ามี index สถานะไม่สมบูรณ์หลงเหลืออยู่หรือไม่ ก่อนแก้ไขและเริ่มใหม่ การมีคำว่า concurrently ไม่ได้ทำให้คำสั่งไร้ผลกระทบ จึงยังต้องติดตาม I/O, latency และ replication lag เช่นเดิม

หลีกเลี่ยงการ Rename Column ใน Deployment เดียว

การ rename column ดูเหมือนเป็นการเปลี่ยนแปลงเล็ก แต่ทำให้ชื่อเดิมหายไปทันที หาก deployment ไม่ได้เปลี่ยนทุก instance พร้อมกัน แอปเวอร์ชันเก่าอาจยังอ่านชื่อเดิม ขณะที่เวอร์ชันใหม่เริ่มอ่านชื่อใหม่ ส่งผลให้บาง request สำเร็จและบาง request ล้มเหลวอย่างคาดเดายาก

แนวทางที่นิ่งกว่าคือเพิ่ม column ใหม่ ให้แอปอ่านหรือเขียนได้ทั้งสองชื่อชั่วคราว แล้ว backfill ข้อมูลเดิม หลังจาก deploy จนครบทุก instance และยืนยันว่าไม่มีโค้ดเก่าใช้งานแล้ว จึงค่อยลบ column เดิมใน migration รอบถัดไป

การลบของเดิมคือขั้น Contract และควรเกิดขึ้นเมื่อผ่านช่วงสังเกตการณ์เรียบร้อยแล้ว ไม่ควรเพิ่มของใหม่และลบของเก่าในรอบเดียว เพราะจะทำให้เสียความสามารถในการ rollback แอปอย่างปลอดภัย

วางแผน Rollback ก่อนเริ่ม Migration

ก่อนรัน migration ต้องตอบให้ได้ว่าหากแอปเวอร์ชันใหม่มีปัญหา จะย้อนกลับไปใช้เวอร์ชันเก่าได้หรือไม่ รวมถึง schema ปัจจุบันยังรองรับโค้ดเก่าอยู่หรือเปล่า และข้อมูลที่เขียนด้วยรูปแบบใหม่จะถูกอ่านอย่างไรหลัง rollback

Rollback ที่ปลอดภัยไม่จำเป็นต้องย้อน SQL ทุกคำสั่งทันที โดยเฉพาะการ DROP COLUMN ซึ่งอาจทำให้ข้อมูลสูญหายถาวร หลายเหตุการณ์ควรปิด feature flag, deploy แอปเวอร์ชันเดิม และปล่อย schema ใหม่ที่ยังไม่สร้างความเสียหายค้างไว้ก่อน จากนั้นจึง cleanup เมื่อระบบกลับมานิ่งแล้ว

สิ่งที่ควรตรวจสอบก่อนรันจริง

ควรซ้อม migration กับ database copy หรือ staging ที่มีปริมาณข้อมูลใกล้เคียง production เพราะฐานข้อมูลขนาดเล็กไม่สามารถสะท้อนระยะเวลารอ lock การสร้าง index และต้นทุนของ backfill ได้อย่างสมจริง

รายการตรวจสอบก่อนเริ่มงานประกอบด้วย:

  • อ่าน SQL ที่ migration tool สร้างขึ้นจริง
  • ตรวจ query plan และ index ที่เกี่ยวข้อง
  • วัดเวลาสร้าง index กับข้อมูลขนาดใกล้เคียง production
  • ทดลองขนาด batch และระยะพักของงาน backfill
  • ตรวจว่า migration tool ครอบคำสั่งใดไว้ใน transaction
  • กำหนดตัวชี้วัดและเงื่อนไขสำหรับหยุด migration
  • เตรียมแผน rollback ของทั้ง application และ schema

ลำดับการทำงานที่นำไปใช้ได้จริง

ลำดับที่ปลอดภัยสำหรับการเปลี่ยน schema ส่วนใหญ่มีดังนี้:

  1. เพิ่มโครงสร้างใหม่แบบ backward compatible
  2. Deploy แอปให้รองรับทั้ง schema เก่าและใหม่
  3. เริ่มเขียนข้อมูลรูปแบบใหม่และ backfill ข้อมูลเดิมเป็น batch
  4. สร้าง index แบบ concurrently เมื่อเหมาะสม
  5. ตรวจข้อมูลและค่อยเพิ่ม NOT NULL หรือ constraint หลังข้อมูลพร้อม
  6. เฝ้าดูระบบจนแน่ใจว่าไม่มี instance หรือโค้ดเก่าใช้งานอยู่
  7. ลบโครงสร้างเดิมใน migration รอบถัดไป

สรุป

Production migration ที่ดีไม่ใช่ migration ที่จบเร็วที่สุด แต่คือ migration ที่รักษาความเข้ากันได้ ควบคุม lock และเปิดทางให้ rollback ได้ตลอดช่วงเปลี่ยนผ่าน แนวคิด Expand/Contract ช่วยเปลี่ยนงานเสี่ยงครั้งใหญ่ให้เป็นขั้นตอนเล็ก ๆ ที่ตรวจสอบและหยุดได้

เมื่อเพิ่ม schema แบบ backward compatible, backfill เป็น batch, ใช้ concurrent index อย่างถูกต้อง และ cleanup ของเก่าในภายหลัง ผู้ใช้งานแทบไม่ควรรู้เลยว่าฐานข้อมูลเพิ่งผ่านการเปลี่ยนแปลงครั้งสำคัญ