---
config:
theme: redux-color
---
erDiagram
direction LR
STORES ||--o{ TRANSACTIONS : receives
STORES {
BIGINT store_id PK
VARCHAR store_name
VARCHAR branch_type
VARCHAR district
BIGINT seats
BIGINT takeaway_heavy_flag
DOUBLE office_traffic_index
DOUBLE mall_traffic_index
DOUBLE delivery_catchment_score
DATE opening_date
BIGINT flagship_flag
BIGINT floor_area_sqm
BIGINT monthly_rent_thb
BIGINT num_employees
BIGINT num_baristas
BIGINT daily_capacity
BIGINT bts_distance_m
BIGINT nearby_competitors
DOUBLE google_rating
BIGINT has_parking
DATE lease_end_date
}
TRANSACTIONS {
BIGINT transaction_id PK
TIMESTAMP order_datetime
DATE order_date
BIGINT hour
VARCHAR day_of_week
BIGINT dow_int
BIGINT month
BIGINT store_id FK
VARCHAR channel
VARCHAR platform
BIGINT customer_id FK "nullable"
BIGINT basket_qty
DOUBLE gross_sales_thb
DOUBLE item_discount_thb
DOUBLE order_discount_thb
DOUBLE delivery_fee_paid_by_customer
DOUBLE platform_subsidy_thb
DOUBLE net_sales_thb
DOUBLE cogs_thb
DOUBLE packaging_cost_thb
DOUBLE platform_commission_thb
DOUBLE labor_alloc_thb
DOUBLE contribution_margin_thb
DOUBLE prep_time_min
DOUBLE wait_time_min
DOUBLE pickup_time_min
BIGINT satisfaction_score
VARCHAR promo_id
VARCHAR weather_flag
BIGINT peak_flag
BIGINT weekend_flag
BIGINT morning_flag
}
วัตถุประสงค์การเรียนรู้
เมื่อจบบทนี้ ผู้เรียนจะสามารถ
- ใช้ฟังก์ชันรวม (Aggregate Functions) ได้แก่
COUNT,SUM,AVG,MIN,MAXเพื่อสร้างตัวชี้วัด - ใช้
GROUP BYเพื่อแยกสรุปตามมิติธุรกิจ เช่น สาขา ช่องทาง วันในสัปดาห์ - แยกความแตกต่างระหว่าง
WHERE(กรองแถว) กับHAVING(กรองกลุ่ม) - ใช้
COUNT(*)เทียบกับCOUNT(column)และCOUNT(DISTINCT column)อย่างถูกต้อง
บทนำ
บทที่ 2 ผู้เรียนได้เรียนรู้วิธีดึงและกรองแถวข้อมูล แต่ในทางปฏิบัติผู้บริหารไม่ต้องการดูแถวในตาราง transactions ทั้ง 53,323 แถว สิ่งที่ผู้บริหารต้องการคือคำตอบในภาพรวม เช่น สาขาใดมียอดขายสูงสุด ช่องทางใดสร้างกำไรได้ดีที่สุด และช่วงเวลาใดมีการใช้บริการมากที่สุด
บทนี้จึงมุ่งแนะนำ Aggregate Functions หรือฟังก์ชันสำหรับสรุปรวมข้อมูล ควบคู่กับการใช้ GROUP BY เพื่อจัดกลุ่มข้อมูลก่อนคำนวณผลลัพธ์ เครื่องมือทั้งสองส่วนนี้มีบทบาทสำคัญในการเปลี่ยนข้อมูลธุรกรรมจำนวนมากให้กลายเป็นตัวชี้วัดทางธุรกิจ หรือ KPI ที่ช่วยสนับสนุนการตัดสินใจของผู้บริหารได้อย่างรวดเร็วและเป็นระบบ
หากผู้บริหารถามว่า
“เดือนที่แล้วเรามียอดขายรวมเท่าไร สาขาใดทำผลงานได้ดีที่สุด และมีสาขาใดที่ควรพิจารณาเปิดเพิ่มหรือปรับกลยุทธ์”
คำตอบย่อมไม่ใช่รายการธุรกรรมทั้งหมดในตาราง แต่เป็นข้อมูลสรุปเพียงไม่กี่บรรทัดที่สะท้อนภาพรวมของธุรกิจได้อย่างชัดเจน ซึ่งก็คือบทบาทสำคัญของการสรุปรวมข้อมูลและการใช้ GROUP BY นั่นเอง ในบทนี้ ตารางหลักที่ใช้คือ transactions ซึ่งบันทึกข้อมูลคำสั่งซื้อ 53,323 รายการ และเชื่อมโยงกับตาราง stores ซึ่งเก็บข้อมูลสาขาทั้ง 6 แห่งผ่าน store_id ดังแสดงในแผนภาพด้านล่าง

รูปที่ 3.1: ความสัมพันธ์ระหว่าง STORES และ TRANSACTIONS หนึ่งสาขารับออเดอร์หลายรายการ
SQL Aggregation ทำงานอย่างไร
graph TB A["คำถามธุรกิจ"] --> B["FROM: โหลดตาราง"] B --> C["WHERE: กรองแถว"] C --> D["GROUP BY: แบ่งกลุ่ม"] D --> E["Aggregate: COUNT SUM AVG"] E --> F["HAVING: กรองกลุ่ม"] F --> G["ORDER BY: เรียงลำดับ"]
รูปที่ 3.2: ลำดับการทำงานของ SQL Aggregation
Aggregate Functions คืออะไร?
Aggregate Functions คือฟังก์ชันสำหรับสรุปข้อมูล ที่ทำการ “คำนวณข้อมูลหลายแถวแล้วให้ผลลัพธ์เป็นค่าเดียว” เช่น หายอดขายรวมของเดือน จำนวนครั้งที่แต่ละเมนูถูกสั่งในวันนี้ หรือหาคะแนนความพึงพอใจเฉลี่ยของลูกค้า เป็นต้น
- ถ้าเปรียบเทียบกับ Excel
- แถว (rows) = รายการแต่ละบิล / แต่ละลูกค้า
- Aggregate functions = ฟังก์ชันอย่าง
SUM,AVERAGE,COUNT
- ใน SQL aggregate function ใช้คำนวณทั้งคอลัมน์หรือทั้งกลุ่มของข้อมูล เช่น
- ยอดขายรวมต่อวัน
- ยอดขายเฉลี่ยต่อบิล
- จำนวนลูกค้าที่ซื้ออย่างน้อย 1 ครั้งในสัปดาห์นั้น
คุณสมบัติสำคัญ
- ทำงานกับ “หลายแถว” แต่คืนค่าเดียว (summary)
- มักใช้คู่กับ
GROUP BYเพื่อสรุปตามมิติธุรกิจ เช่น สรุปตามสาขา ตามวัน ตามสินค้า - ส่วนใหญ่จะ “ไม่สนใจค่า NULL” (ยกเว้น
COUNT(*)) - เหมาะกับการตอบคำถามธุรกิจระดับสรุป เช่น
- เดือนที่แล้วยอดขายรวมเท่าไหร่?
- สาขาไหนยอดขายสูงสุด?
- ค่าเฉลี่ยเวลาให้บริการต่อลูกค้าหนึ่งบิลเป็นเท่าไหร่?
ไวยากรณ์หลักของการใช้ Aggregate functions และ GROUPBY
- มิติ (dimension) = สิ่งที่ต้องการแยกวิเคราะห์ เช่น store_name, channel, day_of_week
- ตัวชี้วัด (measure) = สิ่งที่ต้องการรวม เช่น net_sales_thb, contribution_margin_thb
ตัวอย่างแรกจะใช้ aggregate functions เพื่อสร้างตัวชี้วัดระดับบริษัท (company-wide KPIs) ก่อน แล้วค่อยลงลึกไปยังรายสาขา รายช่องทาง และมิติเวลาต่าง ๆ
-- สรุปสถิติภาพรวมของธุรกรรมทั้งหมด
SELECT
COUNT(*) AS total_orders, -- จำนวนคำสั่งซื้อทั้งหมด
COUNT(DISTINCT customer_id) AS unique_customers, -- จำนวนลูกค้าที่ไม่ซ้ำกัน
MIN(order_date) AS start_date, -- วันที่สั่งซื้อครั้งแรก
MAX(order_date) AS end_date, -- วันที่สั่งซื้อครั้งล่าสุด
ROUND(SUM(net_sales_thb), 0) AS total_revenue, -- รายได้รวมทั้งหมด (บาท)
ROUND(AVG(net_sales_thb), 0) AS avg_order_value, -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
ROUND(MEDIAN(net_sales_thb), 0) AS median_order_value, -- มูลค่าคำสั่งซื้อค่ามัธยฐาน (บาท)
ROUND(MIN(net_sales_thb), 0) AS min_order, -- คำสั่งซื้อที่มีมูลค่าน้อยที่สุด (บาท)
ROUND(MAX(net_sales_thb), 0) AS max_order, -- คำสั่งซื้อที่มีมูลค่ามากที่สุด (บาท)
ROUND(AVG(basket_qty), 2) AS avg_basket -- จำนวนสินค้าเฉลี่ยต่อคำสั่งซื้อ
FROM transactions;| total_orders | unique_customers | start_date | end_date | total_revenue | avg_order_value | median_order_value | min_order | max_order | avg_basket |
|---|---|---|---|---|---|---|---|---|---|
| 53323 | 2111 | 2026-02-01 | 2026-04-01 | 10787401.0 | 202.0 | 153.0 | 68.0 | 695.0 | 1.68 |
จากผลลัพธ์ข้างต้น ขนาดธุรกิจและโครงสร้างการซื้อของลูกค้าในช่วงเวลา 2 เดือน PiCha มีออเดอร์ทั้งหมด 53,323 รายการ จากลูกค้าที่ลงทะเบียนแล้ว 2,111 คน สร้างรายได้รวม 10,787,401 บาท มูลค่าเฉลี่ยต่อออเดอร์ (AOV) อยู่ที่ 202 บาท ออเดอร์เล็กสุด 68 บาท ใหญ่สุด 695 บาท และลูกค้าเฉลี่ยสั่ง 1.68 รายการต่อออเดอร์
ช่องว่างระหว่างออเดอร์มูลค่าต่ำสุด (68 บาท) กับสูงสุด (695 บาท) ต่างกันกว่า 10 เท่า บ่งชี้ว่ามีลูกค้าทั้งกลุ่มที่ซื้อเพียงแก้วเดียว และกลุ่มที่ซื้อหลายแก้วต่อออเดอร์ อัตราส่วนลูกค้า 2,111 คนต่อ 53,323 ออเดอร์ เท่ากับ 25.3 ออเดอร์ต่อคนโดยประมาณในช่วง 2 เดือน อย่างไรก็ตาม ตัวเลขนี้คำนวณจากเฉพาะลูกค้าที่ลงทะเบียน ลูกค้าที่ไม่ระบุตัวตน (รหัสลูกค้าที่สั่งออเดอร์นั้น ๆ มีค่าเป็น NULL)
ขั้นตอนการทำงานของ Aggregate Functions ของตัวอย่างนี้ เป็นดังนี้
- FROM โหลดตาราง transactions ทั้งหมด (53,323 แถว)
- WHERE (ไม่มีเงื่อนไข) ทุกแถวผ่าน
- SELECT + Aggregates รวมทุกแถวเป็นแถวเดียว: นับ, รวม, หาค่าเฉลี่ย
- เมื่อไม่ใช้
GROUP BYทุกแถวจะถูกบีบเป็นแถวเดียว ซึ่งตีความเป็นภาพรวมของกิจการได้
ตัวอย่างนี้สร้าง “ภาพรวมบริษัท 1 บรรทัด” ซึ่งตอบคำถามระดับบนได้ดี แต่คำถามของผู้บริหารมักต้องการรายละเอียดว่า “สาขาไหน ช่องทางไหน หรือวันไหน” ทำให้ผลลัพธ์ต้องแตกออกเป็นหลายบรรทัดตามมิติต่าง ๆ ตรงนี้เองที่ GROUP BY เข้ามาทำหน้าที่สำคัญ
จัดกลุ่มด้วย GROUP BY
เมื่อเข้าใจภาพรวมของกิจการแล้ว ขั้นถัดไปคือการแยกวิเคราะห์ตามมิติทางธุรกิจต่าง ๆ เช่น สาขา ช่องทาง และช่วงเวลา ซึ่งเป็นบทบาทโดยตรงของ GROUP BY
แยกตามสาขา
“แต่ละสาขามีประสิทธิภาพและพฤติกรรมการสั่งซื้อต่างกันอย่างไร เมื่อดูทั้ง
AVGและMEDIANควบคู่กัน?”
-- สรุปยอดขายและผลการดำเนินงานแยกตามสาขา
SELECT
s.store_name, -- ชื่อสาขา
s.branch_type, -- ประเภทสาขา
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รายได้รวม (บาท)
ROUND(AVG(t.net_sales_thb), 0) AS avg_order_value, -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
ROUND(MEDIAN(t.net_sales_thb), 0) AS median_order_value, -- มูลค่าคำสั่งซื้อค่ากลาง (บาท)
avg_order_value - median_order_value AS avg_median_diff, -- ส่วนต่างระหว่างค่าเฉลี่ยและค่ามัธยฐาน (บาท)
ROUND(SUM(t.contribution_margin_thb), 0) AS total_cm, -- กำไรส่วนเกินรวม (บาท)
ROUND(AVG(t.satisfaction_score), 2) AS avg_satisfaction -- คะแนนความพึงพอใจเฉลี่ย
FROM transactions t -- ตารางหลักที่เก็บข้อมูลธุรกรรม
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมตารางสาขาเพื่อดึงชื่อและประเภทสาขา (จะสอนในบทที่ 4)
GROUP BY s.store_name, s.branch_type -- จัดกลุ่มตามชื่อสาขาและประเภทสาขา
ORDER BY total_revenue DESC; -- เรียงจากสาขาที่มีรายได้สูงสุดไปต่ำสุดคำสั่ง JOIN ที่ปรากฏในคำสั่ง SQL จะอธิบายอย่างละเอียดในบทที่ 4 (INNER JOIN และ LEFT JOIN) ในตอนนี้ให้เข้าใจว่า INNER JOIN เชื่อมตาราง transactions กับ stores โดยจับคู่แถวที่มี store_id ตรงกัน เพื่อให้เห็นชื่อสาขาแทนรหัสตัวเลข
| store_name | branch_type | order_count | total_revenue | avg | median | avg_median_diff | total_cm | satisfaction |
|---|---|---|---|---|---|---|---|---|
| PiCha CentralWorld | mall | 11,935 | 2,451,113 | 205 | 158 | 47 | 1,318,149 | 4.19 |
| PiCha Siam Square | street | 10,738 | 2,311,860 | 215 | 180 | 35 | 1,205,243 | 3.96 |
| PiCha ICONSIAM | mall_flagship | 8,702 | 1,763,926 | 203 | 161 | 42 | 966,501 | 4.53 |
| PiCha Vanit Village | mixed_use | 7,257 | 1,465,764 | 202 | 154 | 48 | 797,878 | 4.07 |
| PiCha Park Silom | office | 7,611 | 1,450,877 | 191 | 135 | 56 | 806,787 | 3.66 |
| PiCha Exchange Tower | office | 7,080 | 1,343,861 | 190 | 135 | 55 | 747,494 | 3.63 |
จากตารางนี้ PiCha CentralWorld เป็นสาขาที่ทำรายได้สูงสุดที่ 2,451,113 บาท (11,935 ออเดอร์) ตามด้วย Siam Square ที่ 2,311,860 บาท (10,738 ออเดอร์) สาขา ICONSIAM มีคะแนนความพึงพอใจสูงที่สุดที่ 4.53 ขณะที่สาขา office ทั้งสองแห่ง (Park Silom และ Exchange Tower) มีคะแนนต่ำที่สุดในช่วง 3.63–3.66
หากมองให้ลึกขึ้น จะเห็น trade-off ระหว่างจำนวนออเดอร์กับความพึงพอใจ สาขา office มีออเดอร์ค่อนข้างมาก (7,000–7,600 ออเดอร์) แต่คะแนนความพึงพอใจต่ำที่สุด ในขณะที่ ICONSIAM มีออเดอร์น้อยกว่าแต่ได้คะแนนสูงที่สุด สาขา office ยังมี AOV ต่ำสุด (190–191 บาท) เทียบกับ Siam Square ที่สูงสุด (215 บาท)
avg_median_diff คือระยะห่างระหว่างค่าเฉลี่ยกับค่ากลาง ยิ่งช่องว่างกว้าง ยิ่งมีแนวโน้มว่ามี outlier มูลค่าสูงดึง AVG ให้สูงกว่าพฤติกรรมของลูกค้า “ทั่วไป” ตัวอย่างเช่น
Park Silom: AVG 191 ←── 56 บาท ──→ MEDIAN 135 ← ช่องกว้างสุด
Siam Square: AVG 215 ←── 35 บาท ──→ MEDIAN 180 ← ช่องแคบสุด
Park Silom และ Exchange Tower มี avg_median_diff สูงสุด แม้ลูกค้าส่วนใหญ่จ่ายเพียงประมาณ 135 บาทต่อออเดอร์ แต่มีออเดอร์บางส่วนที่มูลค่าสูงและดึง AVG ขึ้นไปถึง 190–191 บาท หากออกแบบโปรโมชั่นโดยใช้ AVG เป็น baseline ก็อาจประเมินกำลังซื้อของลูกค้าทั่วไปสูงเกินจริง ในทางกลับกัน Siam Square มี avg_median_diff ต่ำที่สุด แสดงว่าการกระจายของออเดอร์สม่ำเสมอกว่า ลูกค้าส่วนใหญ่จ่ายใกล้เคียงกับค่าเฉลี่ย โปรโมชั่นที่อิง AVG จึงสะท้อนพฤติกรรมส่วนใหญ่ได้ดี
สำหรับ Vanit Village ช่องว่างระหว่าง AVG และ MEDIAN อยู่ที่ 48 บาท ใกล้เคียงกับ CentralWorld แต่ระดับความพึงพอใจปานกลาง (4.07) จึงน่าเจาะลึกต่อว่า outlier ที่ดึง AVG ขึ้นมานั้นเกิดในช่องทางใด เช่น delivery หรือ dine-in เพื่อออกแบบมาตรการให้เหมาะสม
การนำไปใช้งาน
- ใช้
MEDIANเป็น baseline ออกแบบโปรโมชั่น ไม่ใช่AVGโดยเฉพาะในสาขา office ที่ลูกค้าทั่วไปใช้จ่ายต่ำกว่าค่าเฉลี่ยมาก - ติดตาม
avg_median_diffทุกเดือน หากช่องว่างขยายตัวอาจสะท้อนว่า group order เพิ่มขึ้น หากแคบลงอาจบ่งชี้ว่าลูกค้าซื้อเดี่ยวมากขึ้น - ใช้ Siam Square เป็นสนามทดลอง campaign ใหม่ เนื่องจากพฤติกรรมลูกค้าสม่ำเสมอและสะท้อนผลของมาตรการได้ชัดเจน
กฎสำคัญ: ทุกคอลัมน์ใน SELECT ที่ไม่ใช่ aggregate function ต้องอยู่ใน GROUP BY ด้วย ถ้าไม่เช่นนั้น SQL จะไม่รู้ว่าควรเลือกค่าใดจากแต่ละกลุ่มมาแสดง
แยกตามช่องทางการขาย
เมื่อเข้าใจความแตกต่างระหว่างสาขาแล้ว ส่วนนี้จะเปลี่ยนมุมมองจาก “สถานที่ขาย” ไปสู่ “ช่องทางการขาย” เพื่อดูว่ารูปแบบการสั่งซื้อและความสามารถในการทำกำไรแตกต่างกันอย่างไร
“แต่ละช่องทางการขายมีพฤติกรรมลูกค้า ขนาดออเดอร์ และความสามารถในการทำกำไรแตกต่างกันอย่างไร?”
SELECT -- เลือกข้อมูล
channel, -- ช่องทางการขาย
COUNT(*) AS order_count, -- นับจำนวนออร์เดอร์
ROUND(SUM(net_sales_thb), 0) AS total_revenue, -- รวมยอดขายสุทธิ (บาท)
ROUND(AVG(net_sales_thb), 0) AS avg_order_value, -- ค่าเฉลี่ยต่อออร์เดอร์ (บาท)
ROUND(MEDIAN(net_sales_thb), 0) AS median_order_value, -- มัธยฐานมูลค่าต่อออร์เดอร์ (บาท)
ROUND(AVG(basket_qty), 2) AS avg_basket, -- ค่าเฉลี่ยจำนวนสินค้าต่อออร์เดอร์
ROUND(SUM(contribution_margin_thb), 0) AS total_cm, -- รวมกำไรส่วนเกิน (บาท)
ROUND(100.0 * SUM(contribution_margin_thb) / SUM(net_sales_thb), 1) AS cm_pct -- contribution margin (%)
FROM transactions -- จากตารางธุรกรรม
GROUP BY channel -- จัดกลุ่มตามช่องทางการขาย
ORDER BY total_revenue DESC; -- เรียงตามยอดขายรวมจากมากไปน้อย| channel | order_count | total_revenue | avg_order_value | median_order_value | avg_basket | total_cm | cm_pct |
|---|---|---|---|---|---|---|---|
| delivery | 18,795 | 5,559,370 | 296 | 284 | 2.20 | 2,547,703 | 45.8% |
| to_go | 24,552 | 3,725,939 | 152 | 115 | 1.40 | 2,349,204 | 63.0% |
| dine_in | 9,976 | 1,502,092 | 151 | 115 | 1.39 | 945,145 | 62.9% |
ช่องทาง delivery สร้างรายได้สูงสุดที่ 5,559,370 บาท หรือประมาณครึ่งหนึ่งของรายได้รวม ทั้งที่มีจำนวนออเดอร์น้อยกว่า to_go เหตุผลคือ AOV ของ delivery สูงกว่ามาก (296 เทียบกับ 152 บาท) อย่างไรก็ตาม CM% ของ delivery ต่ำที่สุดที่ 45.8% เทียบกับ to_go และ dine_in ที่อยู่ราว 63%
กล่าวอีกแบบหนึ่ง delivery สร้างยอดขายตัวเลขใหญ่ แต่กำไรที่เหลือจริงต่อรายได้ต่ำกว่าช่องทางอื่นอย่างชัดเจน เนื่องจากค่าคอมมิชชันแพลตฟอร์ม ค่า packaging และต้นทุนแรงงานที่สูงกว่า การผลักดันให้ลูกค้าส่วนหนึ่งเปลี่ยนจาก delivery มา to_go อาจช่วยเพิ่ม margin ได้โดยไม่จำเป็นต้องหาลูกค้าใหม่ เช่น ด้วยโปรแกรม “รับชาฟรี 1 แก้ว เมื่อมารับที่ร้านครบ 5 ครั้ง” หรือการจัด dedicated pick-up counter เพื่อลดเวลารอของลูกค้า to_go อย่างไรก็ตาม ก่อนปรับกลยุทธ์จำเป็นต้องตรวจสอบก่อนว่าลูกค้า delivery กับ to_go เป็นคนกลุ่มเดียวกันหรือไม่ หากเป็นคนละกลุ่มโดยสิ้นเชิง การจูงใจให้เปลี่ยนช่องทางอาจไม่สำเร็จ
ด้านการกระจายขนาดออเดอร์ ช่องว่างระหว่าง AVG และ MEDIAN ของ delivery มีเพียง 12 บาท (296 เทียบกับ 284) บ่งชี้ว่าลูกค้า delivery มีขนาดการสั่งซื้อค่อนข้างสม่ำเสมอและมี outlier น้อย สอดคล้องกับค่า avg_basket 2.20 ที่แสดงว่าลูกค้ามักสั่งมากกว่า 1 แก้วต่อครั้ง ในขณะที่ to_go และ dine_in มี AVG 151–152 บาท แต่ MEDIAN เพียง 115 บาท ช่องว่าง 37 บาทบ่งบอกว่าลูกค้าส่วนใหญ่ซื้อแก้วเดียวราคาประมาณ 115 บาท และมีกลุ่มเฉพาะบางส่วนที่ซื้อหลายแก้วดึงค่าเฉลี่ยให้สูงขึ้น
แยกตามวันในสัปดาห์
นอกจากสาขาและช่องทางแล้ว “วันในสัปดาห์” เป็นอีกมิติที่สำคัญสำหรับการจัดกำลังคนและวางแผนโปรโมชั่น
-- สรุปยอดขายและความพึงพอใจแยกตามวันในสัปดาห์
SELECT
day_of_week, -- วันในสัปดาห์
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(SUM(net_sales_thb), 0) AS total_revenue, -- รายได้รวม (บาท)
ROUND(AVG(net_sales_thb), 0) AS avg_order_value, -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
ROUND(AVG(satisfaction_score), 2) AS avg_satisfaction -- คะแนนความพึงพอใจเฉลี่ย
FROM transactions -- ตารางหลักที่เก็บข้อมูลธุรกรรม
GROUP BY day_of_week -- จัดกลุ่มตามวันในสัปดาห์
ORDER BY CASE day_of_week -- เรียงตามลำดับวันจันทร์ถึงอาทิตย์ (จะสอนในบทที่ 5)
WHEN 'Monday' THEN 0
WHEN 'Tuesday' THEN 1
WHEN 'Wednesday' THEN 2
WHEN 'Thursday' THEN 3
WHEN 'Friday' THEN 4
WHEN 'Saturday' THEN 5
WHEN 'Sunday' THEN 6 END;คำสั่ง CASE ... WHEN ... THEN ใน ORDER BY ด้านล่างจะอธิบายอย่างละเอียดในบทที่ 5 (CASE Expressions)
| day_of_week | order_count | total_revenue | avg_order_value | avg_satisfaction |
|---|---|---|---|---|
| Monday | 6,445 | 1,285,679 | 199 | 3.99 |
| Tuesday | 7,570 | 1,502,126 | 198 | 3.96 |
| Wednesday | 8,099 | 1,621,162 | 200 | 3.95 |
| Thursday | 7,358 | 1,465,462 | 199 | 4.00 |
| Friday | 7,618 | 1,541,691 | 202 | 4.09 |
| Saturday | 7,726 | 1,599,642 | 207 | 4.13 |
| Sunday | 8,507 | 1,771,639 | 208 | 4.11 |
วันอาทิตย์เป็นวันที่ทำรายได้สูงสุดที่ 1,771,639 บาท (8,507 ออเดอร์, AOV 208 บาท) ในขณะที่วันจันทร์ต่ำสุดที่ 1,285,679 บาท (6,445 ออเดอร์) ห่างกันประมาณ 38% คะแนนความพึงพอใจสูงสุดอยู่ในวันเสาร์ (4.13) และต่ำสุดในวันพุธ (3.95) จะเห็นแนวโน้มรายได้และความพึงพอใจเพิ่มขึ้นจากต้นสัปดาห์ไปสู่ปลายสัปดาห์ เกิด weekend effect ที่ค่อนข้างชัดเจน
วันพุธมีจำนวนออเดอร์สูงเป็นอันดับสอง (8,099) แต่ AOV ไม่สูงเท่าวันหยุดสุดสัปดาห์ ทำให้วันพุธมีลักษณะ “คึกคักแต่สร้างมูลค่าต่อออเดอร์ไม่สูง” ซึ่งอาจเป็นจุดเหมาะสำหรับโปรโมชันแบบ bundle หรือ upsizing เช่น “Wellness Wednesday” เพื่อเพิ่ม AOV ส่วนวันจันทร์ซึ่งมีออเดอร์ต่ำที่สุด ควรตรวจสอบเพิ่มเติมว่าเกิดจาก demand ต่ำจริง หรือมาจากข้อจำกัดด้านการปฏิบัติการบางอย่าง เช่น เวลาเปิด–ปิด หรือจำนวนพนักงาน
GROUP BY หลายคอลัมน์
ตัวอย่างก่อนหน้าใช้ GROUP BY กับมิติเดียวต่อครั้ง ในทางปฏิบัติ มักต้องการแยกวิเคราะห์สองมิติ เช่น “แต่ละสาขาพึ่งพา delivery มากน้อยแค่ไหน” เราสามารถใช้ GROUP BY หลายคอลัมน์เพื่อดูภาพสาขา × ช่องทางขายในตารางเดียวได้
SELECT -- เลือกข้อมูล
s.store_name, -- ชื่อสาขา
t.channel, -- ช่องทางการขาย
COUNT(*) AS order_count, -- นับจำนวนออร์เดอร์
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รวมยอดขายสุทธิ (บาท)
ROUND(AVG(t.net_sales_thb), 0) AS avg_order_value, -- ค่าเฉลี่ยต่อออร์เดอร์ (บาท)
ROUND(100.0 * SUM(t.contribution_margin_thb) / SUM(t.net_sales_thb), 1) AS cm_pct -- contribution margin (%)
FROM transactions t -- จากตารางธุรกรรม
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมกับตารางสาขา
GROUP BY s.store_name, t.channel -- จัดกลุ่มตามชื่อสาขาและช่องทาง
ORDER BY s.store_name, total_revenue DESC; -- เรียงตามชื่อสาขา แล้วยอดขายรวมจากมากไปน้อย| store_name | channel | order_count | total_revenue | avg_order_value | cm_pct |
|---|---|---|---|---|---|
| PiCha Exchange Tower | to_go | 4,614 | 703,563 | 152 | 63.1 |
| PiCha Exchange Tower | delivery | 2,027 | 573,086 | 283 | 45.5 |
| PiCha Exchange Tower | dine_in | 439 | 67,212 | 153 | 63.1 |
| PiCha ICONSIAM | delivery | 3,009 | 917,245 | 305 | 47.3 |
| PiCha ICONSIAM | dine_in | 3,036 | 455,380 | 150 | 62.9 |
| PiCha ICONSIAM | to_go | 2,657 | 391,301 | 147 | 62.9 |
| PiCha Park Silom | to_go | 5,000 | 759,089 | 152 | 63.2 |
| PiCha Park Silom | delivery | 2,128 | 618,342 | 291 | 45.4 |
| PiCha Park Silom | dine_in | 483 | 73,446 | 152 | 63.3 |
| PiCha Siam Square | delivery | 4,728 | 1,399,430 | 296 | 45.0 |
| PiCha Siam Square | to_go | 4,901 | 744,373 | 152 | 63.1 |
| PiCha Siam Square | dine_in | 1,109 | 168,056 | 152 | 62.9 |
| PiCha Vanit Village | delivery | 2,529 | 741,464 | 293 | 46.1 |
| PiCha Vanit Village | to_go | 3,643 | 556,672 | 153 | 63.0 |
| PiCha Vanit Village | dine_in | 1,085 | 167,627 | 154 | 63.0 |
| PiCha centralwOrld | delivery | 4,374 | 1,309,803 | 299 | 45.8 |
| PiCha centralwOrld | to_go | 3,737 | 570,940 | 153 | 63.0 |
| PiCha centralwOrld | dine_in | 3,824 | 570,371 | 149 | 62.9 |
ทุกสาขามีรูปแบบคล้ายกัน คือ delivery มี AOV สูงที่สุด (ประมาณ 283–305 บาท) แต่ CM% ต่ำสุด (45–47%) ในขณะที่ to_go และ dine_in มี CM% ค่อนข้างคงที่ใกล้ 63% สะท้อนว่าโครงสร้างต้นทุนของการขายหน้าร้านไม่ต้องแบกรับ commission ของแพลตฟอร์ม
สาขา Siam Square มีรายได้จาก delivery สูงถึง 1,399,430 บาท คิดเป็นประมาณ 60.5% ของรายได้สาขานี้ ทำให้ Siam Square พึ่งพา delivery สูงที่สุดในบรรดาทุกสาขา หากแพลตฟอร์ม delivery ปรับเพิ่ม commission ในอนาคต Siam Square จะได้รับผลกระทบต่อ margin มากกว่าเพื่อน การสร้างช่องทางสั่งซื้อโดยตรง (direct ordering channel หรือเว็บ) เพื่อลดการพึ่งพา third-party จึงเป็นทางเลือกหนึ่งที่ควรพิจารณา แต่ก่อนลงทุนควรตอบคำถามให้ได้ก่อนว่าลูกค้า delivery เลือกแพลตฟอร์มเพราะอะไร เช่น ความสะดวกในการค้นหา การชำระเงิน หรือความคุ้นชินในการใช้บริการ
จากนี้ไป เราจะเพิ่มความสามารถให้ GROUP BY ด้วยการผสม HAVING เพื่อกรองเฉพาะกลุ่มที่เข้าเกณฑ์ เช่น สาขาที่มียอดขายเกินตัวเลขหนึ่ง หรือสาขาที่จำนวนออเดอร์ต่ำกว่าเกณฑ์
กรองกลุ่มด้วย HAVING
การวิเคราะห์ข้อมูลธุรกิจด้วย SQL มักต้องการ “สรุปเป็นกลุ่ม” เช่น
- ยอดขายรวมรายเดือนต่อสาขา
- จำนวนออร์เดอร์รายวันต่อช่องทาง (
dine-in/delivery) - คะแนนความพึงพอใจเฉลี่ยของลูกค้าแต่ละสาขา
สิ่งที่ใช้คือ GROUP BY ร่วมกับ Aggregate functions (SUM, COUNT, AVG ฯลฯ) แต่แค่สรุปอย่างเดียวอาจยังไม่พอ เพราะในการตัดสินใจเชิงกลยุทธ์ มักสนใจเฉพาะกลุ่มที่เข้าเงื่อนไข เช่น
- สาขาที่ยอดขายถึงเกณฑ์
- สินค้าที่ขายดีจริง
- สาขาที่คะแนนความพึงพอใจต่ำกว่ามาตรฐาน
ตรงนี้เองที่ HAVING มีบทบาทสำคัญ มันคือเครื่องมือในการกรอง “กลุ่ม” หลังจากรวมกลุ่มด้วย GROUP BY แล้ว
flowchart TD
A["ข้อมูลดิบจาก FROM/JOIN"] --> B["WHERE<br/>(กรองแถวดิบ)"]
B --> C["GROUP BY<br/>(จัดกลุ่มตาม key เช่น store_id, month)"]
C --> D["Aggregate Functions<br/>(SUM, COUNT, AVG, ...)"]
D --> E["HAVING<br/>(กรองเฉพาะกลุ่มที่เข้าเงื่อนไข)"]
E --> F["ORDER BY / OUTPUT"]
รูปที่ 3.3: ลำดับการประมวลผล WHERE กรองแถวดิบก่อนจัดกลุ่ม ส่วน HAVING กรองผลลัพธ์หลังจัดกลุ่มแล้ว
WHERE ทำงานก่อนการจัดกลุ่ม ใช้เลือกเฉพาะข้อมูลดิบที่ต้องการ ส่วน HAVING ทำงานหลังการจัดกลุ่ม ใช้เลือกเฉพาะกลุ่มที่ตัวเลขรวมแล้วเข้าเกณฑ์
HAVING เบื้องต้น: กรองกลุ่มจากค่า aggregate โดยตรง
ใช้คำถามว่า “สาขาใดมีรายได้รวมเกิน 2,000,000 บาท” เป็นตัวอย่าง
SELECT
s.store_name, -- ชื่อสาขา
COUNT(*) AS order_count, -- นับจำนวนออร์เดอร์
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รวมยอดขายสุทธิ (บาท)
ROUND(AVG(t.net_sales_thb), 0) AS avg_order_value, -- ค่าเฉลี่ยต่อออร์เดอร์ (บาท)
ROUND(SUM(t.contribution_margin_thb), 0) AS total_cm -- รวมกำไรส่วนเกิน (บาท)
FROM transactions t -- จากตารางธุรกรรม
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมกับตารางสาขา
GROUP BY s.store_name -- จัดกลุ่มตามชื่อสาขา
ORDER BY total_revenue DESC; -- เรียงตามยอดขายรวมจากมากไปน้อยผลลัพธ์ที่ได้จะเป็นยอดขายของแต่ละสาขา
| Store | Orders | Revenue (THB) | Avg Order Value | Contribution Margin |
|---|---|---|---|---|
| PiCha CentralWorld | 11,935 | 2,451,113 | 205 | 1,318,149 |
| PiCha Siam Square | 10,738 | 2,311,860 | 215 | 1,205,243 |
| PiCha ICONSIAM | 8,702 | 1,763,926 | 203 | 966,501 |
| PiCha Vanit Village | 7,257 | 1,465,764 | 202 | 797,878 |
| PiCha Park Silom | 7,611 | 1,450,877 | 191 | 806,787 |
| PiCha Exchange Tower | 7,080 | 1,343,861 | 190 | 747,494 |
หากต้องการแสดงเฉพาะสาขาที่มีรายได้เกิน 2,000,000 บาท สามารถเพิ่ม HAVING SUM(t.net_sales_thb) > 2000000 ต่อจาก GROUP BY s.store_name
-- สรุปยอดขายเฉพาะสาขาที่มีรายได้รวมมากกว่า 2,000,000 บาท
SELECT
s.store_name, -- ชื่อสาขา
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รายได้รวม (บาท)
ROUND(AVG(t.net_sales_thb), 0) AS avg_order_value, -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
ROUND(SUM(t.contribution_margin_thb), 0) AS total_cm -- กำไรส่วนเกินรวม (บาท)
FROM transactions t -- ตารางหลักที่เก็บข้อมูลธุรกรรม
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมตารางสาขาเพื่อดึงชื่อสาขา
GROUP BY s.store_name -- จัดกลุ่มตามชื่อสาขา
HAVING SUM(t.net_sales_thb) > 2000000 -- กรองเฉพาะสาขาที่มีรายได้รวมมากกว่า 2,000,000 บาท
ORDER BY total_revenue DESC; -- เรียงจากสาขาที่มีรายได้สูงสุดไปต่ำสุด| store_name | order_count | total_revenue | avg_order_value | total_cm |
|---|---|---|---|---|
| PiCha centralwOrld | 11,935 | 2,451,113 | 205 | 1,318,149 |
| PiCha Siam Square | 10,738 | 2,311,860 | 215 | 1,205,243 |
จะเห็นว่ามีเพียง 2 สาขาจาก 6 สาขาที่มีรายได้เกิน 2,000,000 บาท ได้แก่ CentralWorld และ Siam Square ทั้งสองสาขาตั้งอยู่ในเขต Pathum Wan ซึ่งเป็น prime retail zone ของกรุงเทพฯ จึงอาจมองได้ว่า PiCha มี “กลไกคู่ขับเคลื่อนการเติบโต” อยู่ในพื้นที่เดียวกัน หากเกิดเหตุไม่คาดคิดในเขตนี้ บริษัทอาจสูญเสียรายได้ถึงครึ่งหนึ่ง จึงเป็นความเสี่ยงเชิงภูมิศาสตร์ที่ควรพิจารณา
ใช้ WHERE และ HAVING ร่วมกัน
สรุปความแตกต่างสำคัญระหว่าง WHERE และ HAVING
WHEREกรองแถวดิบก่อนจัดกลุ่ม ใช้กับเงื่อนไขที่ไม่เกี่ยวกับ aggregate เช่นchannel = 'delivery'หรือเงื่อนไขวันที่GROUP BYจัดกลุ่มแถวที่เหลือให้เป็นกลุ่มตาม key เช่น สาขา เดือน ช่องทางHAVINGกรองผลลัพธ์ระดับกลุ่มหลัง aggregate เช่นSUM(net_sales_thb) > 2000000หรือCOUNT(*) < 2000
ตัวอย่างการใช้ร่วมกัน: ดูเฉพาะยอดขายที่มาจาก delivery ในเดือนกุมภาพันธ์ 2026 และแสดงเฉพาะสาขาที่มีคำสั่งซื้อน้อยกว่า 2,000 รายการ
-- สรุปยอดขายช่องทาง Delivery เดือนกุมภาพันธ์ 2026 เฉพาะสาขาที่มีคำสั่งซื้อน้อยกว่า 2,000 รายการ
SELECT
s.store_name, -- ชื่อสาขา
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รายได้รวม (บาท)
ROUND(AVG(t.net_sales_thb), 0) AS avg_order_value, -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
ROUND(SUM(t.contribution_margin_thb), 0) AS total_cm -- กำไรส่วนเกินรวม (บาท)
FROM transactions t
INNER JOIN stores s
ON t.store_id = s.store_id -- เชื่อมตารางสาขาเพื่อดึงชื่อสาขา
WHERE
t.channel = 'delivery' -- กรองเฉพาะคำสั่งซื้อช่องทาง Delivery
AND t.order_date BETWEEN DATE '2026-02-01' AND '2026-02-28' -- กรองเฉพาะเดือนกุมภาพันธ์ 2026
GROUP BY s.store_name -- จัดกลุ่มตามชื่อสาขา
HAVING COUNT(*) < 2000 -- กรองเฉพาะสาขาที่มีคำสั่งซื้อน้อยกว่า 2,000 รายการ
ORDER BY order_count DESC; -- เรียงจากสาขาที่มีคำสั่งซื้อมากสุดไปน้อยสุด| Store | Orders | Revenue | Avg Order | CM |
|---|---|---|---|---|
| PiCha ICONSIAM | 1,444 | 442,580 | 306 | 208,316 |
| PiCha Vanit Village | 1,190 | 348,203 | 293 | 159,836 |
| PiCha Park Silom | 1,000 | 281,575 | 282 | 128,120 |
| PiCha Exchange Tower | 919 | 256,873 | 280 | 116,591 |
ตัวอย่างแสดงการกรองสองระดับในคำสั่งเดียว: WHERE เลือกเฉพาะแถว delivery เดือนกุมภาพันธ์ แล้ว GROUP BY รวมเป็นกลุ่มตามสาขา จากนั้น HAVING ใช้กรองเฉพาะสาขาที่มีจำนวนออเดอร์น้อยกว่า 2,000 หลังจากการรวมกลุ่มแล้ว
ข้อผิดพลาดที่พบบ่อย:
-- ผิด! ใช้ aggregate ใน WHERE ไม่ได้ SELECT store_name, SUM(net_sales_thb) FROM transactions WHERE SUM(net_sales_thb) > 2000000 -- ❌ กลุ่มยังไม่มี! -- ถูก! ใช้ HAVING หลัง GROUP BY SELECT store_name, SUM(net_sales_thb) FROM transactions GROUP BY store_name HAVING SUM(net_sales_thb) > 2000000; -- ✅ กลุ่มมีแล้ว
การประยุกต์
เมื่อเข้าใจหลักการทำงานของฟังก์ชันสรุปผล (aggregate functions) และคำสั่ง GROUP BY แล้ว ขั้นต่อไปคือการนำเครื่องมือเหล่านี้ไปใช้ตอบคำถามทางธุรกิจที่พบได้บ่อย เช่น การวิเคราะห์ยอดขาย กำไร หรือพฤติกรรมลูกค้าในมิติต่าง ๆ เช่น สาขา ช่องทางการขาย หรือประเภทสินค้า
การคำนวณช่องทางที่ให้ส่วนลดมากที่สุด
สมมติว่าในธุรกิจหนึ่งมีการขายสินค้าผ่านหลายช่องทาง เช่น หน้าร้าน (offline), แอปพลิเคชัน, เว็บไซต์ และแพลตฟอร์มเดลิเวอรี แต่ละคำสั่งซื้อมีข้อมูล “ส่วนลด” ที่ให้กับลูกค้าเก็บอยู่ในฐานข้อมูล เราสามารถใช้ฟังก์ชันสรุปผลร่วมกับ GROUP BY เพื่อหาว่า “ช่องทางใดมีส่วนลดเฉลี่ยสูงที่สุด” หรือ “ช่องทางใดใช้วงเงินส่วนลดรวมมากที่สุด” ได้อย่างเป็นระบบ
SELECT
channel,
COUNT(*) AS order_count,
ROUND(SUM(gross_sales_thb), 0) AS total_gross,
ROUND(SUM(item_discount_thb + order_discount_thb), 0) AS total_discount,
ROUND(100.0 * SUM(item_discount_thb + order_discount_thb) / SUM(gross_sales_thb), 1) AS discount_rate_pct,
ROUND(AVG(item_discount_thb), 2) AS avg_item_discount,
ROUND(AVG(order_discount_thb), 2) AS avg_order_discount
FROM transactions -- จากตารางธุรกรรม
GROUP BY channel -- จัดกลุ่มตามช่องทางการขาย
ORDER BY discount_rate_pct DESC; -- เรียงตามอัตราส่วนลดจากมากไปน้อย| channel | order_count | total_gross | total_discount | discount_rate_pct | avg_item_discount | avg_order_discount |
|---|---|---|---|---|---|---|
| delivery | 18,795 | 5,195,134 | 214,804 | 4.1 | 6.48 | 4.95 |
| dine_in | 9,976 | 1,552,915 | 50,823 | 3.3 | 5.09 | 0 |
| to_go | 24,552 | 3,816,035 | 90,096 | 2.4 | 3.67 | 0 |
ช่องทาง delivery แบกรับภาระส่วนลดสูงสุดที่ 4.1% รวมมูลค่า 214,804 บาท สูงกว่าส่วนลดของช่องทาง to_go อย่างมีนัยสำคัญ ตัวขับเคลื่อนหลักคือ avg_order_discount = 4.95 ซึ่งมีเฉพาะช่องทาง delivery บ่งชี้ว่ามีโปรโมชันระดับออเดอร์บางรูปแบบที่ PiCha แบกรับต้นทุนอยู่
ในทางตรงกันข้าม to_go เป็นช่องทางที่มีประสิทธิภาพด้านส่วนลดสูงที่สุดที่ 2.4% โดยไม่มีส่วนลดระดับออเดอร์ และมีส่วนลดต่อรายการเฉลี่ยต่ำสุดที่ 3.67 บาท ทำให้เป็นช่องทางที่ “สะอาดที่สุด” ในมุมกำไร ส่วน dine_in อยู่ในระดับกลางที่ 3.3% ไม่มีส่วนลดระดับออเดอร์เช่นกัน แต่มีส่วนลดต่อรายการในระดับปานกลาง ซึ่งอาจมาจากโปรโมชันหน้าร้านหรือกิจกรรมการตลาดเฉพาะในร้าน
จากการวิเคราะห์นี้ PiCha ควรตรวจสอบโครงสร้างส่วนลดระดับออเดอร์ของช่องทาง delivery เนื่องจากส่วนต่างประมาณ 1.7 จุดเปอร์เซ็นต์เมื่อเทียบกับ to_go คิดเป็นต้นทุนส่วนลดเพิ่มเติมในระดับที่มีนัยสำคัญ หากส่วนลดนี้เป็นการอุดหนุนจากแพลตฟอร์ม third-party ก็อาจลดภาระของ PiCha ได้ แต่ถ้า PiCha เป็นผู้รับผิดชอบเอง การปรับเงื่อนไขโปรโมชัน delivery ให้เข้มงวดขึ้นสามารถเพิ่ม net margin ได้อย่างชัดเจน
สาขาไหน “ทำกำไร”
รายได้สูงไม่ได้แปลว่ากำไรสูง จำเป็นต้องพิจารณา contribution margin (CM) ซึ่งหักต้นทุนผันแปรทั้งหมดแล้ว
SELECT
s.store_name,
s.branch_type,
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue,
ROUND(SUM(t.cogs_thb), 0) AS total_cogs,
ROUND(SUM(t.platform_commission_thb), 0) AS total_commission,
ROUND(SUM(t.packaging_cost_thb), 0) AS total_packaging,
ROUND(SUM(t.labor_alloc_thb), 0) AS total_labor,
ROUND(SUM(t.contribution_margin_thb), 0) AS total_cm,
ROUND(100.0 * SUM(t.contribution_margin_thb) / SUM(t.net_sales_thb), 1) AS cm_pct
FROM transactions t -- จากตารางธุรกรรม
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมกับตารางสาขา
GROUP BY s.store_name, s.branch_type -- จัดกลุ่มตามชื่อและประเภทสาขา
ORDER BY total_cm DESC; -- เรียงตามกำไรส่วนเกินรวมจากมากไปน้อย| store_name | branch_type | total_revenue | total_cogs | total_commission | total_packaging | total_labor | total_cm | cm_pct |
|---|---|---|---|---|---|---|---|---|
| PiCha CentralWorld | mall | 2,451,113 | 743,820 | 255,344 | 69,834 | 63,966 | 1,318,149 | 53.8% |
| PiCha Siam Square | street | 2,311,860 | 701,334 | 278,819 | 68,518 | 57,946 | 1,205,243 | 52.1% |
| PiCha ICONSIAM | mall_flagship | 1,763,926 | 527,321 | 174,458 | 49,144 | 46,502 | 966,501 | 54.8% |
| PiCha Park Silom | office | 1,450,877 | 443,993 | 120,561 | 39,153 | 40,382 | 806,787 | 55.6% |
| PiCha Vanit Village | mixed_use | 1,465,764 | 444,888 | 143,340 | 40,905 | 38,753 | 797,878 | 54.4% |
| PiCha Exchange Tower | office | 1,343,861 | 410,868 | 111,748 | 36,225 | 37,526 | 747,494 | 55.6% |
จากตารางนี้ CentralWorld สร้าง CM ในเชิงมูลค่าสูงสุด 1,318,149 บาท (53.8%) แต่ในเชิงสัดส่วน cm_pct พบว่าสาขา office ทั้ง Park Silom และ Exchange Tower ทำได้สูงสุดที่ 55.6% สะท้อนว่ารูปแบบธุรกิจออฟฟิศมีโครงสร้างต้นทุนที่ lean กว่า แม้รายได้รวมต่ำกว่า
COGS คิดเป็นประมาณ 30% ของรายได้เกือบทุกสาขา และแรงงานรวมกับ packaging อยู่ราว 5% ใกล้เคียงกัน ความแตกต่างด้านกำไรหลักจึงมาจาก platform commission เป็นสำคัญ โดยเฉพาะ Siam Square ที่จ่าย commission สูงสุดถึง 278,819 บาท (ประมาณ 12.1% ของรายได้) ทำให้ cm_pct ต่ำสุดที่ 52.1% ทั้งที่มีรายได้รวมเป็นอันดับสอง
ICONSIAM ในฐานะ mall_flagship ทำ cm_pct ได้ 54.8% สูงกว่า CentralWorld แสดงว่าบริหารต้นทุนได้สมดุลกว่า โดยเฉพาะด้าน commission ในขณะที่รายได้รวมยังมีช่องว่างให้เติบโตขึ้นอีก หากสามารถเพิ่มยอดขายโดยรักษาโครงสร้างต้นทุนปัจจุบันไว้ ICONSIAM จะเป็นเครื่องยนต์กำไรที่แข็งแรงในกลุ่มห้าง
ข้อเสนอแนะสำหรับผู้บริหาร
- ใช้สาขา office (Park Silom และ Exchange Tower) เป็น benchmark ด้านต้นทุน เพราะมี
cm_pctสูงที่สุด - ลดการพึ่งพา delivery platform ที่ Siam Square เพื่อลด commission และยกระดับ
cm_pctโดยพิจารณาช่องทางขายตรงหรือโปรโมชั่นเฉพาะหน้าร้าน - พิจารณาลงทุนเพิ่มเพื่อขยายยอดขาย ICONSIAM ซึ่งมี margin ดี แต่รายได้รวมยังต่ำกว่าศักยภาพเมื่อเทียบกับ CentralWorld
เมื่อเข้าใจมิติด้านสาขา ช่องทาง และโครงสร้างกำไรแล้ว การมองตามมิติเวลาจะช่วยให้เห็นการเปลี่ยนแปลงของธุรกิจทั้งในระดับเดือน (แนวโน้มภาพใหญ่) และชั่วโมง (การจัดการปฏิบัติการหน้างาน)
รายได้เปลี่ยนแปลงรายเดือนอย่างไร
SELECT
month,
COUNT(*) AS order_count,
ROUND(SUM(net_sales_thb), 0) AS total_revenue,
ROUND(AVG(net_sales_thb), 0) AS avg_order_value,
ROUND(SUM(contribution_margin_thb), 0) AS total_cm,
ROUND(100.0 * SUM(contribution_margin_thb) / SUM(net_sales_thb), 1) AS cm_pct
FROM transactions
GROUP BY month
ORDER BY month;| month | order_count | total_revenue | avg_order_value | total_cm | cm_pct |
|---|---|---|---|---|---|
| 2 | 24,420 | 4,905,954 | 201 | 2,644,375 | 53.9 |
| 3 | 27,805 | 5,660,917 | 204 | 3,076,755 | 54.4 |
| 4 | 1,098 | 220,531 | 201 | 120,923 | 54.8 |
เดือนมีนาคมมีออเดอร์เพิ่มขึ้น 13.9% จากกุมภาพันธ์ (27,805 เทียบกับ 24,420) และรายได้เพิ่มขึ้น 15.4% (5,660,917 เทียบกับ 4,905,954 บาท) ขณะที่ cm_pct ขยับขึ้นเล็กน้อยจาก 53.9% เป็น 54.4% การเติบโตทั้ง volume และ margin พร้อมกันเป็นสัญญาณที่ดีของการเติบโตที่มีคุณภาพ
อย่างไรก็ตาม ข้อมูลเดือนเมษายนมีเพียง 1,098 ออเดอร์ เพราะฐานข้อมูลสิ้นสุดที่วันที่ 1 เมษายน 2026 จึงเป็นข้อมูลเพียง 1 วัน ไม่ใช่เดือนเต็ม และไม่ควรนำไปเปรียบเทียบกับเดือนอื่นโดยตรง หากแนวโน้มกุมภาพันธ์–มีนาคมดำเนินต่อเนื่อง ทีมปฏิบัติการควรเตรียมกำลังคนและสินค้าคงคลังให้รองรับการเติบโตต่อไป แต่ในเชิงสถิติ ข้อมูลเพียง 2 เดือนยังไม่เพียงพอสำหรับการสรุปแนวโน้มระยะยาว
ชั่วโมงไหนขายดีที่สุด
มุมมองรายเดือนช่วยให้เห็นแนวโน้มภาพใหญ่ แต่การจัดตารางพนักงานและเตรียมวัตถุดิบต้องใช้ข้อมูลระดับชั่วโมง
SELECT
hour, -- ชั่วโมง
COUNT(*) AS order_count, -- นับจำนวนออร์เดอร์
ROUND(SUM(net_sales_thb), 0) AS total_revenue, -- รวมยอดขายสุทธิ (บาท)
ROUND(AVG(net_sales_thb), 0) AS avg_order_value -- ค่าเฉลี่ยต่อออร์เดอร์ (บาท)
FROM transactions -- จากตารางธุรกรรม
GROUP BY hour -- จัดกลุ่มตามชั่วโมง
ORDER BY hour; -- เรียงตามชั่วโมง| hour | order_count | total_revenue | avg_order_value |
|---|---|---|---|
| 9 | 2,571 | 497,976 | 194 |
| 10 | 3,044 | 595,380 | 196 |
| 11 | 5,191 | 1,061,147 | 204 |
| 12 | 6,742 | 1,364,026 | 202 |
| 13 | 5,548 | 1,120,226 | 202 |
| 14 | 3,779 | 767,538 | 203 |
| 15 | 3,447 | 688,227 | 200 |
| 16 | 2,930 | 597,503 | 204 |
| 17 | 4,490 | 910,433 | 203 |
| 18 | 5,115 | 1,037,511 | 203 |
| 19 | 4,389 | 898,646 | 205 |
| 20 | 3,098 | 638,302 | 206 |
| 21 | 2,107 | 428,453 | 203 |
| 22 | 872 | 182,033 | 209 |
มี 2 ช่วง peak ที่ชัดเจน ได้แก่ ช่วงกลางวัน (11:00–13:00) และ ช่วงเย็น (17:00–19:00) โดยชั่วโมง 12:00 มีออเดอร์สูงที่สุด 6,742 รายการและรายได้รวม 1,364,026 บาท สูงกว่าชั่วโมง 9:00 อย่างมาก สะท้อนพฤติกรรมการซื้อชาในมื้อเที่ยงและช่วงหลังเลิกงาน
AOV ค่อนข้างนิ่งตลอดทั้งวัน อยู่ในช่วง 194–209 บาท โดยชั่วโมง 22:00 มี AOV สูงสุดที่ 209 บาท แม้มีออเดอร์น้อยที่สุดเพียง 872 รายการ แสดงว่าลูกค้าช่วงดึกอาจสั่งสินค้ามูลค่าสูงกว่า หรือซื้อหลายแก้วต่อครั้งมากกว่าช่วงเวลาอื่น
ช่วง 14:00–16:00 เป็น off-peak ที่ชัดเจน จำนวนออเดอร์ลดลงอย่างเห็นได้ชัด PiCha อาจใช้ช่วงเวลานี้สำหรับโปรโมชัน Happy Hour เพื่อกระตุ้นยอดขาย หรือปรับลดกำลังคนลงเล็กน้อยเพื่อเพิ่มประสิทธิภาพต้นทุนแรงงาน ขณะเดียวกันควรเสริมกำลังคนในช่วง peak 11:00–13:00 และ 17:00–19:00 เพื่อลดเวลารอและรักษาคะแนนความพึงพอใจ
สรุป
ฟังก์ชันรวม หรือ Aggregate Functions เป็นเครื่องมือที่ใช้สรุปข้อมูลจากหลายแถวให้กลายเป็นค่าผลลัพธ์เพียงค่าเดียว เช่น จำนวนรวม ค่าเฉลี่ย หรือค่าสูงสุด ซึ่งเหมาะมากสำหรับงานวิเคราะห์ข้อมูลเชิงธุรกิจ เพราะช่วยให้เรามองภาพรวมของยอดขาย ลูกค้า หรือพฤติกรรมการสั่งซื้อได้อย่างรวดเร็ว
ตารางต่อไปนี้สรุปหน้าที่ของฟังก์ชันรวมที่ใช้บ่อย พร้อมตัวอย่างการประยุกต์ใช้ในบริบทธุรกิจ
| ฟังก์ชัน | ทำอะไร | ตัวอย่างธุรกิจ |
|---|---|---|
COUNT(*) |
นับจำนวนแถวทั้งหมด | มีออเดอร์ทั้งหมดกี่รายการ |
COUNT(column) |
นับจำนวนแถวที่คอลัมน์นั้นไม่เป็น NULL |
มีออเดอร์กี่รายการที่ระบุ customer_id |
COUNT(DISTINCT col) |
นับจำนวนค่าที่ไม่ซ้ำกัน | มีลูกค้าไม่ซ้ำทั้งหมดกี่คน |
SUM(column) |
รวมค่าทั้งหมดในคอลัมน์ | รายได้รวมเป็นเท่าไร |
AVG(column) |
หาค่าเฉลี่ยของคอลัมน์ | มูลค่าออเดอร์เฉลี่ยอยู่ที่เท่าไร |
MEDIAN(column) |
หาค่ากลางของข้อมูล โดยได้รับผลกระทบจาก outlier น้อยกว่าค่าเฉลี่ย | ลูกค้าส่วนใหญ่จ่ายจริงต่อออเดอร์ประมาณเท่าไร |
MIN(column) |
หาค่าต่ำสุด | มูลค่าออเดอร์ที่ต่ำที่สุดคือเท่าไร |
MAX(column) |
หาค่าสูงสุด | มูลค่าออเดอร์ที่สูงที่สุดคือเท่าไร |
GROUP BY ใช้สำหรับจัดกลุ่มแถวที่มีค่าเหมือนกัน แล้วคำนวณค่าสรุปแยกตามแต่ละกลุ่ม เช่น สรุปยอดขายตามสาขา ตามเดือน หรือแยกตามช่องทางการขาย หลักสำคัญคือ คอลัมน์ใดก็ตามที่อยู่ใน SELECT และไม่ได้อยู่ภายใน aggregate function จะต้องระบุไว้ใน GROUP BY ด้วยเสมอ
ส่วน HAVING ใช้กรองผลลัพธ์ “หลังจาก” มีการจัดกลุ่มและคำนวณค่า aggregate แล้ว จึงต่างจาก WHERE ที่ใช้กรองข้อมูลดิบ “ก่อน” การจัดกลุ่ม เรามักใช้ HAVING เมื่อต้องการตั้งเงื่อนไขกับค่าที่คำนวณแล้ว เช่น HAVING SUM(net_sales_thb) > 2000000 เพื่อเลือกเฉพาะกลุ่มที่มียอดขายรวมเกิน 2,000,000 บาท หรือ HAVING COUNT(*) < 2000 เพื่อดูเฉพาะกลุ่มที่มีจำนวนรายการไม่มากนัก
เมื่อใช้ Aggregate Functions ร่วมกับ GROUP BY และ HAVING ได้อย่างถูกต้อง เราจะสามารถเปลี่ยนข้อมูลธุรกรรมจำนวนมากให้กลายเป็นสารสนเทศที่พร้อมใช้ในการตัดสินใจทางธุรกิจได้อย่างมีประสิทธิภาพ
บทที่ 4 เน้นการเชื่อมข้อมูลจากหลายตารางด้วย JOIN
คำถามท้ายบท
- จงหาจำนวนออเดอร์และรายได้รวม แยกตาม
weather_flagและchannel(GROUP BY 2 ระดับ) เรียงตามรายได้รวมจากมากไปน้อย
ผลลัพธ์ที่คาดหวัง
| weather_flag | channel | orders | revenue |
|---|---|---|---|
| sunny | delivery | 13,987 | 4,119,140 |
| sunny | to_go | 18,271 | 2,773,230 |
| sunny | dine_in | 7,334 | 1,102,110 |
| rainy | delivery | 3,459 | 1,033,730 |
| rainy | to_go | 4,737 | 716,317 |
| cloudy | delivery | 1,349 | 406,501 |
| rainy | dine_in | 1,869 | 281,824 |
| cloudy | to_go | 1,544 | 236,394 |
| cloudy | dine_in | 773 | 118,163 |
คลิกเพื่อดูเฉลย
SELECT -- เลือกข้อมูล
weather_flag, -- สภาพอากาศ
channel, -- ช่องทางการขาย
COUNT(*) AS order_count, -- นับจำนวนออร์เดอร์
ROUND(SUM(net_sales_thb), 0) AS total_revenue -- รวมยอดขายสุทธิ (บาท)
FROM transactions -- จากตารางธุรกรรม
GROUP BY weather_flag, channel -- จัดกลุ่มตามสภาพอากาศและช่องทาง
ORDER BY total_revenue DESC; -- เรียงตามยอดขายรวมจากมากไปน้อย- จงหาสาขาที่มีค่าเฉลี่ย
satisfaction_scoreต่ำกว่า 4.0 พร้อมแสดงจำนวนออเดอร์และ CM%
ผลลัพธ์ที่คาดหวัง
| store_name | order_count | avg_satisfaction | cm_pct |
|---|---|---|---|
| PiCha Exchange Tower | 7,080 | 3.63 | 55.6 |
| PiCha Park Silom | 7,611 | 3.66 | 55.6 |
| PiCha Siam Square | 10,738 | 3.96 | 52.1 |
คลิกเพื่อดูเฉลย
-- สรุปสาขาที่มีคะแนนความพึงพอใจเฉลี่ยต่ำกว่า 4.0
SELECT
s.store_name, -- ชื่อสาขา
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(AVG(t.satisfaction_score), 2) AS avg_satisfaction, -- คะแนนความพึงพอใจเฉลี่ย
ROUND(100.0 * SUM(t.contribution_margin_thb) / SUM(t.net_sales_thb), 1) AS cm_pct -- อัตรากำไรส่วนเกิน (%)
FROM transactions t
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมตารางสาขาเพื่อดึงชื่อสาขา
GROUP BY s.store_name -- จัดกลุ่มตามชื่อสาขา
HAVING AVG(t.satisfaction_score) < 4.0 -- กรองเฉพาะสาขาที่มีคะแนนความพึงพอใจเฉลี่ยต่ำกว่า 4.0
ORDER BY avg_satisfaction; -- เรียงจากสาขาที่มีคะแนนความพึงพอใจต่ำสุดไปสูงสุด- จงหาชั่วโมงที่มีออเดอร์มากกว่า 5,000 ครั้ง พร้อมแสดง AOV
ผลลัพธ์ที่คาดหวัง
| hour | orders | AOV |
|---|---|---|
| 12 | 6,742 | 202 |
| 13 | 5,548 | 202 |
| 11 | 5,191 | 204 |
| 18 | 5,115 | 203 |
คลิกเพื่อดูเฉลย
-- สรุปชั่วโมงที่มีคำสั่งซื้อมากกว่า 5,000 รายการ เรียงตามความนิยม
SELECT
hour, -- ชั่วโมงของวัน (0-23)
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(AVG(net_sales_thb), 0) AS avg_order_value -- มูลค่าคำสั่งซื้อเฉลี่ย (บาท)
FROM transactions -- ตารางหลักที่เก็บข้อมูลธุรกรรม
GROUP BY hour -- จัดกลุ่มตามชั่วโมง
HAVING COUNT(*) > 5000 -- กรองเฉพาะชั่วโมงที่มีคำสั่งซื้อมากกว่า 5,000 รายการ
ORDER BY order_count DESC; -- เรียงจากชั่วโมงที่มีคำสั่งซื้อมากสุดไปน้อยสุด- จงคำนวณจำนวนวันที่มีรายได้เกิน 200,000 บาท พร้อมแสดงวันที่ขายดีที่สุดและแย่ที่สุด
ผลลัพธ์ที่คาดหวัง
| วันที่ | รายได้ |
|---|---|
| 2026-02-15 | 318,449 THB (สูงสุด) |
| 2026-03-02 | 97,480 THB (ต่ำสุด) |
คลิกเพื่อดูเฉลย
-- นับจำนวนวันที่มีรายได้สูงกว่า 200,000 บาท พร้อมรายได้สูงสุดและต่ำสุดในกลุ่มนั้น
SELECT
COUNT(*) AS high_revenue_days, -- จำนวนวันที่มีรายได้สูงกว่า 200,000 บาท
MAX(daily_rev) AS best_day_revenue, -- รายได้สูงสุดในบรรดาวันที่ผ่านเกณฑ์
MIN(daily_rev) AS worst_day_revenue -- รายได้ต่ำสุดในบรรดาวันที่ผ่านเกณฑ์
FROM (
SELECT
order_date, -- วันที่สั่งซื้อ
SUM(net_sales_thb) AS daily_rev -- รายได้รวมต่อวัน (บาท)
FROM transactions -- ตารางหลักที่เก็บข้อมูลธุรกรรม
GROUP BY order_date -- จัดกลุ่มตามวันที่
HAVING SUM(net_sales_thb) > 200000 -- กรองเฉพาะวันที่มีรายได้รวมมากกว่า 200,000 บาท
); -- Subquery: สรุปรายได้รายวันที่ผ่านเกณฑ์- จงหาสัดส่วนรายได้ของ YumDash เทียบกับ C2U ในแต่ละสาขา (เฉพาะ delivery)
ผลลัพธ์ที่คาดหวัง
| store_name | YumDash | C2U | YumDash % | C2U % |
|---|---|---|---|---|
| PiCha Siam Square | 893,395 | 506,036 | 63.8% | 36.2% |
| PiCha centralwOrld | 757,696 | 552,107 | 57.8% | 42.2% |
| PiCha ICONSIAM | 575,258 | 341,987 | 62.7% | 37.3% |
| PiCha Vanit Village | 418,180 | 323,284 | 56.4% | 43.6% |
| PiCha Park Silom | 334,641 | 283,701 | 54.1% | 45.9% |
| PiCha Exchange Tower | 318,471 | 254,615 | 55.6% | 44.4% |
คลิกเพื่อดูเฉลย
-- สรุปยอดขายและค่าคอมมิชชันแยกตามสาขาและแพลตฟอร์ม Delivery
SELECT
s.store_name, -- ชื่อสาขา
t.platform, -- ชื่อแพลตฟอร์ม Delivery
COUNT(*) AS order_count, -- จำนวนคำสั่งซื้อ
ROUND(SUM(t.net_sales_thb), 0) AS total_revenue, -- รายได้รวม (บาท)
ROUND(100.0 * SUM(t.platform_commission_thb) / SUM(t.net_sales_thb), 1) AS commission_pct -- อัตราค่าคอมมิชชันของแพลตฟอร์ม (%)
FROM transactions t
INNER JOIN stores s ON t.store_id = s.store_id -- เชื่อมตารางสาขาเพื่อดึงชื่อสาขา
WHERE t.channel = 'delivery' -- กรองเฉพาะคำสั่งซื้อช่องทาง Delivery
GROUP BY s.store_name, t.platform -- จัดกลุ่มตามชื่อสาขาและแพลตฟอร์ม
ORDER BY s.store_name, total_revenue DESC; -- เรียงตามชื่อสาขา และรายได้จากมากไปน้อยในแต่ละสาขา