54 คะแนน โดย GN⁺ 2025-10-19 | 5 ความคิดเห็น | แชร์ทาง WhatsApp
  • แอนตีแพตเทิร์นของ 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 ความคิดเห็น

 
aer0700 2025-10-20

ตอนนี้ไม่มีเวลา ก็เลยบอกตัวเองว่าเอาแค่แก้ปัญหาเฉพาะหน้าก่อน แล้วค่อยมาเขียนใหม่ทีหลัง แต่พอมันสะสมไปเรื่อย ๆ ก็กลายเป็นนรกของคิวรีที่น่าสยดสยอง ผมเองก็เคยสร้างไว้หลายอันเหมือนกัน ทั้งที่ก็รู้อยู่แล้วว่า "ทีหลัง" ที่จะกลับมาเขียนใหม่พวกนั้น จะไม่มีวันมาถึงเลย

 
firefoxsaiko123 2025-10-20

อุ๊ย...

 
ilikeall 2025-10-20

"ปัญหาส่วนใหญ่เกิดจากการแก้ไขปัญหาแบบเฉพาะหน้าเพราะแรงกดดันเรื่องความเร็วและเดดไลน์"
ฮือ..

 
GN⁺ 2025-10-19
ความคิดเห็นจาก Hacker News
  • ถ้าฉันเห็นว่ามีการใช้ DISTINCT ในคิวรี ฉันจะสงสัยว่าคนเขียนอาจจะยังไม่เข้าใจ data model หรือทฤษฎีเซตดีพอ หรืออาจจะไม่เข้าใจทั้งสองอย่าง
    • บางครั้ง DISTINCT ก็อาจเป็นสัญญาณของสคีมาที่ถูก normalize มากเกินไปด้วย เช่น ฉันไม่คิดว่าจำเป็นต้องสร้างตารางอย่าง addresses_cities ขึ้นมาเพียงเพื่อป้องกันการบันทึกชื่อเมืองซ้ำ
    • ประสบการณ์ของฉันก็เกือบจะเหมือนกัน แต่ช่วงหลังมีกรณีหนึ่งที่แม้ join ทุกตัวจะถูกต้องแล้ว พอใส่ DISTINCT ไว้ใน CTE กลับทำให้ประสิทธิภาพดีขึ้นมาก ดูเหมือนว่าเมื่อรับประกันความเป็นเอกลักษณ์ของเรคอร์ดได้ ตัว query planner จะทำ optimization ได้
    • ฉันเคยเพิ่ม LIMIT 1 ลงไปในคิวรีเพราะคาดว่าผลลัพธ์จะออกมาได้มากสุดแค่หนึ่งรายการ แล้วก็ได้รับฟีดแบ็กทำนองเดียวกันว่าไม่ค่อยดีนัก แต่บนตารางใหญ่ ๆ พอเจอเรคอร์ดที่ต้องการแล้ว DB ก็มักยังไล่สำรวจทั้งตารางต่ออยู่ดี (ทั้ง sqlite, mysql, postgresql)
    • ขอถามหน่อยว่าจะรู้ได้อย่างไรว่าปลอดภัยที่จะตัด DISTINCT ออกจากคิวรี SELECT x FROM t แม้เราจะเห็นจากสคีมาของ t ว่า x มีข้อจำกัด PRIMARY หรือ UNIQUE แต่เดี๋ยวใครสักคนก็อาจลบข้อจำกัด UNIQUE ออกได้ แล้วข้อมูลซ้ำก็จะเกิดขึ้นจนต้องมานั่งงงว่าเพราะอะไร SQL ไม่ใช่ภาษาแบบเซต (set) แต่เป็นภาษาแบบ bag ตอนรันไทม์ ถ้าเจอ rel t และแอตทริบิวต์ x ก็แค่ส่งค่ากลับมา อาจมีค่าซ้ำ อาจเปลี่ยนชนิดข้อมูลได้ ถ้าคุณต้องการ Set ก็ต้องระบุ DISTINCT ให้ชัดเจน โดย query planner จะไม่ทำ deduplication ถ้ารันไทม์พบว่าเป็น UNIQUE หรือ PRIMARY
    • ใน Cypher กลับตรงกันข้ามเลย เวลาใช้ neo4j กับข้อมูลซับซ้อน โหนดซ้ำหลุดเข้ามาในผลลัพธ์ได้ง่ายมากจน DISTINCT กลายเป็นสิ่งจำเป็น โดยเฉพาะเมื่อใช้ความสัมพันธ์แบบความยาวแปรผัน ถ้าไม่มี DISTINCT จะช้าลงและมีข้อมูลซ้ำเยอะ
  • ฉันเคยเขียนทูโทเรียลยาวราว 9,000 ตัวอักษรเป็นสองพาร์ตเกี่ยวกับการออกแบบโครงสร้างคิวรีให้ถูกต้องโดยไม่ต้องพึ่ง DISTINCT
    https://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
    • บทความดีมาก บุ๊กมาร์กไว้แล้ว แล้วก็เพิ่งรู้ว่านี่เป็นหนังสือจริง ๆ ด้วย
  • สิ่งหนึ่งที่ไม่ค่อยถูกพูดถึงคือคิวรีที่ใช้ค้นหา "สิ่งที่ไม่มีอยู่" เช่นการใช้ != หรือ NOT IN (...) ซึ่งส่วนใหญ่แล้วไม่มีประสิทธิภาพ (แม้ว่าในบางกรณีที่ผลลัพธ์ถูกทำให้แคบลงมากจากเงื่อนไขอื่นแล้วอาจใช้ได้) และการเข้าใจว่า DB จัดการกับ null อย่างไรก็สำคัญมาก เช่น null กับสตริงว่างเหมือนกันหรือไม่, null == null หรือไม่ ซึ่งอาจต่างกันไปในแต่ละ DB
    • เรื่องการจัดการ null และการทำดัชนี DB ที่ฉันเคยใช้จะไม่สร้างดัชนีให้ค่า null ดังนั้นคิวรี WHERE col IS NULL จึงทำงานไม่มีประสิทธิภาพแม้ col จะมีดัชนีก็ตาม ถ้าจำเป็นจริง ๆ แนะนำให้สร้างคอลัมน์ char(1) หรือ bit เพื่อระบุว่า col เป็น null หรือไม่ แล้วทำดัชนีบนฟิลด์นั้นแทน
    • ที่บอกว่า != หรือ NOT IN (...) แทบจะไม่มีประสิทธิภาพเสมอ อยากรู้ว่าเพราะอะไร ถ้าค่าด้านขวาเป็นค่าคงที่ ก็น่าจะ lookup ผ่าน hash table ได้และโดยทั่วไปก็น่าจะมีประสิทธิภาพ อยากรู้ว่ามีทางเลือกที่มีประสิทธิภาพกว่านี้ไหม
  • ฉันไม่คิดว่า 'anti-pattern' ที่ยกมาทั้งหมดจะเป็น anti-pattern จริง ๆ ปัญหาที่เงื่อนไขในคิวรีไม่สอดคล้องกับดัชนี สุดท้ายก็มาจากการไม่เข้าใจหลักการของดัชนี และหลายปัญหาที่พูดถึงที่นี่ก็เกี่ยวข้องกับการออกแบบสคีมาฐานข้อมูลมากกว่าตัว SQL เอง ถ้าต้องใช้ DISTINCT ก็อาจแปลว่าการออกแบบ primary key ยังไม่ถูกต้อง การซ้อน view มากเกินไปก็สะท้อนว่าการออกแบบตารางฐานไม่ดี การทำ DB modeling ที่ดีจะป้องกันปัญหาพวกนี้ได้ตั้งแต่ต้น
  • 'anti-pattern' เหล่านี้จริง ๆ แล้วเป็นแค่ทางอ้อมง่าย ๆ ที่เกิดจากข้อจำกัดของการออกแบบภาษา SQL (หรือการที่มันไม่ได้ถูกออกแบบไว้) ฉันกำลังสร้างภาษาใหม่ที่ทำงานบนฐานข้อมูล SQL และอยากมีทางเลือกที่ดีกว่าสำหรับปัญหาแต่ละแบบ ตอนนี้ยังไม่เสร็จและเอกสารก็ยังน้อย แต่ถ้าสนใจก็อยากได้ฟีดแบ็กที่ https://lutra-lang.org
    • คำว่า "ฐานข้อมูล SQL" ค่อนข้างกำกวม SQL ไม่ได้มีใช้แค่กับฐานข้อมูลเชิงสัมพันธ์ แต่ยังมีตัวอย่างการนำไปใช้กับ DB ที่ไม่ใช่เชิงสัมพันธ์ด้วย ผู้เชี่ยวชาญรับรู้ปัญหาของ SQL มานานแล้ว และก็มีทางเลือกอย่าง Tutorial D ของ Chris Date และ Hugh Darwen มาแต่ก่อนแล้ว ถึงอย่างนั้นตัวเลือกเหล่านี้ก็ไม่อาจตั้งหลักได้ เพราะมีทั้งโค้ด SQL และเครื่องมือที่สะสมมาหลายทศวรรษ ส่วนตัวฉันได้ความมั่นคงในการทำงานและรายได้สม่ำเสมอมาหลายสิบปีเพราะ SQL ดังนั้นแม้จะอยากได้ภาษาที่ดีกว่านี้ แต่ในอีกมุมหนึ่งฉันก็มองสถานการณ์นี้ในแง่บวก
    • โปรเจกต์ดูน่าสนใจ ถ้าพัฒนาไปได้สมบูรณ์ขึ้นกว่านี้ฉันจะคอยติดตามแน่นอน
  • anti-pattern ที่ใหญ่ที่สุดคือการไม่มองว่า SQL เป็นภาษาโปรแกรมจริง ๆ ไม่ใช่แค่ภาษา query ฉันแนะนำให้จัด indentation ของโค้ดให้สม่ำเสมอ และจัดกลุ่มส่วนที่เกี่ยวข้องกันในเชิงตรรกะ ควรเปลี่ยน subquery ให้เป็น CTE และการเขียนคอมเมนต์อย่างมีประสิทธิภาพก็สำคัญด้วย ดูสไตล์ของฉันได้ที่: https://bentilly.blogspot.com/2011/02/sql-formatting-style.html
    • ฉันคิดว่าการถกเถียงเรื่องสไตล์โค้ดแบบนี้แทบไม่มีความหมายเลยถ้าไม่มีเครื่องมือ linter ที่เหมาะสม
  • เคล็ดลับสำคัญที่สุดที่ช่วยให้คิวรีของฉันเร็วขึ้นและใช้ทรัพยากรเซิร์ฟเวอร์น้อยลง คือการทำให้คิวรี sargable มากขึ้น
    https://en.wikipedia.org/wiki/Sargable
    https://www.brentozar.com/blitzcache/non-sargable-predicates/
    • อยากรู้ว่าคำว่า "sargable" ถูกใช้จริงในคอมมูนิตี้ไหนบ้าง ฉันทำ SQL มานานกว่า 20 ปี แต่แทบไม่เคยเห็นคำนี้ใน manual, Stack Overflow หรือ HN เลย อยากรู้ว่ามันนิยมใช้ใน DB ไหน บริษัทไหน หรือคอมมูนิตี้โอเพนซอร์สแบบใดเป็นพิเศษ
    • ตอนตามหา etymology ของ sargable คำตอบใน StackOverflow นี้ช่วยได้ https://dba.stackexchange.com/a/217983
      คำว่า "sargable" เป็น portmanteau ของ "Search ARGument ABLE"
  • ปัญหาการใช้ CASE WHEN มากเกินไปจำนวนมากแก้ได้ด้วยการรวม logic ไว้ที่เดียวใน UDF (User Defined Function)
    การใช้ฟังก์ชันกับคอลัมน์ที่ทำดัชนีไว้เป็นสัญญาณว่าคิวรีนั้นไม่ sargable
    แทนที่จะใช้ DISTINCT แบบพร่ำเพรื่อ คิวรีที่ใช้ de-dupe จาก fanout ที่เกิดจาก join ให้สอดคล้องกับ grain ของตาราง เช่น
    ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
    
    แบบนี้มีประโยชน์มาก บาง DB รองรับฟีเจอร์ QUALIFY ซึ่งทำให้คิวรีสะอาดขึ้นมาก
    คำอธิบายเรื่อง sargable
    QUALIFY in Redshift
    • ปัญหาความไม่ sargable แก้ได้ง่ายด้วย expression index อย่างน้อยฉันคิดว่าใน sqlite เป็นแบบนั้น
  • บางครั้งการซ้อน view ก็เป็นสิ่งจำเป็นจริง ๆ ในซอฟต์แวร์ POS ของเรา เราใช้ nested view เยอะมากเพื่อสร้าง backbone view ที่ทำให้มองธุรกรรมได้อย่างสะอาดในที่เดียว ไม่อย่างนั้นก็ต้องเขียน where clause แยกตามแต่ละตาราง และต้องจัดการเงื่อนไขอย่าง void/คืนสินค้า/ยกเลิก ซ้ำไปซ้ำมาทุกครั้ง พอมีการเปลี่ยนแปลงก็ต้องไปแก้ view/procedure หลายสิบตัว สำหรับเคสของเรา การใช้ nested view ใช้งานได้จริงกว่ามาก
  • ประเด็นเรื่องการใช้ฟังก์ชันบนคอลัมน์ที่ทำดัชนีไว้ควรมีคำอธิบายที่ชัดเจนกว่านี้ เมื่อมีฟังก์ชันอยู่บนคอลัมน์ที่ทำดัชนี ประสิทธิภาพของดัชนีจะหายไป และในทางปฏิบัติมักกลายเป็น full scan จนช้าลง ฉันเรียนรู้เรื่องนี้จากการเจอด้วยตัวเอง
    • มีเอกสารที่คนอ้างถึงกันบ่อยเกี่ยวกับเรื่องนี้ https://use-the-index-luke.com/sql/where-clause/obfuscation
    • โซลูชันที่ยกมา (เช่น เพิ่มดัชนีให้คอลัมน์ UPPER(name)) อย่างน้อยใน MS SQL Server ไม่ใช่วิธีที่ดีที่สุด ไม่แน่ใจว่า DB อื่นรองรับแค่ไหน แต่ทางแก้ที่ดีกว่าคือสร้าง computed column ที่ไม่สนตัวพิมพ์ด้วย COLLATE ไปเลย
      ALTER TABLE example ADD name_ci AS name COLLATE SQL_Latin1_General_CI_AS;
      
      (ปรับตามความต้องการได้)
    • ในบล็อกที่เกี่ยวข้องมีพิมพ์ผิด บรรทัดแรกควรเขียนเป็นตัวพิมพ์ใหญ่ ถ้าคุณสร้างดัชนีไว้กับข้อมูลที่ผ่านฟังก์ชันมาแล้ว ก็ไม่จำเป็นต้องสแกนทั้งตารางตอนคิวรี แน่นอนว่าในตัวอย่างนี้ใช้ collation ที่ไม่สนตัวพิมพ์ตั้งแต่แรกจะดีกว่า แต่โดยทั่วไปแล้วแนวคิดหลักก็ยังถูกต้อง
    • "ฉันเรียนรู้เรื่องนี้จากการเจอด้วยตัวเอง" ฟังดูเหมือนเป็นคำขวัญของนักพัฒนา SQL เลย อย่างไรก็ตาม SQL เปลี่ยนแปลงอย่างมั่นคงมาเป็นเวลานาน ดังนั้นการรู้หลุมพรางพวกนี้ล่วงหน้าจึงมีประโยชน์ไปได้อีกนาน
 
ahwjdekf 2025-10-21

สิ่งที่สำคัญที่สุดกลับตกหล่นไปนะ

  • การใช้ orm