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

สคีมา vs การปรับแต่งคิวรี: ความหมายของบทความนี้
เมื่อแอปรู้สึกช้า สัญชาตญาณแรกมักจะเป็นการ “แก้ SQL” ความรู้สึกแบบนั้นเข้าใจได้: คิวรีตัวเดียวเห็นได้ ชี้วัดได้ และหาง่ายที่จะตำหนิ คุณสามารถรัน EXPLAIN เพิ่มดัชนี ปรับ JOIN และบางครั้งก็เห็นผลทันที
แต่ในช่วงแรกของผลิตภัณฑ์ ปัญหาความช้ามักมาจาก รูปร่างของข้อมูล เท่า ๆ กับข้อความคิวรี ถ้าสคีมาบังคับให้คุณต่อสู้กับฐานข้อมูล การปรับแต่งคิวรีจะกลายเป็นวงจรตบหนูไม่สิ้นสุด
การออกแบบสคีมา (ภาษาเรียบง่าย)
การออกแบบสคีมา คือวิธีที่คุณจัดระเบียบข้อมูล: ตาราง คอลัมน์ ความสัมพันธ์ และกฎต่าง ๆ รวมถึงการตัดสินใจ เช่น:
- อะไรคือ “สิ่ง” ที่ควรมีตารางของตัวเอง (ผู้ใช้, คำสั่งซื้อ, เหตุการณ์)
- ตารางเชื่อมโยงกันอย่างไร (one-to-many, many-to-many)
- อะไรต้องเป็นเอกลักษณ์หรือต้องมีค่า (ข้อจำกัด)
- จะแสดงสถานะและประวัติอย่างไร (timestamps, ฟิลด์สถานะ, ระเบียน audit)
การออกแบบสคีมาที่ดีจะทำให้วิธีการถามคำถามตามธรรมชาติเป็นวิธีที่ เร็ว ด้วย
การปรับแต่งคิวรี (ภาษาเรียบง่าย)
การปรับแต่งคิวรี คือการปรับปรุงวิธีดึงหรืออัพเดตข้อมูล: เขียนคิวรีใหม่ เพิ่มดัชนี ลดงานที่ไม่จำเป็น และหลีกเลี่ยงรูปแบบที่ทำให้เกิดการสแกนขนาดใหญ่
ทั้งสองสำคัญ—แต่ลำดับเวลาสำคัญกว่า
บทความนี้ไม่ได้บอกว่า “สคีมาดี คิวรีแย่” แต่มันเกี่ยวกับลำดับการทำงาน: ทำให้พื้นฐานของสคีมาฐานข้อมูลถูกต้องก่อน แล้วค่อยปรับคิวรีที่จำเป็นจริง ๆ
คุณจะได้เรียนรู้ว่าทำไมการตัดสินใจด้านสคีมามักเป็นตัวกำหนดประสิทธิภาพในช่วงแรก วิธีสังเกตว่าเมื่อไรสคีมาเป็นคอขวดจริง ๆ และวิธีพัฒนามันอย่างปลอดภัยเมื่อแอปเติบโต บทความนี้เขียนเพื่อทีมผลิตภัณฑ์ ผู้ก่อตั้ง และนักพัฒนาที่สร้างแอปจริง ๆ — ไม่ใช่นักฐานข้อมูลเฉพาะทาง
ทำไมการออกแบบสคีมาจึงขับเคลื่อนประสิทธิภาพช่วงแรก
ในช่วงต้น ประสิทธิภาพมักไม่ใช่เรื่อง SQL อัจฉริยะ แต่มาจากปริมาณข้อมูลที่ฐานข้อมูลถูกบังคับให้แตะต้อง
โครงสร้างกำหนดปริมาณที่คุณต้องสแกน
คิวรีจะมีความคัดกรองได้เท่าที่โมเดลข้อมูลอนุญาต ถ้าคุณเก็บ status, type หรือ owner ในฟิลด์ที่ไม่มีโครงสร้างชัดเจน (หรือกระจายอยู่ในตารางที่ไม่สอดคล้องกัน) ฐานข้อมูลมักต้องสแกนแถวมากขึ้นเพื่อหาว่าสิ่งใดตรงกัน
สคีมาที่ดีจะลดพื้นที่ค้นหาโดยธรรมชาติ: คอลัมน์ชัดเจน ประเภทข้อมูลสอดคล้อง และตารางมีขอบเขตชัด ทำให้คิวรีคัดกรองได้เร็วกว่านี้และอ่านหน้าข้อมูลจากดิสก์หรือหน่วยความจำน้อยลง
ขาดคีย์ทำให้งานแพงขึ้น
เมื่อขาด primary key และ foreign key (หรือไม่ได้บังคับใช้) ความสัมพันธ์กลายเป็นการเดา และนั่นผลักงานไปที่เลเยอร์คิวรี:
- การ
JOINจะใหญ่ขึ้นเพราะไม่มีเส้นทางการเชื่อมที่มีดัชนีเชื่อถือได้ - ตัวกรองซับซ้อนขึ้นเพราะต้องชดเชยการทำซ้ำ ค่า null และค่าที่ “เกือบตรงกัน”
ถ้าไม่มีคอนสเทรนต์ ข้อมูลไม่ดีจะสะสม—ทำให้คิวรีช้าลงเมื่อแถวเพิ่มขึ้น
ดัชนีตามสคีมา (และไม่ได้แก้ได้ทุกอย่าง)
ดัชนีมีประโยชน์เมื่อมันตรงกับเส้นทางการเข้าถึงที่คาดเดาได้: การเชื่อมผ่าน foreign key การกรองโดยคอลัมน์ที่กำหนดอย่างดี การเรียงตามฟิลด์ที่ใช้บ่อย ถ้าสคีมาเก็บแอตทริบิวต์สำคัญในตารางผิด ผสมความหมายในคอลัมน์เดียว หรือพึ่งพาการแยกข้อความ ดัชนีไม่สามารถช่วยได้ — คุณยังคงต้องสแกนและแปลงข้อมูลมากเกินไป
เร็วโดยดีฟอลต์
ด้วยความสัมพันธ์ที่สะอาด ตัวระบุตัวตนที่มั่นคง และขอบเขตตารางที่สมเหตุสมผล คิวรีประจำวันจำนวนมากจะเป็น “เร็วโดยดีฟอลต์” เพราะแตะข้อมูลน้อยลงและใช้เงื่อนไขที่เป็นมิตรกับดัชนี การปรับแต่งคิวรีจึงเป็นขั้นตอนสุดท้าย แทนที่จะเป็นการดับไฟซ้ำ ๆ
ความเป็นจริงในช่วงเริ่มต้น: การเปลี่ยนแปลงเป็นเรื่องปกติ
ผลิตภัณฑ์ในช่วงเริ่มต้นไม่ได้มี “ความต้องการคงที่” แต่มีการทดลอง ฟีเจอร์ถูกปล่อย ถูกเขียนใหม่ หรือหายไป ทีมขนาดเล็กต้องจัดการความกดดันของโรดแมป การซัพพอร์ต และโครงสร้างพื้นฐาน โดยมีเวลาจำกัดในการกลับมาทบทวนการตัดสินใจเดิม
สิ่งที่เปลี่ยนบ่อยที่สุด
ไม่ค่อยใช่ข้อความ SQL ที่เปลี่ยนก่อน แต่มักเป็นความหมายของข้อมูล: สถานะใหม่ ความสัมพันธ์ใหม่ ฟิลด์ “เราจำเป็นต้องเก็บ…” ใหม่ และเวิร์กโฟลว์ทั้งหมดที่ไม่ได้จินตนาการตอนเปิดตัว การเปลี่ยนแปลงแบบนี้เป็นเรื่องปกติ—และเป็นเหตุผลที่การเลือกสคีมาสำคัญในช่วงแรก
ทำไมการแก้สคีมาทีหลังยากกว่าการแก้คิวรี
การเขียนคิวรีใหม่มักถอยกลับได้และเป็นท้องถิ่น: คุณส่งการปรับปรุง วัดผล และยกเลิกถ้าจำเป็น
การเขียนสคีมาใหม่ต่างออกไป เมื่อคุณเก็บข้อมูลลูกค้าที่แท้จริงแล้ว การเปลี่ยนโครงสร้างกลายเป็นโปรเจกต์:
- มิเกรชั่นที่ล็อกตาราง หรือทำให้การเขียนช้าลงช่วงพีค
- การ backfill เพื่อเติมคอลัมน์ใหม่หรือสร้างข้อมูลอนุพันธ์ใหม่
- การเขียนคู่หรือเงา เพื่อให้แอปทำงานระหว่างการเปลี่ยนผ่าน
- ความเสี่ยง downtime ถ้าการเปลี่ยนไม่สามารถทำออนไลน์ได้
แม้มีเครื่องมือดี การเปลี่ยนสคีมายังต้องการการประสานงาน: อัพเดตโค้ดแอป ซิงค์การปรับใช้งาน และตรวจสอบความถูกต้องของข้อมูล
การตัดสินใจในช่วงแรกส่งผลสะสม
เมื่อฐานข้อมูลยังเล็ก สคีมาที่ไม่เรียบร้อยอาจดูว่า “ใช้ได้” แต่เมื่อแถวเพิ่มจากพันเป็นล้าน การออกแบบเดิมจะสร้างการสแกนที่ใหญ่ขึ้น ดัชนีหนักขึ้น และการ join ที่แพงขึ้น—แล้วฟีเจอร์ใหม่ทุกตัวก็สร้างบนฐานนั้น
เป้าหมายในช่วงแรกจึงไม่ใช่ความสมบูรณ์แบบ แต่เป็นการเลือกสคีมาที่รับการเปลี่ยนแปลงได้โดยไม่บังคับให้เกิดมิเกรชั่นเสี่ยงทุกครั้งที่ผลิตภัณฑ์เรียนรู้สิ่งใหม่
พื้นฐานการออกแบบที่ป้องกันคิวรีช้า
ปัญหา “คิวรีช้า” ส่วนใหญ่ในช่วงเริ่มต้นไม่ได้เกี่ยวกับเทคนิค SQL แต่เกี่ยวกับความไม่ชัดเจนในโมเดลข้อมูล ถ้าสคีมาทำให้ไม่ชัดเจนว่าเรคอร์ดแทนอะไร หรือเรคอร์ดเชื่อมโยงกันอย่างไร ทุกคิวรีจะเขียน รัน และบำรุงรักษายากขึ้น
เริ่มจากชุดเอนทิตีหลักเล็ก ๆ
เริ่มต้นด้วยการตั้งชื่อไม่กี่สิ่งที่ผลิตภัณฑ์ของคุณขาดไม่ได้: ผู้ใช้ บัญชี คำสั่งซื้อ การสมัคร เหตุการณ์ ใบแจ้งหนี้—สิ่งที่สำคัญจริง ๆ แล้วกำหนดความสัมพันธ์อย่างชัดเจน: one-to-many, many-to-many (มักใช้ตารางเชื่อม), และความเป็นเจ้าของ (ใคร “เป็นเจ้าของ” อะไร)
การตรวจสอบเชิงปฏิบัติ: สำหรับแต่ละตาราง คุณควรเติมประโยค “แถวหนึ่งในตารางนี้แทน ___” ได้ ถ้าทำไม่ได้ แสดงว่าตารางนั้นอาจผสมแนวคิดหลายอย่าง ซึ่งต่อมาจะบังคับให้ต้องกรองและ join ซับซ้อน
ตั้งชื่อและกำหนดความเป็นเจ้าของอย่างสม่ำเสมอ
ความสม่ำเสมอป้องกันการ join โดยบังเอิญและพฤติกรรม API ที่สับสน เลือกคอนเวนชัน (snake_case vs camelCase, *_id, created_at/updated_at) แล้วยึดตามนั้น
นอกจากนี้ให้ตัดสินใจว่าใครเป็นเจ้าของฟิลด์ เช่น billing_address เป็นของ order (สแนปชอตในเวลานั้น) หรือของ user (ค่าดีฟอลต์ปัจจุบัน)? ทั้งสองเป็นไปได้—แต่การผสมโดยไม่ตั้งเจตนาชัดเจนจะสร้างคิวรีที่ช้าและผิดพลาดเมื่อพยายาม “หาความจริง”
เลือกประเภทข้อมูลให้ตรงกับความเป็นจริง
ใช้ประเภทที่หลีกเลี่ยงการแปลงขณะรัน:
- ใช้ timestamp พร้อมนโยบายเขตเวลา (time zone) ที่คุณเข้าใจ
- ใช้ decimal สำหรับเงิน (ไม่ใช้ float)
- ใช้ enum หรือตารางอ้างอิงสำหรับหมวดหมู่ที่รู้จัก
เมื่อประเภทผิด ฐานข้อมูลจะเปรียบเทียบได้ไม่มีประสิทธิภาพ ดัชนีกลายเป็นไร้ประโยชน์ และคิวรีมักต้อง casting
อย่าทำซ้ำข้อเท็จจริงโดยไม่มีแผน
การเก็บข้อเท็จจริงเดียวกันในหลายที่ (เช่น order_total และ sum(line_items)) สร้างการเลื่อนค่า หากคุณเก็บค่าที่ได้มาจากการคำนวณ ให้จดบันทึก กำหนดแหล่งที่มาของความจริง และบังคับให้อัพเดตอย่างสม่ำเสมอ (มักผ่านตรรกะแอปพลิเคชันพร้อมคอนสเทรนต์)
คีย์และคอนสเทรนต์: ความเร็วเริ่มจากความสมบูรณ์ของข้อมูล
ฐานข้อมูลที่เร็วมักเป็นฐานข้อมูลที่ คาดเดาได้ คีย์และคอนสเทรนต์ทำให้ข้อมูลของคุณคาดเดาได้โดยป้องกันสถานะ “เป็นไปไม่ได้” เช่น ความสัมพันธ์ที่ขาดหาย ตัวตนที่ซ้ำ หรือค่าที่ไม่ได้มีความหมายตามที่แอปคิด ความสะอาดนั้นส่งผลโดยตรงต่อประสิทธิภาพเพราะฐานข้อมูลสามารถสมมติได้ดีขึ้นเมื่อวางแผนคิวรี
คีย์หลัก: ทุกตารางต้องมีตัวระบุที่เสถียร
ทุกตารางควรมี primary key (PK): คอลัมน์ (หรือชุดคอลัมน์เล็ก ๆ) ที่ระบุแถวอย่างไม่ซ้ำและไม่เปลี่ยน นี่ไม่ใช่แค่ทฤษฎีฐานข้อมูล—มันทำให้คุณ join ตารางได้มีประสิทธิภาพ แคชอย่างปลอดภัย และอ้างอิงเรคอร์ดโดยไม่ต้องเดา
PK ที่เสถียรยังหลีกเลี่ยงวิธีแก้ไขที่แพง หากตารางไม่มีตัวระบุที่แท้จริง แอปจะเริ่ม “ระบุ” แถวด้วยอีเมล ชื่อ เวลา หรือชุดคอลัมน์—นำไปสู่ดัชนีที่กว้างขึ้น การ join ช้าลง และกรณีขอบเมื่อค่านั้นเปลี่ยน
คีย์อ้างอิง: ความสมบูรณ์ที่ช่วยตัววางแผน
Foreign key (FK) บังคับความสัมพันธ์: orders.user_id ต้องชี้ไปยัง users.id ที่มีอยู่ หากไม่มี FK การอ้างอิงที่ไม่ถูกต้องจะคืบคลานเข้ามา (คำสั่งซื้อสำหรับผู้ใช้ที่ถูกลบ ความเห็นสำหรับโพสต์ที่หายไป) และคิวรีทุกตัวต้องกรองป้องกัน ทำ left-join และจัดการ null
เมื่อมี FK ตัววางแผนคิวรีมักจะสามารถปรับปรุงการ join ได้อย่างมั่นใจเพราะความสัมพันธ์ถูกกำหนดและรับประกัน คุณยังมีโอกาสน้อยที่จะสะสมแถวร้างที่ทำให้ตารางและดัชนีบวมขึ้นเมื่อเวลาผ่านไป
คอนสเทรนต์เป็นราวป้องกันเพื่อข้อมูลที่สะอาดและเร็วขึ้น
คอนสเทรนต์ไม่ใช่ระเบียบมากมาย—แต่เป็นราวป้องกัน:
- UNIQUE ป้องกันการซ้ำซ้อนซึ่งบังคับให้แอปทำการค้นและทำความสะอาดเพิ่ม เช่น
users.emailที่เป็น canonical - NOT NULL หลีกเลี่ยงตรรกะสามสถานะและสาขาการจัดการ null ที่ไม่คาดคิดในคิวรี
- CHECK จำกัดค่าให้อยู่ในชุดที่รู้จัก (เช่น
status IN ('pending','paid','canceled'))
ข้อมูลที่สะอาดหมายถึงคิวรีที่เรียบง่ายกว่า เงื่อนไข fallback น้อยลง และการ join “เพื่อความไม่ประมาท” น้อยลง
แนวทางปฏิบัติที่ช้าลงที่พบบ่อย
- ไม่มี foreign keys: คุณจะจ่ายในภายหลังด้วยงานทำความสะอาดแถวร้างและตรรกะคิวรีที่ซับซ้อน
- ฟิลด์อีเมลซ้ำกัน (เช่น
users.emailและcustomers.email): ได้ตัวตนขัดแย้งและดัชนีซ้ำซ้อน - สถานะเป็นสตริงแบบอิสระ: พิมพ์ผิดเช่น "Cancelled" vs "canceled" สร้างเซ็กเมนต์ที่ซ่อนอยูที่ทำให้ตัวกรองและรายงานแตก
ถ้าคุณต้องการความเร็วตั้งแต่ต้น ให้ทำให้การเก็บข้อมูลไม่ดีเป็นเรื่องยาก ฐานข้อมูลจะให้รางวัลคุณด้วยแผนที่ง่ายขึ้น ดัชนีเล็กลง และความประหลาดใจด้านประสิทธิภาพที่น้อยลง
การทำ normalization vs denormalization: สมดุลเชิงปฏิบัติ
Normalization คือแนวคิดง่าย ๆ: เก็บแต่ละ “ข้อเท็จจริง” ไว้ที่เดียวเพื่อไม่ให้ข้อมูลซ้ำ เมื่อค่าซ้ำไปหลายตารางหรือคอลัมน์ การอัพเดตก็เสี่ยง—สำเนาหนึ่งเปลี่ยน อีกสำเนาไม่เปลี่ยน และแอปจะแสดงคำตอบขัดแย้ง
การทำ normalization (ค่าเริ่มต้น): ข้อเท็จจริงหนึ่งแห่ง บ้านหนึ่งแห่ง
ในทางปฏิบัติ normalization คือการแยกเอนทิตีเพื่อให้อัพเดตสะอาดและคาดเดาได้ เช่น ชื่อและราคาสินค้าอยู่ในตาราง products ไม่ควรทำซ้ำในแต่ละแถวคำสั่งซื้อ ชื่อหมวดหมู่อยู่ใน categories แล้วอ้างอิงด้วย ID
สิ่งนี้ลด:
- ข้อมูลซ้ำซ้อน (พื้นที่เก็บน้อยลง ความไม่สอดคล้องน้อยลง)
- ข้อผิดพลาดในการอัพเดต (เปลี่ยนครั้งเดียว สะท้อนทุกที่)
- บั๊กสับสนว่า “ค่าไหนถูกต้อง?”
เมื่อการทำ normalization เกินไปก็มีผลเสีย
การทำ normalization มากเกินไปเมื่อคุณแยกข้อมูลเป็นตารางเล็ก ๆ หลายตารางที่ต้อง join ตลอดเวลาสำหรับหน้าจอทั่วไป ฐานข้อมูลยังคืนผลถูกต้อง แต่การอ่านปกติจะช้าลงและซับซ้อนขึ้นเพราะแต่ละคำขอจำเป็นต้อง join หลายตาราง
สัญญาณในช่วงต้น: หน้าจอ “ง่าย” (เช่น ประวัติคำสั่งซื้อ) ต้อง join 6–10 ตาราง และประสิทธิภาพแปรผันตามทราฟฟิกและความร้อนของแคช
แนวทางปฏิบัติ: ทำ normalization กับข้อเท็จจริงหลัก และ denormalize สำหรับการอ่านที่ฮอตสุด
สมดุลที่สมเหตุสมผลคือ:
- Normalize ข้อเท็จจริงหลักและการเป็นเจ้าของ (แหล่งที่มาของความจริง). เก็บคุณลักษณะสินค้าใน
products, ชื่อหมวดหมู่ในcategories, และความสัมพันธ์ผ่าน foreign keys. - Denormalize อย่างระมัดระวังสำหรับการอ่านที่ร้อนที่สุด — แต่เฉพาะเมื่ออธิบายประโยชน์ได้และมีแผนการรักษาความถูกต้อง
Denormalization หมายถึงการทำสำเนาข้อมูลเล็ก ๆ โดยตั้งใจเพื่อให้คิวรีที่ใช้บ่อยถูกลง (join น้อยลง รายการเร็วขึ้น). คำสำคัญคือ ระมัดระวัง: ทุกฟิลด์ที่ทำซ้ำต้องมีแผนการอัพเดต
ตัวอย่าง: สินค้า หมวดหมู่ และรายการคำสั่งซื้อ
โครงแบบ normalized อาจเป็น:
products(id, name, price, category_id)categories(id, name)orders(id, customer_id, created_at)order_items(id, order_id, product_id, quantity, unit_price_at_purchase)
สังเกตสิ่งเล็ก ๆ ที่ชนะ: order_items เก็บ unit_price_at_purchase (การทำ denormalize แบบหนึ่ง) เพราะคุณต้องการความถูกต้องทางประวัติศาสตร์แม้ราคาสินค้าจะเปลี่ยนทีหลัง การทำซ้ำนี้ตั้งใจและมีเสถียรภาพ
ถ้าหน้าจอที่ใช้บ่อยที่สุดของคุณคือ “คำสั่งซื้อพร้อมสรุปรายการ” คุณอาจ denormalize product_name เข้าไปใน order_items เพื่อหลีกเลี่ยงการ join products ในทุก ๆ รายการ — แต่ก็ต่อเมื่อคุณพร้อมที่จะรักษามันให้สอดคล้อง (หรือยอมรับว่ามันเป็นสแนปชอตตอนซื้อ)
กลยุทธ์ดัชนีตามสคีมา ไม่ใช่กลับกัน
ดัชนีมักถูกมองเป็นปุ่มวิเศษ แต่ทำงานได้ดีเมื่อโครงสร้างตารางสมเหตุสมผล หากคุณยังเปลี่ยนชื่่อคอลัมน์ แยกตาราง หรือเปลี่ยนวิธีการสัมพันธ์ของเรคอร์ด ชุดดัชนีของคุณก็จะต้องเปลี่ยนด้วย ดัชนีทำงานดีที่สุดเมื่อคอลัมน์ (และวิธีที่แอปกรอง/เรียงตามคอลัมน์เหล่านั้น) มีความเสถียรพอที่จะไม่ต้องสร้างใหม่ทุกสัปดาห์
เริ่มจากคำถามที่แอปของคุณถามบ่อยที่สุด
คุณไม่ต้องทำนายสมบูรณ์ แต่ต้องมีรายการสั้น ๆ ของคิวรีที่สำคัญที่สุด:
- “ค้นหาผู้ใช้โดยอีเมล”
- “แสดงคำสั่งซื้อล่าสุดสำหรับลูกค้า”
- “แสดงใบแจ้งหนี้โดยสถานะ เรียงจากใหม่สุด”
ประโยคเหล่านี้แปลตรงไปยังคอลัมน์ที่ควรมีดัชนี ถ้าคุณพูดสิ่งเหล่านี้ไม่ออก มักเป็นปัญหาความชัดเจนของสคีมา ไม่ใช่ปัญหาดัชนี
ดัชนีผสม (composite indexes) อธิบายแบบง่าย
ดัชนีผสมครอบคลุมมากกว่าหนึ่งคอลัมน์ ทิศทางของคอลัมน์สำคัญเพราะฐานข้อมูลสามารถใช้ดัชนีได้จากซ้ายไปขวา
ตัวอย่าง: ถ้าคุณมักกรองโดย customer_id แล้วเรียงตาม created_at ดัชนี (customer_id, created_at) มักมีประโยชน์ ส่วนลำดับกลับ (created_at, customer_id) อาจไม่ช่วยคิวรีเดียวกันมากนัก
อย่าทำดัชนีทุกอย่าง
ดัชนีเพิ่มต้นทุน:
- เขียนช้าลง: การแทรก/อัพเดตต้องอัพเดตดัชนีด้วย
- ใช้พื้นที่มากขึ้น: ดัชนีอาจกลายเป็นส่วนใหญ่ของขนาดฐานข้อมูล
- ความซับซ้อนมากขึ้น: ดัชนีเพิ่มความยากในการบำรุงรักษาและปรับแต่ง
สคีมาที่สะอาดและสม่ำเสมอจะลดชุดดัชนีที่ “ถูกต้อง” ให้เหลือเพียงชุดเล็ก ๆ ที่ตรงกับรูปแบบการเข้าถึงจริง—โดยไม่ต้องจ่ายภาษีการเขียนและพื้นที่เก็บตลอดเวลา
ประสิทธิภาพการเขียน: ต้นทุนที่ซ่อนอยู่ของสคีมาที่ยุ่งเหยิง
แอปที่ช้าไม่ใช่แค่การอ่านช้า ปัญหาประสิทธิภาพช่วงแรกมักเกิดขึ้นระหว่างการแทรกและอัพเดต—การสมัครผู้ใช้ การชำระเงิน งานแบ็กกราวด์—เพราะสคีมาที่ยุ่งทำให้งานเขียนแต่ละครั้งต้องทำงานเพิ่ม
ทำไมการเขียนแพงขึ้น
ตัวเลือกสคีมาไม่กี่อย่างจะเพิ่มต้นทุนของการเปลี่ยนแปลงทุกครั้ง:
- แถวกว้าง: ยัดคอลัมน์สิบยี่สิบลงในตารางเดียวทำให้แถวใหญ่ขึ้น I/O มากขึ้น และแคชสั่นคลอน—แม้ว่าส่วนใหญ่ของคอลัมน์จะใช้น้อย
- ดัชนีเยอะเกินไป: ดัชนีเร่งอ่าน แต่การแทรก/อัพเดตต้องอัพเดตดัชนีทุกอัน
- ทริกเกอร์และ cascade: ทริกเกอร์อาจซ่อนงาน (การแทรก/อัพเดตเพิ่มเติม) หลัง
INSERTธรรมดา การ cascade ของ foreign key อาจถูกต้องแต่ก็เพิ่มงานตอนเขียนที่เติบโตตามข้อมูลที่เกี่ยวข้อง
อ่านหนัก vs เขียนหนัก: เลือกความเจ็บปวดอย่างมีสติ
ถ้า workload ของคุณเป็น อ่านหนัก (ฟีด หน้าค้นหา) คุณจ่ายได้กับดัชนีมากขึ้นและบางครั้ง denormalization เฉพาะจุด ถ้าทำงานเป็น เขียนหนัก (การเก็บเหตุการณ์ ปริมาณคำสั่งซื้อสูง) ให้จัดลำดับความสำคัญของสคีมาที่ทำให้การเขียนเรียบง่ายและคาดเดาได้ แล้วเพิ่มการปรับแต่งการอ่านเฉพาะที่จำเป็น
รูปแบบช่วงแรกที่ทำร้ายการเขียน
- บันทึก audit เยอะ: ดีสำหรับ compliance แต่หลีกเลี่ยงการบันทึก snapshot ขนาดใหญ่ทุกการอัพเดต
- ตารางเหตุการณ์: แบบ append-only ขยายตัวดี แต่กลายเป็นบวมถ้าคุณเก็บ payload ซ้ำ ๆ
- soft deletes: สะดวก แต่เพิ่มขนาดดัชนีและอาจทำให้การอัพเดตและคิวรีช้าลงหากไม่วางแผน
รักษาการเขียนให้เรียบง่ายขณะที่เก็บประวัติ
แนวทางปฏิบัติ:
- เก็บ “สถานะปัจจุบัน” ในตารางหนึ่ง และ ประวัติ ในตาราง append-only แยกต่างหาก
- ให้แถวประวัติ แคบ (เก็บเฉพาะสิ่งที่ต้องการจริง ๆ: ใคร/เมื่อไร/อะไรเปลี่ยน)
- เพิ่มดัชนีให้ประวัติตามรูปแบบการเข้าถึงจริง (มักเป็น
entity_id,created_at) - หลีกเลี่ยงทริกเกอร์สำหรับ auditing ในตอนแรก; ใช้การเขียนจากแอปโดยตรงเพื่อให้ค่าใช้จ่ายมองเห็นได้และทดสอบได้
เส้นทางการเขียนที่สะอาดให้ headroom และทำให้การปรับแต่งคิวรีในภายหลังง่ายขึ้นมาก
ORM และ API ขยายการตัดสินใจด้านสคีมา
ORM ทำให้การทำงานกับฐานข้อมูลดูง่าย: คุณกำหนดโมเดล เรียกเมทอด แล้วข้อมูลก็ออกมา ความจับค่อยคือ ORM อาจซ่อน SQL ที่แพงจนกว่าจะถึงเวลาที่มันสร้างปัญหา
ORM: ความสะดวกที่อาจปกปิดรูปแบบช้าที่เกิดขึ้น
กับดักสองอย่างที่พบบ่อย:
- join ที่ไม่มีประสิทธิภาพ:
.include()หรือ serializer ซ้อน ๆ ดูเรียบง่ายแต่สามารถกลายเป็น join กว้าง แถวซ้ำ หรือการ sort ขนาดใหญ่ได้—โดยเฉพาะถ้าความสัมพันธ์ไม่ได้กำหนดชัด - N+1 queries: คุณเรียก 50 เรคอร์ด แล้ว ORM เงียบ ๆ วิ่งอีก 50 คิวรีเพื่อโหลดข้อมูลที่เกี่ยวข้อง มันมักทำงานในสภาพแวดล้อม dev แต่ล่มเมื่อเจอทราฟฟิกจริง
สคีมาที่ออกแบบดีช่วยลดโอกาสเกิดรูปแบบเหล่านี้และทำให้ตรวจจับง่ายขึ้นเมื่อเกิดขึ้น
ความสัมพันธ์ชัดเจนทำให้การใช้ ORM ปลอดภัยขึ้น
เมื่อมี foreign keys, unique constraints, และ NOT NULL ORM สามารถสร้างคิวรีที่ปลอดภัยกว่าและโค้ดของคุณจะพึ่งพาสมมติฐานที่สม่ำเสมอได้
ตัวอย่าง: การบังคับให้ orders.user_id ต้องมี (FK) และ users.email เป็นเอกลักษณ์ ป้องกันคลาสของ edge case ที่มักกลายเป็นการตรวจสอบระดับแอปและงานคิวรีเพิ่ม
API เปลี่ยนการตัดสินใจสคีมาเป็นพฤติกรรมของผลิตภัณฑ์
การออกแบบ API อยู่หลังสคีมา:
- ID ที่เสถียร (และชนิดของคีย์ที่สอดคล้อง) ทำให้ URL, แคช, และสถานะฝั่งไคลเอนต์ง่ายขึ้น
- การแบ่งหน้า ทำงานได้ดีที่สุดเมื่อคุณสามารถเรียงตามคอลัมน์ที่มีดัชนีและเพิ่มขึ้นต่อเนื่อง (มักเป็น
created_at+id) - การกรอง เป็นไปอย่างคาดเดาเมื่อคอลัมน์แสดงแอตทริบิวต์จริง (ไม่ใช่สตริงโอเวอร์โหลดหรือ JSON blob) และคอนสเทรนต์รักษาค่าชัดเจน
ทำให้เป็นเวิร์กโฟลว์ ไม่ใช่ภารกิจกู้ภัย
ปฏิบัติต่อการตัดสินใจสคีมาเป็นงานวิศวกรรมอันดับหนึ่ง:
- ใช้มิเกรชั่นสำหรับทุกการเปลี่ยน แก้ไขเหมือนโค้ด
- เพิ่มการ “ทบทวนสคีมา” เบา ๆ สำหรับ endpoint ใหม่: ตารางอะไร คีย์อะไร คอนสเทรนต์อะไร รูปแบบคิวรีอย่างไร
- ในสเตจจิ้ง บันทึกคิวรี ORM และตรวจจับรูปแบบ N+1 ก่อนขึ้น production
ถ้าคุณสร้างอย่างรวดเร็วด้วย workflow แบบ chat-driven (เช่น สร้างแอป React พร้อม backend Go/PostgreSQL โดยใช้ Koder.ai) ให้ถือว่า “ทบทวนสคีมา” เป็นส่วนหนึ่งของการสนทนาแต่เนิ่น ๆ คุณยังสามารถวน iterate ได้เร็ว แต่ต้องมีคอนสเทรนต์ คีย์ และแผนมิเกรชั่นอย่างรอบคอบก่อนทราฟฟิกมาถึง
สัญญาณเตือนว่า สคีมาเป็นคอขวด
ปัญหาบางอย่างไม่ได้มาจาก “SQL แย่” แต่เป็นฐานข้อมูลที่สู้กับรูปร่างของข้อมูล ถ้าคุณเห็นปัญหาแบบเดียวกันข้ามหลาย endpoint และรายงาน มันมักเป็นสัญญาณของสคีมา ไม่ใช่โอกาสปรับจูนคิวรี
อาการที่พบบ่อยให้จับตา
ตัวกรองที่ช้าคือสัญญาณคลาสสิก ถ้าตัวเงื่อนไขง่าย ๆ เช่น “ค้นหาคำสั่งโดยลูกค้า” หรือ “กรองตามวันที่สร้าง” ช้าอย่างต่อเนื่อง ปัญหาอาจเป็นความขาดแคลนความสัมพันธ์ ประเภทข้อมูลไม่ตรง หรือคอลัมน์ที่ไม่สามารถทำดัชนีได้อย่างมีประสิทธิภาพ
อีกป้ายแดงคือจำนวนการ join พุ่งขึ้น: คิวรีที่ ควร join 2–3 ตาราง กลับต้องเชน 6–10 ตารางเพื่อหาคำตอบพื้นฐาน (มักจากการ over-normalize, polymorphic pattern, หรือการยัดทุกอย่างในตารางเดียว)
นอกจากนี้ให้ระวังคอลัมน์ที่ควรเป็น enum แต่มีค่าที่ไม่สอดคล้อง—โดยเฉพาะฟิลด์สถานะ ("active", "ACTIVE", "enabled", "on") ความไม่สอดคล้องบังคับให้คิวรีต้องป้องกัน (เช่น LOWER(), COALESCE(), OR-chain) ซึ่งจะช้าไม่ว่าเทคนิคจะเยอะแค่ไหน
เช็คลิสต์สคีมา (ตรวจสอบเร็ว)
- ดัชนีหายบน foreign key (การ join จะกลายเป็น full scan เมื่อโต)
- ประเภทข้อมูลผิด (เช่น ID เก็บเป็นสตริง, วันที่เก็บเป็นข้อความ, เงินเก็บเป็น float)
- ตาราง EAV (Entity–Attribute–Value) ใช้สำหรับข้อมูลหลัก: ยืดหยุ่นดีตอนแรก แต่การกรอง/เรียงจะกลายเป็นหลาย join และ predicate ยากต่อการทำดัชนี
การวินิจฉัยง่าย ๆ ที่ไม่พึ่งเครื่องมือ
เริ่มจากตรวจสอบความเป็นจริง: จำนวนแถวต่อโต๊ะ และ cardinality ของคอลัมน์หลัก (มีค่าต่างกันกี่ค่า) ถ้าคอลัมน์ status ควรมี 4 ค่าแต่คุณเจอ 40 ค่า แสดงว่าสคีมาเริ่มรั่วแล้ว
จากนั้นดูแผนคิวรีของ endpoint ที่ช้า ถ้าคุณเห็น sequential scan ซ้ำบนคอลัมน์ join หรือชุดผลลัพธ์กลางขนาดใหญ่ บ่อยครั้งสคีมาและการมิกซ์ดัชนีคือรากเหง้า
สุดท้าย เปิดบันทึกคิวรีช้าและทบทวน ถ้ามีคิวรีหลากหลายช้าในรูปแบบคล้ายกัน (ตารางเดิม เงื่อนไขเดิม) นั่นมักเป็นปัญหาเชิงโครงสร้างที่ควรแก้ในระดับโมเดล
พัฒนาสคีมาอย่างปลอดภัยเมื่อคุณเติบโต
การตัดสินใจสคีมาในช่วงแรกแทบไม่คงอยู่เมื่อเจอผู้ใช้จริง เป้าหมายไม่ใช่ “ทำให้สมบูรณ์แบบ” แต่คือการเปลี่ยนโดยไม่ทำลาย production สูญเสียข้อมูล หรือล็อกทีมเป็นสัปดาห์
กระบวนการเปลี่ยนแปลงเบา ๆ ที่ทำซ้ำได้
เวิร์กโฟลว์ปฏิบัติที่ปรับขนาดได้ตั้งแต่แอปคนเดียวไปจนถึงทีมใหญ่:
- ออกแบบโมเดล: เขียนรูปร่างใหม่ (ตาราง/คอลัมน์ ความสัมพันธ์ และแหล่งที่มาของความจริง). รวมตัวอย่างเรคอร์ดและ edge case
- มิเกรต: เพิ่มโครงสร้างใหม่ในแบบที่คอมแพติเบิลย้อนหลัง (เพิ่มคอลัมน์/ตารางก่อน; หลีกเลี่ยงการลบหรือเปลี่ยนชื่อทันที)
- backfill: เติมฟิลด์ใหม่จากข้อมูลเดิมเป็นแบทช์ ติดตามสถานะเพื่อให้สามารถหยุดแล้วเริ่มใหม่ได้
- ตรวจสอบ: เพิ่มคอนสเทรนต์หลังจากข้อมูลสะอาดแล้ว (เช่น NOT NULL, foreign keys). รันการตรวจเปรียบเทียบผลลัพธ์เก่า vs ใหม่
ฟีเจอร์แฟลกและการเขียนคู่ (ใช้ประหยัด)
การเปลี่ยนสคีมาเกือบทั้งหมดไม่ต้องใช้รูปแบบ rollout ซับซ้อน ชอบแนวทาง “ขยาย-แล้ว-หด”: เขียนโค้ดให้สามารถอ่านทั้งแบบเก่าและแบบใหม่ แล้วสลับเขียนเมื่อมั่นใจ
ใช้ feature flags หรือ dual writes เมื่อจำเป็นจริง ๆ (ทราฟฟิกสูง backfill ยาว หรือต้องมีหลายบริการ) หากเขียนคู่ ให้เพิ่มมอนิเตอร์เพื่อตรวจหาความเบี้ยวและกำหนดว่าฝั่งไหนชนะเมื่อขัดแย้ง
การย้อนกลับและการทดสอบมิเกรชั่นที่สะท้อนความจริง
การย้อนกลับที่ปลอดภัยเริ่มจากมิเกรชั่นที่ย้อนกลับได้ ฝึกเส้นทาง “undo”: การ drop คอลัมน์ทำได้ง่าย แต่การคืนค่าข้อมูลที่ถูกเขียนทับไม่ง่าย
ทดสอบมิเกรชั่นบน ข้อมูลขนาดใกล้เคียงจริง การมิเกรชั่นที่เสร็จใน 2 วินาทีบนแลปท็อปอาจล็อกตารางเป็นนาทีใน production ใช้วัดกับจำนวนแถวและดัชนีที่ใกล้เคียง
นี่คือที่เครื่องมือแพลตฟอร์มลดความเสี่ยง: การปรับใช้อย่างเชื่อถือได้พร้อม snapshot/rollback (และความสามารถส่งออกโค้ดเมื่อจำเป็น) ทำให้การวน iterate บนสคีมาและตรรกะแอปปลอดภัยขึ้น ถ้าคุณใช้ Koder.ai ให้พึ่ง snapshots และโหมดวางแผนเมื่อคุณจะนำมิเกรชั่นที่ต้องการลำดับขั้นตอนอย่างรอบคอบ
จดบันทึกการตัดสินใจสำหรับคนถัดไป (รวมถึงตัวคุณในอนาคต)
เก็บบันทึกสั้น ๆ ของสคีมา: อะไรเปลี่ยน ทำไม และแลกเปลี่ยนอะไรไว้ เชื่อมโยงจาก /docs หรือ README ของรีโป ใส่บันทึกเช่น “คอลัมน์นี้ denormalized โดยตั้งใจ” หรือ “foreign key เพิ่มหลัง backfill เมื่อ 2025-01-10” เพื่อให้การเปลี่ยนในอนาคตไม่ต้องทำผิดซ้ำ
เมื่อไรควรปรับคิวรี (และลำดับการทำงานที่สมเหตุสมผล)
การปรับคิวรีสำคัญ—แต่มันให้ผลดีที่สุดเมื่อสคีมาไม่สู้คุณ ถ้าตารางขาดคีย์ชัดเจน ความสัมพันธ์ไม่สอดคล้อง หรือ “หนึ่งแถวต่อหนึ่งสิ่ง” ถูกละเมิด คุณอาจเสียเวลาหลายชั่วโมงปรับคิวรีที่อาจถูกเขียนทิ้งสัปดาห์หน้า
ลำดับความสำคัญเชิงปฏิบัติ
-
แก้บล็อกเกอร์ของสคีมาก่อน เริ่มจากสิ่งที่ทำให้การคิวรีถูกต้องยาก: ขาด primary key, foreign key ไม่สอดคล้อง, คอลัมน์ผสมหลายความหมาย, แหล่งข้อมูลความจริงซ้ำ หรือประเภทที่ไม่ตรงความจริง (เช่น วันที่เก็บเป็นสตริง)
-
ทำให้รูปแบบการเข้าถึงคงที่ เมื่อโมเดลข้อมูลสะท้อนพฤติกรรมแอป (และคาดว่าจะเป็นแบบนี้สักสองสามสปรินต์) การปรับคิวรีจะทนต่อเวลาได้
-
ปรับคิวรีบนคิวรีชั้นนำ—ไม่ใช่ทุกคิวรี ใช้ logs/APM เพื่อระบุคิวรีที่ช้าที่สุดและเกิดบ่อยที่สุด endpoint หนึ่งที่ถูกเรียก 10,000 ครั้งต่อวันมักให้ผลมากกางานรายงานที่เรียกไม่บ่อย
กฎ 80/20 ของการปรับคิวรีช่วงต้น
ชัยชนะส่วนใหญ่ในช่วงแรกมาจากชุดการกระทำเล็ก ๆ:
- เพิ่มดัชนีที่ถูกต้อง สำหรับฟิลเตอร์และการ join ที่พบบ่อย (และยืนยันว่ามันถูกใช้)
- คืนคอลัมน์ให้น้อยลง (หลีกเลี่ยง
SELECT *, โดยเฉพาะบนตารางกว้าง) - หลีกเลี่ยง join ที่ไม่จำเป็น — บางครั้งมี join เพราะสคีมาบังคับให้คุณต้อง “ค้นพบ” แอตทริบิวต์พื้นฐาน
ตั้งความคาดหวัง: มันต่อเนื่อง แต่วางรากฐานก่อน
งานปรับแต่งประสิทธิภาพไม่เคยจบ แต่เป้าหมายคือทำให้คาดเดาได้ ด้วยสคีมาที่สะอาด ฟีเจอร์ใหม่แต่ละอันจะเพิ่มโหลดแบบเพิ่มทีละนิด; กับสคีมาที่ยุ่ง ฟีเจอร์แต่ละอันเพิ่มความสับสนแบบทวีคูณ
เช็คลิสต์สัปดาห์นี้
- จัดลำดับ 5 คิวรีที่ช้าที่สุด และ 5 คิวรีที่เกิดบ่อยที่สุด
- สำหรับแต่ละรายการ ยืนยัน: primary key มีอยู่, การ join เป็น key-to-key, และประเภทข้อมูลถูกต้อง
- เพิ่ม ดัชนีหนึ่งตัว ที่ตรงกับตัวกรอง/การเรียงหลัก
- แทนที่
SELECT *ในหนึ่งเส้นทางฮอต - วัดใหม่และเก็บบันทึกง่าย ๆ ของ “ก่อน/หลัง” สำหรับสปรินต์ถัดไป
คำถามที่พบบ่อย
ควรแก้ไขสคีมาก่อนปรับแต่งคิวรี SQL หรือไม่?
เริ่มจากสคีมา ตารางที่ชัดเจน ID ที่คงที่ คีย์นอก และชนิดข้อมูลที่ถูกต้อง ช่วยลดงานที่ทุกคิวรีต้องทำ จากนั้นค่อยปรับแต่งคิวรีที่บันทึกล็อกบอกว่าสำคัญจริง ๆ
จะรู้ได้อย่างไรว่าสคีมาเป็นสาเหตุของคิวรีที่ทำงานช้า?
คุณอาจมีปัญหาที่สคีมาหากเอนด์พอยต์หลายส่วนที่ไม่เกี่ยวข้องกันทำงานช้า ตัวกรองง่าย ๆ ต้องสแกนตารางขนาดใหญ่ หรือหน้าจอพื้นฐานต้องใช้การ join จำนวนมาก การแปลงชนิดข้อมูลซ้ำ ๆ การแยกวิเคราะห์ข้อความ และการตรวจสอบค่า null เพื่อป้องกันข้อผิดพลาดก็เป็นสัญญาณเตือนเช่นกัน
ทำไมทุกตารางต้องมีคีย์หลัก?
ทุกตารางควรมีคีย์หลักที่ระบุแต่ละแถวได้อย่างไม่ซ้ำกันและไม่เปลี่ยนแปลง ใช้ ID นี้สำหรับการ join และการอ้างอิง แทนชื่อ อีเมล หรือเวลา
คีย์นอกช่วยเรื่องประสิทธิภาพของฐานข้อมูลหรือไม่?
คีย์นอกช่วยป้องกันความสัมพันธ์ที่ไม่ถูกต้อง เช่น คำสั่งซื้อที่อ้างถึงผู้ใช้ที่ไม่มีอยู่จริง ช่วยให้ข้อมูลสะอาดขึ้นและทำให้การ join เข้าใจได้ง่ายขึ้น แต่คุณยังควรทำดัชนีให้คอลัมน์คีย์นอกที่ใช้ใน join บ่อย ๆ
ควรเพิ่มดัชนีให้ทุกคอลัมน์หรือไม่?
ไม่ควร ทำดัชนีให้คอลัมน์ที่แอปของคุณใช้ค้นหา join หรือเรียงลำดับระเบียนเป็นประจำ แต่ละดัชนีที่เพิ่มเข้ามายังใช้พื้นที่จัดเก็บเพิ่มและทำให้การแทรกกับการอัปเดตต้องทำงานมากขึ้น
ดัชนีแบบผสมคืออะไร และทำไมลำดับคอลัมน์จึงสำคัญ?
ดัชนีแบบผสมเก็บหลายคอลัมน์ตามลำดับที่กำหนด สำหรับคิวรีที่กรองด้วย customer_id และเรียงตาม created_at ดัชนีที่มี customer_id ตามด้วย created_at มักสอดคล้องกับรูปแบบการเข้าถึงข้อมูล
ควรทำ denormalize ฐานข้อมูลเมื่อใด?
ทำให้ข้อเท็จจริงหลักเป็นแบบปกติก่อน โดยให้ข้อเท็จจริงแต่ละอย่างมีที่อยู่ที่ชัดเจนเพียงแห่งเดียว และเชื่อมตารางด้วย ID ทำ denormalize เฉพาะการอ่านที่เกิดบ่อยและวัดผลแล้ว พร้อมระบุว่าค่าที่คัดลอกมาเป็นค่าปัจจุบันหรือภาพรวมข้อมูลย้อนหลัง
ชนิดข้อมูลใดช่วยป้องกันปัญหาด้านประสิทธิภาพที่พบบ่อย?
ใช้เลขทศนิยมสำหรับเงิน ใช้ชนิดข้อมูล date หรือ timestamp จริงสำหรับเวลา และใช้ชนิด ID ที่ตรงกันในตารางที่เกี่ยวข้อง การเก็บวันที่หรือ ID เป็นข้อความบังคับให้ต้องแปลงข้อมูล และอาจทำให้ใช้ดัชนีได้อย่างไม่มีประสิทธิภาพ
วิธีที่ปลอดภัยที่สุดในการเปลี่ยนสคีมาในระบบจริงคืออะไร?
เริ่มด้วยการทำให้การเปลี่ยนแปลงเข้ากันได้กับของเดิม: เพิ่มตารางหรือคอลัมน์ใหม่ เติมข้อมูลย้อนหลังเป็นชุด ตรวจสอบความถูกต้องของข้อมูล แล้วค่อย ๆ เปลี่ยนการอ่านและเขียนของแอปพลิเคชัน ลบโครงสร้างเดิมเมื่อเส้นทางใหม่พิสูจน์แล้วว่าเชื่อถือได้เท่านั้น
ควรปรับแต่งอะไรหลังจากสคีมามีความเสถียรแล้ว?
ค้นหาคิวรีที่ช้าที่สุดและเกิดบ่อยที่สุด ตรวจสอบแผนการทำงาน และตรวจสอบสคีมาก่อน จากนั้นเพิ่มดัชนีที่ตรงจุด ดึงเฉพาะคอลัมน์ที่จำเป็น ตัด join ที่ไม่จำเป็น และวัดผลก่อนทำการเปลี่ยนแปลงครั้งต่อไป