ข้าม​ไป​ยัง​เนื้อหา

migration บรรทัด​เดียว​ที่​ไม่​ได้​แตะ​ข้อมูล​สัก​หน้า แต่​หยุด query ทุก​ตัว​ที่​ตาม​มา

คุณ​รีวิว SQL ของ migration มา​แล้ว มัน​มี​บรรทัด​เดียว เป็น ADD COLUMN ที่​ไม่มี default คุณ​รัน​มัน​บน staging มัน​เสร็จ​ก่อน​ที่​คุณ​จะ​ละ​สายตา​จาก​จอ คุณ​จึง​กด deploy ตอน​บ่าย

แล้ว​เว็บ​ก็​ค้าง ไม่ใช่​ช้า — ค้าง ทุก​หน้าที่​แตะ​ตาราง orders หมุน​ค้าง​พร้อม​กัน

พอ​เข้าไป​ดู pg_stat_activity คุณ​เห็น​สิ่ง​ที่​ไม่​คาด​คิด คำ​สั่ง​ที่​ค้าง​อยู่​ไม่ใช่ migration มัน​คือ SELECT ธรรมดา​ของ​หน้า​ราย​กา​รอ​อเด​อร์​ที่​ปกติ​คืน​ทันที และ​มัน​ไม่​ได้​ค้าง เพราะ​ตาราง​ถูก​ล็อก​อยู่ มัน​ค้าง​เพราะ​มัน​ต่อ​คิว​อยู่​หลัง​คำ​สั่ง​ที่​ยัง​ไม่​ได้​ล็อก​ด้วย​ซ้ำ

หน้า​นี้​วัด​คิว​นั้น​บน PostgreSQL 18.6 ตัว​จริง ผล​คือ reader ที่ ไม่​ได้​ทำ​อะไร​เลย หนึ่ง​ตัว บวก ALTER TABLE ที่ ไม่​ได้​แตะ​ข้อมูล​สัก​หน้า หนึ่ง​บรรทัด เท่ากับ 8 คำ​สั่ง​ที่​ไม่​ได้​รัน จาก 8 คำ​สั่ง​ที่​เข้า​มา

และ​ผู้ร้าย​ไม่ใช่ migration ตัว​ที่​ถือ lock อยู่​จริง​คือ session ที่​สถานะ idle in transaction — เปิด transaction ค้าง​ไว้​แล้วไป​ทำ​อย่าง​อื่น ALTER TABLE เป็น​แค่​ตัว​ที่​บังเอิญ​มา​ยืน​ต่อ​คิว​หลัง​มัน แล้วก​ลาย​เป็น​กำแพง​ให้​คน​ที่มา​ทีหลัง​ทั้งหมด

ทุก​ช่อง​ใน​หน้า​นี้​เป็น​จำนวนนับ ไม่มี​วินาที​สัก​ตัว เพราะ​ข้อ​อ้าง​ของ​หน้า​นี้​ไม่​ได้​พูด​ถึง​ความเร็ว​เครื่อง

คอร์ส EF Core ของ​ไซต์​นี้​เตือน​เรื่อง​นี้​ไว้​แล้ว แต่​เตือน​โดย​ไม่มี​ตัวเลข​สัก​ตัว

บท​เรียน​เรื่อง migration ของ​คอร์ส EF Core เขียน​ไว้​ว่า​ให้​มอง​หา​คำ​สั่ง​อันตราย​ใน migrate.sql ก่อน​ขึ้น prod แล้ว​วงเล็บ​กำกับ​ไว้​ว่า

ALTER COLUMN ... TYPE (อาจ rewrite ทั้ง​ตาราง​และ​ล็อก​ไว้​นาน)

อีก​ย่อหน้า​หนึ่ง​จัด​กลุ่ม​ก้าว​แบบ​นั้น​ว่า Breaking (เสี่ยง​ข้อมูล/เสี่ยง​ล็อก​ตาราง)

สอง​ประโยค​นั้น​ไม่​ผิด แต่​ไม่มี​ตัวเลข​สัก​ตัว “นาน” คือ​เท่าไร “เสี่ยง” คือ​เสี่ยง​ต่อ​อะไร

และ​มัน​ผูก​คำ​ว่า​อันตราย​ไว้​กับ การ rewrite กับ​ข้อมูล​หาย ซึ่ง​เป็น​แกน​ที่​มี​จริง แต่​กลไก​ที่​ทำให้​เว็บ​หยุด​ไม่ใช่​แกน​นั้น มัน​คือ คิว​ของ lock และ​บน​แกน​นั้น ALTER TABLE ทุก​รูป​เท่า​กัน​หมด

