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

ทำไม index ที่​คุณ​เพิ่ง​เพิ่ม​ถึง​ไม่​ถูก​ใช้

เรื่อง​นี้​เกิด​ขึ้น​ซ้ำ​แล้ว​ซ้ำ​อีก​ใน​ทุก​ทีม

หน้า​รายการออเดอร์​ช้า มี​คน​ดู query แล้ว​บอกว่า “ตาราง​นี้​ไม่มี index บน placed_at นี่​นา” เพิ่ม index วัด​ใหม่ — เร็ว​ขึ้น​นิดเดียว​หรือ​ไม่​ขึ้น​เลย

จาก​นั้น​บทสนทนา​มัก​เลี้ยว​ไป​ทาง​เดิม​ทุก​ครั้ง คือ​ไป​สงสัย​ว่า index ตัว​นั้น “ไม่​ทำงาน” หรือ​ฐาน​ข้อมูล “ไม่​ยอม​ใช้​มัน”

แล้ว​ลงเอย​ด้วย​การ​เพิ่ม index ตัว​ที่​สอง ตัว​ที่​สาม จน​ตาราง​มี index เจ็ด​ตัว​ที่​ไม่มี​ใคร​กล้า​ลบ

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

ธีสิส​ของ​หน้า​นี้

index เป็น​รายการ​ที่ เรียง​ไว้​แล้ว ประโยชน์​ทั้งหมด​ของ​มัน​มา​จาก​การ​ที่​เอนจินกระโดด​ข้าม​ไป​ยัง​ช่วง​ที่​ต้องการ​ได้ โดย​ไม่​ต้อง​อ่าน​ตั้งแต่​ต้น

คำถาม​จริง​จึง​ไม่ใช่ “มี index ไหม” แต่​คือ:

“เงื่อนไข​ของ​ฉัน​แปล​เป็น​ช่วง​ต่อ​เนื่อง​บน​ลำดับ​ที่ index เรียง​ไว้​ได้​หรือ​เปล่า และ​แปล​ได้​กี่​ช่วง”

คำ​ตอบ​มี​สาม​แบบ และ​ราคา​ต่าง​กัน​คนละ​ขั้น:

  • แปล​ได้​ช่วง​เดียว — ทาง​ที่​ถูก​ที่สุด
  • ต้อง​ใช้​หลาย​ช่วง — ราคา​ระดับ​กลาง
  • แปล​ไม่​ได้​เลย — index จะ​ยัง​อยู่​ครบ ยัง​กิน​พื้นที่ ยัง​ทำให้​ทุก INSERT ช้า​ลง แต่​ไม่มี​ใคร​ใช้​มัน

ก่อน​จะ​เดา​ว่า​ทำไม ให้​ถาม​เอนจินตรง ๆ ทุก​ตัว​มี​คำ​สั่ง​บอกว่า​มัน​วางแผน​จะ​ทำ​อะไร และ​คำ​สั่ง​นี้​ไม่​ได้​รัน query จริง จึง​ถาม​ได้​แม้​บน​ฐาน​โปร​ดัก​ชัน

เอนจินวิธี​ขอ​แผน
SQLiteEXPLAIN QUERY PLAN SELECT ...
PostgreSQLEXPLAIN SELECT ... — เติม ANALYZE เมื่อ​ต้องการ​ให้​รัน​จริง​แล้ว​รายงาน​เวลา​ที่​ใช้
SQL ServerSET SHOWPLAN_TEXT ON หรือ​กด​ปุ่ม​ดู execution plan ใน SSMS
MySQL / MariaDBEXPLAIN SELECT ...

กล่อง​ข้าง​ล่าง​คือ SQLite เพราะ​เป็น​เอนจิน​เดียว​ที่​รัน​ซ้ำ​ได้​ทันที​ใน​เครื่อง​ทุก​คน โดย​ไม่​ต้อง​ติดตั้ง​อะไร​เลย

ตาราง​ที่​ใช้​คือ orders ของ domain ฟู้ด​เดลิ​เวอรี​เดียว​กับ​ที่​คอร์ส​ทั้ง​สาย​ใน​ไซต์​นี้​ใช้ ขนาด 200,000 แถว ลูกค้า 2,000 คน และ​มี index ครบ​ตาม​ที่​คน​ปกติ​จะ​สร้าง

