26 พ.ค. 2568·อ่าน 2 นาที

การเปลี่ยนดัชนีใน PostgreSQL โดยไม่หยุดให้บริการ: คู่มือปฏิบัติที่ปลอดภัย

การเปลี่ยนดัชนีใน PostgreSQL โดยไม่หยุดให้บริการ ด้วย CREATE INDEX CONCURRENTLY การตรวจสอบล็อกพื้นฐาน และขั้นตอนย้อนกลับชัดเจนเพื่อให้ทราฟฟิกโปรดักชันไม่สะดุด

การเปลี่ยนดัชนีใน PostgreSQL โดยไม่หยุดให้บริการ: คู่มือปฏิบัติที่ปลอดภัย

ทำไมการเปลี่ยนดัชนีถึงทำให้เกิดการหยุดให้บริการ (และจะหลีกเลี่ยงได้อย่างไร)

การทำงานกับดัชนีฟังดูไม่อันตราย — คุณแค่เพิ่มโครงสร้างช่วยเหลือ ใน PostgreSQL อย่างไรก็ตาม การสร้าง ลบ หรือสลับดัชนีอาจต้องการล็อกที่บล็อกเซสชันอื่น ๆ หากตารางมีการใช้งาน การรอเหล่านั้นจะสะสมและแอปจะเริ่มรู้สึกว่าพัง

การหยุดให้บริการไม่ค่อยเป็นแบนเนอร์แจ้งเตือนชัดเจน มันมักปรากฏเป็นเพจที่ค้าง งานแบ็กกราวด์ที่ตามไม่ทัน และคิวคำขอที่รอฐานข้อมูล คนหนึ่งกด "ค้นหา" แล้วเจอ timeout ขณะที่เครื่องมือช่วยงานและหน้าแอดมินค่อย ๆ ช้าลงเพราะคำสั่งง่าย ๆ ไม่สามารถได้ล็อกที่ต้องการ

"รันตอนกลางคืนก็ได้" มักล้มเหลวด้วยเหตุผลสองประการ ระบบหลายแห่งไม่เคยเงียบจริง (ผู้ใช้ทั่วโลก งานแบตช์ ETL แบ็กอัพ) และการดำเนินการดัชนีอาจใช้เวลานานกว่าที่คาดเพราะอ่านข้อมูลเยอะและแย่ง CPU กับดิสก์ หากหน้าต่างเวลาหมดก่อนการสร้างจะเสร็จ คุณจะต้องเลือกระหว่างรอต่อหรือยกเลิกการทำงาน

การเปลี่ยนดัชนีโดยไม่หยุดให้บริการไม่ใช่เวทมนตร์ มันคือการเลือกการดำเนินการที่บล็อกน้อยสุด ตั้งกรอบควบคุม (timeout และการตรวจพื้นที่ดิสก์) และเฝ้าดูฐานข้อมูลขณะทำงาน

คู่มือนี้เน้นนิสัยปฏิบัติในโปรดักชัน:

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

สิ่งที่คู่มือนี้ไม่ลงรายละเอียด: ทฤษฎีการออกแบบดัชนีเชิงลึก การปรับจูนคิวรีโดยรวม หรือการรีแฟคเตอร์สคีมาที่เขียนข้อมูลจำนวนมาก

โมเดลง่าย ๆ ของล็อกที่อยู่เบื้องหลังงานดัชนี

PostgreSQL ใช้ล็อกเพื่อรักษาความถูกต้องของข้อมูลเมื่อหลายเซสชันแตะต้องตารางเดียวกัน ล็อกคือกฎที่บอกว่าใครอ่านหรือเขียนวัตถุได้ในขณะนี้ และใครต้องรอ

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

การสร้างดัชนีกับการใช้ดัชนี

การใช้ดัชนีมักไม่แพงในแง่ล็อก SELECT, UPDATE และ DELETE สามารถอ่านหรือบำรุงรักษาดัชนีได้พร้อมกับเซสชันอื่น ๆ

การสร้างดัชนีแตกต่างออกไป PostgreSQL ต้องสแกนตาราง จัดเรียงหรือแฮชคีย์ และเขียนโครงสร้างใหม่ลงดิสก์ งานนั้นใช้เวลา และเวลานี่เองที่เปลี่ยน "ล็อกเล็ก" ให้เป็น "ปัญหาใหญ่" ในโปรดักชัน

