เคล็ดลับและเทคนิค SQL
(github.com/ben-n93)- เป็นรายการที่รวบรวม นิสัยการเขียน SQL และรูปแบบคิวรี ที่ใช้บ่อยในงานวิเคราะห์ข้อมูล โดยมีข้อสมมติว่าอาจไม่ได้ใช้ได้เหมือนกันกับ RDBMS ทุกตัว
- ในด้านความอ่านง่าย แนะนำวิธีที่ทำให้คิวรีอ่านและแก้ไขได้ง่ายขึ้น เช่น comma นำหน้า,
WHERE 1=1, การเยื้อง, CTE, คอมเมนต์ และUSING - ในด้านการประมวลผลข้อมูล ยกตัวอย่างไวยากรณ์ที่ใช้จริงในการกรองผลลัพธ์ สร้างผลรวม และตรวจสอบความแตกต่างของตาราง เช่น anti-join,
QUALIFY,GROUP BY ROLLUP,EXCEPT - ในด้านประสิทธิภาพและความถูกต้อง
NOT INที่มีNULLปนอยู่, การแปลงชนิดข้อมูลโดยนัย, และการชนกันของ alias ของฟิลด์ที่คำนวณ อาจทำให้ผลลัพธ์หรือความเร็วของคิวรีไม่เสถียรได้ - สำหรับคิวรีที่ซับซ้อน นิสัยพื้นฐานอย่างการเข้าใจลำดับการทำงาน ตรวจสอบเอกสาร ระบุที่มาของคอลัมน์ และตั้งชื่อคิวรีที่บันทึกไว้ มีความสำคัญต่อการดีบักและการนำกลับมาใช้ซ้ำ
นิสัยที่ช่วยให้การเขียน SQL อ่านง่ายขึ้น
- repository นี้เป็นรายการที่สรุป เคล็ดลับและเทคนิค SQL ที่ได้เรียนรู้มาตลอดหลายปี โดยเน้นสิ่งที่มีประโยชน์ในงานประจำวันของนักวิเคราะห์ข้อมูล และสิ่งที่อยากรู้ตั้งแต่ตอนเริ่มเขียน SQL
- เคล็ดลับบางข้ออาจไม่เหมาะกับ RDBMS ทุกตัว
-
comma นำหน้าและ
ANDนำหน้า- สำหรับการคั่นฟิลด์ใน
SELECTแนะนำให้ใช้ comma นำหน้า แทน comma ต่อท้าย - ทำให้เห็นชัดขึ้นว่าเป็นคอลัมน์ใหม่หรือเป็นโค้ดที่ตัดบรรทัดลงมา
- แม้ความยาวบรรทัดจะแตกต่างกัน ก็หาจุดที่ลืมใส่ comma ได้ง่ายขึ้น
- ด้วยเหตุผลเดียวกัน สามารถใส่
ANDนำหน้าเงื่อนไขในWHEREได้เช่นกัน
- สำหรับการคั่นฟิลด์ใน
-
ทำให้ทดสอบเงื่อนไขง่ายขึ้นด้วย
WHERE 1=1- หากใส่ เงื่อนไข dummy
1=1ในWHEREแม้จะคอมเมนต์เงื่อนไขออกระหว่างทดสอบ คิวรีก็จะไม่พัง - แม้จะคอมเมนต์เงื่อนไขทั้งหมดออกไป ก็ยังเหลือ
1=1ทำให้คิวรียังรันต่อได้
- หากใส่ เงื่อนไข dummy
-
การเยื้องและ formatter
-
พิจารณาใช้ CTE สำหรับคิวรีที่ซับซ้อน
- หากซ้อน inline view มากกว่า 2–3 ชั้น คิวรีมักกลายเป็นสิ่งที่เข้าใจยากเมื่อกลับมาดูอีกครั้งหลังผ่านไปหลายสัปดาห์
- CTE ถูกนำเสนอเป็นวิธีทำให้คิวรียาว ๆ เป็นระเบียบขึ้น และช่วยเรื่องการนำกลับมาใช้ซ้ำกับการดีบัก
-
คอมเมนต์ควรอธิบาย “ทำไม”
- เมื่อเวลาผ่านไป อาจจำได้ยากว่าทำไมถึงต้องประมวลผลบางอย่างแบบนั้น
- โดยทั่วไป คอมเมนต์ควรอธิบายว่า ทำไม ถึงทำแบบนั้น มากกว่าจะอธิบายว่าโค้ดทำงาน “อย่างไร”
- ตัวอย่างคือการใส่คอมเมนต์ให้เงื่อนไขที่ตัดเนื้อหา archive ออก เพราะ CMS ใหม่ไม่สามารถจัดการรูปแบบวิดีโอ archive ได้
-
ใช้
USINGเมื่อ join คอลัมน์ชื่อเดียวกัน- เมื่อ join ด้วยคอลัมน์ที่มีชื่อเดียวกันในสองตาราง การใช้
USINGจะเขียน join ได้กระชับกว่าON USINGจะ ลบคอลัมน์ร่วมที่ซ้ำกัน ออกจากผลลัพธ์และคืนกลับมาเพียงคอลัมน์เดียว- หากใช้
ONแล้วไม่ได้ระบุคอลัมน์ร่วมให้ชัดเจน อาจเกิดข้อผิดพลาดambiguous column nameได้
- เมื่อ join ด้วยคอลัมน์ที่มีชื่อเดียวกันในสองตาราง การใช้
ไวยากรณ์ที่มีประโยชน์ต่อการประมวลผลข้อมูล
-
ใช้ anti-join เพื่อหาแถวที่ไม่มีในอีกตาราง
- anti-join ใช้เมื่อต้องการคืนค่าแถวที่มีอยู่ในตารางหนึ่ง แต่ไม่มีแถวที่ match กันในอีกตาราง
- ตัวอย่างกล่าวถึงสถานการณ์ที่ต้องการดึงเฉพาะ
video_idของเนื้อหาที่ยังไม่ถูก archive - มีหลายวิธีในการทำ
- ใช้
LEFT JOINแล้วกรองเฉพาะแถวที่ key ของตารางที่ match เป็นNULL - ใช้
NOT INกับ subquery - ใช้
NOT EXISTSกับ correlated subquery - ไม่แนะนำให้ใช้
NOT INเพราะค่าNULLอาจทำให้ไม่ทำงานตามที่ตั้งใจ
-
กรองผลลัพธ์ของ window function ด้วย
QUALIFYQUALIFYช่วยให้กรองผลลัพธ์ของคิวรีตามผลลัพธ์ของ window function ได้- สามารถกรองได้โดยไม่ต้องใช้ inline view จึงลดจำนวนบรรทัดของโค้ดได้
- ตัวอย่างเลือกตลาด 10 อันดับแรกของแต่ละผลิตภัณฑ์ด้วย
DENSE_RANK()แล้วกรองด้วยQUALIFY - มีข้อจำกัดว่า
QUALIFYดูเหมือนจะมีให้ใช้เฉพาะใน data warehouse ขนาดใหญ่ เช่น Snowflake, Amazon Redshift, Google BigQuery
-
GROUP BYและORDER BYตามตำแหน่งคอลัมน์- สามารถใช้ ตำแหน่งคอลัมน์ แทนชื่อคอลัมน์ได้ เช่น
GROUP BY 1,ORDER BY 2 - อาจมีประโยชน์สำหรับคิวรีชั่วคราวหรือคิวรีใช้ครั้งเดียว
- สำหรับ production code แนะนำให้ refer ถึงชื่อคอลัมน์โดยตรงเสมอ
- สามารถใช้ ตำแหน่งคอลัมน์ แทนชื่อคอลัมน์ได้ เช่น
-
สร้างผลรวมด้วย
GROUP BY ROLLUPGROUP BY ROLLUPใช้สร้าง subtotal และ grand total ได้- ตัวอย่างสร้างแถวผลรวมเงินเดือนทั้งหมด พร้อมกับคำนวณผลรวมเงินเดือนแยกตามแผนก
- เอกสาร Transact-SQL อธิบายว่า
ROLLUPสร้างกลุ่มตามชุดค่าผสมของนิพจน์คอลัมน์ และลดจำนวนกลุ่มจากขวาไปซ้ายเพื่อสร้าง subtotal และ grand total - หากใช้
COALESCEจะสามารถแสดงแถวผลรวมทั้งหมดเป็นTotalได้ - ต้องใส่ใจคอลัมน์สำหรับ sort เพื่อให้แถวผลรวมทั้งหมดมาอยู่ท้ายผลลัพธ์
-
หา difference ระหว่าง result set สองชุดด้วย
EXCEPTEXCEPTคืนค่าแถวที่มีในผลลัพธ์ของคิวรีแรก แต่ไม่มีในผลลัพธ์ของคิวรีที่สอง- หากใช้
EXCEPTร่วมกับUNION ALLจะสามารถตรวจสอบได้ว่าสองตารางมีข้อมูลเหมือนกันหรือไม่ - หากไม่มีแถวที่คืนกลับมา แสดงว่าสองตารางเหมือนกัน
- หากมีแถวที่คืนกลับมา แถวเหล่านั้นคือสาเหตุที่ทำให้เกิดความแตกต่าง
รูปแบบที่บั่นทอนประสิทธิภาพและความถูกต้อง
-
สำหรับคอลัมน์ที่อาจเป็น
NULLใช้NOT EXISTSดีกว่าNOT IN- หากคอลัมน์ที่ใช้เปรียบเทียบอนุญาตให้เป็น
NULLได้NOT INมักอาจช้ากว่าNOT EXISTS - ผู้เขียนพบปรากฏการณ์นี้ใน Snowflake และ PostgreSQL Wiki หน้า Don’t Do This ระบุว่า
NOT IN (SELECT ...)ไม่ได้รับการ optimize ที่ดี - หากค่าที่ใช้เปรียบเทียบมี
NULLอยู่NOT INจะไม่ทำงานตามที่ตั้งใจ - การที่คอลัมน์อนุญาตให้เป็น
NULLไม่ได้แปลว่ามีค่าNULLจริง ๆ แต่เมื่อทำงานกับตารางที่แก้ไขไม่ได้NOT EXISTSอาจช่วยปรับปรุงความเร็วได้
- หากคอลัมน์ที่ใช้เปรียบเทียบอนุญาตให้เป็น
-
การแปลงชนิดข้อมูลโดยนัยอาจทำให้ช้าลงหรือล้มเหลวได้
- หากใส่ค่าที่มี data type ต่างจากคอลัมน์ลงในเงื่อนไข ฐานข้อมูลอาจพยายามทำ การแปลงชนิดข้อมูลโดยนัย
- ตัวอย่างกล่าวถึงกรณีเปรียบเทียบคอลัมน์
video_idที่เป็นชนิด string กับ integer200050 - การพึ่งพาการแปลงชนิดข้อมูลโดยนัยอาจก่อปัญหาได้
- หากมีค่าที่แปลงไม่ได้ อาจเกิด error
- คิวรีอาจช้าลงเพราะมีงานเพิ่มเติมในการแปลงแต่ละค่าให้เป็นชนิดที่กำหนด
- ควรใช้ data type เดียวกับคอลัมน์ หรือหากต้องการหลีกเลี่ยง error สามารถใช้ฟังก์ชันอย่าง
TRY_TO_NUMBERของ Snowflake ได้ - ผลกระทบด้านความเร็วขึ้นอยู่กับขนาด dataset ที่ประมวลผล
ข้อผิดพลาดที่พบบ่อย
-
NOT INกับNULLNOT INจะไม่ทำงานหากค่าที่ใช้เปรียบเทียบมีNULLNULLแทนค่า Unknown ดังนั้น SQL engine จึงไม่สามารถยืนยันได้ว่าค่าที่ตรวจสอบไม่ได้อยู่ใน list- ในกรณีนี้ การใช้
NOT EXISTSเป็นทางเลือกหนึ่ง
-
การชนกันของ alias ของฟิลด์ที่คำนวณ
- หากตั้งชื่อฟิลด์ที่คำนวณให้เหมือนกับคอลัมน์เดิม อาจเกิดพฤติกรรมที่ไม่คาดคิดได้
- เอกสาร
GROUP BYของ Snowflake ระบุว่า หากชื่อในGROUP BYmatch ได้ทั้งชื่อคอลัมน์และ alias จะใช้ชื่อคอลัมน์ - ในตัวอย่าง หากสร้าง alias ด้วย
LEFT(product, 1) AS productแล้วใช้GROUP BY productจะถูก group ด้วยคอลัมน์productเดิม ไม่ใช่ตัวอักษรแรก จึงคืนค่ากลับมา 3 แถว - วิธีแก้มีสองแบบ
- ใช้ alias ที่ไม่ซ้ำ เช่น
product_letter - ระบุนิพจน์ให้ชัดเจน เช่น
GROUP BY LEFT(product, 1) - ใน window function ก็อาจเกิดปัญหา alias ได้เช่นกัน
- ในตัวอย่าง แม้จะใช้
CASEเปลี่ยน revenue ของRobotเป็น 0 แต่การเปลี่ยนแปลงถูกนำไปใช้หลังจาก window function ทำงานแล้ว ทำให้อันดับไม่เป็นไปตามที่คาด - หากทำได้ ควรใช้ alias ที่ไม่ซ้ำ หรือใส่นิพจน์คำนวณโดยตรงใน
ORDER BYของ window function
-
ระบุว่าคอลัมน์เป็นของตารางใด
- ในคิวรีซับซ้อนที่มีหลาย join ควรสามารถ trace ปัญหาของค่ากลับไปถึงตารางต้นทางได้
- เมื่อสองตารางมีชื่อคอลัมน์เดียวกัน หากไม่ระบุว่าคอลัมน์เป็นของตารางใด RDBMS อาจแจ้ง error
- ตัวอย่างใส่ table alias เช่น
vc.video_id,metadata.seasonเพื่อทำให้ที่มาของคอลัมน์ชัดเจน
ลำดับการทำงาน เอกสาร และชื่อที่บันทึก
-
เข้าใจลำดับการทำงานของ SQL
- หนึ่งในคำแนะนำที่สำคัญที่สุดสำหรับผู้เรียน SQL คือการเข้าใจ ลำดับการทำงานของ clause
- หากรู้ลำดับการทำงาน วิธีเขียนคิวรีอาจเปลี่ยนไปอย่างมาก
- แนะนำแหล่งอ้างอิง A beginner’s guide to the true order of SQL operations
-
อ่านเอกสารให้จบ
- มีกรณีที่ใช้
GREATEST()ใน Snowflake เพื่อคืนค่าวันที่ล่าสุดจากหลายคอลัมน์วันที่ GREATEST()จะคืนค่าNULLหาก argument ตัวใดตัวหนึ่งเป็นNULL- หากอ่านเอกสารต่ออีกหน่อย ก็สามารถใช้
GREATEST_IGNORE_NULLS()แทนCOALESCE(GREATEST(...), ...)ได้ - ในหลายกรณี การกวาดตาดูเอกสารใช้เวลาไม่ถึง 1 นาที และช่วยลดความพยายามในการหาสาเหตุที่ทำงานไม่ตรงกับที่คาดได้
- มีกรณีที่ใช้
-
ใช้ชื่อที่อธิบายได้สำหรับคิวรีที่บันทึกไว้
- เพื่อหลีกเลี่ยงสถานการณ์ที่หาคิวรีที่ต้องรันซ้ำหรือใช้อ้างอิงไม่เจอ ควรบันทึกด้วย ชื่อที่อธิบายได้
- ชื่อที่บันทึกมักประกอบด้วยหัวข้อของคิวรี เดือนที่รัน และชื่อผู้ร้องขอ
- ตัวอย่างคือรูปแบบ
Lapsed users analysis - 2023-09-01 - Olivia Roberts
2 ความคิดเห็น
เครื่องหมายจุลภาคนำหน้าในโพสต์นี้ถูกเขียนเป็นจุลภาคต่อท้ายทั้งหมดเลยครับ ในต้นฉบับใส่เป็นแบบนำหน้าไว้
ความคิดเห็นจาก Hacker News
ทิปที่ฉันอยากเสริมคือ: ต้องเรียนรู้ DB server ที่ใช้อยู่ให้ดี และตรวจ execution plan บ่อย ๆ เพราะอาจได้ผลลัพธ์ที่คาดไม่ถึง ควรปรับแล้วตรวจใหม่เสมอ
โดยทั่วไป
EXISTSมักเร็วกว่าINและNOT EXISTSก็ทำงานต่างจากEXCEPTในการจัดการNULLแทนที่จะ join ตารางแล้วค่อยกรองแถวออกด้วยอะไรอย่างDISTINCTการใช้ subquery column ในรายการSELECTบางครั้งอาจเร็วกว่าได้มาก แม้จะต้องดึงค่ามากกว่า 10 ค่าจากตารางเดียวกันก็ยังเป็นแบบนั้นได้ และอาจยังจริงอยู่แม้ DB server จะรองรับ lateral join ก็ตาม แต่ subquery ต้องคืนค่าได้ไม่เกินหนึ่งแถวถ้าไม่ใช่ query ที่รันครั้งเดียว ก็ควรหลีกเลี่ยงไม่ให้มี table scan ทั้งตาราง table scan ของวันนี้อาจกลายเป็นเหตุขัดข้องของวันพรุ่งนี้ได้ จึงควรเพิ่ม index ไว้ และอย่าลืมว่าปกติแล้ว clause
GROUP BYมักมีผลอย่างมากต่อการใช้ indexถ้าจำเป็นต้อง filter ด้วย expression เช่น ต้องตรวจว่าบาง substring เท่ากับค่าที่กำหนดหรือไม่ ก็สามารถเพิ่ม computed column แล้วสร้าง index บนคอลัมน์นั้นได้ บาง DB รองรับ expression index โดยตรงด้วย การใช้
UNION ALLแทนORก็มักทำให้เร็วขึ้นมากใน query ที่ซับซ้อนหรือมีหลายเงื่อนไขORถ้า DB ไม่สามารถจัดลำดับการ filter ได้อย่างฉลาดพอ การ
JOINsubquery เพื่อบังคับลำดับก็มีประโยชน์เช่นกันสิ่งที่น่าสนใจใน Postgres คือ และ DB อื่นก็น่าจะเป็นได้เหมือนกัน การทำ manual sharding ให้กับงาน
INSERT (SELECT ...)ตามจำนวน CPU core สามารถทำให้เร็วขึ้นเกือบเป็นเส้นตรงได้ แม้จะมี join ราว 10 ตัวก็ยังทำได้ ให้ดูEXPLAINก่อน หา join ที่อยู่ชั้นในสุดหรือชั้นนอกสุด แล้วรัน query แบบขนานแยกตามช่วงแถว (id >= start AND id < end) ได้เลย เมื่อ 6 ปีก่อนฉันใช้วิธีนี้บ่อยมากในงานหนึ่งด้วยเหตุผลประหลาดบางอย่าง Postgres 10+ เพิ่มความสามารถด้าน parallelism เข้ามาแล้ว แต่เท่าที่รู้ก็ยังไม่ก้าวหน้าถึงระดับนี้SELECTหมายถึงอะไรแน่เช่น ถ้ารัน
SELECT column1, (SELECT column2, column3, ... FROM table_b WHERE table_a.id = table_b.a_id) FROM table_aก็จะได้ข้อความ “subquery must return only one column” ตามคาด หมายถึงให้คืนหลายคอลัมน์มาเป็น record/composite type เหรอ?ส่วนที่บอกว่า clause
GROUP BYมักมีผลต่อการใช้ index ทำไมถึงเป็นอย่างนั้น ฉันยังนึกภาพไม่ค่อยออก แต่สำหรับคนที่สงสัย บทความนี้อธิบายเป็นขั้นตอนได้ดีมาก: https://www.brentozar.com/archive/2015/06/indexing-for-group...EXPLAINและเรียนรู้วิธีตีความมันด้วยเครื่องมือที่คุณถนัด และควร monitor query ด้วยที่สตาร์ตอัปก่อนหน้านี้ฉันติดตั้ง PgHero ไว้ ซึ่งช่วยมากจริง ๆ ทั้งในการปรับจูนประสิทธิภาพและจัดลำดับความสำคัญ
มี query ที่ table scan ทั้งตาราง เป็นกลยุทธ์ที่มีประสิทธิภาพที่สุดอยู่เหมือนกัน โดยมากคือ query วิเคราะห์/สรุปผลที่อ่านทั้งตาราง และบางครั้งแม้จะดึงมาเพียง 50% ของทั้งตาราง table scan ก็ยังดีกว่า
และฉันก็ไม่ค่อยเข้าใจว่า table scan แบบอ่านอย่างเดียวจะนำไปสู่เหตุขัดข้องได้อย่างไร เพราะมันไม่ได้บล็อกการเข้าถึงพร้อมกัน ข้อเสียก็มีแค่เพิ่มภาระ I/O เท่านั้น ซึ่งถ้าเซิร์ฟเวอร์รับแค่นี้ไม่ไหว ก็ถือว่าสเปกต่ำเกินไปอย่างหนักตั้งแต่แรก
ตัวอย่าง 3 อันในส่วน “ความอ่านง่าย” ดูแปลก ๆ สองอันแรกพยายามทำให้เขียนง่ายขึ้นโดย ยอมลดความอ่านง่าย แบบตรงตัว ส่วนอันสุดท้ายก็ดูเป็นสัตว์ประหลาดอ่านยากที่แทบจะกู้ไม่ได้แม้จะจัดย่อหน้าแล้วก็ตาม
ฉันคิดว่านักพัฒนาดูประวัติ commit กันมากพอ ๆ กับที่ดูซอร์สโค้ดจริง
เจอบ่อยพอจนตอนนี้ไม่ได้รู้สึกขัดตาเท่าไรแล้ว
ฉันไม่เห็นว่ามีปัญหาอะไร
และก็ดูไม่เห็นว่ามีอะไรผิด
SELECTเป็นบรรทัดละคอลัมน์ แต่กลับปล่อยบรรทัดยาว 150 ตัวอักษรไว้แบบเดิม? นี่คือ นิยามของความอ่านง่ายที่พังไปแล้ว เรื่อง comma ยังไม่ต้องเริ่มเลยใน code review ไม่มีใครอ่านบรรทัดยาว ๆ ได้จริงจัง นั่นคือปัญหาใหญ่ที่สุดของ AngularJS merge ถูกทำผิดและทุกอย่างพัง เพราะพอสายตาไปถึงคอลัมน์ที่ 90 ก็เริ่มเบลอแล้ว ฉันผ่านทีมที่มี code review มาเกินครึ่งโหลและเจอเหมือนกันทุกครั้ง ต่อให้ตั้งใจระวังและพยายามหลีกเลี่ยงปัญหานี้มากแค่ไหน ฉันเองก็ยังพลาดอยู่ด้วยความถี่ประมาณครึ่งหนึ่งของคนอื่น
แบ่งบรรทัดกันเถอะ โดยเฉพาะถ้าจะเอาตัวอย่างไปให้คนอื่นดู ยิ่งควรทำแบบนั้น
เคล็ดลับในการจัดการ stored procedure ที่ซับซ้อนมีดังนี้
WHEREclause หากพยายามทำJOINหรือทำงานซับซ้อน มีโอกาสสูงที่จะ timeoutถ้าทำข้อ 5 โดยไม่ทำข้อ 6 ก็มีโอกาสสูงที่จะไม่เห็นว่าตัวเองกำลังทำสิ่งที่ไม่เหมาะสม คำแนะนำของผมคือหลีกเลี่ยงการ optimize เร็วเกินไป เขียนในแบบที่ตรงไปตรงมาที่สุดก่อน แล้วค่อย optimize เมื่อจำเป็น สิ่งสำคัญที่สุดคืออย่าเขียน SQL แบบเชิงกระบวนการ คุณกำลังอธิบายข้อมูลที่ต้องการ ไม่ได้สั่ง engine ว่าควรไปดึงมาอย่างไร
ในทางกลับกัน Microsoft ก็ชอบเตือนตลอดว่าอย่าพยายามจูนมัน ราวกับว่า query planner รู้ดีที่สุด
เวอร์ชัน ของ DB ก็อาจมีผลได้เช่นกัน
ผมไม่ชอบการพัฒนาแบบ “เผื่อไว้ก่อน” ทั้งเรื่องอินเทอร์เฟซและ placeholder อย่าง
where 1=1ก็เหมือนกันให้ทำเมื่อจำเป็น อย่าทำเพียงเพราะคิดว่าวันหนึ่งในอนาคตอาจต้องใช้ โค้ดโปรดักชันไม่ใช่ที่สำหรับทิ้งตัวช่วยของฝั่งพัฒนาไว้ ระหว่างพัฒนาจะทำแบบไหนก็ได้ แต่ในโค้ดโปรดักชัน ความอ่านง่ายและเจตนาที่ชัดเจน สำคัญกว่ามาก
ขอเสริมเรื่อง “anti join” อีกอย่างหนึ่ง ถ้าแค่ต้องการตรวจว่ามีแถวที่ตรงเงื่อนไขอยู่ในตารางใหญ่อื่นหรือ subquery หรือไม่ การใช้ EXISTS จะดีกว่า
INหรือLEFT JOINEXISTSจะคืนค่าเป็นจริงทันทีที่เจอรายการที่ตรงกัน ส่วนLEFT JOINและINนั้น engine จะรวบรวมผลทั้งหมดก่อนประเมิน(NOT) EXISTSให้ execution plan ที่ดีกว่าหรืออย่างน้อยก็เท่ากับ(LEFT) JOINหรือ(NOT) INแถมยังสื่อเจตนาได้ชัดเจนกว่า
เรื่อง “ใส่คอมเมนต์ในโค้ด” มีคำแนะนำบ่อย ๆ ว่าอย่างน้อยใน MSSQL ควรใช้
/**/แทน--ในคอมเมนต์ เพราะฟีเจอร์อย่าง Query Store มักเก็บคิวรีโดยไม่มีการขึ้นบรรทัดใหม่ ทำให้พอดึงคิวรีออกมาจากตรงนั้นแล้ว แทนที่จะใช้ตัวจัดรูปแบบของ IDE ได้ทันที กลับต้องมานั่งแก้ทั้งหมดด้วยมือOBJECT_DEFINITIONselect name,cast((select OBJECT_DEFINITION(object_id) for xml path('')) as xml) from sys.proceduresเพราะมันเก็บบรรทัดใหม่ไว้ จึงอาจจัดระเบียบได้ง่ายขึ้น แต่ตัวอักษร XML อื่น ๆ จะเสีย เช่น
>ถูกเปลี่ยนเป็น>อีกทางเลือกหนึ่งคือใช้VARBINARYแล้วหาอะไรสักอย่างมาแปลงกลับทุกคนดูจะเดือดกับข้อเสนอเรื่อง comma กันมาก แล้ว
1=1ในWHEREclause นี่ถือว่าเป็นความคิดที่ดีหรือ? ถ้าเห็นใน code review ผมไม่รู้จริง ๆ ว่าควรคิดยังไงกับคนเขียนWHEREstatement จะไม่กระทบบรรทัดอื่น เลยทำให้ code review ง่ายขึ้นแต่ถ้าใช้ด้วยเหตุผลแบบในกรณีนี้ คือเพื่อเพิ่มเงื่อนไขแบบไดนามิก ที่ที่ผมทำงานอยู่คงโดนไล่ออกแน่
มีใครพอจะแชร์แนวทางทั่วไปได้ไหมว่า ควรขีดเส้นระหว่างการเพิ่มความเร็วด้วยการตั้งค่า DB ซึ่งแทบจะเป็นแนวทางแบบ “ซื้อ” กับการ “สร้าง” ด้วยการลงมือทำเองเกือบทั้งหมดไว้ตรงไหน? จากประสบการณ์อันจำกัดของฉัน DBA เก่ง ๆ มักได้ค่าตอบแทนสูงกว่ามากและไปทำงานที่อื่นกัน เลยทำให้งานนี้มักตกมาที่นักพัฒนาแอป อย่างที่พูดไว้ข้างบน การรู้เรื่อง DB จึงสำคัญ
ตัวอย่างที่พบบ่อยคือข้อมูลที่สะสมจำนวนมากตามกาลเวลา และข้อมูลล่าสุดคือส่วนที่ถูกเข้าถึงบ่อยที่สุด DBA อาจทำให้การเข้าถึงยังคงเร็วได้ด้วยการทำ partitioning หรือ partial index แต่นักพัฒนาแอปก็อาจเลือกย้ายเรคคอร์ดเก่าไปไว้ในตาราง archive แยกต่างหากแบบเบื้องหลัง พร้อมทั้งยังรองรับความสามารถอย่างการค้นหาครอบคลุมชุดข้อมูลทั้งหมดในท้ายที่สุดได้ด้วย บางครั้งก็รู้สึกว่าเครื่องมือน่าจะช่วยทำงานตั้งต้นอย่างการแยกตารางหนึ่งออกเป็นหลายตารางในจังหวะที่เหมาะสมได้ค่อนข้างอัตโนมัติ เช่นในกรณีที่ถูกจำกัดด้วยการขาดฟีเจอร์ของ cloud DB
อีกทางเลือกหนึ่งด้านการจัดการคือเก็บ blob/ไฟล์ขนาดใหญ่ทั้งหมดไว้ในฐานข้อมูลแยกต่างหาก หรือไม่ก็ใน file system เพื่อใช้การตั้งค่าการจัดเก็บอีกแบบหนึ่ง ซึ่งเรื่องนี้ก็เป็นได้ทั้งสิ่งที่ DB รองรับให้ หรือสิ่งที่ต้องจัดการเองแบบแมนนวล
ในกรณีสุดโต่ง คุณอาจไปไกลถึงขั้นทำดัชนีขึ้นมาเองก็ได้ โดยมีตารางขนาดมหึมาหนึ่งตารางที่มีเพียง primary key แบบ auto-increment กับคอลัมน์จำนวนมาก และสร้างอีกตารางแยกที่มี ID นั้นพร้อมคอลัมน์ไม่กี่ตัวที่ใช้ค้นหาได้ อาจไปไกลถึง full-text search หรือ vector ก็ได้
เคล็ดลับที่มีประโยชน์สำหรับการทำ materialized view pattern แบบแมนนวลบน MSSQL 2016+ คือใช้ partition switching ร่วมด้วย ซึ่งมีอธิบายและทำไว้ดีใน https://github.com/cajuncoding/SqlBulkHelpers?tab=readme-ov-... มันเป็นไลบรารีเล็ก ๆ ที่ฉันเจอโดยบังเอิญ แต่กลับมีประโยชน์เชิงพาณิชย์มากที่สุดตัวหนึ่ง ทั้งที่อันดับค้นหาไม่เด่นและดาวก็น้อย โดยโฟกัสที่การ bulk insert เข้า MSSQL ด้วย .NET ฉันคิดว่านี่เป็นตัวอย่างที่ดีของการขีดเส้นระหว่างการซื้อ/สร้างได้อย่างเหมาะสมผ่านการทำ automation ให้กับ partition switching
สิ่งที่ขาดไป: ควรเลิกใช้
SELECT *ได้แล้ว แทบจะแน่นอนว่าคุณไม่ได้ต้องการความกว้างทั้งตาราง และการทำแบบนั้นจะเพิ่มทั้งข้อมูลที่ต้องกรองและส่งต่อ แถมยังไปขัดขวางฟีเจอร์ดี ๆ อย่าง semi join ด้วยถ้าเป็นนักพัฒนา ก็ใช่เลย
SELECT *มีหลุมพราง และเกือบทุกครั้งควรระบุคอลัมน์ให้ชัด หรือใช้ query builder ที่ช่วยทำให้แทนแต่ถ้าเป็นนักวิเคราะห์ ชีวิตมันสั้น และบางครั้งคุณก็อาจไม่อยากพิมพ์ทุกคอลัมน์ทั้งหมด
SELECT *ก็พอรับได้อาจจะนอกประเด็นไปหน่อย แต่การที่ผู้ดูแล ปิด pull request ไปเฉย ๆ โดยไม่มีคอมเมนต์หรือการพูดคุยใด ๆ ถือเป็นแนวทางที่ยอมรับได้ไหม?
ถามในฐานะคนที่เคยหรือพยายามจะมีส่วนร่วมกับ repository นี้เป็นครั้งคราว
ตัวอย่าง: https://github.com/ben-n93/SQL-tips-and-tricks/pulls?q=is%3A...