ลอง​กด​สลับ​รูปร่าง​ของ query ดู:

  • คู่​แรก (รูปร่าง​ของ​เงื่อนไข) เป็น​คู่​เดียว​ที่​ให้​ผลลัพธ์​ชุด​เดียวกันเป๊ะ ต่าง​กัน​แค่​วิธี​เขียน
  • อีก​สาม​คู่ จงใจ​ถาม​คนละ​คำถาม​กัน เพื่อ​ให้​เห็น​ว่า​รูปร่าง​ของ​คำถาม​เปลี่ยน​แผน​อย่างไร

ให้​ดู​บรรทัด “คืน … แถว” ใต้​แผน​ประกอบ​ทุก​ครั้ง

เปิดแผนดูเอง

orders 200,000 แถวSQLite 3.53.1

จะตัดเอาเฉพาะออเดอร์ของวันที่ 15 มิ.ย. ด้วยอะไร

query ที่รันจริง

SELECT id, customer_id, status, restaurant, placed_at, total_satang
FROM orders
WHERE date(placed_at) = '2024-06-15'

EXPLAIN QUERY PLAN ตอบว่า

SCAN orders

SCAN — ไล่ทั้งตาราง · คืน 284 แถว

column ถูกห่อด้วย function เอนจินจึงไม่รู้ว่า date() ของค่าหนึ่งจะออกมาเป็นอะไรจนกว่าจะคำนวณ และต้องคำนวณให้ครบทุกแถวก่อนถึงจะรู้ว่าแถวไหนเข้าเงื่อนไข index ที่เรียงตามค่า placed_at ดิบ ๆ จึงไม่มีประโยชน์ตรงนี้เลย แผนบอกตรง ๆ ว่า SCAN orders — ไม่แตะ index สักตัว

SCANSCAN orders
ทุกบรรทัดในกล่อง EXPLAIN QUERY PLAN คือผลลัพธ์จริงจาก SQLite 3.53.1 บนตาราง 200,000 แถว ไม่มีบรรทัดไหนถูกพิมพ์เอง — สร้างซ้ำได้ด้วย python3 scripts/index-plans/probe.py
  • SEARCH — เอนจินกระโดด​เข้าไป​ที่​ช่วง​ที่​ต้องการ​ใน index แล้ว​อ่าน​เฉพาะ​ช่วง​นั้น
  • SCAN — มัน ไม่มี​จุด​ให้​กระโดด​เข้า จึง​ต้อง​เดิน​ไป​ตาม​ลำดับ

SCAN ไม่​ได้​แปล​ว่า​ต้อง​อ่าน​จน​จบ​เสมอ​ไป ถ้า​มี LIMIT และ​ลำดับ​ที่​เดิน​อยู่​ตรง​กับ​ที่​ขอ มัน​หยุด​กลาง​ทาง​ได้

ถ้า​แผน​ของ​คุณ​ขึ้น SCAN orders ทั้ง​ที่​เพิ่ง CREATE INDEX ไป นั่น​คือ​หลักฐาน​ว่า index ตัว​นั้น​ไม่​ได้​ถูก​ใช้ ไม่ใช่​ความ​รู้สึก

flowchart TB
  Q[Your WHERE clause]
  Q --> T{Can it be written as a continuous range over the index order}
  T -->|One range| S[SEARCH - jump straight to it]
  T -->|No range| F[SCAN - no seek key, walk in order]
  W[Wrapping the column in a function] --> F
  L[Leading wildcard in LIKE] --> F
  M[Skipping the leading column] --> K[Depends on the statistics - skip-scan or SCAN]

ภาพ 1 — คำถาม​ข้อ​เดียว​ที่​ตัดสิน​ทุก​อย่าง ไม่ใช่ “มี index ไหม” แต่​คือ​เงื่อนไข​นี้​แปล​เป็น​ช่วง​ต่อ​เนื่อง​บน​ลำดับ​ที่ index เรียง​ไว้​ได้​หรือ​ไม่ · กิ่ง​ขวา​ล่าง​คือ​กิ่ง​ที่​หัวข้อ​ถัด​ไป​เปิด​ดู เพราะ​ผล​ของ​มัน​ไม่​ตายตัวอย่าง​ที่​หลาย​คน​คิด

