เรียนรู้พรอมต์สำหรับ Claude Code ที่ช่วยให้มิเกรชัน PostgreSQL ปลอดภัยด้วยรูปแบบ expand-contract, การ backfill, และแผน rollback รวมถึงสิ่งที่ต้องตรวจบนสเตจก่อนปล่อย

\nYou are helping me plan a PostgreSQL expand-contract migration.\n\nContext\n- App: [what the feature does, who uses it]\n- Database: PostgreSQL [version if known]\n- Table sizes: [rough row counts], write rate: [low/medium/high]\n- Zero/near-zero downtime required: [yes/no]\n\nGoal\n- Change: [describe the schema change]\n- Current schema (relevant parts):\n [paste CREATE TABLE or \\d output]\n- How the app will change (expand phase and contract phase):\n - Expand: [new columns/indexes/triggers, dual-write, read preference]\n - Contract: [when/how we stop writing old fields and remove them]\n\nHard safety requirements\n- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.\n- If any step can block writes, call it out explicitly and suggest alternatives.\n- Use small, reversible steps. No “big bang” changes.\n\nDeliverables\n1) UP migration SQL (expand)\n - Use clear comments.\n - If you propose indexes, tell me if they should be created CONCURRENTLY.\n - If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.\n\n2) Verification queries\n - Queries to confirm the new schema exists.\n - Queries to confirm data is being written to both old and new structures (if dual-write).\n - Queries to estimate whether the change caused bloat/slow queries/locks.\n\n3) Rollback plan (realistic)\n - DOWN migration SQL (only if it is truly safe).\n - If down is not safe, write a rollback runbook:\n - how to stop the app change\n - how to switch reads back\n - what data might be lost or need re-backfill\n\n4) Runbook notes\n - Exact order of operations (including app deploy steps).\n - What to monitor during the run (errors, latency, deadlocks, lock waits).\n - “Stop/continue” checkpoints.\n\nOutput format\n- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md\n\n\nสองบรรทัดพิเศษที่ช่วยในทางปฏิบัติ:\n\n- ให้บอกว่าแต่ละขั้นตอนที่บล็อกการเขียนให้ติดป้าย RISK: blocks writes และแนะนำเวลาที่ควรรัน (off-peak vs anytime)\n- บังคับให้ซื่อสัตย์เรื่องล็อก: "If you're not sure whether a statement takes an ACCESS EXCLUSIVE lock, say so and offer a safer option."\n\n## การปฏิบัติต่อการดำเนินการสคีมาทั่วไปและวิธีขอ SQL ที่ปลอดภัยกว่า\n\nการเปลี่ยนสคีมาเล็กๆ ก็ยังทำให้เกิดปัญหาได้ถ้ามันล็อกเป็นเวลานาน rewrite ตารางใหญ่ หรือพังกลางทาง เมื่อใช้ Claude Code สำหรับมิเกรชัน ให้ขอ SQL ที่หลีกเลี่ยงการ rewrite และทำให้แอปทำงานได้ในขณะที่ฐานข้อมูลปรับตาม\n\n### เพิ่มคอลัมน์และค่าเริ่มต้น (โดยไม่ล็อกนาน)\n\nการเพิ่มคอลัมน์แบบ nullable มักปลอดภัย การเพิ่มคอลัมน์พร้อม default แบบ non-null อาจเสี่ยงบนเวอร์ชันเก่าของ Postgres เพราะอาจ rewrite ทั้งตาราง\n\nวิธีปลอดภัยคือทำเป็นสองขั้น: เพิ่มคอลัมน์เป็น NULL โดยไม่มี default, backfill ทีละน้อย, แล้วตั้ง default สำหรับแถวใหม่ และเพิ่ม NOT NULL เมื่อข้อมูลสะอาด\n\nถ้าต้องบังคับ default ทันที ให้ขอคำอธิบายพฤติกรรมล็อกสำหรับเวอร์ชัน Postgres ของคุณและแผนสำรองถ้ารันนานกว่าที่คาด\n\n### ดัชนี, FK, ข้อจำกัด, การลบ\n\nสำหรับดัชนีบนตารางใหญ่ ให้ขอ CREATE INDEX CONCURRENTLY เพื่อให้การอ่าน/เขียนยังไหลได้ และระบุว่าไม่สามารถรันใน transactional block ซึ่งหมายความว่าเครื่องมือมิเกรชันของคุณต้องรองรับขั้นตอนนอกทรานแซกชัน\n\nสำหรับ foreign key ทางที่ปลอดภัยคือเพิ่มเป็น NOT VALID ก่อน แล้วค่อย validate ทีหลัง วิธีนี้ทำให้ขั้นแรกเร็วขึ้น ในขณะที่ยังบังคับสำหรับการเขียนใหม่\n\nเมื่อทำให้ข้อจำกัดเข้มขึ้น (NOT NULL, UNIQUE, CHECK) ให้ขอว่า "clean first, enforce second." มิเกรชันควรตรวจหาแถวที่ไม่สะอาด แก้ไข จากนั้นค่อยเปิดกฎเข้มงวด\n\nถ้าต้องการเช็คลิสต์สั้นๆ สำหรับใส่ในพรอมต์ ให้กระชับ:\n\n- ใส่หมายเหตุเรื่องล็อกและเวลาที่คาด\n- ใช้ CONCURRENTLY สำหรับดัชนีขนาดใหญ่และระบุขีดจำกัดของทรานแซกชัน\n- ชอบ NOT VALID แล้ว VALIDATE สำหรับ FK ใหม่\n- แยก backfill ออกจากการบังคับ NOT NULL/UNIQUE\n- ลบอ็อบเจ็กต์หลังรอบการปล่อยเต็มและยืนยันว่าไม่มีใครอ่านมันแล้ว\n\n## การขอ backfill ที่ช้า มั่นคง และกู้คืนได้\n\nBackfill เป็นจุดที่เจ็บปวดที่สุด ไม่ใช่ ALTER TABLE โดยตรง พรอมต์ที่ปลอดภัยปฏิบัติต่อ backfill เหมือนงานควบคุม: วัดได้ รีสตาร์ทได้ และไม่กดระบบ\n\nเริ่มด้วยการตั้งการตรวจรับที่รันง่ายและยากที่จะเถียง: นับแถวที่คาด ค่า null เป้าหมาย และ spot checks สักหน่อย (เช่น เปรียบเทียบค่าเก่า vs ใหม่สำหรับ 20 ID แบบสุ่ม)\n\nแล้วขอแผนแบทช์ ช่วยให้ล็อกสั้นและลดความประหลาดใจ คำขอที่ดีจะระบุ:\n\n- วิธีแบทช์ (ช่วง primary key หรือหน้าต่างเวลา เช่น created_at)\n- ขนาดแบทช์เป้าหมาย (เช่น 5,000 ถึง 50,000 แถว)\n- ว่าจะหน่วงระหว่างแบทช์บนตารางร้อนหรือไม่\n- ให้แต่ละแบทช์เป็นทรานแซกชันชัดเจน (ไม่ใช่ทรานแซกชันใหญ่อันเดียว)\n\nบังคับให้ idempotent เพราะ backfill มักล้มกลางทาง SQL ควรรันซ้ำได้โดยไม่ทำสำเนาหรือทำลายข้อมูล แบบทั่วไปคือ "อัปเดตเฉพาะที่ new column ยังเป็น NULL" หรือกฎที่กำหนดผลลัพธ์ได้แน่นอน\n\nอธิบายด้วยว่าแอปยังถูกต้องอย่างไรระหว่าง backfill ถ้ามีการเขียนใหม่เข้ามา คุณต้องมีสะพาน: dual-write ในโค้ดแอป, ทริกเกอร์ชั่วคราว, หรือ read-fallback บอกว่าคุณใช้แนวทางไหนได้อย่างปลอดภัย\n\nสุดท้าย ต้องมีการหยุดและต่อให้เป็นส่วนหนึ่งของดีไซน์ ขอการติดตามความคืบหน้าและจุดตรวจ เช่น ตารางเล็กๆ เก็บ last processed ID และคำสั่งที่รายงานความคืบหน้า (แถวที่อัปเดต, last ID, เวลาเริ่ม)\n\nตัวอย่าง: เพิ่ม users.full_name สร้างจาก first_name และ last_name แบบปลอดภัยคืออัปเดตเฉพาะแถวที่ full_name IS NULL ทำเป็นช่วง ID บันทึกความคืบหน้า และให้การสมัครใหม่ถูกเขียนอย่างถูกต้องจนกว่าจะสลับเสร็จ\n\n## วิธีขอแผน rollback ที่ใช้ได้จริง\n\nแผน rollback ไม่ใช่แค่ "เขียน down migration" มันคือสองปัญหา: ย้อนสคีมา และจัดการข้อมูลที่เปลี่ยนขณะที่เวอร์ชันใหม่ทำงาน การ rollback สคีมามักเป็นไปได้ ข้อมูลที่เปลี่ยนมักย้อนกลับไม่ได้ เว้นแต่คุณวางแผนตั้งแต่ต้น\n\nระบุให้ชัดเจนว่า rollback หมายถึงอะไรสำหรับการเปลี่ยนแปลงนั้น หากคุณจะลบคอลัมน์หรือเขียนค่าแทนที่ ให้ขอคำตอบที่เป็นจริงเช่น: "Rollback คืนความเข้ากันของแอปได้ แต่ข้อมูลเดิมไม่สามารถกู้คืนได้หากไม่มี snapshot." ความซื่อสัตย์นี้ช่วยให้ปลอดภัย\n\nขอทริกเกอร์ rollback ที่ชัดเจนเพื่อไม่ให้มีการถกเถียงขณะเกิดเหตุ ตัวอย่าง:\n\n- อัตราข้อผิดพลาดหรือ latency เกินค่าที่กำหนดเป็นเวลา 10 นาที\n- แผนการ query ถดถอย (เช่น seq scan บนตารางร้อน)\n- งาน backfill ตกหล่นเกิน N ชั่วโมง\n- การตรวจสอบข้อมูลล้มเหลว (null ที่ไม่ควรมี ซ้ำ หายไป)\n- ขั้นตอนมิเกรชันบล็อกการเขียนนานเกิน X วินาที\n\nต้องให้แพ็กเกจ rollback ทั้งหมด ไม่ใช่แค่ SQL: DOWN SQL (ถ้าปลอดภัย), ขั้นตอนโค้ด/คอนฟิกของแอปเพื่อความเข้ากันได้ และวิธีหยุดงานแบ็กกราวด์\n\nแพทเทิร์นพรอมต์นี้มักเพียงพอ:\n\n\nProduce a rollback plan for this migration.\nInclude: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.\nState what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.\nInclude rollback triggers with thresholds.\n\n\nก่อนปล่อย จัดเก็บ "safety snapshot" เบาๆ เพื่อเปรียบเทียบก่อนและหลัง:\n\n- นับแถวของตารางที่ได้รับผลกระทบ (และส่วนย่อยที่สำคัญ)\n- ชุดตัวอย่างของ query ที่คาดผลลัพธ์\n- เกรณฑ์รวมง่ายๆ (sum, min/max) สำหรับคอลัมน์ที่แตะต้อง\n- รายการ ID สั้นๆ สำหรับ spot-check ก่อนและหลัง\n\nและชัดเจนว่าควรไม่ rollback เมื่อใด ถ้าเพียงเพิ่มคอลัมน์ nullable และแอป dual-write การแก้ข้างหน้า (hotfix โค้ด หยุด backfill แล้วต่อ) มักปลอดภัยกว่าการย้อนกลับและสร้างความเบี้ยวของข้อมูลมากขึ้น\n\n## ข้อผิดพลาดทั่วไปที่ต้องระวังเมื่อตั้งค่ามิเกรชันด้วย AI\n\nAI เขียน SQL ได้เร็ว แต่มันไม่เห็นฐานข้อมูลโปรดักชันของคุณ ความล้มเหลวมักเกิดเมื่อพรอมต์คลุมเครือและโมเดลเติมช่องว่างเอง\n\nกับดักทั่วไปคือข้ามสคีมาปัจจุบัน หากคุณไม่วาง definition ของเทเบิล ดัชนี และข้อจำกัด SQL อาจชี้ไปที่คอลัมน์ที่ไม่มีหรือพลาดกฎ unique ที่ทำให้ backfill กลายเป็นงานช้าและล็อกหนัก\n\nข้อผิดพลาดอีกอย่างคือรวม expand, backfill, และ contract ในดีพลอยเดียว ทำให้ไม่มีทางหนี หาก backfill ใช้เวลานานหรือผิดกลางทาง คุณจะติดกับแอปที่คาดหวังสภาพสุดท้าย\n\nปัญหาที่พบบ่อยที่สุด:\n\n- Backfill ที่ไม่ idempotent และไม่มีการติดตามความคืบหน้า\n- เพิ่ม NOT NULL, UNIQUE, หรือตั้ง FK ก่อนล้างและ validate ข้อมูล\n- ทรานแซกชันที่รันนานโดยไม่มี lock/statement timeouts\n- ไม่มีคำสั่งตรวจสอบ ทำให้ปัญหาแฝงจนผู้ใช้เจอเอง\n\nตัวอย่างชัดเจน: "เปลี่ยนชื่อคอลัมน์และอัปเดตแอป" หากแผนที่สร้างมาเปลี่ยนชื่อและ backfill ในทรานแซกชันเดียว backfill ช้าอาจล็อกและทำให้ทราฟฟิกเสีย หรือต้องบังคับให้พรอมต์ขนาดเล็ก แบทช์ explicit timeouts และคำสั่งตรวจสอบก่อนลบเส้นทางเก่า\n\n## สิ่งที่ต้องตรวจสอบบนสเตจก่อนปล่อย\n\nสเตจค้นพบปัญหาที่ไม่ปรากฏในฐานข้อมูลเดวิเลปขนาดเล็ก: ล็อกยาว, null ที่ไม่คาดคิด, ดัชนีหาย, หรือโค้ดทางผ่านที่ลืม\n\nก่อนอื่นตรวจว่าสคีมาในสเตจตรงตามแผนหลังมิเกรชัน: คอลัมน์ ชนิด ค่า default ข้อจำกัด และดัชนี ตรวจอย่างละเอียด หนึ่งดัชนีหายก็พอจะเปลี่ยน backfill ให้เป็นเคราะห์ร้าย\n\nแล้วรันวิเคราะห์มิเกรชันกับ dataset ที่สมจริง นั่นควรเป็นสำเนาล่าสุดของโปรดักชันที่มาสก์ข้อมูลสำคัญ หากทำไม่ได้ ให้แมตช์ปริมาณและจุดร้อนของโปรดักชัน (ตารางใหญ่ แถวกว้าง ตารางที่มีดัชนีเยอะ) บันทึกเวลาของแต่ละขั้นตอนเพื่อเตรียมคาดการณ์ในโปรดักชัน\n\nเช็คลิสต์สั้นสำหรับสเตจ:\n\n- สคีมาตรงตามแผน (คอลัมน์ ชนิด ข้อจำกัด ดัชนี)\n- บันทึกเวลาในข้อมูลที่สมจริง\n- ทดสอบความเข้ากันได้: แอปเก่ากับสคีมาใหม่ และแอปใหม่กับสคีมาเก่า (เมื่อแผนระบุว่าควรใช้งานได้)\n- รันคำสั่งตรวจสอบ: อัตรา null, นับแถว, ตรวจหา orphan สำหรับ FK ใหม่, ตัวอย่างการอ่าน\n- เฝ้าดูสัญญาณการปฏิบัติการระหว่างรัน: locks, deadlocks, timeouts, slow queries\n\nสุดท้าย ทดสอบฟลว์ผู้ใช้จริง ไม่ใช่แค่ SQL สร้าง แก้ไข และอ่านระเบียนที่เปลี่ยนแปลง หากใช้ expand/contract ยืนยันว่าทั้งสองสคีมาทำงานได้จนกว่าจะ cleanup เสร็จ\n\n## ตัวอย่างที่เป็นจริง: เปลี่ยนคอลัมน์โดยไม่ทำให้ผู้ใช้พัง\n\nสมมติคุณมีคอลัมน์ users.name เก็บชื่อเต็มอย่าง "Ada Lovelace" คุณต้องการ first_name และ last_name แต่ไม่สามารถทำให้การสมัคร การแสดงโปรไฟล์ หรือหน้าฝ่ายแอดมินพังขณะเปลี่ยนแปลง\n\nเริ่มด้วยขั้นตอน expand ที่ปลอดภัยแม้ไม่มีโค้ดใหม่ปล่อย: เพิ่มคอลัมน์ nullable เก็บของเก่าไว้ และหลีกเลี่ยงล็อกยาว\n\nsql\nALTER TABLE users ADD COLUMN first_name text;\nALTER TABLE users ADD COLUMN last_name text;\n\n\nจากนั้นอัปเดตพฤติกรรมแอปให้รองรับทั้งสองรูปแบบ ใน Release 1 แอปควรอ่านจากคอลัมน์ใหม่เมื่อมีค่า fallback ไปที่ name เมื่อเป็น null และเขียนทั้งสองที่เพื่อให้ข้อมูลใหม่สอดคล้อง\n\nต่อมาคือ backfill รันงานแบทช์ที่อัปเดตเป็นชิ้นเล็กๆ ต่อรอบ บันทึกความคืบหน้า และหยุดได้อย่างปลอดภัย ตัวอย่าง: อัปเดต users ที่ first_name เป็น null ตามลำดับ ID ทีละ 1,000 แถว และบันทึกจำนวนแถวที่เปลี่ยน\n\nก่อนเข้มงวด ให้ validate ในสเตจ:\n\n- สมัครใหม่เติม first_name และ last_name และยังตั้ง name\n- ผู้ใช้เดิมแสดงได้แม้มีแค่ name\n- Backfill หยุดและเริ่มใหม่ได้โดยไม่ทำงานซ้ำ\n- ไม่มี null ที่ไม่คาดคิดหลงเหลือหลัง backfill เสร็จ\n- คิวรีพื้นฐานบน users ไม่ช้าลงอย่างมีนัยสำคัญ\n\nRelease 2 สลับการอ่านไปคอลัมน์ใหม่เท่านั้น หลังจากนั้นค่อยเพิ่มข้อจำกัด (เช่น SET NOT NULL) และลบ name ในการปล่อยแยกครั้งถัดไป\n\nสำหรับ rollback ให้ทำให้ง่าย แอปยังอ่าน name ในการเปลี่ยนผ่าน และ backfill หยุดได้ หากต้องย้อน Release 2 ให้สลับการอ่านกลับไป name และทิ้งคอลัมน์ใหม่ไว้จนกว่าจะเสถียร\n\n## ขั้นตอนต่อไป: เปลี่ยนพรอมต์ของคุณให้เป็นนิสัยมิเกรชันที่ทำซ้ำได้\n\nปฏิบัติต่อการเปลี่ยนแต่ละครั้งเหมือน runbook เล็กๆ เป้าหมายไม่ใช่พรอมต์ที่สมบูรณ์แบบ แต่เป็นนิสัยที่บังคับรายละเอียดที่ถูกต้อง: สคีมา ข้อจำกัด แผนรัน และ rollback\n\nมาตรฐานสิ่งที่ต้องมีในทุกคำขอมิเกรชัน:\n\n- สคีมาปัจจุบันและการเปลี่ยนที่ชัดเจน (ตาราง คอลัมน์ ดัชนี)\n- ข้อจำกัดและข้อมูลทราฟฟิก (ขนาดตาราง อัตราเขียน downtime ที่อนุญาต)\n- ลำดับการปล่อย (expand, ปล่อยแอป, backfill, contract)\n- วิธีสังเกตความคืบหน้า (queries/metrics เวลาโดยประมาณ)\n- ขั้นตอน rollback (จะย้อนอะไรก่อน อะไรอาจคงเหลือเป็นข้อมูลที่ต้องกลับมา)\n\nกำหนดเจ้าของแต่ละขั้นก่อนจะรัน SQL ใครเป็นคนดูแลอะไรจะช่วยป้องกัน "ทุกคนคิดว่าอีกคนทำ": นักพัฒนารับผิดชอบพรอมต์และโค้ดมิเกรชัน, ops ควบคุมเวลาและการมอนิเตอร์ใน production, QA ยืนยันพฤติกรรมบนสเตจและขอบกรณี, และคนหนึ่งคนเป็นผู้ตัดสินใจ go/no-go\n\nถ้าคุณสร้างแอปผ่านแชท การร่างลำดับก่อนสร้าง SQL ช่วยได้ สำหรับทีมที่ใช้ Koder.ai, Planning Mode เป็นที่ที่เหมาะจะเขียนลำดับและ snapshot รวม rollback ช่วยลดความเสี่ยงหากเกิดสิ่งไม่คาดคิดระหว่าง rollout\n\nหลังปล่อย ให้กำหนดเวลา cleanup ของ contract ทันทีขณะที่บริบทยังสด เพื่อไม่ให้คอลัมน์เก่าและโค้ดความเข้ากันคงค้างเป็นเดือนการเปลี่ยนสคีมาเสี่ยงเมื่อ โค้ดแอป สถานะฐานข้อมูล และจังหวะการดีพลอยไม่ตรงกัน\n\nโหมดล้มเหลวที่พบบ่อย:\n\n- โค้ดเวอร์ชันเก้าเข้าถึงคอลัมน์/ข้อจำกัดใหม่แล้วเกิดข้อผิดพลาด\n- มิเกรชันล็อกตารางที่ร้อนและคำขอเกิด timeout\n- การเปลี่ยน "เล็กน้อย" ทำให้ข้อมูลถูกเขียนทับหรือลบโดยไม่ตั้งใจ\n- งานดัชนี/ข้อจำกัดใช้เวลานานกว่าที่คิดและทำให้ query ช้า
ใช้รูปแบบ expand/contract ที่ปลอดภัย:\n\n- Expand: เพิ่มคอลัมน์/ตาราง/ดัชนีแบบ nullable หรือในทางที่เข้ากันได้\n- Compatibility: ดีพลอยโค้ดที่อ่าน/เขียนกับทั้งสองรูปแบบ\n- Backfill: คัดลอกข้อมูลทีละน้อยพร้อมจุดตรวจ\n- Contract: บีบกฎให้เข้มขึ้นและลบฟิลด์เก่าเมื่อผ่านรอบการปล่อยทั้งหมด\n\nวิธีนี้ทำให้ทั้งเวอร์ชันแอปเก่าและใหม่ทำงานได้ระหว่างการเปิดตัว
โมเดลอาจสร้าง SQL ที่ถูกต้อง แต่ ไม่ปลอดภัยสำหรับโหลดงานของคุณ\n\nความเสี่ยงเฉพาะจาก AI:\n\n- เดาโครงสร้างตาราง/คอลัมน์หรือพลาดข้อจำกัดสำคัญ\n- เสนอการมิเกรชันแบบ "big bang" ที่ตัดทาง rollback\n- มองข้ามพฤติกรรมล็อก ข้อจำกัดของทรานแซกชัน และการสร้างดัชนีที่ใช้เวลานาน\n- ย่อหน้าเกี่ยวกับ rollback โดยเฉพาะเมื่อข้อมูลถูกแปลงหรือลบ\n\nปฏิบัติต่อผลลัพธ์จาก AI เป็นฉบับร่างและขอแผนการรัน การตรวจสอบ และขั้นตอน rollback เสมอ
ใส่เฉพาะข้อเท็จจริงที่มิเกรชันต้องใช้:\n\n- ชิ้นส่วน CREATE TABLE ที่เกี่ยวข้อง (บวกดัชนี, FK, UNIQUE/CHECK, ทริกเกอร์)\n- เวอร์ชัน Postgres และวิธีรันมิเกรชัน (ทรานแซกชันเดียว vs หลายขั้นตอน)\n- ขนาดสเกล: จำนวนแถว ขนาดตาราง อัตราเขียน และจราจรสูงสุด\n- วิธีที่แอปใช้ข้อมูล (อ่าน/เขียน/งานแบ็กกราวด์สำคัญ)\n- ข้อจำกัดเข้มงวด (ไม่มี downtime, หลีกเลี่ยง full-table rewrite, ข้อจำกัดล็อก)\n- ผลลัพธ์ที่ต้องการ: UP SQL + คำสั่งตรวจสอบ + แผน rollback + runbook\n\nสิ่งนี้ช่วยป้องกันการเดาชื่อและบังคับลำดับที่ถูกต้อง
กฎเริ่มต้น: แยกงานออกจากกัน\n\nตัวอย่างการแยกงานที่เป็นประโยชน์:\n\n- มิเกรชัน 1: ขยายสคีมา (คอลัมน์/ตารางใหม่ อาจตั้งค่า NOT VALID สำหรับข้อจำกัด)\n- ดีพลอยแอป: โค้ดความเข้ากันได้ (read-fallback หรือ dual-write)\n- งาน backfill: อัปเดตเป็นชุดพร้อมติดตามความคืบหน้า\n- มิเกรชัน 2: contract (validate ข้อจำกัด, ตั้ง NOT NULL, ลบคอลัมน์เก่า)\n\nการรวมทุกอย่างในครั้งเดียวทำให้การแก้ปัญหาและ rollback ยากขึ้น
รูปแบบที่แนะนำ:\n\n1) ADD COLUMN ... NULL โดยไม่ตั้ง default (เร็ว)\n2) ทำ backfill เป็นชุด\n3) ตั้ง default สำหรับแถวใหม่\n4) เพิ่ม NOT NULL หลังการยืนยัน\n\nการเพิ่ม default แบบ non-null อาจเสี่ยงบนบางเวอร์ชันเพราะอาจ rewrite ทั้งตาราง หากต้องการ default ทันที ให้ขอคำอธิบายพฤติกรรมล็อกและแผนสำรอง
ขอ:\n\n- CREATE INDEX CONCURRENTLY สำหรับตารางใหญ่/ร้อน\n- แจ้งว่าไม่สามารถรันใน transaction block ได้ (เครื่องมือของคุณต้องรองรับ)\n- คาดการณ์เวลาและสิ่งที่ต้องเฝ้าดู (lock waits, latency ของ query)\n\nเพื่อการตรวจสอบ ให้รวมเช็คว่า index ถูกสร้างและถูกใช้งาน (เช่น เปรียบเทียบ EXPLAIN ก่อน/หลังในสเตจ)
ใช้ NOT VALID ก่อน แล้วค่อย validate ภายหลัง:\n\n- เพิ่ม FK เป็น NOT VALID เพื่อให้ก้าวแรกไม่รบกวนมาก\n- รัน VALIDATE CONSTRAINT ในขั้นตอนแยกเมื่อคุณพร้อมเฝ้าดู\n\nวิธีนี้ยังบังคับ FK สำหรับการเขียนใหม่ ในขณะที่คุณควบคุมเวลาของการตรวจสอบที่มีค่าใช้จ่าย
งาน backfill ที่ดีต้องเป็น แบบแบทช์ ทำซ้ำได้ และรีซูมได้\n\nข้อกำหนดปฏิบัติ:\n\n- แบทช์โดยช่วง primary key หรือตามช่วงเวลา\n- อัปเดตเฉพาะแถวที่ยังต้องทำ (WHERE new_col IS NULL)\n- ให้แต่ละแบทช์เป็นทรานแซกชันสั้น ๆ และอาจหน่วงระหว่างแบทช์\n- ติดตามความคืบหน้า (last processed ID, rows updated, เวลาเริ่ม)\n- รับประกันว่าแอปยังทำงานถูกต้องระหว่าง backfill (dual-write, trigger ชั่วคราว, หรือ read-fallback)\n\nการออกแบบแบบนี้ทำให้ backfill ทนทานต่อการทำงานจริง
เป้าหมาย rollback ปกติ: คืนความเข้ากันของแอปให้เร็ว แม้ว่าข้อมูลจะไม่ถูกย้อนกลับอย่างสมบูรณ์\n\nแผน rollback ที่ใช้งานได้ควรมี:\n\n- ระบุว่า DOWN SQL ปลอดภัยจริงหรือไม่; ถ้าไม่ ให้เตรียม runbook แทน\n- ลำดับที่ชัดเจน: หยุด/พักงาน backfill, ดีพลอยโค้ดเปลี่ยนแปลง, แล้วทำขั้นตอนสคีมา\n- เกณฑ์ทริกเกอร์ rollback ที่ชัดเจน (อัตราข้อผิดพลาด, latency, lock waits, การตรวจสอบข้อมูลล้มเหลว)\n- บอกว่าคืนอะไรได้ (สคีมา) vs คืนอะไรไม่ได้ (ข้อมูล)\n\nบ่อยครั้ง rollback ที่ปลอดภัยที่สุดคือสลับการอ่านกลับไปยังฟิลด์เก่า ในขณะที่คอลัมน์ใหม่ยังคงอยู่