สิ่งที่ CONCURRENTLY เปลี่ยน (และสิ่งที่ไม่เปลี่ยน)

CREATE INDEX แบบปกติต้องการล็อกหนักที่บล็อกการเขียนตลอดระยะเวลาการสร้าง CREATE INDEX CONCURRENTLY ถูกออกแบบมาให้การอ่านและเขียนปกติยังคงไหลได้ขณะสร้างดัชนี

แต่ "concurrent" ไม่ได้แปลว่า "ไม่มีล็อก" คุณยังมีช่วงล็อกสั้น ๆ ตอนเริ่มและตอนจบ และการสร้างอาจล้มเหลวหรือรอหากมีสิ่งอื่นถือล็อกที่ไม่เข้ากัน

ผลลัพธ์ที่สำคัญ:

  • การสร้างแบบไม่ concurrent อาจบล็อกการแทรก อัปเดต และลบบนตาราง
  • การสร้างแบบ concurrent ปกติอนุญาตให้การอ่านและเขียนได้ แต่สามารถช้าหรือหยุดได้จากทรานแซกชันที่รันนาน
  • ขั้นตอนการปิดงานยังต้องล็อกสั้น ๆ ดังนั้นระบบที่มีการใช้งานมากอาจเห็นการรอสั้น ๆ

เลือกแนวทางที่เหมาะสม: concurrent หรือปกติ

คุณมีตัวเลือกหลักสองแบบเมื่อต้องเปลี่ยนดัชนี: สร้างดัชนีแบบปกติ (เร็ว แต่บล็อก) หรือสร้างแบบ CONCURRENTLY (โดยทั่วไปไม่บล็อกทราฟฟิกแอป แต่นานกว่าและไวต่อทรานแซกชันที่รันนาน)

เมื่อใด CONCURRENTLY เป็นตัวเลือกที่ถูกต้อง

ใช้ CREATE INDEX CONCURRENTLY เมื่อเทเบิลให้บริการทราฟฟิกจริงและคุณไม่สามารถหยุดการเขียนได้ มันมักเป็นตัวเลือกที่ปลอดภัยเมื่อ:

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

เมื่อใดการสร้างดัชนีแบบปกติยอมรับได้

CREATE INDEX แบบปกติอาจพอใช้ได้เมื่อเทเบิลเล็ก ทราฟฟิกต่ำ หรือคุณมีหน้าต่างควบคุม มันมักเสร็จเร็วกว่าและง่ายกว่าในการรัน

พิจารณาแนวทางปกติถ้าการสร้างเสร็จอย่างสม่ำเสมอในสเตจและคุณสามารถหยุดการเขียนชั่วคราวได้ (แม้สั้น ๆ)

ถ้าคุณต้องการความเป็นเอกลักษณ์ ตัดสินใจก่อน CREATE UNIQUE INDEX CONCURRENTLY ทำงานได้ แต่จะล้มเหลวหากมีค่าซ้ำ ในหลายระบบโปรดักชัน การค้นหาและแก้ไขค่าซ้ำมักเป็นโครงการจริงจัง

การตรวจสอบก่อนทำงานในโปรดักชัน

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

  1. แน่ใจว่าคุณไม่ได้อยู่ภายในทรานแซกชัน CREATE INDEX CONCURRENTLY จะล้มเหลวถ้ารันหลัง BEGIN และบางเครื่องมือ GUI ห่อคำสั่งในทรานแซกชัน หากไม่แน่ใจ เปิดเซสชันใหม่แล้วรันคำสั่งดัชนีเท่านั้นที่นั่น

  2. ตั้งความคาดหวังเรื่องเวลาและดิสก์ การสร้างแบบ concurrent มักใช้เวลานานกว่าการสร้างปกติและต้องการพื้นที่ทำงานเพิ่มขึ้น ขึ้นการวางแผนสำหรับดัชนีใหม่รวมถึงโอเวอร์เฮดชั่วคราว และยืนยันว่ามีพื้นที่ว่างบนดิสก์อย่างสบายใจ

  3. ตั้งค่า timeout ให้สอดคล้องกับเป้าหมาย คุณต้องการให้การสร้างล้มเร็วถ้าไม่ได้ล็อก แต่ไม่ต้องการให้เซสชันตายกลางคันจาก statement timeout ที่เข้มงวด

  4. เก็บ baseline คุณต้องการหลักฐานว่าการเปลี่ยนช่วยได้และวิธีด่วนในการสังเกตการถดถอย บันทึก snapshot ก่อน: เวลาคิวรีช้า EXPLAIN (ANALYZE, BUFFERS) ตัวอย่าง และมุมมองรวดเร็วของ CPU IO การเชื่อมต่อ และพื้นที่ว่างบนดิสก์