“ถ้า​ไม่​ใช้ column นำ index จะ​ใช้​ไม่​ได้” — กฎ​นี้​ไม่​ผิด แต่​ก็​ไม่​ได้​ตอบแทน​แผน​ของ​คุณ​ได้

ใน​กล่อง​ข้าง​บน มิติ ลำดับ column ใน index ทดสอบ​กฎ​ข้อ​นี้​ตรง ๆ — index มี (customer_id, status) แล้ว query ถาม​ด้วย status อย่าง​เดียว

ผล​ที่​ได้​บน​ข้อมูล​ชุด​นี้​ไม่ใช่ SCAN อย่าง​ที่​กฎ​บอก แต่​เป็น

SEARCH orders USING INDEX ix_orders_customer_status (ANY(customer_id) AND status=?)

ANY(customer_id) คือ​ของ​จริง​ที่​เกิด​ขึ้น เอนจินวน​เปิด​ค่า​ของ customer_id ที​ละ​ค่า​จน​ครบ​ทุก​ค่าที่​มี​อยู่ แล้ว seek ด้วย status ใน​แต่ละ​ค่า

เทคนิค​นี้​ชื่อ skip-scan และ SQLite จะ​เลือก​ใช้​ก็​ต่อ​เมื่อ​สถิติ​จาก ANALYZE บอกว่า column นำ​มี​ค่า​ไม่​ซ้ำ​กัน​น้อย​พอที่​จะ​คุ้ม (ใน​ตาราง​นี้​ลูกค้า 2,000 คน​ต่อ 200,000 แถว)

อย่า​อ่าน​ตรง​นี้​ผิด​ด้าน

skip-scan ไม่​ได้​แปล​ว่า​กฎ​เรื่อง column นำ​เป็น​เรื่อง​โกหก

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

ถ้า​ลูกค้า​มี 200,000 คน​แทนที่​จะ​เป็น 2,000 การวน​เปิด​ที​ละ​ค่า​จะ​ไม่​คุ้ม แล้ว​แผน​ก็​จะ​กลับ​ไป​เป็น SCAN

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

การ​มี index ที่​มี column นำ​เป็น status ยัง​คง​เป็น​คำ​ตอบ​ที่​ดี​กว่า​สำหรับ query ที่​ถาม​ด้วย status เป็น​หลัก — skip-scan เป็น​ตาข่าย​กัน​ตก ไม่ใช่​การ​ออกแบบ

SEARCH ไม่​ได้​แปล​ว่า​เร็ว และ SCAN ไม่​ได้​แปล​ว่า​ช้า

หัวข้อ​ที่​มีชื่อ​ว่า “SEARCH ไม่​ได้​แปล​ว่า​เร็ว และ SCAN ไม่​ได้​แปล​ว่า​ช้า”

สอง​คำ​นี้​บอกว่า​เอนจิน เข้าถึง​ข้อมูล​ด้วย​วิธี​ไหน ไม่​ได้​บอกว่า​เร็ว​หรือ​ช้า

มี​อย่าง​น้อย​สาม​กรณี​ที่​อ่าน​ผิด​กัน​บ่อย

query ที่​เอา​ออเดอร์​ล่าสุด 20 รายการ​ของ​ทั้ง​ระบบ (ไม่​กรอง​ลูกค้า) ได้​แผน​ว่า SCAN orders USING INDEX ix_orders_placed_at

เอนจิน​ไล่ index จาก​ปลาย​เข้า​มา​แล้ว​หยุด​เมื่อ​ครบ 20 แถว คำ​ว่า SCAN ตรง​นี้​จึง​เป็น​ทาง​ที่​ถูก​ที่สุด​เท่า​ที่​มี ไม่ใช่​ปัญหา

ใน​กล่อง​ข้าง​บน มิติ ORDER BY แสดง​แผนที่​ขึ้น SEARCH ตาม​ด้วย​บรรทัด​ที่​สอง​ว่า USE TEMP B-TREE FOR ORDER BY