หลัก​ฐาน​อยู่​ใน​รายการ​ของ​บท​เรียน​เอง มัน​ให้​มอง​หา​สาม​คำ​สั่ง — DROP COLUMN, ALTER COLUMN ... TYPE และ ALTER COLUMN ... SET NOT NULL — แต่​มี​ตัว​เดียว​ที่ rewrite

ตาราง​ใน​หัวข้อ​ถัด​ไป​วัด​ทั้ง​สาม​คำ​สั่ง​นั้น DROP COLUMN กับ SET NOT NULL ไม่​เปลี่ยน relfilenode และ​ไม่​ขยับ​จำนวน​หน้า​สัก​หน้า มี​แต่ ALTER COLUMN TYPE ที่​เขียน​ตาราง​ใหม่​ทั้ง​ใบ

สาม​คำ​สั่ง​นั้น​จึง​มี​อย่าง​เดียว​ที่​เหมือน​กัน​จริง ๆ คือ ทั้ง​สาม​ถือ ACCESS EXCLUSIVE ซึ่ง​เป็น​แกน​ที่​บท​เรียน​ไม่​ได้​พูด​ถึง

และ ADD COLUMN ที่​ไม่มี default ซึ่ง​ก่อ​คิว​ทั้งหมด​ใน​การ​วัด​ข้าง​บน​นี้ ไม่​อยู่​ใน​รายการ​นั้น​เลย

อัน​ไหน​วัด​เอง อัน​ไหน​อ่าน​มา

หน้า​นี้​วัด​ทั้ง​สาม​คำ​สั่ง​นั้น​เอง ไม่​ได้​ยก​พฤติกรรม​มา​จาก​เอกสาร​แล้ว​เชื่อ​ตาม · ร่าง​แรก​ของ​หน้า​นี้​อ้าง​เอกสาร​สำหรับ DROP COLUMN กับ SET NOT NULL แล้ว​จึง​กลับ​ไป​วัด​เพิ่ม เพราะ​ข้อ​อ้าง​ที่​วัด​เอง​กับ​ข้อ​อ้าง​ที่​อ่าน​มา ไม่​ควร​ปน​กัน​ใน​ตาราง​เดียว

สิ่ง​ที่​ยัง​เป็นการ​อ้าง​เอกสาร​ล้วน ๆ ใน​หน้า​นี้​เหลือ​แค่​คำ​นิยาม​ของ lock แต่ละ​ตัว · ส่วน​กฎ​เรื่อง​คิว หา​ใน​เอกสาร​ไม่​เจอ จึง​วัด​เอา​เอง ดู​กล่อง​ใน​หัวข้อ​กลไก

หน้า​นี้​จ่าย​หนี้​ก้อน​นั้น อ่าน​บท​เรียน​ต้นทาง​ได้ที่ migration ที่ apply แล้ว​ย้อน​ได้

เอกสาร​บอก​ว่า ACCESS SHARE ไม่​ชน​กับ ACCESS SHARE แต่​ไม่​ได้​บอก​สิ่ง​ที่​เกิด​ขึ้น​จริง

PostgreSQL มี​ตาราง​บอก​ว่า lock ตัว​ไหน​ชน​กับ​ตัว​ไหน SELECT ขอ ACCESS SHARE และ​เอกสาร เขียน​ไว้​ตรง​ตัว​ว่า “an ACCESS SHARE lock can be held by multiple transactions” ส่วน ALTER TABLE ขอ ACCESS EXCLUSIVE ซึ่ง​ชน​กับ​ทุก​ตัว

อ่าน​แค่​ตาราง​นั้น​แล้ว​คุณ​จะ​สรุป​ว่า SELECT สอง​ตัว​อยู่​ด้วย​กัน​ได้​เสมอ ซึ่ง​จริง และ​คุณ​จะ​สรุป​ต่อว่า SELECT ตัว​ใหม่​จึง​เข้า​ได้​เสมอ​ตราบ​ใด​ที่​ยัง​ไม่มี​ใคร​ถือ ACCESS EXCLUSIVE อยู่​จริง ซึ่ง​ผิด