การตั้งค่าเซสชันปลอดภัยที่ทีมหลายแห่งใช้เป็นจุดเริ่มต้น (ปรับตามกฎของคุณ):

-- Run in the same session that will build the index
SET lock_timeout = '2s';
SET statement_timeout = '0';

ทีละขั้นตอน: สร้างดัชนีด้วย CONCURRENTLY

เลือกเส้นทางการปรับใช้ของคุณ
ปรับใช้ไปยัง AppMaster Cloud หรือรันบน AWS, Azure, Google Cloud หรือแบบโฮสต์เอง
ปรับใช้แอป

ใช้ CREATE INDEX CONCURRENTLY เมื่อคุณต้องการให้ทราฟฟิกแอปยังวิ่งและยอมรับเวลาสร้างที่นานขึ้น

ก่อนอื่น ตัดสินใจให้ชัดเจนว่าคุณกำลังสร้างอะไร:

  • ระบุลำดับคอลัมน์อย่างเฉพาะเจาะจง (มันมีผล)
  • พิจารณาว่าดัชนีแบบบางส่วน (partial index) เพียงพอหรือไม่ ถ้าคิวรีส่วนใหญ่กรองแถวที่ "active" ดัชนีแบบ partial อาจเล็กกว่า เร็วกว่า และถูกบำรุงรักษาถูกกว่า

การรันอย่างปลอดภัยมีลักษณะดังนี้: จดเป้าหมายและชื่อดัชนี รันการสร้างนอกทรานแซกชัน ติดตามจนเสร็จ แล้วยืนยันว่า planner ใช้มันก่อนจะลบอะไร

-- Example: speed up searches by email for active users
CREATE INDEX CONCURRENTLY idx_users_active_email
ON public.users (email)
WHERE status = 'active';

-- Validate it exists
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'users';

-- Check the plan can use it
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM public.users
WHERE status = 'active' AND email = '[email protected]';

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

การยืนยันไม่ใช่แค่ "ดัชนีมีอยู่" ยืนยันว่า planner สามารถเลือกใช้งานได้ แล้วสังเกตเวลาคิวรีจริงหลังปรับใช้ หากดัชนีใหม่ไม่ถูกใช้ อย่ารีบลบดัชนีเก่า ให้แก้คิวรีหรือคำนิยามดัชนีก่อน

ทีละขั้นตอน: แทนที่หรือลบดัชนีโดยไม่บล็อก

รูปแบบปลอดภัยที่สุดคือเพิ่มก่อน ให้ทราฟฟิกได้ประโยชน์จากดัชนีใหม่ แล้วค่อยลบดัชนีเก่าแบบทีหลังด้วยวิธีค่อยเป็นค่อยไป แบบนี้จะมี fallback ที่ใช้งานได้

สลับดัชนีเก่าเป็นดัชนีใหม่ (ลำดับปลอดภัย)

  1. สร้างดัชนีใหม่ด้วย CREATE INDEX CONCURRENTLY.

  2. ยืนยันว่ามันถูกใช้ เช็ค EXPLAIN บนคิวรีช้าที่คุณสนใจ และติดตามการใช้งานดัชนีเมื่อเวลาผ่านไป

  3. หลังจากยืนยันแล้วเท่านั้น ให้ลบดัชนีเก่าแบบ concurrent หากความเสี่ยงสูง ให้เก็บไว้ทั้งสองดัชนีครบรอบธุรกิจก่อนลบ

การลบดัชนี: เมื่อ CONCURRENTLY ใช้ได้ (และเมื่อไม่)

สำหรับดัชนีปกติที่คุณสร้างเอง DROP INDEX CONCURRENTLY มักเป็นตัวเลือกที่ถูกต้อง สองข้อควรรู้: มันไม่สามารถรันภายในทรานแซกชันบล็อก และยังต้องล็อกสั้น ๆ ตอนเริ่มและตอนจบ จึงอาจถูกดีเลย์โดยทรานแซกชันที่รันนาน