บรรทัด​ที่​สอง​นี่แหละ​คือ​ราคา — เอนจิน​หา​แถว​ของ​ลูกค้า​คน​นั้น​เจอ​ด้วย index จริง แต่​เรียง​ตาม​ที่​ขอ​ไม่​ได้ จึง​ต้อง​อ่าน​ให้​ครบ​ทุก​แถว​ของ​ลูกค้า​คน​นั้น แล้ว​เปิด​โต๊ะ​เรียง​ใหม่​ใน​หน่วย​ความ​จำ

LIMIT 20 ใน​กรณี​นี้​ไม่​ได้​ช่วย​ลด​การ​อ่าน​ลง​เลย

มิติ LIKE ใน​กล่อง​ข้าง​บน​ขึ้น SEARCH และ​คืน​มา 74,988 แถว​จาก 200,000 แถว การกระโดด​ถูก​จุด​ไม่​ได้​ช่วย​อะไร​มาก เมื่อ​ช่วง​ที่​กระโดด​ไป​ถึง​กิน​พื้นที่​ขนาด​นั้น

ตรง​กัน​ข้าม ฝั่ง LIKE '%ครัว' ที่​ได้ SCAN นั้น​คืน​มา 0 แถว — เอนจิน​ไล่​ครบ​ทั้ง 200,000 แถว​เพื่อ​จะ​พบ​ว่า​ไม่มี​สัก​แถว​ที่​ตรง

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

เส้น​แบ่ง อะไร​จริง​กับ​ทุก​เอนจิน อะไร​เป็น​ของ SQLite

หัวข้อ​ที่​มีชื่อ​ว่า “เส้น​แบ่ง อะไร​จริง​กับ​ทุก​เอนจิน อะไร​เป็น​ของ SQLite”

นี่​คือ​ส่วน​ที่​บทความ​แนว​นี้​มัก​เบลอ แล้ว​ผู้​อ่าน​ก็​เอา​ไป​ใช้​ผิด​เอนจิน

สี่​ข้อ​นี้​เป็น​ผล​จาก​การ​ที่​ข้อมูล​ถูก​เรียง​ไว้ ไม่ใช่​ผล​จาก​การ​ตัดสิน​ใจ​ของ​ผู้​พัฒนา​เอนจิน​คน​ไหน

  • ห่อ column ด้วย function แล้ว seek ไม่​ได้ เพราะ​เอนจิน​ไม่รู้​ว่า function จะ​ให้​ผล​อะไร​จนกว่า​จะ​คำนวณ​ที​ละ​แถว จึง​หา​จุด​เริ่ม​ช่วง​ใน index ไม่​ได้ — แม้​ใน​กรณี​ที่​แถว​ที่​ตรง​เงื่อนไข​จะ​อยู่​ติด​กัน​จริง ๆ ก็ตาม
  • LIKE ที่​เปิด​ชาย​ซ้าย​หา​ช่วง​เริ่มต้น​ไม่​ได้ เพราะ​แถว​ที่​ตรง​เงื่อนไข​ไม่​ได้​อยู่​ติด​กัน​ใน​ลำดับ​ที่​เรียง​ไว้
  • composite index เรียง​ตาม column แรก​ก่อน column ถัด​ไป​เรียง​กันเอง​ภายใน​ค่า​ของ column แรก ลำดับ​นี้​จึง​สำคัญ​เสมอ
  • index ที่​เรียง​ตรง​กับ ORDER BY ทำให้​ไม่​ต้อง​เรียง​ใหม่ ประโยชน์​ข้อ​นี้​แยก​จาก​ประโยชน์​ใน​การกรอง และ​คน​มัก​ลืม​นับ

ขยาย​ข้อ​แรก​อีก​นิด เพราะ​เป็น​ข้อ​ที่​คน​แปลก​ใจ​บ่อย​ที่สุด — date(placed_at) บน​ข้อความ ISO-8601 ที่​ผล​ของ​มัน​เรียง​ตาม​ค่า​ดิบ​พอดี ก็​ยัง​ไม่​ถูก​ใช้ ทาง​แก้​คือ index บน​นิพจน์ หรือ​ย้าย function ไป​อยู่​ฝั่ง​ค่า​คงที่ เช่น​เขียน​เป็น​ช่วง​แทน