คำขอ​ที่​ยัง​ไม่​ได้​รับ​ก็​กัน​ทาง​เหมือน​กัน คำขอ ACCESS SHARE ตัว​ใหม่​ที่มา​ทีหลัง​คำขอ ACCESS EXCLUSIVE ที่​ยัง​รอ​อยู่ จะ​ต้อง​รอ​ต่อ​ท้าย​มัน ทั้ง​ที่​มัน​ไม่​ได้​ชน​กับ​ใคร​ที่​ถือ​อยู่​เลย

ข้อ​นี้​ไม่​ได้​อยู่​ใน​เอกสาร

ผม​ไป​หา​ประโยค​ที่​เขียน​กฎ​นี้​ไว้​ใน​เอกสาร​ทางการ​ของ PostgreSQL 18 แล้ว​ไม่​เจอ หน้า Explicit Locking บอก​แค่​ว่า transaction ที่​ขอ lock “will wait indefinitely for conflicting locks to be released” กับ​ให้​ตาราง​ว่า​อะไร​ชน​กับ​อะไร

ช่อง​ว่าง​นั้น​คือ​เหตุผล​ที่​คน​แปลก​ใจ ตาราง​ความ​ขัด​แย้ง​ตอบ​คำถาม​ว่า “ถือ​พร้อม​กัน​ได้​ไหม” ไม่​ได้​ตอบ​คำถาม​ว่า “เข้า​คิว​แซง​กัน​ได้​ไหม” ผม​จึง​ไม่​อ้าง​เอกสาร​ข้อ​นี้ ผม​วัด​มัน​แทน

สอง arm ที่​ต่าง​กัน​บรรทัด​เดียว แล้ว​ดู​ว่า​บรรทัด​นั้น​เปลี่ยน​อะไร

ตัว​วัด​ขับ psql หลาย session ผ่าน pty แล้ว​สลับ​คิว​ตาม​ลำดับ​ที่​เขียน​ตายตัว​ไว้​ใน code ไม่มี sleep ไม่มี thread แข่ง​กัน — anomaly ที่​ต้อง​พึ่ง​ความเร็ว​เครื่อง​คือ anomaly ที่​สอน​ไม่​ได้

สิ่ง​ที่​ตัดสิน​ว่า session หนึ่ง​ถูก​กัก​ไว้ ไม่ใช่​นาฬิกา แต่​เป็น​แถว​ใน pg_locks ที่ granted = false ซึ่ง​อ่าน​มา​จาก​คนละ connection กับ session ที่​กำลัง​รอ​อยู่

ตาราง​ที่​ใช้​คือ orders 50,000 แถว ปิด autovacuum ทั้ง​ที่​ระดับ​เซิร์ฟเวอร์​และ​ระดับ​ตาราง เพราะ worker ของ autovacuum ถือ SHARE UPDATE EXCLUSIVE ซึ่ง​ชน​กับ ACCESS EXCLUSIVE ถ้า​ไม่​ปิด แถว​ของ​มัน​จะ​โผล่​ใน pg_locks แล้ว​ถูก​นับ​เป็น​ความ​ยาว​คิว ทั้ง​ที่​ไม่ใช่ session ที่​เรา​สั่ง

สอง arm ต่าง​กัน​บรรทัด​เดียว

ขั้นแผง​ควบคุมarm ที่​วัด​คิว
1session R เปิด transaction แล้ว SELECT หนึ่ง​ครั้งเหมือน​กัน
2R นั่ง​เฉย ๆ ไม่ commitเหมือน​กัน
3—session D ยิง ALTER TABLE
4reader อีก 8 ตัว​ยิง SELECTเหมือน​กัน
5อ่าน pg_locks จาก​คนละ connectionเหมือน​กัน

แผง​ควบคุม​มี​ไว้​ตัด​ข้อ​แก้ตัว​ว่า “ก็ reader ตัว​ยาว​นั่น​แหละ​ที่​บล็อก” ถ้า​ข้อ​นั้น​จริง reader ทั้ง 8 ตัว​ต้อง​ติด​คิว​ใน​แผง​ควบคุม​ด้วย

reader ตัว​ยาว​ไม่​ได้​บล็อก​ใคร​เลย จน​กระทั่ง​มี ALTER มา​ต่อ​คิว​คั่น​กลาง

ติด​คิว ไม่​ได้​รันผ่าน ได้​คำ​ตอบแถว​ใน pg_locks ที่​ยัง​ไม่​ได้​รับ
แผง​ควบคุม ไม่มี ALTER080
มี ALTER คั่น​กลาง809