ถ้าดัชนีนั้นเกิดจาก PRIMARY KEY หรือ UNIQUE constraint ปกติจะไม่สามารถลบโดยตรง ต้องเปลี่ยน constraint ด้วย ALTER TABLE ซึ่งอาจต้องล็อกเข้มกว่า ถือเป็นงานบำรุงรักษาที่แยกต่างหากและต้องวางแผน

การเปลี่ยนชื่อดัชนีเพื่อความชัดเจน

การเปลี่ยนชื่อ (ALTER INDEX ... RENAME TO ...) มักเร็ว แต่หลีกเลี่ยงถ้าเครื่องมือหรือมิเกรชันอ้างอิงชื่อดัชนี นิสัยที่ปลอดภัยคือเลือกชื่อที่ชัดเจนตั้งแต่ต้น

หากยังต้องการดัชนีเก่า

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

ตรวจสอบล็อกและความคืบหน้าขณะสร้างดัชนี

เปลี่ยนนิสัยการทำงานฐานข้อมูลให้เป็นการปล่อยเวอร์ชัน
สร้างเครื่องมือภายในด้วย PostgreSQL และปล่อยการเปลี่ยนแปลงพร้อมแผนการเปิดตัวและย้อนกลับที่ชัดเจน
ลองใช้ AppMaster

แม้จะใช้ CREATE INDEX CONCURRENTLY คุณก็ควรดูว่าเกิดอะไรขึ้นแบบเรียลไทม์ เหตุการณ์ที่น่าสะดุ้งส่วนใหญ่เกิดจากสองสิ่ง: เซสชันที่บล็อกที่คุณไม่สังเกต หรือทรานแซกชันที่รันนานทำให้การสร้างติดรอ

หาผู้ที่ถูกบล็อก (ใครบล็อกใคร)

เริ่มจากหาดูเซสชันที่รอกล็อก:

SELECT
  a.pid,
  a.usename,
  a.application_name,
  a.state,
  a.wait_event_type,
  a.wait_event,
  now() - a.xact_start AS xact_age,
  left(a.query, 120) AS query
FROM pg_stat_activity a
WHERE a.wait_event_type = 'Lock'
ORDER BY xact_age DESC;

ถ้าคุณต้องการตัวบล็อกที่แน่นอน ให้ติดตาม blocked_pid ไปยัง blocking_pid:

SELECT
  blocked.pid  AS blocked_pid,
  blocking.pid AS blocking_pid,
  now() - blocked.xact_start AS blocked_xact_age,
  left(blocked.query, 80)  AS blocked_query,
  left(blocking.query, 80) AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY (pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

ดูความคืบหน้าและสัญญาณว่า "ติด"

PostgreSQL เปิดเผยความคืบหน้าในการสร้างดัชนี หากคุณเห็นไม่มีความคืบหน้าเป็นเวลานาน ให้มองหาทรานแซกชันที่รันนาน (มักเป็นเซสชัน idle ที่ถือ snapshot เก่า)

SELECT
  pid,
  phase,
  lockers_total,
  lockers_done,
  blocks_total,
  blocks_done,
  tuples_total,
  tuples_done
FROM pg_stat_progress_create_index;

ติดตามแรงกดดันของระบบด้วย: ดิสก์ IO ความหน่วงการจำลอง และเวลาคิวรีที่เพิ่มขึ้น การสร้างแบบ concurrent เป็นมิตรกับ uptime แต่ยังอ่านข้อมูลเยอะ

กฎง่าย ๆ ที่ใช้ได้ในโปรดักชัน:

  • รอถ้าความคืบหน้ากำลังเคลื่อนไหวและผลกระทบต่อผู้ใช้ต่ำ
  • ยกเลิกและเลื่อนถ้าการสร้างติดหลังทรานแซกชันยาวที่คุณไม่สามารถยุติได้อย่างปลอดภัย
  • หยุดในช่วงทราฟฟิกสูงถ้า IO กระทบต่อคำถามที่ผู้ใช้เห็น
  • ยุติเป็นทางเลือกสุดท้าย และทำหลังจากยืนยันว่าเซสชันนั้นกำลังทำอะไร

สำหรับการสื่อสารทีม ให้รายงานสั้น ๆ: เวลาเริ่ม เฟสปัจจุบัน อะไรถูกบล็อก (ถ้ามี) และเมื่อคุณจะตรวจอีกครั้ง

แผนย้อนกลับ: วิธีถอยกลับอย่างปลอดภัย

ทำงานประจำให้เป็นอัตโนมัติอย่างปลอดภัย
แทนที่สคริปต์ที่เปราะบางด้วยแอปและการอัตโนมัติที่ทีมของคุณดูแลรักษาได้
เริ่มใช้ฟรี

การเปลี่ยนดัชนียังคงมีความเสี่ยงต่ำเมื่อคุณวางแผนการออกก่อนเริ่ม แผนย้อนกลับที่ปลอดภัยมักไม่ใช่การยกเลิกอย่างรุนแรง แต่เป็นการหยุดงานใหม่และเก็บดัชนีเก่าไว้

วิธีที่พบบ่อยที่งานดัชนีล้มเหลว

ความล้มเหลวในโปรดักชันส่วนใหญ่คาดการณ์ได้: การสร้างโดน timeout ใครบางคนยกเลิกระหว่างเหตุการณ์ เซิร์ฟเวอร์พื้นที่ดิสก์ใกล้เต็ม หรือการสร้างแข่งขันกับทราฟฟิกปกจนนำไปสู่ความหน่วงที่ผู้ใช้เห็น

กับ CREATE INDEX CONCURRENTLY การยกเลิกมักปลอดภัยสำหรับแอปเพราะคำสั่งยังคงรัน แต่มีเรื่องทำความสะอาด: การยกเลิกหรือการล้มเหลวของ concurrent build อาจทิ้งดัชนีที่ไม่ถูกต้องไว้

กฎการยกเลิกและทำความสะอาดอย่างปลอดภัย

การยกเลิกการสร้างแบบ concurrent ไม่ได้ย้อนกลับเหมือนทรานแซกชันปกติ PostgreSQL อาจทิ้งดัชนีที่มีอยู่แต่นำมาใช้ไม่ได้โดย planner

-- Cancel the session building the index (use the PID you identified)
SELECT pg_cancel_backend(\u003cpid\u003e);

-- If the index exists but is invalid, remove it without blocking writes
DROP INDEX CONCURRENTLY IF EXISTS your_index_name;

ก่อนลบ ให้ยืนยันสิ่งที่คุณกำลังดู:

SELECT
  c.relname AS index_name,
  i.indisvalid,
  i.indisready
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname = 'your_index_name';

ถ้า indisvalid = false มันไม่ได้ถูกใช้และปลอดภัยที่จะลบ

เช็คลิสต์การย้อนกลับแบบใช้งานได้จริงเมื่อแทนที่ดัชนีที่มีอยู่:

  • เก็บดัชนีเก่าจนกว่าดัชนีใหม่จะถูกสร้างและใช้งานได้เต็มที่
  • หากการสร้างใหม่ล้มหรือติด ยกเลิกแล้วลบดัชนีใหม่ที่ไม่ถูกต้องแบบ concurrent
  • หากคุณลบดัชนีเก่าไปแล้ว ให้สร้างมันกลับด้วย CREATE INDEX CONCURRENTLY เพื่อกู้สถานะก่อนหน้า
  • หากปัญหาเกิดจากพื้นที่ดิสก์ ให้เคลียร์พื้นที่ก่อนแล้วลองใหม่
  • หากเกิดจาก timeout ให้กำหนดหน้าต่างที่เงียบกว่าแทนการบังคับทำทันที

ตัวอย่าง: คุณเริ่มสร้างดัชนีใหม่สำหรับการค้นหาฝ่ายแอดมิน มันรัน 20 นาทีแล้วมีการแจ้งเตือนพื้นที่ดิสก์ ยกเลิกการสร้าง ลบดัชนีใหม่ที่ไม่ถูกต้องแบบ concurrent และเก็บดัชนีเก่าให้บริการ คุณสามารถลองใหม่หลังเคลียร์พื้นที่โดยไม่กระทบผู้ใช้

ข้อผิดพลาดทั่วไปที่ทำให้เกิดการหยุดให้บริการโดยไม่คาดคิด

เหตุการณ์รอบดัชนีส่วนใหญ่ไม่เกิดจาก PostgreSQL "ช้า" แต่เกิดเพราะรายละเอียดเล็ก ๆ ทำให้การเปลี่ยนปลอดภัยกลายเป็นการบล็อก

1) รัน concurrent build ภายในทรานแซกชัน

