GROUP BY และ HAVING
ประมาณ 25 นาที
เป้าหมายของบท
- ใช้ COUNT/SUM/AVG/MIN/MAX สรุปข้อมูลเป็นกลุ่มด้วย GROUP BY
- กรอง "กลุ่ม" ด้วย HAVING (ต่างจาก WHERE ที่กรองทีละแถว)
- จัดกลุ่มด้วยหลายคอลัมน์พร้อมกัน
สรุปข้อมูลเป็นกลุ่มด้วย GROUP BY
จนถึงตอนนี้เราดูข้อมูลทีละแถว แต่คำถามธุรกิจส่วนใหญ่ถามถึง “ภาพรวม” เช่น “แต่ละเมืองมีลูกค้ากี่คน”
ต้องใช้ ฟังก์ชัน aggregate รวมกับ GROUP BY:
| ฟังก์ชัน | ทำอะไร |
|---|---|
COUNT(*) |
นับจำนวนแถว |
SUM(col) |
รวมค่า |
AVG(col) |
ค่าเฉลี่ย |
MIN(col) / MAX(col) |
ค่าต่ำสุด / สูงสุด |
SELECT city, COUNT(*) AS n FROM customers GROUP BY city
กติกาสำคัญ: ทุกคอลัมน์ที่อยู่ใน SELECT (ที่ไม่ใช่ aggregate function) ต้องอยู่ใน GROUP BY ด้วย
ถ้าลืม จะได้ error ทันที (ในฐานข้อมูลส่วนใหญ่ รวมถึง DuckDB ที่ใช้ในเว็บนี้ — บางระบบเก่าอาจ “เดา” ให้เงียบๆ ซึ่งอันตรายกว่า) เพราะคอลัมน์ที่ไม่ได้ GROUP BY มีหลายค่าในหนึ่งกลุ่ม ไม่มีคำตอบเดียวให้แสดง
กรอง “กลุ่ม” ด้วย HAVING
WHERE กรองทีละแถวก่อนจัดกลุ่ม แต่ถ้าอยากกรองผลลัพธ์ หลัง รวมกลุ่มแล้ว (เช่น “เฉพาะเมืองที่มีลูกค้ามากกว่า 300 คน”)
ต้องใช้ HAVING แทน:
SELECT city, COUNT(*) AS n FROM customers
GROUP BY city
HAVING COUNT(*) > 300
| WHERE | HAVING | |
|---|---|---|
| ทำงานตอนไหน | ก่อนจัดกลุ่ม | หลังจัดกลุ่มและคำนวณ aggregate แล้ว |
| ใช้ aggregate function ได้ไหม | ไม่ได้ (error) | ได้ |
| กรองอะไร | ทีละแถว | ทีละกลุ่ม |
GROUP BY หลายคอลัมน์
SELECT city, channel, COUNT(*) AS n
FROM customers
GROUP BY city, channel
ยิ่งใส่คอลัมน์เยอะใน GROUP BY กลุ่มก็ยิ่งละเอียดขึ้น — แต่ละแถวผลลัพธ์คือ 1 คู่ค่าที่ไม่ซ้ำกันของทุกคอลัมน์ที่ GROUP BY ไว้
เงื่อนไขแบบมีเงื่อนไขซ้อนใน aggregate
บางครั้งอยากนับเฉพาะบางเงื่อนไขภายในกลุ่มเดียวกัน (เช่น “กี่ % ของลูกค้าในเมืองนี้มาจาก organic”)
ใช้ CASE WHEN ... THEN 1 ELSE 0 END ข้างใน SUM/COUNT ได้:
SELECT city,
SUM(CASE WHEN channel = 'organic' THEN 1 ELSE 0 END) AS n_organic
FROM customers
GROUP BY city
วิธีนี้ใช้ได้กับฐานข้อมูลแทบทุกยี่ห้อ (พกไปใช้ตอนสัมภาษณ์ได้เลย)
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
จำนวนลูกค้าต่อเมือง เรียงจากมากไปน้อย
ทีมการตลาดอยากรู้ว่าเมืองไหนมีลูกค้าเยอะที่สุด เพื่อวางแผนเปิดสาขาใหม่
ถ้าเขียน
SELECT city, COUNT(*) FROM customersเฉยๆ (ไม่มี GROUP BY) จะ error ทันที เพราะ SQL ไม่รู้ว่าจะ "นับแยกตาม" อะไร ต้องบอกด้วย GROUP BY ว่าจะรวมกลุ่มตามคอลัมน์ไหน
เมืองไหนมีลูกค้ามากกว่า 300 คน
ต่อจากตัวอย่างที่แล้ว อยากเห็นเฉพาะเมืองที่มีลูกค้าค่อนข้างเยอะ (มากกว่า 300 คน) เท่านั้น
เริ่มจากนับจำนวนลูกค้าต่อเมืองเหมือนตัวอย่างก่อนหน้า
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
หาราคาเฉลี่ยของสินค้าแต่ละหมวด ปัดเป็นจำนวนเต็ม เรียงจากแพงไปถูก
เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
นับจำนวนออเดอร์แต่ละสถานะ (status)
หาราคาต่ำสุดและสูงสุดของสินค้าแต่ละหมวด (ตั้งชื่อคอลัมน์ lo และ hi)
นับจำนวนลูกค้าที่สมัครในแต่ละปี (ตั้งชื่อคอลัมน์ y และ n)
นับจำนวนออเดอร์แต่ละช่องทางชำระเงิน (payment_method)
🟡 ประยุกต์
หมวดสินค้าที่มีจำนวนสินค้าน้อยกว่า 20 รายการ
ช่องทางชำระเงินที่มีออเดอร์อย่างน้อย 1000 รายการ เรียงจากมากไปน้อย
ส่วนลดเฉลี่ย (ปัด 1 ตำแหน่ง) ของออเดอร์ที่มีส่วนลด แยกตามช่องทางชำระเงิน
🔴 ท้าทาย
เมืองที่มีสัดส่วนลูกค้าจากช่องทาง organic มากกว่า 46% ของลูกค้าทั้งหมดในเมืองนั้น (ปัดเป็น 1 ตำแหน่งทศนิยม, คอลัมน์ pct_organic) เรียงจากมากไปน้อย
Quiz
สรุป
- COUNT/SUM/AVG/MIN/MAX สรุปค่าของกลุ่มแถวที่ GROUP BY จัดไว้
- ทุกคอลัมน์ใน SELECT ต้องอยู่ใน GROUP BY หรือเป็น aggregate function เท่านั้น ไม่งั้น error
- WHERE กรองก่อนจัดกลุ่ม (ใช้ aggregate ไม่ได้) ส่วน HAVING กรองหลังจัดกลุ่ม (ใช้ aggregate ได้)
คำถามสัมภาษณ์
อธิบายลำดับการประมวลผลจริงของ SQL query หนึ่งคำสั่ง
แม้จะเขียนตามลำดับ SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT แต่ SQL ประมวลผลจริงตามลำดับนี้: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
นี่คือเหตุผลที่ WHERE ใช้ aggregate function ไม่ได้ (ยังไม่ถึงขั้น GROUP BY) แต่ HAVING ใช้ได้ (ทำงานหลัง GROUP BY แล้ว) และทำไม ORDER BY ถึงใช้ชื่อ alias จาก SELECT ได้ (SELECT ทำงานก่อน ORDER BY)