reader ตัว​ยาว​ตัว​เดียวกัน ค้าง​อยู่​ใน​สถานะ​เดียวกัน ต่าง​กัน​แค่​มี ALTER TABLE หนึ่ง​บรรทัด มา​ต่อ​คิว​คั่น​กลาง แล้ว​คำ​สั่ง​ที่​ผ่าน​ไป​ได้​เปลี่ยน​จาก 8 เป็น

0

แถว​ใน pg_locks คือ 9 — reader 8 ตัว บวก ALTER เอง​อีก​หนึ่ง สภาพ​คิว​ตอน​นั้น​หน้าตา​แบบ​นี้

ลำดับlock ที่​ขอได้​รับ​แล้วสถานะ​ของ session
ตัว​ที่​ถือ​อยู่AccessShareLock✅idle in transaction
ALTER TABLEAccessExclusiveLock❌active
reader × 8AccessShareLock❌active

บรรทัด​แรก​คือ​ทั้ง​เรื่อง session ที่​ก่อ outage นี้​อยู่​ใน​สถานะ idle in transaction มัน​ไม่​ได้​กำลัง query อะไร​อยู่ มัน​เปิด transaction ทิ้ง​ไว้ แล้วไป​ทำ​อย่าง​อื่น อาจ​จะ​รอ HTTP call อาจ​จะ​เป็น ORM ที่​ลืม​ปิด อาจ​จะ​เป็น​คน​เปิด psql ค้าง​ไว้

reader ทั้ง 8 ตัว​ขอ lock ตัว​เดีย​วกันเป๊ะ กับ​ตัว​ที่ session แรก ถือ​อยู่​และ​ได้​รับ​ไป​แล้ว สอง​ตัว​นั้น​ไม่​ชน​กัน มัน​แค่​บังเอิญ​มา​ทีหลัง ALTER

สาม​ข้อ​ที่​การ​วัด​นี้​ยืนยัน
  • คิว​นี้​ผูก​กับ​ตาราง ไม่ใช่​ทั้ง​ฐาน​ข้อมูล session ใหม่​ที่​ถาม​อีก​ตาราง​หนึ่ง​ตอน​คิว​ตัน​ที่สุด ผ่าน​ไป​ได้​ทันที การ​วัด​บันทึก​ข้อ​นี้​ไว้​เป็น otherTableCompleted
  • reader ที่​รอ​อยู่​ไม่​ได้​ผิด มัน​แค่​ไม่​ได้​รัน พอ session แรก commit ทั้ง 8 ตัว​คืนคำ​ตอบ​ที่​ถูก​ต้อง​ออก​มา​ครบ ไม่มี​ตัว​ไหน error ไม่มี​ตัว​ไหน​ตอบ​เพี้ยน
  • ตัว​ที่​ก่อ​คิว​ไม่​ได้​ทำงาน​เลย ALTER ที่​ใช้​คือ alter table orders add column note text ซึ่ง​หัวข้อ​ถัด​ไป​วัด​ได้​ว่า​มัน​เขียน heap ใหม่ 0 หน้า

ALTER TABLE ไม่​ได้​แพง​เท่า​กัน​ทุก​รูป แต่​ล็อก​เท่า​กัน​ทุก​รูป

หัวข้อ​ที่​มีชื่อ​ว่า “ALTER TABLE ไม่​ได้​แพง​เท่า​กัน​ทุก​รูป แต่​ล็อก​เท่า​กัน​ทุก​รูป”

lock เหมือน​กัน​หมด ต่าง​กัน​ที่​จำนวน​หน้าที่​ต้อง​เขียน​ใหม่

คำ​เตือน​ของ​คอร์ส EF Core แยก ALTER COLUMN TYPE ออก​จาก ADD COLUMN ซึ่ง​ถูก​ใน​แง่​ของ​งาน และ​ตาราง​ข้าง​ล่าง​ยืนยัน​ให้ · แต่​บน​แกน​ของ lock สอง​รูป​นั้น​เท่า​กัน​เป๊ะ

ตัว​วัด​ยิง​หก​รูป​ใส่​ตาราง​เดิม แล้ว​ดู​สอง​อย่าง lock ที่​มัน​ขอ กับ relfilenode เปลี่ยน​ไหม (relfilenode เปลี่ยน = PostgreSQL เขียน​ตาราง​ใหม่​ลง file ใหม่​แล้ว​ทิ้ง file เก่า เป็น​คำ​ตอบ​ตรง​ตัว​กว่า​ดู​ขนาด​ของ file)