CREATE INDEX CONCURRENTLY ไม่สามารถรันในทรานแซกชันบล็อกได้ หลายเครื่องมือมิเกรชันห่อการเปลี่ยนแปลงทั้งหมดในทรานแซกชันเดียวโดยดีฟอลต์ ผลลัพธ์อาจเป็น error ตายตัว (ในกรณีดีที่สุด) หรือการดีพลอยยุ่งกับการลองใหม่ซ้ำ ๆ

ก่อนรันมิเกรชัน ให้ยืนยันว่าเครื่องมือของคุณสามารถรันคำสั่งโดยไม่มีทรานแซกชันภายนอก หรือแยกมิเกรชันเป็นขั้นตอนที่ไม่ใช้ทรานแซกชันพิเศษ

2) เริ่มตอนทราฟฟิกพีค

การสร้างดัชนีแบบ concurrent ลดการบล็อก แต่ยังเพิ่มโหลด: อ่านเพิ่ม เขียนเพิ่ม และกดดัน autovacuum การเริ่มสร้างในช่วงที่ทราฟฟิกพุ่งอาจสร้างความช้าจนรู้สึกเหมือนล่ม

เลือกช่วงเวลาที่เงียบและปฏิบัติเหมือนงานบำรุงรักษาทั่วไป

3) มองข้ามทรานแซกชันที่รันนาน

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

ฝึกนิสัย: ตรวจสอบทรานแซกชันที่รันนานก่อนเริ่ม และอีกครั้งหากความคืบหน้าหยุด

4) ลบสิ่งผิดพลาด (หรือทำลาย constraint)

ทีมมักลบดัชนีโดยจำชื่อจากความจำ หรือเอาดัชนีที่เป็นฐานของกฎความเป็นเอกลักษณ์ทิ้ง หากลบวัตถุผิด คุณอาจสูญเสียการบังคับใช้ (unique constraints) หรือทำให้ประสิทธิภาพคิวรีถดถอยทันที

เช็คลิสต์ความปลอดภัยด่วน: ยืนยันชื่อตัวดัชนีในแคตตาล็อก ยืนยันว่ามันสนับสนุน constraint หรือไม่ ตรวจสอบ schema และตาราง และแยกขั้นตอน "สร้างใหม่" ออกจาก "ลบเก่า" มีคำสั่งย้อนกลับพร้อมก่อนเริ่ม

ตัวอย่างสมจริง: เร่งการค้นหาฝ่ายแอดมิน

สร้างพอร์ทัลลูกค้า
สร้างพอร์ทัลลูกค้าที่ประสิทธิภาพและความพร้อมใช้งานยังคงสม่ำเสมอเมื่อปริมาณการใช้งานเติบโต
สร้างโปรเจกต์

ปัญหาทั่วไปคือการค้นหาฝ่ายแอดมินที่รวดเร็วในสเตจแต่ช้าในโปรดักชัน สมมติว่าคุณมีตาราง tickets ขนาดใหญ่ (หลายสิบล้านแถว) เบื้องหลังแผงแอดมินภายใน และเอเจนต์มักค้นหา "ตั๋วเปิดของลูกค้าหนึ่ง รายการล่าสุดก่อน"

คิวรีเป็นแบบนี้:

SELECT id, customer_id, subject, created_at
FROM tickets
WHERE customer_id = $1
  AND status = 'open'
ORDER BY created_at DESC
LIMIT 50;

ดัชนีเต็มบน (customer_id, status, created_at) ช่วยได้ แต่เพิ่มภาระการเขียนทุกครั้งที่อัปเดตตั๋ว รวมถึงแถวที่ปิดแล้ว หากแถวส่วนใหญ่ไม่ใช่ open ดัชนี partial มักเป็นทางออกที่ง่ายกว่า

CREATE INDEX CONCURRENTLY tickets_open_by_customer_created_idx
ON tickets (customer_id, created_at DESC)
WHERE status = 'open';

ไทม์ไลน์ปลอดภัยในโปรดักชัน:

  • Preflight: ยืนยันว่าโครงคิวรีนิ่งและตารางมีพื้นที่ว่างพอสำหรับสร้างดัชนีใหม่
  • Build: รัน CREATE INDEX CONCURRENTLY ในเซสชันแยกด้วยการตั้งค่า timeout ที่ชัดเจน
  • Validate: รัน ANALYZE tickets; และยืนยันว่า planner ใช้ดัชนีใหม่
  • Cleanup: เมื่อตรวจสอบมั่นใจแล้ว ลบดัชนีเก่าที่ซ้ำซ้อนด้วย DROP INDEX CONCURRENTLY

