Case study · ระบบ NPA · 2026

ทำหน้ารายการหลักประกันให้เร็วขึ้น 89 เท่า

หน้ารายการหลักประกันเป็นหน้าที่ทีม NPA เปิดทั้งวัน แต่ทุกครั้งที่กดเปลี่ยนหน้าต้องรอเกือบ 20 วินาที นี่คือวิธีที่ผมหาสาเหตุ แก้ และพิสูจน์ว่าข้อมูลที่แสดงยังถูกต้องเหมือนเดิมทุกแถว

เปิดหน้ารายการ
18 วิ 0.2 วิ
Export ทั้งหมด
35 วิ 4 วิ
ข้อมูลที่ต้องอ่าน (logical reads)
257,424 9,510
เวลา CPU
2,188 ms 78 ms

01 ปัญหา

หน้ารายการแสดงทีละ 20 แถวจากทรัพย์ราว 3,500 รายการ แต่ใช้เวลา 18–20 วินาทีทุกหน้า หน้าลึกๆ ช้าพอกัน และปุ่ม Export ใช้ 35 วินาที ผู้ใช้จึงต้องรอแทบทุกครั้งที่ค้นหาหรือเปลี่ยนหน้า

02 หาสาเหตุ

  • query นับเลขแถวและยอดรวมด้วย ROW_NUMBER() / COUNT() OVER() ทำให้ฐานข้อมูลต้องประมวลผลทุกแถวที่ผ่านตัวกรอง (3,500+ แถว) ให้ครบก่อน แล้วค่อยตัดเหลือ 20 แถว
  • ตัวค้นหา "สถานะช่างล่าสุด" (OUTER APPLY … TOP 1) จึงถูกรัน 3,500 ครั้ง แทนที่จะเป็น 20 ครั้ง และตารางที่มันค้นไม่มี index ที่ใช้ได้
  • ส่วนแบ่งหน้า join ครบ 14 ตาราง ทั้งที่เงื่อนไขค้นหาใช้แค่ไม่กี่ตาราง หนึ่งในนั้นเป็นการ join ข้ามฐานข้อมูลที่ต้องแปลง collation จึงใช้ index ไม่ได้ พอเป็นหน้าที่ 2 ขึ้นไป optimizer เลือก nested loop ที่อ่านตารางนั้นซ้ำทุกแถว

03 วิธีแก้

  • แบ่งหน้าก่อนบนคอลัมน์แคบๆ (id + เลขแถว + ยอดรวม) ด้วย OFFSET / FETCH แล้วค่อย join กลับไปดึงข้อมูลเต็มและสถานะช่างเฉพาะ 20 แถวของหน้านั้น
  • ในขั้นแบ่งหน้า join เฉพาะตารางที่มีผลต่อจำนวนแถว และตารางที่เงื่อนไขค้นหาอ้างถึงจริง
  • บังคับ hash join กับข้อมูลข้ามฐานข้อมูล ให้อ่านครั้งเดียวแทนการอ่านซ้ำทุกแถว
  • เปลี่ยนตัวหาสถานะช่างล่าสุดเป็น derived table + ROW_NUMBER ให้อ่านตารางครั้งเดียว ทั้งตอนแสดงหน้าและตอน Export

04 พิสูจน์ว่าไม่พัง

  • ตรวจก่อนแก้ว่า join ไม่ทำให้แถวซ้ำ: COUNT(*) = COUNT(DISTINCT id) = 3,503
  • เทียบผลลัพธ์กับ query เดิมทีละแถว ใน 9 ชุดตัวกรอง ตรงกันทั้งหมด ไม่มีแถวไหนต่าง
  • เคสขอบ (ช่วงหน้าที่ผิด) ยังคืนผลแบบเดิม คือ 0 แถว ไม่ใช่ทุกแถว
  • จดเหตุผลของ collation และ hash hint ไว้ในเอกสาร GOTCHAS คนที่มาแก้ต่อจะได้ไม่ลบทิ้งโดยไม่รู้

05 ผลที่วัดได้

วัดที่ก่อนหลังเร็วขึ้น
หน้า 1 (ไม่กรอง)17,962 ms203 ms88.5×
หน้า 3 (ไม่กรอง)18,352 ms205 ms89.5×
หน้าลึก18,566 ms140 ms133×
Export35 วิ4 วิ8.8×
Logical reads257,4249,51027×
CPU2,188 ms78 ms28×

06 บั๊กที่เจอระหว่างทาง

คอลัมน์ "ช่างปรับสถานะ" ขึ้นเป็น "-" ทั้งที่ช่างกดอัปเดตแล้ว เพราะแอปมือถือกับหน้าเว็บเขียนสถานะลงคนละที่ (วัดจริง: 15 ทรัพย์มีสถานะ แต่มีประวัติแค่ 3) แก้ให้ทั้งสองทางเขียนครบทั้งสองที่ และกันแถวซ้ำเวลาช่างกดส่งฟอร์มหลายรอบ

07 อีกหน้าที่ช้า แต่คนละสาเหตุ

หน้ารับเรื่องคดีขับไล่ช้าด้วยอีกเหตุผล: query ที่ทำงานแบบ parallel รอกันเองตอนเครื่องฐานข้อมูลมีงานเยอะ พอจำกัดให้ขั้นนั้นทำงานแบบเดียว (MAXDOP 1) แท็บแรกเร็วขึ้นจาก 33.8 วิ เหลือ 1.9–2.4 วิ และ Excel จาก 13.5 วิ เหลือ 2–3 วิ ผลลัพธ์เท่าเดิม

08 AI อยู่ตรงไหนในงานนี้

ทุก commit ของงานนี้ทำร่วมกับ Claude Code ส่วนที่ผมรับผิดชอบเองคือวัดเวลาบนข้อมูลชุดเดียวกัน ตรวจผลลัพธ์ทีละแถวว่าไม่เปลี่ยน แล้วจึงขึ้นระบบพร้อม rollback