รูป​ของ ALTER TABLElock ที่​ขอหน้า​ก่อนหน้า​หลังเขียน​ใหม่​กี่​หน้า
add column note textAccessExclusiveLock271271ไม่​แตะ heap
add column tag text default 'x'AccessExclusiveLock271271ไม่​แตะ heap
drop column note_legacyAccessExclusiveLock271271ไม่​แตะ heap
alter column note_legacy set not nullAccessExclusiveLock271271ไม่​แตะ heap
add column token uuid default gen_random_uuid()AccessExclusiveLock271384384 หน้า
alter column total_satang type bigintAccessExclusiveLock271320320 หน้า

ช่อง lock ทั้ง​ช่อง​มี​ค่า​เดียว ทุก​รูป​ขอ AccessExclusiveLock เท่า​กัน​หมด เอกสาร​ของ ALTER TABLE เขียน​ไว้​ว่า “An ACCESS EXCLUSIVE lock is acquired unless explicitly noted.” และ​หก​รูป​นี้​ไม่มี​รูป​ไหน​ถูก note ไว้

ความ​ต่าง​อยู่​ที่​ช่อง​สุดท้าย 4 รูป​แรก​ไม่​แตะ heap สัก​หน้า อีก 2 รูป​เขียน​ตาราง​ใหม่​ทั้ง​ใบ

แถว​ที่​สาม​คือ​แถว​ที่​ตอบ​ทั้ง​บทความ DROP COLUMN ไม่​ได้​ลบ​ข้อมูล​ออก​จาก​หน้า​สัก​หน้า มัน​แก้ catalog แล้ว​จบ จำนวน​หน้า​เท่า​เดิม​เป๊ะ · แต่​มัน​ถือ lock ตัว​เดียว​กับ ALTER COLUMN TYPE ที่​เขียน​ใหม่​ทั้ง​ตาราง คำ​สั่ง​ที่​ไม่​ทำ​อะไร​กับ​ข้อมูล​เลย ก็​หยุด​เว็บ​ได้​เท่า​กัน

สี่​รูป​ที่​ไม่​แตะ heap ไม่​ฟรี​เพราะ​เหตุผล​เดียวกัน

หัวข้อ​ที่​มีชื่อ​ว่า “สี่​รูป​ที่​ไม่​แตะ heap ไม่​ฟรี​เพราะ​เหตุผล​เดียวกัน”

ตั้งแต่ PostgreSQL 11 การ​เพิ่ม column ที่​มี default แบบ​ไม่ volatile จะ​เก็บ​ค่า​นั้น​ไว้​ใน metadata ของ​ตาราง​แล้ว​คืนให้​ตอน​อ่าน​แถว​เก่า ไม่​ต้อง​ไล่​เขียน​ทุก​แถว เอกสาร​เขียน​ว่า “In neither case is a rewrite of the table required.”

แต่ default ที่ volatile ไม่​ได้ เพราะ​ทุก​แถว​ต้อง​ได้​คนละ​ค่า จึง​ต้อง​ไล่​เขียน​จริง เอกสาร​เขียน​ว่า​มัน “will cause the entire table and its indexes to be rewritten” การ​วัด​ข้าง​บน​ยืนยัน​ทั้ง​สอง​ข้อ — default 'x' ไม่ rewrite ส่วน default gen_random_uuid() rewrite

สิ่ง​ที่​ตัดสิน​ไม่ใช่​คำ​ว่า ADD COLUMN แต่​เป็น default ที่​ตาม​หลัง​มา

อีก​สอง​รูป​ที่​ไม่​แตะ heap ไป​ถึง​จุด​เดียวกัน​คนละ​ทาง

DROP COLUMN ไม่​ได้​ลบ​ข้อมูล​จริง มัน​ทำให้ column นั้น​มอง​ไม่​เห็น​ใน​ระดับ catalog แล้ว​ปล่อย​ให้​ที่​ว่าง​ทยอย​คืน​ตอน​แถว​ถูก update

ส่วน SET NOT NULL ต้อง อ่าน ทั้ง​ตาราง​เพื่อ​ยืนยัน​ว่า​ไม่มี​แถว​ไหน​เป็น null แต่​ไม่​ได้ เขียน อะไร​ใหม่​เลย จำนวน​หน้า​จึง​เท่า​เดิม