ลักษณะความสำเร็จ:

  • การค้นหาฝ่ายแอดมินลดจากวินาทีเหลือมิลลิวินาทีสำหรับลูกค้าทั่วไป
  • การอ่านและการเขียนปกติยังทำงานระหว่างการสร้าง
  • CPU และดิสก์ IO เพิ่มขึ้นขณะสร้างแต่ยังอยู่ในขอบเขตความปลอดภัยปกติ
  • คุณสามารถชี้ตัวเลขก่อน/หลังที่ชัดเจน: เวลาคิวรี แถวที่สแกน และประวัติล็อก

เช็คลิสต์ด่วนและขั้นตอนถัดไป

งานดัชนีปลอดภัยเมื่อคุณปฏิบัติเหมือนการปล่อยเวอร์ชันขนาดเล็ก: เตรียมตัว เฝ้าดูขณะรัน แล้วยืนยันผลก่อนจะทำความสะอาด

ก่อนเริ่ม:

  • ตั้ง timeout เพื่อไม่ให้ล็อกแปลก ๆ ค้างนาน
  • ยืนยันพื้นที่ดิสก์เพียงพอสำหรับการสร้างดัชนีใหม่
  • มองหาทรานแซกชันที่รันนานซึ่งอาจชะลอการสร้าง
  • เลือกหน้าต่างที่ทราฟฟิกต่ำและนิยามว่า "เสร็จ" คืออะไร
  • จดแผนย้อนกลับไว้ตอนนี้

ขณะรัน:

  • เฝ้าดูการบล็อกและโซ่การรอล็อก
  • ติดตามความคืบหน้าด้วย pg_stat_progress_create_index
  • สังเกตอาการที่แอป: อัตราผิดพลาด timeout และ endpoint ช้าที่เกี่ยวกับตารางนั้น
  • พร้อมยกเลิกหากการรอล็อกเพิ่มหรือตัวชี้วัดผู้ใช้พุ่ง
  • บันทึกสิ่งที่เกิดขึ้น: เวลาเริ่ม เวลาเสร็จ และการแจ้งเตือนที่เกิด

หลังเสร็จ ยืนยันว่าดัชนีถูกต้อง รันหนึ่งหรือสองคิวรีหลักเพื่อตรวจดูแผนและเวลา จากนั้นจึงค่อยลบดัชนีเก่าแบบไม่บล็อก

ถ้าคุณทำบ่อย ๆ ให้เปลี่ยนเป็นขั้นตอนการส่งมอบที่ทำซ้ำได้: runbook เล็ก ๆ ซ้อมในสเตจที่เหมือนโปรดักชัน และมีผู้รับผิดชอบชัดเจนคอยเฝ้าดู

ถ้าคุณกำลังสร้างเครื่องมือภายในหรือแผงผู้ดูแลด้วย AppMaster (appmaster.io) ควรถือการเปลี่ยนแปลงฐานข้อมูล เช่น การสร้างดัชนี เป็นส่วนหนึ่งของเช็คลิสต์การปล่อยเดียวกับการอัปเดตแบ็กเอนด์: วัดผล เฝ้าดู และมีแผนย้อนกลับที่สามารถใช้ได้ทันที

คำถามที่พบบ่อย

ทำไมการเพิ่มหรือเปลี่ยนดัชนีถึงทำให้เกิดการหยุดให้บริการได้?

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

เมื่อใดควรใช้ CREATE INDEX CONCURRENTLY แทน CREATE INDEX ปกติ?

ใช้ CREATE INDEX CONCURRENTLY เมื่อเทเบิลมีทราฟฟิกจริงและคุณไม่สามารถหยุดการเขียนได้ โดยทั่วไปเป็นตัวเลือกที่ปลอดภัยสำหรับเทเบิลขนาดใหญ่หรือมีการใช้งานสูง แม้มันจะช้ากว่าและอาจถูกรบกวนโดยทรานแซกชันที่รันนาน

CONCURRENTLY หมายความว่า “ไม่มีล็อกเลย” ไหม?

