JOIN ทุกแบบ
ประมาณ 30 นาที
เป้าหมายของบท
- เชื่อมตารางด้วย INNER/LEFT/RIGHT/FULL JOIN และรู้ว่าแบบไหนให้ผลต่างกันยังไง
- หา "แถวที่ไม่มีคู่" ด้วย LEFT JOIN ร่วมกับ IS NULL
- เชื่อมได้มากกว่า 2 ตาราง และระวังปัญหาแถวเบิ้ล (fan-out)
ทำไมต้อง JOIN
ข้อมูลของร้านค้าออนไลน์กระจายอยู่หลายตาราง — ลูกค้าอยู่ที่ customers ออเดอร์อยู่ที่ orders
รายการสินค้าในแต่ละออเดอร์อยู่ที่ order_items และรายละเอียดสินค้าอยู่ที่ products
JOIN คือการเชื่อมตารางเหล่านี้เข้าด้วยกันตาม “คีย์” ที่เชื่อมโยงกัน
SELECT o.order_id, c.city
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN vs LEFT JOIN vs RIGHT JOIN vs FULL JOIN
| แบบ | เก็บแถวไหนไว้ |
|---|---|
INNER JOIN (หรือเขียนแค่ JOIN) |
เฉพาะแถวที่จับคู่ได้ทั้งสองฝั่ง |
LEFT JOIN |
ทุกแถวจากตารางซ้าย ต่อให้หาคู่ในตารางขวาไม่เจอก็ใส่ NULL แทน |
RIGHT JOIN |
ทุกแถวจากตารางขวา (ตรงข้ามกับ LEFT) |
FULL JOIN |
ทุกแถวจากทั้งสองฝั่ง ไม่ว่าจะจับคู่ได้หรือไม่ |
ลองเชื่อม customers กับ orders ทั้ง 4 แบบดู แล้วสังเกตจำนวนแถวที่ได้:
| แบบ | จำนวนแถว |
|---|---|
| INNER | 6,938 |
| LEFT | 7,399 |
| RIGHT | 6,938 |
| FULL | 7,399 |
สังเกตว่า RIGHT เท่ากับ INNER และ FULL เท่ากับ LEFT ในกรณีนี้ — เพราะทุกออเดอร์อ้างอิงลูกค้าที่มีจริงเสมอ
(ไม่มี “ออเดอร์กำพร้า”) ความต่างระหว่าง RIGHT/FULL กับ INNER/LEFT จะเห็นชัดก็ต่อเมื่อฝั่งขวา (หรือทั้งสองฝั่ง)
มีแถวที่จับคู่ไม่ได้ ซึ่งในที่นี้เกิดฝั่ง customers แทน — มีลูกค้า 461 คนที่ไม่เคยมีออเดอร์เลย ทำให้ LEFT JOIN
ได้แถวเพิ่มมา 461 แถว (7,399 − 6,938) นี่คือสิ่งที่ LEFT JOIN เผยให้เห็น
หา “แถวที่ไม่มีคู่” (anti-join)
รูปแบบที่ใช้บ่อยมากคือหา “อะไรที่ไม่มี” เช่น ลูกค้าที่ไม่เคยสั่งซื้อ:
SELECT c.customer_id FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL
หลักการ: LEFT JOIN ทำให้แถวที่จับคู่ไม่ได้มีค่า NULL ในคอลัมน์ของตารางขวา กรองด้วย IS NULL
บนคอลัมน์คีย์ของตารางขวา ก็จะเหลือเฉพาะแถวที่ “ไม่มีคู่” จริงๆ
เชื่อมได้มากกว่า 2 ตาราง — และปัญหาแถวเบิ้ล (fan-out)
SELECT p.category, SUM(i.quantity * i.unit_price) AS revenue
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
JOIN products p ON i.product_id = p.product_id
GROUP BY p.category
เชื่อมกี่ตารางก็ได้ต่อกันเรื่อยๆ แต่ต้องระวัง fan-out: ถ้าตารางที่เชื่อมมีความสัมพันธ์แบบ
“หนึ่งต่อหลาย” (1 ออเดอร์มีหลายรายการสินค้า) การ JOIN จะคูณแถวขึ้นตามจำนวนรายการ ทำให้ COUNT(*)
นับเกินจำนวนที่ต้องการ (เช่น นับ “รายการสินค้า” ทั้งที่ตั้งใจนับ “ออเดอร์”) วิธีแก้คือใช้ COUNT(DISTINCT ...)
บนคีย์ของตารางที่ไม่อยากให้นับซ้ำ
USING เมื่อชื่อคอลัมน์ตรงกัน
ถ้าคอลัมน์ที่ใช้เชื่อมชื่อเดียวกันทั้งสองตาราง เขียนสั้นลงได้ด้วย USING:
SELECT * FROM order_items i JOIN products p USING(product_id)
-- เท่ากับ: JOIN products p ON i.product_id = p.product_id
Self-join
JOIN ตารางกับตัวเองก็ทำได้ ใช้เมื่อต้องการเทียบแถวในตารางเดียวกันเอง เช่น “หาสินค้าคู่ที่ราคาใกล้กัน”
ต้องตั้งชื่อ alias ให้ต่างกัน (เช่น p1, p2) และมักต้องมีเงื่อนไข p1.id < p2.id กันไม่ให้นับคู่ซ้ำ
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
ลูกค้าคนไหนที่ยังไม่เคยสั่งซื้อเลย
ทีมการตลาดอยากส่งโปรโมชันไปกระตุ้นลูกค้าที่สมัครแล้วแต่ยังไม่เคยสั่งซื้อเลยสักครั้ง
ลอง INNER JOIN
customersกับordersก่อน — สังเกตว่าจำนวนแถวที่ได้ (6938) ไม่ใช่จำนวนลูกค้า แต่เป็นจำนวนออเดอร์ เพราะลูกค้าคนเดียวมีได้หลายออเดอร์ INNER JOIN จึงคูณแถวตามนั้น
รายได้รวมแต่ละหมวดสินค้า (เฉพาะออเดอร์ completed)
อยากรู้ว่าหมวดไหนทำรายได้ให้ร้านมากที่สุด นับเฉพาะออเดอร์ที่จบสมบูรณ์ (completed)
รายได้คำนวณจาก
order_items(quantity × unit_price) เชื่อมกับordersเพื่อกรองสถานะได้
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
นับจำนวนออเดอร์ (ไม่ซ้ำ) ที่ซื้อสินค้าหมวด Electronics ให้ถูกต้อง (ไม่ติดปัญหานับซ้ำแบบตัวอย่าง)
เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
จับคู่ order_items กับ products ด้วย USING แล้วนับจำนวนแถวทั้งหมด
ออเดอร์กับเมืองของลูกค้าที่สั่ง (JOIN customers) 5 ออเดอร์แรกสุด (order_id น้อยสุด)
จำนวนลูกค้าที่ยังไม่เคยสั่งซื้อเลย แยกตามเมือง เฉพาะเมือง Phuket และ Hat Yai
รายได้รวม (quantity × unit_price) ของออเดอร์ที่ completed ทั้งหมด
🟡 ประยุกต์
หมวดสินค้าที่มีรายได้มากที่สุด (เฉพาะ completed) และรายได้เท่าไร
5 ลูกค้าเมือง Bangkok ที่ใช้จ่ายมากที่สุด (เฉพาะ completed) เรียงจากมากไปน้อย
จำนวนลูกค้าที่ไม่เคยสั่งซื้อเลย แยกตามเมือง เรียงจากมากไปน้อย
🔴 ท้าทาย
จำนวนคู่สินค้าในหมวดเดียวกันที่ราคาต่างกันไม่เกิน 10 บาท (self-join กับตัวเอง) นับแต่ละคู่แค่ครั้งเดียว (ไม่นับ (A,B) ซ้ำกับ (B,A) และไม่นับคู่กับตัวเอง)
Quiz
สรุป
- LEFT JOIN เก็บทุกแถวจากตารางซ้าย ใส่ NULL เมื่อหาคู่ในตารางขวาไม่เจอ ส่วน INNER JOIN ตัดแถวที่ไม่มีคู่ทิ้ง
- หา "แถวที่ไม่มีคู่" ด้วย LEFT JOIN + WHERE คอลัมน์คีย์ของตารางขวา IS NULL
- JOIN กับตารางแบบ one-to-many (เช่น order_items) ทำให้แถวเบิ้ล ระวัง COUNT(*) นับซ้ำ ใช้ COUNT(DISTINCT ...) แทน
คำถามสัมภาษณ์
เจอ bug ว่าตัวเลขหลัง JOIN เยอะกว่าที่ควรจะเป็น สาเหตุที่เป็นไปได้คืออะไร และแก้ยังไง
สาเหตุที่พบบ่อยที่สุดคือ fan-out: JOIN กับตารางที่มีความสัมพันธ์แบบ one-to-many (เช่น 1 ออเดอร์มีหลายรายการสินค้าใน order_items) ทำให้แถวของฝั่ง "one" ถูกคูณซ้ำตามจำนวนแถวฝั่ง "many"
วิธีตรวจสอบ: ลองนับจำนวนแถวก่อนและหลัง JOIN เทียบกัน ถ้าหลัง JOIN เพิ่มขึ้นมาก ให้สงสัยความสัมพันธ์แบบ one-to-many ก่อนเสมอ
วิธีแก้: ใช้ COUNT(DISTINCT ...) เพื่อนับไม่ซ้ำ หรือ aggregate ตารางลูกให้เป็นแถวเดียวต่อคีย์ก่อน
(เช่นด้วย subquery/CTE) แล้วค่อย JOIN