- แอนตีแพตเทิร์นของ SQL ทำให้การบำรุงรักษาคิวรีและ data pipeline ยากขึ้น และทำให้ประสิทธิภาพช้ากว่าที่คาดไว้
- ตัวอย่างที่พบบ่อย ได้แก่ การใช้ CASE WHEN มากเกินไป, การใช้ฟังก์ชันกับคอลัมน์ที่มีดัชนี, SELECT *, การใช้ DISTINCT มากเกินไป, วิวและซับคิวรีที่ซ้อนกัน, และ โครงสร้างการพึ่งพาที่ลึกเกินไป
- ปัญหาส่วนใหญ่เกิดจาก การแก้ปัญหาเฉพาะหน้าภายใต้แรงกดดันด้านความเร็วและเดดไลน์ ซึ่งในระยะยาวจะทำลายทั้งความน่าเชื่อถือของข้อมูลและความเร็วในการพัฒนา
- แนวทางแก้ไขที่จำเป็นคือ การกำหนด join ให้ชัดเจน, การใช้ตารางมิติ, การตัดการซ้อนที่ไม่จำเป็น, และการจัดระเบียบวิวเป็นระยะ
- ควรมอง SQL ไม่ใช่แค่สคริปต์ แต่เป็น โค้ดสำหรับ production ที่ทีมร่วมกันดูแล, และการออกแบบตั้งแต่ต้นโดยคำนึงถึงความอ่านง่ายจะช่วยลดการทำงานซ้ำภายหลัง
บทนำ
- วันนี้จะอธิบายโดยเน้นตัวอย่าง แอนตีแพตเทิร์น SQL ที่เกิดขึ้นบ่อยและมีผลกระทบสูงบางกรณี
- ปัญหาเหล่านี้ก่อให้เกิดวงจรแย่ ๆ เช่น ความน่าเชื่อถือของข้อมูลลดลง และความเร็วในการพัฒนาคิวรีลดลง
- รายการด้านล่างไม่ได้ครอบคลุมทุกกรณีทั้งหมด และหากต้องการความเข้าใจที่ลึกขึ้น ขอแนะนำ หนังสือของ Bill Karwin
คำสั่ง CASE WHEN ที่ซับซ้อนเกินไป
- ในระบบขนาดใหญ่ มักใช้ คำสั่ง CASE WHEN เพื่อแปลงรหัสสถานะ (เช่น 1=สินค้าหมด) ให้อ่านเข้าใจได้โดยมนุษย์
- การเพิ่ม logic ของ CASE WHEN นี้ไว้เฉพาะในวิวเดียวเพื่อเร่งการพัฒนา dashboard หรือรายงาน อาจสะดวกในระยะสั้นแต่ถือเป็น แอนตีแพตเทิร์น ในระยะยาว
- สิ่งนี้ทำให้เกิดปัญหาการคัดลอก logic ซ้ำ ๆ และการตีความไม่ตรงกัน รวมถึงทำให้สภาพแวดล้อมของคิวรีโดยรวมยุ่งเหยิง
- วิธีแก้คือสร้าง ตารางมิติ (dimension table) หรือ วิวส่วนกลาง สำหรับแปลงรหัสสถานะแยกไว้ เพื่อให้สามารถนำกลับมาใช้ซ้ำได้
การใช้ฟังก์ชันกับคอลัมน์ที่มีดัชนี
- หาก ใช้ฟังก์ชันกับคอลัมน์ที่มีดัชนี เช่น “WHERE UPPER(name) = 'ABC'” ประสิทธิภาพของดัชนีจะหายไป
- ใน SQL Server และระบบอื่น ๆ อาจทำให้เกิด full table scan ที่ไม่จำเป็น
- วิธีแก้คือ สร้างดัชนีแยกสำหรับคอลัมน์ที่ผ่านการใช้ฟังก์ชัน หรือ แปลงค่าขาเข้าก่อนเพื่อทำให้เงื่อนไขของคิวรีเรียบง่ายขึ้น
การใช้ SELECT * ในวิว
- ตอนพัฒนาวิว การใช้ SELECT * อาจดูสะดวก แต่เมื่อโครงสร้าง (schema) เปลี่ยน วิวก็อาจพังได้ง่าย
- เนื่องจากอาจรวมคอลัมน์ที่ไม่จำเป็นเข้าไปด้วย จึงก่อให้เกิด การพึ่งพาโดยไม่ตั้งใจและปัญหาด้านประสิทธิภาพ ดังนั้นควรระบุคอลัมน์อย่างชัดเจน
ใช้ DISTINCT มากเกินไปเพื่อ “แก้” ข้อมูลซ้ำ
- เมื่อเกิดผลลัพธ์ซ้ำจาก join ที่ผิด การใช้ SELECT DISTINCT เพื่อแก้ชั่วคราวคือการ ซ่อนปัญหาความถูกต้องสมบูรณ์ของข้อมูล
- สาเหตุที่แท้จริงคือเงื่อนไข join ไม่สมบูรณ์ หรือ นิยามความสัมพันธ์ (1:1, 1:N เป็นต้น) ผิดพลาด
- วิธีแก้ที่ถูกต้องคือ ปรับปรุง logic ของ join เพื่อทำให้นิยามความสัมพันธ์ชัดเจน และ ทำให้ความสอดคล้องของความสัมพันธ์ถูกต้องก่อนการ aggregate หรือทำรายงาน
การซ้อนวิวมากเกินไป (Excessive View Layer Stacking)
- หากหลายทีม reuse วิวเดิมแล้วสร้างวิวใหม่ทับซ้อนต่อไปเรื่อย ๆ สายโซ่การพึ่งพาจะซับซ้อนขึ้นและประสิทธิภาพจะลดลงอย่างรวดเร็ว
- การดีบักจะยากขึ้น และการขยายคิวรีจะยากราวกับการ “ขุดค้นทางโบราณคดี”
- จำเป็นต้องมีแนวทาง เช่น ทำให้ logic การแปลงข้อมูลแบนลง (flatten) เป็นระยะ และสำหรับการคำนวณที่ซับซ้อนให้ materialize เป็น base view หรือตารางที่ชัดเจน
ซับคิวรีที่ลึกเกินไป
- ซับคิวรีที่ซ้อนลึก ตั้งแต่ 3–4 ชั้นขึ้นไป ทำให้อ่านยากและดีบักได้ลำบาก
- มีกรณีที่ต้องจัดการกับซับคิวรียาวเกิน 5000 บรรทัดด้วย
- การใช้ CTE (Common Table Expression) จะช่วยให้แยกขั้นตอนเชิงตรรกะได้ง่ายขึ้น และเพิ่ม ความชัดเจนในการอ่าน (readability) ของคิวรี
บทสรุป
- SQL อาจดูเรียบง่ายบนผิวหน้า แต่เมื่อระบบใหญ่ขึ้น ความซับซ้อนก็จะเพิ่มขึ้นตามธรรมชาติ
- แอนตีแพตเทิร์น ส่วนใหญ่มักไม่ได้เกิดจากเจตนาไม่ดี แต่เป็นผลจาก การประนีประนอมเพื่อให้ได้ “ผลลัพธ์ที่รวดเร็ว” (ความเร็ว, เดดไลน์, การแก้ปัญหาเฉพาะหน้า)
- หาก ดูแล SQL เหมือนโค้ด (version control, code review, การออกแบบที่ชัดเจน) ก็จะช่วยให้ได้ทั้งประสิทธิภาพการทำงานและความน่าเชื่อถือในระยะยาว
- การลงทุนเวลาเพียงไม่กี่นาทีในช่วงออกแบบแรกเริ่ม เพื่อคิดเรื่องความชัดเจนและความสม่ำเสมอ จะช่วยลดงานแก้ซ้ำและความสับสนในอนาคตได้มาก
5 ความคิดเห็น
ตอนนี้ไม่มีเวลา ก็เลยบอกตัวเองว่าเอาแค่แก้ปัญหาเฉพาะหน้าก่อน แล้วค่อยมาเขียนใหม่ทีหลัง แต่พอมันสะสมไปเรื่อย ๆ ก็กลายเป็นนรกของคิวรีที่น่าสยดสยอง ผมเองก็เคยสร้างไว้หลายอันเหมือนกัน ทั้งที่ก็รู้อยู่แล้วว่า "ทีหลัง" ที่จะกลับมาเขียนใหม่พวกนั้น จะไม่มีวันมาถึงเลย
อุ๊ย...
"ปัญหาส่วนใหญ่เกิดจากการแก้ไขปัญหาแบบเฉพาะหน้าเพราะแรงกดดันเรื่องความเร็วและเดดไลน์"
ฮือ..
ความคิดเห็นจาก Hacker News
SELECT x FROM tแม้เราจะเห็นจากสคีมาของ t ว่า x มีข้อจำกัด PRIMARY หรือ UNIQUE แต่เดี๋ยวใครสักคนก็อาจลบข้อจำกัด UNIQUE ออกได้ แล้วข้อมูลซ้ำก็จะเกิดขึ้นจนต้องมานั่งงงว่าเพราะอะไร SQL ไม่ใช่ภาษาแบบเซต (set) แต่เป็นภาษาแบบ bag ตอนรันไทม์ ถ้าเจอ rel t และแอตทริบิวต์ x ก็แค่ส่งค่ากลับมา อาจมีค่าซ้ำ อาจเปลี่ยนชนิดข้อมูลได้ ถ้าคุณต้องการ Set ก็ต้องระบุ DISTINCT ให้ชัดเจน โดย query planner จะไม่ทำ deduplication ถ้ารันไทม์พบว่าเป็น UNIQUE หรือ PRIMARYhttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
WHERE col IS NULLจึงทำงานไม่มีประสิทธิภาพแม้ col จะมีดัชนีก็ตาม ถ้าจำเป็นจริง ๆ แนะนำให้สร้างคอลัมน์ char(1) หรือ bit เพื่อระบุว่า col เป็น null หรือไม่ แล้วทำดัชนีบนฟิลด์นั้นแทนhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
คำว่า "sargable" เป็น portmanteau ของ "Search ARGument ABLE"
การใช้ฟังก์ชันกับคอลัมน์ที่ทำดัชนีไว้เป็นสัญญาณว่าคิวรีนั้นไม่ sargable
แทนที่จะใช้ DISTINCT แบบพร่ำเพรื่อ คิวรีที่ใช้ de-dupe จาก fanout ที่เกิดจาก join ให้สอดคล้องกับ grain ของตาราง เช่น แบบนี้มีประโยชน์มาก บาง DB รองรับฟีเจอร์ QUALIFY ซึ่งทำให้คิวรีสะอาดขึ้นมาก
คำอธิบายเรื่อง sargable
QUALIFY in Redshift
UPPER(name)) อย่างน้อยใน MS SQL Server ไม่ใช่วิธีที่ดีที่สุด ไม่แน่ใจว่า DB อื่นรองรับแค่ไหน แต่ทางแก้ที่ดีกว่าคือสร้าง computed column ที่ไม่สนตัวพิมพ์ด้วย COLLATE ไปเลย (ปรับตามความต้องการได้)สิ่งที่สำคัญที่สุดกลับตกหล่นไปนะ