ข้อ​นี้​จริง​กับ​ทุก​เอนจิน​ใน​ความหมาย​ว่า ไม่มี​เอนจิน​ไหน​รับประกัน​ให้ ส่วน​จะ​มี​เอนจิน​ไหน optimise กรณี function ที่​รักษา​ลำดับ​ให้​บ้าง ต่าง​กัน​ไป​ตาม​เอนจิน​และ​ตาม​รุ่น

เป็น​ของ SQLite ใน​รุ่น​และ​การ​ตั้ง​ค่าที่​หน้า​นี้​ใช้

หัวข้อ​ที่​มีชื่อ​ว่า “เป็น​ของ SQLite ใน​รุ่น​และ​การ​ตั้ง​ค่าที่​หน้า​นี้​ใช้”

อย่า​เอา​ไป​อ้าง​กับ​เอนจินอื่น​โดย​ไม่​วัด

  • ชื่อ​ใน​แผน (SEARCH / SCAN / USE TEMP B-TREE) เป็น​คำ​ของ SQLite เอนจินอื่น​ใช้​คำ​อื่น เช่น Postgres ใช้ Index Scan / Seq Scan / Sort
  • การ​มี skip-scan — ใน​หน้า​นี้ SQLite เลือก​ใช้ ส่วน​เอนจินอื่นรองรับ​หรือ​ไม่​นั้น​ต่าง​กัน​ไป​ตาม​เอนจิน​และ​ตาม​รุ่น ห้าม​เดา ให้​เปิด​แผน​ของ​เอนจินตัวเอง​ดู
  • เรื่อง LIKE กับ collation — เล่า​แยก​ข้าง​ล่าง เพราะ​เป็น​จุด​ที่​หลอก​คน​มาก​ที่สุด​ใน​หน้า​นี้

ใน​กล่อง​ข้าง​บน LIKE 'ครัว%' ไป​ใช้ index ตัว​ที่​สร้าง​ด้วย COLLATE NOCASE ไม่ใช่ index ธรรมดา

และ​เหตุผล​ไม่ใช่​เรื่อง​ลำดับ​ของ​ข้อมูล ชื่อ​ร้าน​ใน​ตาราง​นี้​เป็น​ภาษา​ไทย​ล้วน BINARY กับ NOCASE เรียง​ออก​มา​เหมือน​กัน​ทุก​ประการ index ธรรมดา​จึง​ตอบ​คำถาม​นี้​ได้​อยู่​แล้ว​ใน​ทาง​ทฤษฎี

สิ่ง​ที่​เกิด​ขึ้น​จริง​คือ SQLite ตัดสิน​จาก collation ที่ ประกาศ​ไว้​บน index ล้วน ๆ

พอ LIKE ไม่​สน​ตัวพิมพ์​เล็ก​ใหญ่​เป็น​ค่า​เริ่มต้น index ที่​ประกาศ​เป็น BINARY จึง​ถูก​ตัด​ออก​ทันที โดย​ไม่​สน​ว่า​ข้อมูล​จริง​เรียง​อย่างไร ถ้า​ใน​ฐาน​มี​แต่ index ธรรมดา บรรทัด​นั้น​จะ​กลาย​เป็น SCAN ทั้ง​ที่ pattern ถูกต้อง​แล้ว

ใน​ทาง​กลับ​กัน GLOB 'ครัว*' ซึ่ง​เทียบ​แบบ​สน​ตัวพิมพ์ กลับ​ไป​ใช้ index ธรรมดา​ได้

เป็น​คู่​ที่​ยืนยัน​ว่าที่ index ตัว​นั้น​ถูก​ข้าม ไม่ใช่​เพราะ​มัน​เรียง​ผิด แต่​เพราะ กติกา​ของ​ตัว​ดำเนิน​การ​กับ collation ที่​ประกาศ​ไว้​ไม่​ตรง​กัน

เอนจินอื่นมี​กติกา​ของ​ตัวเอง​ใน​เรื่อง​เดียวกัน​นี้ ให้​ไป​หา​คำ​ว่า collation กับ operator class ใน​คู่มือ​ของ​เอนจิน​นั้น อย่า​เอา​กติกา​ของ SQLite ไป​สวม