ไม่ใช่. มันลดการบล็อก แต่ไม่ใช่ปลอดล็อกทั้งหมด คุณยังมีช่วงล็อกสั้น ๆ ตอนเริ่มและตอนจบ และการสร้างดัชนีอาจรอได้หากเซสชันอื่นถือล็อกที่ไม่เข้ากันหรือทรานแซกชันที่รันนานป้องกันขั้นตอนสุดท้าย

ทำไมการบอกว่า “ทำตอนกลางคืนก็ได้” มักล้มเหลว?

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

ฉันควรตรวจอะไรบ้างก่อนรันการสร้างดัชนีแบบ concurrent ในโปรดักชัน?

ก่อนอื่นแน่ใจว่าคุณไม่ได้อยู่ภายในทรานแซกชัน เพราะ CREATE INDEX CONCURRENTLY จะล้มเหลวถ้าวิ่งหลัง BEGIN และบางเครื่องมือ GUI อาจห่อคำสั่งในทรานแซกชันโดยเงียบ ๆ ต่อไปตรวจสอบว่ามีพื้นที่ว่างบนดิสก์เพียงพอสำหรับดัชนีใหม่และค่าโอเวอร์เฮดชั่วคราว แล้วตั้ง lock_timeout สั้น ๆ เพื่อให้ล้มเหลวเร็วเมื่อไม่สามารถได้ล็อกที่ต้องการ

ควรกำหนด timeouts แบบไหนสำหรับการเปลี่ยนดัชนีอย่างปลอดภัย?

จุดเริ่มต้นทั่วไปคือการรันในเซสชันเดียวกันที่สร้างดัชนีนั้น: SET lock_timeout = '2s'; และ SET statement_timeout = '0'; นี่ช่วยให้คุณหลีกเลี่ยงการรอนานเกินไปบนล็อกโดยไม่ทำให้เซสชันถูกฆ่ากลางคันจาก statement timeout ที่เผื่อไว้แน่นเกินไป

จะรู้ได้อย่างไรว่าการสร้างดัชนีแบบ concurrent ติด และควรมองอะไรเป็นอันดับแรก?

เริ่มด้วย pg_stat_progress_create_index เพื่อดูเฟสและว่าบล็อกหรือทูเพิลกำลังเคลื่อนไหวหรือไม่ หากความคืบหน้าหยุดชะงัก ตรวจสอบ pg_stat_activity เพื่อดูการรอล็อกและมองหาทรานแซกชันที่รันนาน โดยเฉพาะเซสชัน idle ที่ถือ snapshot เก่า

วิธีที่ปลอดภัยที่สุดในการแทนที่ดัชนีที่มีอยู่โดยไม่บล็อกทราฟฟิกคืออะไร?

สร้างดัชนีใหม่แบบ concurrent ยืนยันว่า planner ใช้มันได้ (และการตอบคำถามจริงดีขึ้น) แล้วค่อย DROP INDEX CONCURRENTLY ของดัชนีเก่า วิธี "เพิ่มก่อน ลบทีหลัง" นี้เก็บ fallback ที่ใช้งานได้ถ้าดัชนีใหม่ไม่ได้ผลหรือทำให้การทำงานถดถอย

ผมสามารถลบดัชนีแบบ concurrent ได้ตลอดไหม?

DROP INDEX CONCURRENTLY มักปลอดภัยสำหรับดัชนีทั่วไป แต่ก็ยังต้องมีล็อกสั้น ๆ และไม่สามารถรันภายในทรานแซกชันบล็อกได้ ถ้าดัชนีนั้นรองรับ PRIMARY KEY หรือ UNIQUE constraint ปกติคุณต้องเปลี่ยน constraint ด้วย ALTER TABLE ซึ่งอาจต้องล็อกที่เข้มกว่าและต้องวางแผนเพิ่ม

ฉันจะย้อนกลับอย่างปลอดภัยได้อย่างไรถ้าการสร้างดัชนีแบบ concurrent ล้มเหลวหรือถูกยกเลิก?

ยกเลิกเซสชันที่กำลังสร้างดัชนีนั้น แล้วตรวจสอบว่ามีดัชนีไม่ถูกต้องค้างอยู่หรือไม่ หาก indisvalid เป็น false ให้ลบด้วย DROP INDEX CONCURRENTLY และรักษาดัชนีเก่าไว้ ถ้าคุณลบดัชนีเก่าไปแล้ว ให้สร้างกลับด้วย CREATE INDEX CONCURRENTLY เพื่อกู้พฤติกรรมเดิม

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม