โจทย์ SQL แนวสัมภาษณ์
ประมาณ 40 นาที
เป้าหมายของบท
- แก้โจทย์ "อันดับที่ N" แบบคลาสสิกโดยไม่ใช้ LIMIT/OFFSET
- รวมเทคนิคจากบทก่อนหน้า (subquery, join, window function, date) แก้โจทย์ธุรกิจที่ซับซ้อนขึ้น
- รู้ทันกับดักที่พบบ่อยในห้องสัมภาษณ์ (RANK vs DENSE_RANK เมื่อมี tie, ลำดับการทำงานของ WHERE กับ window function)
ทบทวนก่อนเข้าห้องสัมภาษณ์
บทนี้ไม่มีเนื้อหาใหม่ แต่รวมเทคนิคจาก 6 บทที่ผ่านมาเข้าด้วยกันในรูปแบบที่คำถามสัมภาษณ์จริงมักถาม: คำถามที่ดูตรงไปตรงมาแต่ซ่อนกับดักไว้ และคำถามที่ต้องใช้หลายเทคนิคผสมกัน
รูปแบบ “อันดับที่ N” — คำถามคลาสสิกที่พบบ่อยมาก
คำถามแบบ “หาเงินเดือนสูงสุดอันดับ 2” หรือ “สินค้าราคาถูกอันดับ 3” เป็นคำถามที่พบได้บ่อยมากในห้องสัมภาษณ์ บางบริษัทจะขอให้ทำโดยไม่ใช้ LIMIT/OFFSET เพื่อทดสอบว่าเข้าใจแนวคิดการจัดอันดับข้อมูลจริงๆ
มี 2 วิธีหลัก:
- Subquery เทียบค่า: หาค่าที่ “มากที่สุดที่ยังน้อยกว่าค่าสูงสุด” — ใช้ได้ดีเมื่อต้องการแค่ค่าเดียว ไม่ต้องมีคอลัมน์อื่นประกอบ
- DENSE_RANK แล้วกรอง: ยืดหยุ่นกว่า ยังคืนคอลัมน์อื่นๆ มาด้วยได้ และสำคัญที่สุดคือ ทำงานถูกต้องแม้มีค่าซ้ำกันที่อันดับสูงกว่า
จุดที่มักพลาด: ใช้ RANK แทน DENSE_RANK — ถ้าอันดับ 1 มีค่าซ้ำกัน 2 แถว RANK จะข้ามอันดับ 2 ไปเลย
(กลายเป็น 1, 1, 3) ทำให้กรอง WHERE rnk = 2 แล้วไม่เจอผลลัพธ์ใดๆ ทั้งที่ควรมีคำตอบ
รวมหลายเทคนิคเข้าด้วยกัน
โจทย์ธุรกิจในงานจริง (และในห้องสัมภาษณ์ระดับที่สูงขึ้น) มักไม่ได้ใช้แค่เทคนิคเดียว เช่น:
- “ลูกค้าที่ซื้อซ้ำแต่ไม่เคยยกเลิกเลย” → GROUP BY/HAVING (หา “ซื้อซ้ำ”) + NOT EXISTS (กรอง “ไม่เคยยกเลิก”)
- “session ที่ผ่านครบทุกขั้นของ funnel” → COUNT DISTINCT + subquery ห่อกรองค่า
- “หมวดที่มีสัดส่วนรายได้เกิน 10%” → CTE คำนวณยอดรวมจริงแยกไว้ก่อน + window function
เวลาเจอโจทย์ที่ซับซ้อน ให้แตกออกเป็นขั้นย่อยๆ ก่อน (มักทำเป็น CTE ทีละขั้น) แล้วค่อยประกอบกัน ไม่ต้องพยายามเขียนให้จบในคำสั่งเดียวตั้งแต่แรก
กับดักที่ต้องระวังเป็นพิเศษ: WHERE กับ window function
ถ้าใช้ SUM(...) OVER () (ไม่มี PARTITION BY) เพื่อหา “สัดส่วนต่อยอดรวม” แต่คำนวณมันในจุดที่ query
มี WHERE กรองแถวออกไปก่อนแล้ว window function จะเห็นแค่แถวที่เหลือหลังกรอง ไม่ใช่ข้อมูลทั้งหมดตั้งแต่ต้น
ทำให้ “สัดส่วน” ที่ได้คลาดเคลื่อนไปจากความเป็นจริง
ถ้าต้องการฐานคำนวณจากข้อมูลทั้งหมด (ก่อนกรอง) ต้องแยกคำนวณยอดรวมไว้ต่างหากใน CTE ก่อน
แล้วค่อยเอามาประกอบกับข้อมูลที่กรองแล้วทีหลัง (ดูตัวอย่างในแบบฝึก e9)
เตรียมตัวสำหรับคำถามที่ไม่ใช่โค้ด
ห้องสัมภาษณ์ SQL มักมีคำถามเชิงแนวคิดปนมาด้วย เช่น “อธิบายลำดับการทำงานของ query” หรือ “เจอผลลัพธ์ที่ดูเยอะเกินจริงหลัง JOIN ควรตรวจอะไรก่อน” — คำถามเหล่านี้วัดว่าเข้าใจ SQL จริงๆ ไม่ใช่แค่จำ syntax ได้ ทบทวนหัวข้อ “คำถามสัมภาษณ์” ท้ายทุกบทของโมดูลนี้ให้คล่องก่อนเข้าสัมภาษณ์จริง
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
สินค้าราคาแพงอันดับ 2 ของหมวด Beauty โดยไม่ใช้ LIMIT/OFFSET
โจทย์คลาสสิกในห้องสัมภาษณ์: หา "อันดับที่ 2" โดยไม่ให้ใช้ LIMIT/OFFSET (เพื่อทดสอบว่าเข้าใจการเทียบค่าและ window function จริงๆ ไม่ใช่จำ syntax) ใช้หมวด Beauty เพราะมีสินค้าราคาสูงสุดเท่ากันอยู่ 2 ชิ้นพอดี ทำให้เห็นปัญหาที่ซ่อนอยู่ชัดเจน
วิธีที่คุ้นเคย:
ORDER BY ... DESC LIMIT 1 OFFSET 1ใช้งานได้จริงในหลายกรณี แต่ลองดูผลลัพธ์ให้ดี — ได้ "Beauty Item 11" ราคา 1759 ซึ่งซ้ำกับราคาสูงสุด (Beauty Item 06 ก็ 1759) นี่ไม่ใช่ "ราคาอันดับ 2" ในทางความหมาย เพราะราคาที่ต่ำกว่าอันดับ 1 จริงๆ คือ 1309
ลูกค้าที่ "ซื้อซ้ำ" แต่ไม่เคยยกเลิกออเดอร์เลยสักครั้ง
รวมเทคนิค GROUP BY/HAVING (บทที่ 2) กับ NOT EXISTS (บทที่ 4) เข้าด้วยกัน
หาลูกค้าที่ "ซื้อซ้ำ" ก่อน (มีออเดอร์มากกว่า 1 ครั้ง)
session ไหนผ่านครบทุกขั้นของ funnel (visit → view_product → add_to_cart → checkout → purchase)
ทีมผลิตภัณฑ์อยากรู้ว่า session ไหนที่ผู้ใช้เดินทางครบทุกขั้นของ funnel จนซื้อสำเร็จ
นับจำนวน event_type ที่ต่างกัน (ไม่ซ้ำ) ในแต่ละ session — session ที่ผ่านครบทุกขั้นจะมีค่านี้เท่ากับ 5
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
หาสินค้าราคาถูกอันดับ 2 (ไม่ใช้ LIMIT/OFFSET) ในหมวด Home
เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
5 ลูกค้าที่สมัครก่อนใครในระบบ (ถ้าวันสมัครซ้ำกัน ให้ customer_id น้อยกว่าถือว่าสมัครก่อน)
จำนวนออเดอร์ของแต่ละวิธีจ่ายเงิน ที่มีการให้ส่วนลด (discount_pct ไม่ใช่ NULL)
จำนวนลูกค้าที่เคยซื้อสินค้าหมวด Books (นับไม่ซ้ำคน)
จำนวนลูกค้าที่ไม่เคยจ่ายด้วย cod เลยสักครั้ง
🟡 ประยุกต์
หมวดสินค้าที่มีราคาสูงสุด (MAX price) เป็นอันดับ 2 เมื่อเทียบราคาสูงสุดของแต่ละหมวดด้วยกัน (ไม่ใช้ LIMIT/OFFSET)
ลูกค้าที่ออเดอร์แรกสุดของตัวเอง (เรียงตามวันที่ แล้ว order_id เป็น tie-break) มีมูลค่ารวมมากกว่า 5000 บาท มีกี่คน
เดือนไหน (ปีไหน) ที่มีลูกค้าสมัครใหม่มากที่สุด
ค่าเฉลี่ยจำนวนวันระหว่างการสั่งซื้อแต่ละครั้ง ของลูกค้าที่มีออเดอร์อย่างน้อย 2 ครั้ง (ปัด 1 ตำแหน่ง)
🔴 ท้าทาย
หมวดสินค้าที่มีสัดส่วนรายได้ (completed) มากกว่า 10% ของรายได้รวมทั้งหมด (ปัด 1 ตำแหน่งเป็น %)
ระวัง: ถ้าคำนวณ % ด้วย SUM(...) OVER () วางไว้ในจุดที่มันถูกคำนวณหลังจากที่ WHERE
กรองแถวออกไปแล้ว window จะคิดจากยอดของแถวที่เหลือหลังกรองเท่านั้น ไม่ใช่ยอดรวมทั้งหมดจริง
(ตัวอย่างที่ผิด: Electronics จะได้ 70.2% ทั้งที่จริงคือ 59% เพราะฐานหารเหลือแค่ Electronics+Home+Sports)
ต้องแยกคำนวณยอดรวมจริงไว้ก่อนใน CTE ต่างหาก แล้วค่อยกรอง
จำนวนลูกค้าที่เคยซื้อ (completed) ทั้งหมวด Electronics และหมวด Books (อาจคนละออเดอร์ก็ได้)
Quiz
สรุป
- โจทย์ "อันดับที่ N" แบบไม่ใช้ LIMIT/OFFSET ทำได้ด้วย DENSE_RANK (ปลอดภัยกว่า RANK เมื่อมี tie) หรือ correlated subquery
- โจทย์ธุรกิจซับซ้อนมักรวมหลายเทคนิคเข้าด้วยกัน (join + subquery + window function + date) ไม่ใช่ใช้ทีละอย่าง
- window function ที่คำนวณหลัง WHERE กรองแล้ว จะเห็นแค่ข้อมูลที่เหลือ ไม่ใช่ข้อมูลทั้งหมดตั้งแต่ต้น — ถ้าต้องการฐานคำนวณจากข้อมูลทั้งหมดต้องแยกคำนวณไว้ก่อน
คำถามสัมภาษณ์
ระหว่างทำโจทย์ SQL สดในห้องสัมภาษณ์ ควรอธิบายความคิดยังไงให้ผู้สัมภาษณ์ตามทัน
เริ่มจากพูดสรุปสั้นๆ ว่าเข้าใจโจทย์ว่ายังไง แล้วถามกลับถ้ามีจุดที่ไม่ชัดเจน (เช่น "นับรวมออเดอร์ที่ยกเลิกด้วยไหม") จากนั้นเล่าแผนคร่าวๆ ก่อนเริ่มเขียน (เช่น "ผมจะ join ตาราง A กับ B ก่อน แล้วค่อย group by") ระหว่างเขียนโค้ด พูดเหตุผลของแต่ละขั้นออกมา ไม่ใช่แค่พิมพ์เงียบๆ และถ้าเขียนแบบง่ายได้ผลลัพธ์คร่าวๆ ก่อน ค่อยบอกว่าจะปรับปรุงยังไงต่อ (เช่น เพิ่มเงื่อนไข หรือแก้ปัญหา edge case) ก็เป็นการแสดงกระบวนการคิดที่ดี
ยกตัวอย่างวิธีหา "อันดับที่ N" โดยไม่ใช้ LIMIT/OFFSET พร้อมอธิบายว่าทำไมบางบริษัทถามแบบนี้
ตัวอย่าง: หาพนักงานที่เงินเดือนสูงเป็นอันดับ 2 — ใช้ DENSE_RANK() OVER (ORDER BY salary DESC) แล้วห่อ
ด้วย subquery กรอง WHERE rnk = 2 (เลือก DENSE_RANK ไม่ใช่ RANK เพราะถ้าเงินเดือนสูงสุดซ้ำกันหลายคน
RANK จะข้ามอันดับ 2 ไปเลย)
บริษัทถามแบบนี้เพราะ LIMIT/OFFSET เป็นเพียง syntax เฉพาะของบางฐานข้อมูล และไม่ได้บอกว่าเข้าใจแนวคิด เรื่องการจัดอันดับข้อมูลจริงๆ — การห้ามใช้ LIMIT/OFFSET บังคับให้ต้องคิดด้วย window function หรือ subquery ซึ่งเป็นทักษะที่ใช้แก้ปัญหาที่ซับซ้อนกว่านี้ได้ด้วย
เจอผลลัพธ์หลัง JOIN ที่ดูเยอะเกินจริง ควรตรวจสอบอะไรก่อน
ให้สงสัยปัญหา fan-out ก่อนเป็นอันดับแรก (ทบทวนจากบทที่ 3) — เกิดเมื่อ JOIN กับตารางที่มีความสัมพันธ์ แบบ one-to-many เช่น 1 ออเดอร์มีหลายรายการสินค้า ทำให้แถวของฝั่ง "one" ถูกคูณซ้ำ
วิธีตรวจสอบเร็วๆ:ลองนับจำนวนแถวก่อน JOIN เทียบกับหลัง JOIN ถ้าต่างกันมาก ให้ดูว่า JOIN
กับตารางไหนที่อาจมีหลายแถวต่อ 1 คีย์ แล้วแก้ด้วย COUNT(DISTINCT ...) บนคีย์ที่ไม่อยากนับซ้ำ
หรือ aggregate ตารางลูกให้เหลือแถวเดียวต่อคีย์ก่อน (ด้วย subquery/CTE) แล้วค่อย JOIN