SET NOT NULL เป็น​ตัว​ที่​หลอก​ตา​ที่สุด​ใน​หก​รูป เพราะ​มัน​อ่าน​ทั้ง​ตาราง​จริง ๆ งาน​ของ​มัน​โต​ตาม​ขนาด​ตาราง แต่​ไป​โต​ใน​ช่อง​ที่​ตาราง​ข้าง​บน​ไม่​ได้​วัด — ตาราง​นั้น​นับ หน้าที่​เขียน​ใหม่ ไม่​ได้​นับ หน้าที่​อ่าน

สาม​กลไก ไม่ใช่​สอง​กลไก

หก​รูป​ข้าง​บน​แยก​ได้​เป็น​สาม​กลุ่ม ไม่ใช่​สอง​กลุ่ม​อย่าง​ที่​ตาราง​ชวน​ให้​คิด

  • แก้ catalog แล้ว​จบ — ADD COLUMN 2 แบบ​ที่​ไม่ volatile และ DROP COLUMN · งาน​คงที่ ไม่​ขึ้น​กับ​ขนาด​ตาราง
  • อ่าน​ทั้ง​ตาราง แต่​ไม่​เขียน — SET NOT NULL · งาน​โต​ตาม​ขนาด​ตาราง แต่​เลี่ยง​ได้​ด้วย CHECK constraint ที่ valid อยู่​ก่อน ซึ่ง หน้า​นี้​ไม่​ได้​วัด​ทาง​เลี่ยง​นั้น
  • เขียน​ตาราง​ใหม่​ทั้ง​ใบ — default ที่ volatile และ ALTER COLUMN TYPE

ทั้ง​สาม​กลุ่ม​ถือ ACCESS EXCLUSIVE เท่า​กัน กลุ่ม​ไหน​ถือ​นาน​กว่า​เป็น​อีก​เรื่อง แต่​คิว​ที่​หน้า​นี้​วัด เกิด​ตั้งแต่​ก่อน​ที่​กลุ่ม​ไหน​จะ​ได้ lock ด้วย​ซ้ำ

สอง​อย่าง​นี้​ต้อง​แยก​กัน​ให้​ขาด
  • เวลา​ที่​ถือ lock — ยาว​ตาม​จำนวน​หน้าที่​ต้อง​เขียน ALTER COLUMN TYPE บน​ตาราง​ล้าน​แถว ถือ ACCESS EXCLUSIVE ไว้​จนกว่า​จะ​เขียน​เสร็จ นี่​คือ​สิ่ง​ที่​คำ​เตือน​ของ​คอร์ส​พูด​ถึง
  • เวลา​ที่ รอ lock — ยาว​ตาม​อายุ​ของ transaction ที่​ค้าง​อยู่ ไม่​เกี่ยว​กับ​งาน​ของ DDL เลย นี่​คือ​สิ่ง​ที่​การ​วัด​หน้า​นี้​จับ​ได้ และ​เป็น​สิ่ง​ที่​คำ​เตือน​ของ​คอร์ส​ไม่​ได้​พูด​ถึง

ADD COLUMN มี​ข้อ​แรก​เป็น​ศูนย์ แต่​ข้อ​สอง​เท่ากับ​ทุก​รูป ระหว่าง​ที่​มัน​รอ คิว​ก็​ยาว​ขึ้น​เรื่อย ๆ เหมือน​กัน

คำ​สั่ง​เดียวกัน ลำดับ​เดียวกัน ต่าง​กัน​ที่ DDL ยอม​แพ้​แทนที่​จะ​รอ

ตัว​วัด​รัน arm ที่​สาม​ด้วย​ลำดับ​เดียว​กับ arm ที่​วัด​คิว​ทุก​ประการ ต่าง​กัน​บรรทัด​เดียว คือ session ที่​จะ​ยิง ALTER ตั้ง SET lock_timeout = '250ms' ไว้​ก่อน

ติด​คิวผ่านแถว​ใน pg_locks ที่​ยัง​ไม่​ได้​รับcolumn ลง​จริง
ไม่​ตั้ง lock_timeout809✅
ตั้ง 250ms080❌

ALTER คืน SQLSTATE 55P03 (lock_not_available) แล้ว​ปล่อย​คิว​ทันที reader ที่​ตาม​มา​จึง​ไม่มี​คำขอ​ที่​ยัง​ไม่​ได้​รับ​ให้​ต้อง​รอ​ต่อ​ท้าย ทั้ง 8 ตัว​ผ่าน​ไป​ได้ และ column ไม่​ได้​ลง

นี่​คือ​การ​เลือก​ที่​ต้อง​เลือก​ไว้​ล่วง​หน้า ไม่​ตั้ง = migration สำเร็จ​แน่นอน แต่​คุณ​ไม่รู้ ว่า​มัน​จะ​พา​คิว​ยาว​แค่​ไหน​ไป​ด้วย ตั้ง = migration ล้ม​บ่อย​ขึ้น แต่​ล้ม​แบบ​ที่​ไม่มี​ใคร​นอก​ทีม​รู้สึก

ที่​สำคัญ​กว่า​คือ มัน​ล้ม​พร้อม​รหัส​ที่ pipeline อ่าน​ได้ SQLSTATE 55P03 แยก​ออก​จาก error อื่น​ได้​ชัดเจน จึง​เขียน​เป็น​เงื่อนไข retry ได้​ตรง ๆ ต่าง​จาก​การ​ปล่อย​ให้​มัน​ค้าง​ซึ่ง​ไม่มี​รหัส​อะไร​ออก​มา​เลย

-- ตั้งที่ระดับ session ของ migration ไม่ใช่ที่ระดับเซิร์ฟเวอร์
-- ระดับเซิร์ฟเวอร์จะไปโดน query ปกติของแอปด้วย
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN note text;
lock_timeout ไม่​ได้​กัน​การ rewrite ที่​ยาว

lock_timeout นับ​เฉพาะ​ช่วง​ที่ รอ lock พอได้ lock มา​แล้ว​มัน​หยุด​นับ

แปล​ว่า​มัน​กัน​กรณี​ของ​หน้า​นี้​ได้​ครบ แต่​กัน ALTER COLUMN TYPE ที่​ได้ lock ไป​แล้ว แล้ว​กำลัง​เขียน​ตาราง​ใหม่​ทั้ง​ใบ​ไม่​ได้​เลย สอง​กรณี​นั้น​ต้อง​ใช้​เครื่อง​มือ​คนละ​ตัว และ​หน้า​นี้​วัด​แค่​กรณี​แรก · หน้า​นี้​ไม่​ได้​วัด​ว่าการ rewrite กิน​เวลา​เท่าไร

สาม​ข้อ เรียง​ตาม​ลำดับ​ที่​ควร​ทำ

หนึ่ง ตั้ง lock_timeout ที่ session ของ migration เป็น​บรรทัด​เดียว​และ​ได้​ผล​ตาม​ที่​วัด​ไว้ ข้าง​บน ตั้ง​สั้น ๆ แล้ว retry ดี​กว่า​ตั้ง​ยาว​แล้ว​หวัง​ว่า​จะ​ไม่มี​ใคร​ค้าง

สอง ไล่ transaction ที่​ค้าง ตัว​ที่​ก่อ​เรื่อง​ใน​การ​วัด​นี้​อยู่​ใน​สถานะ idle in transaction ซึ่ง​เป็น​สถานะ​ที่​หา​เจอ​ได้​ล่วง​หน้า idle_in_transaction_session_timeout ตัด​มัน​ทิ้ง​ได้​อัตโนมัติ และ​ควร​ตั้ง​ไว้​ก่อน​ที่​จะ​มี migration

สาม รู้​ว่า​ก้าว​ไหน rewrite ตาราง​ใน​หัวข้อ​ข้าง​บน​คือ​คำ​ตอบ​สำหรับ​สี่​รูป​ที่​พบ​บ่อย​ที่สุด รูป​ที่ rewrite ต้อง​แตก​เป็น​หลาย​ก้าว หรือ​ใช้​ท่า​ที่​เขียน column ใหม่​แล้ว​ค่อย​สลับ ไม่ใช่​ยิง​ตูม​เดียว​แล้ว​หวัง

