Data Analyst › SQL

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

วิธีนี้ใช้ได้กับฐานข้อมูลแทบทุกยี่ห้อ (พกไปใช้ตอนสัมภาษณ์ได้เลย)

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

จำนวนลูกค้าต่อเมือง เรียงจากมากไปน้อย

ทีมการตลาดอยากรู้ว่าเมืองไหนมีลูกค้าเยอะที่สุด เพื่อวางแผนเปิดสาขาใหม่

  1. ถ้าเขียน SELECT city, COUNT(*) FROM customers เฉยๆ (ไม่มี GROUP BY) จะ error ทันที เพราะ SQL ไม่รู้ว่าจะ "นับแยกตาม" อะไร ต้องบอกด้วย GROUP BY ว่าจะรวมกลุ่มตามคอลัมน์ไหน

    SQL

เมืองไหนมีลูกค้ามากกว่า 300 คน

ต่อจากตัวอย่างที่แล้ว อยากเห็นเฉพาะเมืองที่มีลูกค้าค่อนข้างเยอะ (มากกว่า 300 คน) เท่านั้น

  1. เริ่มจากนับจำนวนลูกค้าต่อเมืองเหมือนตัวอย่างก่อนหน้า

    SQL

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

หาราคาเฉลี่ยของสินค้าแต่ละหมวด ปัดเป็นจำนวนเต็ม เรียงจากแพงไปถูก เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

นับจำนวนออเดอร์แต่ละสถานะ (status)

🟢 พื้นฐานยังไม่ได้ทำ

หาราคาต่ำสุดและสูงสุดของสินค้าแต่ละหมวด (ตั้งชื่อคอลัมน์ lo และ hi)

🟢 พื้นฐานยังไม่ได้ทำ

นับจำนวนลูกค้าที่สมัครในแต่ละปี (ตั้งชื่อคอลัมน์ y และ n)

🟢 พื้นฐานยังไม่ได้ทำ

นับจำนวนออเดอร์แต่ละช่องทางชำระเงิน (payment_method)

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

หมวดสินค้าที่มีจำนวนสินค้าน้อยกว่า 20 รายการ

🟡 ประยุกต์ยังไม่ได้ทำ

ช่องทางชำระเงินที่มีออเดอร์อย่างน้อย 1000 รายการ เรียงจากมากไปน้อย

🟡 ประยุกต์ยังไม่ได้ทำ

ส่วนลดเฉลี่ย (ปัด 1 ตำแหน่ง) ของออเดอร์ที่มีส่วนลด แยกตามช่องทางชำระเงิน

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

เมืองที่มีสัดส่วนลูกค้าจากช่องทาง organic มากกว่า 46% ของลูกค้าทั้งหมดในเมืองนั้น (ปัดเป็น 1 ตำแหน่งทศนิยม, คอลัมน์ pct_organic) เรียงจากมากไปน้อย

Quiz

1. WHERE กับ HAVING ต่างกันยังไง
2. ทำไม SELECT city, COUNT(*) FROM customers (ไม่มี GROUP BY) ถึง error
3. GROUP BY city, channel ต่างจาก GROUP BY city ยังไง

สรุป

  • 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)