กติกา​ของ​ตัวเลข​ใน​หน้า​นี้

แผน​ทุก​บรรทัด​ใน​หน้า​นี้​เป็น​ผลลัพธ์​ตรง ๆ จาก SQLite ที่​รัน​บน​ตาราง 200,000 แถว ผลิต​โดย scripts/index-plans/probe.py ใน repo ของ​ไซต์​นี้

script นั้น​ใช้​แต่ library มาตรฐาน​ของ Python ไม่​ต้อง​ต่อ​เน็ต ไม่​ต้อง​มี server

และ​มี​เทส​ที่​รัน script ซ้ำ​แล้ว​เทียบ​กับ file ข้อมูล​ที่ commit ไว้​ที​ละ byte ถ้า​ใคร​แก้ตัวเลข​ใน​หน้า​นี้​ด้วย​มือ เทส​จะ​แดง​ทันที

รูปร่าง​ที่​พัง​มัก​ไม่​ได้​ถูก​เขียน​เป็น SQL คน​เขียน​ไม่​ได้​พิมพ์ date(placed_at) ด้วย​ตัวเอง แต่​เขียน LINQ ที่​แปล​ออก​มา​เป็น​แบบ​นั้น

รูปแบบ​ที่​ควร​ระวัง​คือ​ทุก​ครั้ง​ที่​มี การ​เรียก method หรือ​อ่าน property คร่อม column ใน​ฝั่ง​ซ้าย​ของ​การ​เปรียบเทียบ

// ฝั่งซ้ายถูกห่อ — เสี่ยงกลายเป็นเงื่อนไขที่ seek ไม่ได้
var slow = db.Orders.Where(o => o.PlacedAt.Date == day);
var alsoSlow = db.Orders.Where(o => o.CustomerId.ToString() == id);
var andThis = db.Orders.Where(o => o.Restaurant.ToLower().Contains(term));
// เขียนเป็นช่วงบน column ดิบ — เอนจินยัง seek ได้
var fast = db.Orders.Where(o => o.PlacedAt >= day && o.PlacedAt < day.AddDays(1));

ประเด็น​ไม่​ได้​อยู่​ที่​ว่า LINQ สาม​บรรทัด​แรก​แปล​ออก​มา​เป็น SQL หน้าตา​แบบ​ไหน​เป๊ะ ๆ ซึ่ง​เปลี่ยน​ไป​ตาม​รุ่น​ของ EF Core และ​ตาม provider

ประเด็น​คือ อย่า​เดา ให้​ทำ​สอง​ขั้นตอน​นี้​แทน:

  1. เรียก .ToQueryString() บน IQueryable ดู SQL ที่​จะ​ถูก​ส่ง​จริง
  2. เอา SQL นั้น​ไป​ขอ​แผน​จาก​เอนจิน

รวม​กัน​ใช้​เวลา​ไม่​ถึง​หนึ่ง​นาที และ​ตอบ​คำถาม​ที่​การ​อ่าน code เปล่า ๆ ตอบ​ไม่​ได้

เรื่อง N+1, Include และ​การ project ให้​เหลือ​เฉพาะ column ที่​ใช้ ซึ่ง​เป็น​อีก​ครึ่ง​หนึ่ง​ของ​งาน query ที่​ช้า อยู่​ใน​บทเรียน query ที่​ไม่​ฆ่า​ฐาน​ข้อมูล ของ​คอร์ส EF Core

หน้า​นี้​จงใจ​ไม่​ทับ​กับ​บทเรียน​นั้น — บทเรียน​นั้น​ว่าด้วย จำนวน query ที่​ถูก​ส่ง ส่วน​หน้า​นี้​ว่าด้วย สิ่ง​ที่​เกิด​ขึ้น​หลัง​จาก query ตัว​เดียว​ถูก​ส่ง​ไป​แล้ว

ทำไม​หน้า​นี้​ไม่มี​ตัวเลข​ว่า​เร็ว​ขึ้น​กี่​เท่า

หัวข้อ​ที่​มีชื่อ​ว่า “ทำไม​หน้า​นี้​ไม่มี​ตัวเลข​ว่า​เร็ว​ขึ้น​กี่​เท่า”

เพราะ​ตัวเลข​นั้น​เป็น​ของ​เครื่อง​ที่​วัด ไม่ใช่​ของ query

ตอน​เตรียม​หน้า​นี้ คู่ date(placed_at) กับ​ช่วง​เวลา​ถูก​จับ​เวลา​บน​หลาย​เครื่อง ได้​อัตราส่วน​ไม่​ตรง​กัน​เลย​สัก​ครั้ง ทั้ง​ที่​แผนที่​ออก​มา​เหมือน​กัน​ทุก​ตัว​อักษร

ตัวเลข​พวก​นั้น​ขึ้น​กับ cache ขึ้น​กับ disk ขึ้น​กับ​ว่า​เครื่อง​กำลัง​ทำ​อะไร​อยู่​ตอน​นั้น

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

สิ่ง​ที่​ยืนยัน​ซ้ำ​ได้​และ​ไม่​ขึ้น​กับ​เครื่อง​คือ แผน หน้า​นี้​จึง​ยืนยัน​แค่​แผน

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

ถ้า index ที่​เพิ่ง​เพิ่ม​ไม่​ถูก​ใช้ ให้​ไล่​ตาม​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “ถ้า index ที่​เพิ่ง​เพิ่ม​ไม่​ถูก​ใช้ ให้​ไล่​ตาม​นี้”

ห้า​ข้อ​นี้​เรียง​ตาม​ลำดับ​ที่​ควร​ทำ​จริง ข้อ 1 คือ​ข้อ​ที่​คน​ข้าม​บ่อย​ที่สุด

  1. ขอ​แผน​ก่อน​แตะ​อย่าง​อื่น แล้ว​อ่าน​ให้​ครบ​ทุก​บรรทัด ไม่ใช่​แค่​คำ​แรก SEARCH ที่​ตาม​ด้วย USE TEMP B-TREE หรือ​ที่​มี ANY(...) อยู่​ใน​วงเล็บ ยัง​เป็น​ปัญหา​ของ index อยู่ (ข้อ 3 กับ​ข้อ 4 คือ​ทาง​แก้​ของ​สอง​กรณี​นั้น) สิ่ง​ที่​ห้าม​ทำ​คือ​เพิ่ม index โดย​ไม่​ได้​ดู​แผน
  2. ดู​ฝั่ง​ซ้าย​ของ​ทุก​เงื่อนไข มี function มี​การ​แปลง​ชนิด หรือ​มี​การ​คำนวณ​คร่อม column อยู่​หรือ​เปล่า ถ้า​มี ให้​ย้าย​ไป​อยู่​ฝั่ง​ค่า​คงที่ หรือ​เขียน​เป็น​ช่วง​แทน
  3. เทียบ​ลำดับ column ของ index กับ​เงื่อนไข​ที่​ใช้​จริง column นำ​ต้อง​เป็น column ที่ query ถาม​เสมอ ไม่ใช่ column ที่​ดู​สำคัญ​ที่สุด
  4. ดู ORDER BY เป็น​เงื่อนไขข้อ​หนึ่ง​ด้วย ถ้า​แผน​มี​บรรทัด​ที่​บอกว่า​ต้อง​เรียง​ใหม่ การ​เติม column ที่​ใช้​เรียง​ต่อ​ท้าย index มักได้​ผลกว่าการ​สร้าง index ใหม่​ทั้ง​ตัว
  5. สงสัย​สถิติ​เป็น​ข้อ​สุดท้าย ถ้า​ทุก​อย่าง​ข้าง​บน​ถูก​แล้ว​แผน​ยัง​เลือก​ทาง​ที่​แปลก ให้​อัปเดต​สถิติ (ANALYZE หรือ​คำ​สั่ง​เทียบเท่า​ของ​เอนจิน​นั้น) แล้ว​ขอ​แผน​ใหม่

ข้อ 1 เป็น​ข้อ​ที่​ประหยัด​เวลา​ได้​มาก​ที่สุด ถ้า​คุณ​เดา​ว่า​ฐาน​ข้อมูล​กำลัง​ทำ​อะไร​อยู่ แสดง​ว่า​คุณ​ยัง​ไม่​ได้​ถาม​มัน


อ่าน​ต่อ