ข้อ​ที่​หน้า​นี้​ไม่​ได้​วัด
  • ไม่​ได้​วัด​ว่า​คิว​ยาว​เท่าไร​ต่อ​หนึ่ง​หน่วย​เวลา 8 คือ​จำนวน reader ที่​ตัว​วัด​ส่ง​เข้าไป ไม่ใช่​ผลลัพธ์ ข้อ​อ้าง​ของ​หน้า​นี้​คือ เข้า​มา​กี่​ตัว รอ​ครบ​ทุก​ตัว ไม่ใช่ รอ​กี่​ตัว​ต่อ​วินาที
  • ไม่​ได้​วัด​ตาราง​ที่​มี​คน​เขียน​อยู่​ตลอด ตาราง​ที่​วัด​นิ่ง ไม่มี​ใคร​เขียน​แทรก​ระหว่าง​การ​วัด
  • ไม่​ได้​วัด CREATE INDEX CONCURRENTLY หรือ​ท่า​ที่​ไม่​ใช้ ACCESS EXCLUSIVE หน้า​นี้​วัด​หก​รูป​ที่​ระบุ​ไว้​ใน​ตาราง​เท่านั้น
  • ไม่​ได้​วัด​ทาง​เลี่ยง scan ของ SET NOT NULL เอกสาร​บอก​ว่า​มี CHECK constraint ที่ valid อยู่​ก่อน​แล้ว​จะ​ข้าม scan ได้ · หน้า​นี้​วัด​เฉพาะ​ทาง​ที่​ไม่มี constraint นั้น
  • ไม่​ได้​วัด​ว่า EF Core สร้าง SQL แบบ​ไหน หน้า​นี้​วัด​ที่​ชั้น SQL ตรง ๆ คอร์ส EF Core เป็น​ที่​ของ​คำถาม​นั้น
  • ตัวเลข​จำนวน​หน้า​ผูก​กับ block_size และ​รุ่น​ของ​เอน​จิน ที่​วัด​คือ 8192 byte ตัว​ตรวจ​จึง​ตรวจ​ว่า rewrite หรือ​ไม่ rewrite ไม่​ได้​ตรึง​จำนวน​หน้า​ที​ละ​ตัว

ตัวเลข​ทุก​ตัว​ใน​หน้า​นี้​มา​จาก​สาม​คำ​สั่ง​ใน repo ของ​ไซต์​นี้

Terminal window
# ต้องมี Docker จึงอยู่นอก npm test — กติกาเดียวกับ verify:pagination-pg
KAEN_PG_CONTAINER=kaen-pg-ddl-lock scripts/ddl-lock/up.sh
npm run verify:ddl-lock
docker rm -f kaen-pg-ddl-lock
# ไม่ต้องมี Docker — อ่านผลที่ commit ไว้ แล้วยิงการกลายพันธุ์ใส่ตัวตรวจ
python3 scripts/ddl-lock/probe.py --selftest

npm run verify:ddl-lock ไม่​ได้​เทียบ​กับ​ตัวเลข​ที่ commit ไว้​เฉย ๆ มัน​วัด​ใหม่​ทั้ง​ชุด แล้ว​ตรวจ​ว่า​ข้อ​อ้าง​ของ​หน้า​นี้​ยัง​เป็น​จริง​ไหม — แผง​ควบคุม​ยัง​ไม่มี​ใคร​ติด​คิว arm ที่​มี ALTER ยัง​ติด​ครบ​ทุก​ตัว ALTER ที่​ก่อ​คิว​ยัง​เป็น​รูป​ที่​ไม่ rewrite และ lock_timeout ยัง​คืน​รหัส​เดิม

ส่วน --selftest ไม่​ต้อง​มี Docker มัน​อ่าน​ผล​ที่ commit ไว้ แล้ว​บิด​ผล​นั้น​ที​ละ​ข้อ​อ้าง เพื่อ​บังคับ​ว่า​ตัว​ตรวจ​ต้อง​แดง​ทุก​แบบ ด่าน​ที่​ไม่​เคย​ถูก​ทำให้​แดง คือ​ด่าน​ที่​ยัง​ไม่มี​ใคร​รู้​ว่า​มัน​ตรวจ​อะไร — และ​เพราะ​มัน​ไม่​ต้อง​มี Docker มัน​จึง​อยู่​ใน​เกต​ของ CI ได้ ต่าง​จาก --check

🔗 อ้างอิง​ต้นทาง
  • PostgreSQL 18 — Explicit Locking — ตาราง​ความ​ขัด​แย้ง และ​ประโยค “an ACCESS SHARE lock can be held by multiple transactions” (ตรวจ​แล้ว 2026-08-22)
  • PostgreSQL 18 — ALTER TABLE — “An ACCESS EXCLUSIVE lock is acquired unless explicitly noted.” และ​กฎ rewrite ของ default ที่ volatile กับ​ไม่ volatile (ตรวจ​แล้ว 2026-08-22)
  • PostgreSQL 18 — Client Connection Defaults — lock_timeout และ idle_in_transaction_session_timeout (ตรวจ​แล้ว 2026-08-22)
  • PostgreSQL 18 — PostgreSQL Error Codes — 55P03 คือ lock_not_available (ตรวจ​แล้ว 2026-08-22)

วัด​บน PostgreSQL 18.6 · aarch64-unknown-linux-musl · autovacuum ปิด · block_size 8192 byte