6 JOIN ใน SQL ที่คุณควรรู้ (พร้อมตัวอย่างชัดเจนและเข้าใจง่าย)
เรียนรู้ 6 ประเภท JOIN ใน SQL ที่นักวิเคราะห์ควรรู้—INNER, LEFT, RIGHT, FULL OUTER, CROSS และ SELF—พร้อมตัวอย่างใช้งานและข้อควรระวัง

JOIN ใน SQL คืออะไร และทำไมต้องใช้
SQL JOIN ช่วยให้คุณรวมแถวจากสองตาราง (หรือมากกว่า) เป็นผลลัพธ์เดียวด้วยการจับคู่คอลัมน์ที่เกี่ยวข้อง—โดยปกติจะเป็น ID
ทำไม JOIN สำคัญ
ฐานข้อมูลจริงมักจะแยกข้อมูลเป็นหลายตารางเพื่อไม่ให้ซ้ำซ้อน เช่น ชื่อของลูกค้าเก็บในตาราง customers ขณะที่การสั่งซื้อเก็บในตาราง orders JOIN คือวิธีที่คุณเชื่อมข้อมูลเหล่านั้นกลับเมื่อคุณต้องการคำตอบ
ดังนั้น JOIN จึงถูกใช้ทั่วในการรายงานและการวิเคราะห์:
- สร้างรายงานยอดขายที่มีชื่อลูกค้า ยอดคำสั่งซื้อ และสถานะการชำระเงิน
- หา ลูกค้าที่ยังไม่เคยสั่งซื้อ
- ตรวจสอบความไม่ตรงกัน เช่น คำสั่งซื้อที่ไม่มีการชำระเงิน
- สร้างสรุป “หนึ่งแถวต่อหนึ่งลูกค้า” จากหลายแถวที่เกี่ยวข้อง
ถ้าไม่มี JOIN คุณจะต้องรันคำสั่งแยกแล้วรวมผลด้วยมือ—ช้า เสี่ยงผิดพลาด และทำซ้ำยาก
ถ้าคุณสร้างผลิตภัณฑ์บนฐานข้อมูลเชิงสัมพันธ์ (แดชบอร์ด พื้นที่แอดมิน เครื่องมือภายใน พอร์ทัลลูกค้า) JOIN คือสิ่งที่เปลี่ยน "ตารางดิบ" ให้เป็นมุมมองที่ผู้ใช้เห็นได้ แพลตฟอร์มอย่าง Koder.ai (ที่สร้างแอป React + Go + PostgreSQL จากการคุย) ยังคงต้องพึ่งพื้นฐาน JOIN ที่ดีเมื่อต้องการหน้ารายการ รายงาน และหน้าตรวจสอบ เพราะตรรกะฐานข้อมูลยังคงสำคัญ แม้ว่าการพัฒนาจะเร็วขึ้น
6 ประเภท JOIN ที่คุณควรรู้
คู่มือนี้เน้น JOIN หกแบบที่ครอบคลุมงาน SQL ประจำวันได้ส่วนใหญ่:
- INNER JOIN: คืนเฉพาะแถวที่จับคู่กันทั้งสองตาราง (เหมาะกับ “แสดงความสัมพันธ์ที่ยืนยันแล้ว”)
- LEFT JOIN: เก็บแถวทั้งหมดจากตารางซ้าย และดึงข้อมูลจากขวาเมื่อมีการจับคู่ (เหมาะกับ “รวมข้อมูลที่อาจจะหายไป”)
- RIGHT JOIN: กระจกของ LEFT JOIN (ไม่ค่อยใช้ แต่มีประโยชน์ตามสไตล์การเขียน)
- FULL OUTER JOIN: เก็บทุกแถวจากทั้งสองฝั่งและจับคู่เมื่อเป็นไปได้ (ดีสำหรับการไกล่เกลี่ย)
- CROSS JOIN: สร้างทุกชุดคู่ของแถว (ใช้สร้างปฏิทิน สถานการณ์ หรือข้อมูลทดสอบ—แต่ใช้ผิดง่าย)
- SELF JOIN: ต่อ таблица กับตัวเอง (สะดวกกับโครงสร้างลำดับชั้นเช่น พนักงาน/ผู้จัดการ)
หมายเหตุสั้นๆ เกี่ยวกับซินแทกซ์
รูปแบบ JOIN คล้ายกันในฐานข้อมูลส่วนใหญ่ (PostgreSQL, MySQL, SQL Server, SQLite) มีความแตกต่างเล็กน้อย—โดยเฉพาะการสนับสนุน FULL OUTER JOIN และพฤติกรรมขอบกรณี—แต่แนวคิดและรูปแบบหลักนำไปใช้ได้ข้ามระบบ
ตารางตัวอย่างที่ใช้ (customers, orders, payments)
เพื่อให้ตัวอย่าง JOIN เรียบง่าย เราจะใช้สามตารางขนาดเล็กที่สะท้อนการตั้งค่าจริง: ลูกค้าสร้างคำสั่งซื้อ และคำสั่งซื้ออาจ (หรืออาจจะไม่) มีการชำระเงิน
โน้ตสั้นก่อนเริ่ม: ตารางตัวอย่างด้านล่างแสดงเฉพาะบางคอลัมน์ แต่ตัวอย่างบางคำสั่งจะอ้างถึงฟิลด์เพิ่มเติม (เช่น order_date, created_at, status, หรือ paid_at) ให้ถือว่าเป็นคอลัมน์ “ทั่วไป” ที่มักมีในสคีมาของการใช้งานจริง
1) customers
Primary key: customer_id
| customer_id | name |
|---|---|
| 1 | Ava |
| 2 | Ben |
| 3 | Chen |
| 4 | Dia |
2) orders
Primary key: order_id
Foreign key: customer_id → customers.customer_id
| order_id | customer_id | order_total |
|---|---|---|
| 101 | 1 | 50 |
| 102 | 1 | 120 |
| 103 | 2 | 35 |
| 104 | 5 | 70 |
สังเกตว่า order_id = 104 อ้างอิง customer_id = 5 ซึ่ง ไม่มี ในตาราง customers กรณี "ไม่มีการจับคู่" นี้มีประโยชน์เพื่อดูพฤติกรรมของ LEFT JOIN, RIGHT JOIN, และ FULL OUTER JOIN
3) payments
Primary key: payment_id
Foreign key: order_id → orders.order_id
| payment_id | order_id | amount |
|---|---|---|
| 9001 | 101 | 50 |
| 9002 | 102 | 60 |
| 9003 | 102 | 60 |
| 9004 | 999 | 25 |
รายละเอียดสำคัญสองข้อสำหรับการสอน:
order_id = 102มี สองแถวการชำระ (แบ่งจ่าย) เมื่อคุณ joinordersกับpaymentsคำสั่งซื้อนั้นจะแสดง สองครั้ง — นี่คือที่มาของการเกิดซ้ำที่มักทำให้คนตกใจpayment_id = 9004อ้างอิงorder_id = 999ซึ่งไม่มีในordersนี่คืออีกกรณีของการ “ไม่จับคู่”
คาดหวังอะไรเมื่อ JOIN ตารางเหล่านี้
- แถวที่จับคู่ได้: เช่น ลูกค้า 1 ↔ คำสั่งซื้อ 101/102; คำสั่งซื้อ 101 ↔ การชำระ 9001
- แถวที่ไม่จับคู่: เช่น ลูกค้า 3 และ 4 ไม่มีคำสั่งซื้อ; คำสั่งซื้อ 104 ไม่มีลูกค้า; การชำระ 9004 ไม่มีคำสั่งซื้อ
- การซ้ำ: การ join
ordersกับpaymentsจะทำให้คำสั่งซื้อ 102 ซ้ำเพราะมีการชำระสองรายการ
INNER JOIN: เก็บเฉพาะแถวที่จับคู่
INNER JOIN คืนเฉพาะแถวที่มีการจับคู่ใน ทั้งสอง ตาราง ถ้าลูกค้าไม่มีคำสั่งซื้อ เขาจะไม่ปรากฏ ถ้าคำสั่งซื้ออ้างอิงลูกค้าที่ไม่มี (ข้อมูลไม่ดี) คำสั่งซื้อจะไม่ปรากฏเช่นกัน
รูปแบบพื้นฐาน
คุณเลือกตาราง “ซ้าย” แล้ว join กับตาราง “ขวา” โดยระบุเงื่อนไขใน ON:
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id;
แกนหลักคือบรรทัด ON o.customer_id = c.customer_id: บอก SQL ว่าแถวใดเกี่ยวข้องกัน
กรณีใช้งานจริง: ลูกค้าที่เคยสั่งซื้อ
ถ้าต้องการรายการเฉพาะลูกค้าที่เคยสั่งซื้อจริงๆ พร้อมรายละเอียดคำสั่งซื้อ INNER JOIN คือทางเลือกตามธรรมชาติ:
SELECT
c.name,
o.order_id,
o.total_amount
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;
เป็นประโยชน์สำหรับงานอย่าง “ส่งอีเมลติดตามคำสั่งซื้อ” หรือ “คำนวณรายได้ต่อลูกค้า” เมื่อคุณสนใจเฉพาะลูกค้าที่มีการซื้อ
ความผิดพลาดบ่อย: ลืมเงื่อนไข join
ถ้าคุณเขียน join แต่ลืม ON (หรือ join บนคอลัมน์ผิด) คุณอาจสร้าง Cartesian product (ทุกลูกค้าจับคู่กับทุกคำสั่งซื้อ) หรือได้ผลลัพธ์ผิดพลาดอย่างเงียบๆ
ตัวอย่างไม่ดี (อย่าใช้):
SELECT c.name, o.order_id
FROM customers c
JOIN orders o;
จงใส่เงื่อนไข join ที่ชัดเจนใน ON เสมอ (หรือ USING ในกรณีที่เหมาะสม)
LEFT JOIN: เก็บทุกแถวจากตารางซ้าย
LEFT JOIN คืน แถวทั้งหมดจากตารางซ้าย และเพิ่มข้อมูลจากตารางขวาเมื่อมีการจับคู่ ถ้าไม่มีการจับคู่ คอลัมน์ฝั่งขวาจะเป็น NULL
เมื่อใช้
ใช้ LEFT JOIN เมื่อต้องการรายการสมบูรณ์จากตารางหลัก พร้อมข้อมูลที่เกี่ยวข้องเมื่อมี
ตัวอย่าง: “แสดง ลูกค้าทั้งหมด และรวมคำสั่งซื้อของพวกเขา ถ้ามี”
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id;
- ลูกค้าที่มีคำสั่งซื้อจะปรากฏพร้อมรายละเอียด
- ลูกค้าที่ไม่มีคำสั่งซื้อยังปรากฏ แต่
o.order_idจะเป็นNULL
หาแถวที่ “ไม่มีการจับคู่” (รูปแบบคลาสสิก)
ตัวอย่าง: “ลูกค้าคนไหน ไม่เคยสั่งซื้อ บ้าง?”
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
เงื่อนไข WHERE ... IS NULL เก็บเฉพาะแถวจากตารางซ้ายที่การ join ไม่พบการจับคู่
ระวัง: การจับคู่หลายรายการทำให้แถวทวีคูณ
LEFT JOIN อาจทำให้แถวของตารางซ้าย “ซ้ำ” เมื่อฝั่งขวามีหลายแถวที่จับคู่
ถ้าลูกค้าคนหนึ่งมี 3 คำสั่งซื้อ ลูกค้าคนนั้นจะปรากฏ 3 ครั้ง—ครั้งละหนึ่งคำสั่งซื้อ นี่เป็นพฤติกรรมปกติ แต่จะทำให้สับสนหากคุณพยายามนับลูกค้า
ตัวอย่างการนับที่อาจผิด:
SELECT COUNT(*)
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id;
ถ้าต้องการนับลูกค้า ควรนับคีย์ของลูกค้า (เช่น COUNT(DISTINCT c.customer_id)) ขึ้นกับสิ่งที่คุณวัด
RIGHT JOIN: เก็บทุกแถวจากตารางขวา
RIGHT JOIN เก็บ แถวทั้งหมดจากตารางขวา และเฉพาะแถวที่จับคู่จากตารางซ้าย หากไม่มีการจับคู่ คอลัมน์ฝั่งซ้ายจะเป็น NULL มันเหมือนภาพสะท้อนของ LEFT JOIN
ตัวอย่างง่ายๆ
สมมติอยากแสดง การชำระทั้งหมด แม้ว่าจะจับกับคำสั่งซื้อไม่ได้ (อาจเพราะคำสั่งซื้อถูกลบหรือข้อมูลการชำระไม่สะอาด)
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM orders o
RIGHT JOIN payments p
ON o.order_id = p.order_id;
ผลลัพธ์:
- การชำระทั้งหมด ถูกใส่เข้ามา (เพราะ
paymentsอยู่ฝั่งขวา) - ถ้าการชำระไม่มีคำสั่งซื้อที่ตรงกัน
o.order_idและo.customer_idจะเป็นNULL
คำสั่งเดียวกันเขียนเป็น LEFT JOIN (มักชอบแบบนี้มากกว่า)
บ่อยครั้งคุณสามารถเขียน RIGHT JOIN ให้เป็น LEFT JOIN โดยสลับลำดับตาราง:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM payments p
LEFT JOIN orders o
ON o.order_id = p.order_id;
จะได้ผลเหมือนกัน แต่มักอ่านง่ายกว่าเพราะเริ่มจากตารางหลักที่คุณสนใจ (ตรงนี้คือ payments)
ความอ่านง่าย: ทำไมหลายทีมหลีกเลี่ยง RIGHT JOIN
แนวทางการเขียน SQL ส่วนใหญ่ขอให้เริ่มจากตารางหลักแล้ว LEFT JOIN ตารางเพิ่มเติม เมื่อความสัมพันธ์ optional เขียนเป็น LEFT JOIN เสมอ คิวรีจะอ่านง่ายขึ้น
เมื่อ RIGHT JOIN ยังมีประโยชน์
RIGHT JOIN อาจสะดวกเมื่อคุณแก้คิวรีเดิมและพบว่าตารางที่ต้องเก็บไว้ดันอยู่ฝั่งขวา แทนที่จะเขียนคิวรียาวใหม่ทั้งหมด การเปลี่ยน JOIN หนึ่งบรรทัดเป็น RIGHT JOIN อาจเร็วและเสี่ยงน้อย
FULL OUTER JOIN: เก็บทุกแถวจากทั้งสองตาราง
FULL OUTER JOIN คืน ทุกแถวจากทั้งสองตาราง
- ถ้าแถวจับคู่กัน จะได้แถวเดียวรวมกัน (เหมือน
INNER JOIN) - ถ้าแถวมีแค่ฝั่งซ้าย จะยังปรากฏ โดยคอลัมน์ฝั่งขวาจะเป็น
NULL - ถ้าแถวมีแค่ฝั่งขวา จะยังปรากฏ โดยคอลัมน์ฝั่งซ้ายจะเป็น
NULL
เมื่อต้องการใช้
กรณีธุรกิจคลาสสิกคือ ไกล่เกลี่ย orders กับ payments:
- ดูคำสั่งซื้อที่ชำระแล้ว (จับคู่)
- คำสั่งซื้อที่ยังไม่ชำระ (order มี แต่ payment หาย)
- การชำระที่ไร้คำสั่งซื้อ (payment มี แต่ order ไม่มี)
ตัวอย่าง:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount
FROM orders o
FULL OUTER JOIN payments p
ON p.order_id = o.order_id;
ฐานข้อมูลที่รองรับ
FULL OUTER JOIN รองรับใน PostgreSQL, SQL Server, และ Oracle
มัน ไม่รองรับ ใน MySQL และ SQLite (ต้องใช้วิธีแก้ไข)
ทางเลือกที่พกพาได้: UNION ของ LEFT JOIN และ RIGHT JOIN
ถ้าฐานข้อมูลไม่รองรับ FULL OUTER JOIN คุณสามารถจำลองได้โดยรวม:
- แถวทั้งหมดจาก
orders(พร้อมการจับคู่เมื่อมี) - แถวทั้งหมดจาก
paymentsที่ไม่จับกับorders
รูปแบบหนึ่งที่พบบ่อย:
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.order_id
UNION
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
RIGHT JOIN payments p
ON p.order_id = o.order_id;
เคล็ดลับ: เมื่อเห็น NULL ฝั่งใดฝั่งหนึ่ง นั่นคือสัญญาณว่าข้อมูลจากอีกฝั่ง “หายไป” ซึ่งต้องการสำหรับการตรวจสอบและไกล่เกลี่ย
CROSS JOIN: สร้างทุกการจับคู่ (ใช้อย่างระมัดระวัง)
CROSS JOIN คืน ทุกการจับคู่ที่เป็นไปได้ ของแถวจากสองตาราง ถ้าตาราง A มี 3 แถว และตาราง B มี 4 แถว ผลลัพธ์จะมี 3 × 4 = 12 แถว เรียกว่า Cartesian product
ฟังดูน่ากลัว—และเป็นได้—แต่มีประโยชน์เมื่อต้องการสร้างการผสมผสานจริงๆ
ตัวอย่างเล็กๆ ที่ปลอดภัย: ขนาด × สี (สร้าง SKU)
สมมติคุณเก็บตัวเลือกสินค้าแยกตาราง:
sizes: S, M, Lcolors: Red, Blue
CROSS JOIN สร้างตัวแปรทั้งหมด (ใช้สร้าง SKU ล่วงหน้า สร้างแคตตาล็อก หรือทดสอบ):
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
ผลลัพธ์ (3 × 2 = 6 แถว):
- S / Red
- S / Blue
- M / Red
- M / Blue
- L / Red
- L / Blue
คำเตือน: ผลลัพธ์โตเร็ว
เพราะจำนวนแถวถูกคูณ CROSS JOIN อาจขยายเร็วมาก:
- 10,000 ลูกค้า × 50 ผลิตภัณฑ์ = 500,000 แถว
- 100,000 × 100,000 = 10,000,000,000 แถว
ซึ่งทำให้คิวรีช้าหลายและใช้หน่วยความจำมาก หากต้องการการผสม ให้จำกัดขนาดตารางนำเข้าและพิจารณาเพิ่ม LIMIT หรือกรองอย่างรอบคอบ
SELF JOIN: ต่อโต๊ะกับตัวเอง
SELF JOIN ก็คือการ join ตารางกับตัวมันเอง ใช้เมื่อแถวหนึ่งในตารางสัมพันธ์กับอีกแถวหนึ่งในตารางเดียวกัน—เช่น โครงสร้างลำดับชั้นพนักงาน/ผู้จัดการ
ทำไมต้องมี alias และช่วยยังไง
เพราะใช้ตารางเดียวกันสองครั้ง คุณต้องตั้ง alias ให้แต่ละ "สำเนา" เพื่อให้คิวรีอ่านง่ายและบอก SQL ว่าหมายถึงฝั่งไหน
รูปแบบที่นิยม:
eสำหรับ employeemสำหรับ manager
ตัวอย่างปฏิบัติ: พนักงานและชื่อผู้จัดการ
สมมติ employees มี:
idnamemanager_id(ชี้ไปที่idของพนักงานคนอื่น)
เพื่อแสดงพนักงานแต่ละคนพร้อมชื่อผู้จัดการ:
SELECT
e.id,
e.name AS employee_name,
m.name AS manager_name
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;
จัดการพนักงานระดับบนสุด (manager_id เป็น NULL)
คิวรีใช้ LEFT JOIN ไม่ใช่ INNER JOIN เพราะบางคนอาจไม่มีผู้จัดการ (เช่น CEO) ในกรณีเหล่านั้น manager_id มักเป็น NULL และ LEFT JOIN จะยังคงเก็บแถวนั้นไว้พร้อมให้ manager_name เป็น NULL ถ้าใช้ INNER JOIN พนักงานระดับบนจะหายไปเพราะไม่มีแถว manager ที่ตรงกัน
เงื่อนไขการ JOIN: ON vs USING (และทำไมสำคัญ)
JOIN ไม่ได้ "รู" ว่าตารางสองตารางสัมพันธ์กันอย่างไร—คุณต้องบอก โดยใส่เงื่อนไขการจับคู่ ซึ่งควรวางถัดจาก JOIN เพราะเป็นการอธิบายว่าตารางจับคู่กันอย่างไร ไม่ใช่การกรองผลลัพธ์สุดท้าย
ON: ยืดหยุ่นที่สุดและใช้บ่อยสุด
ใช้ ON เมื่อคุณต้องการควบคุมการจับคู่เต็มที่—ชื่อคอลัมน์ต่างกัน หลายเงื่อนไข หรือกฎเพิ่มเติม
SELECT
c.customer_id,
c.name,
o.order_id,
o.created_at
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c.customer_id;
ON ยังใช้สำหรับการจับคู่ซับซ้อน (เช่น จับสองคอลัมน์) โดยไม่ทำให้คิวรีสับสน
USING: สั้น แต่ใช้ได้เฉพาะคอลัมน์ชื่อเดียวกัน
บางฐานข้อมูล (เช่น PostgreSQL และ MySQL) รองรับ USING เป็นชอร์ตฮันด์เมื่อทั้งสองตารางมีคอลัมน์ชื่อเดียวกันและต้องการ join บนคอลัมน์นั้น
SELECT
customer_id,
name,
order_id
FROM customers
JOIN orders
USING (customer_id);
ข้อดีอย่างหนึ่ง: USING มักส่งคืนคอลัมน์ customer_id เพียงคอลัมน์เดียวในผลลัพธ์ (ไม่ใช่ 2 คอลัมน์)
หลีกเลี่ยงชื่อคอลัมน์คลุมเครือ: ใส่คำนำตารางเสมอ
เมื่อ join แล้ว ชื่อคอลัมน์มักซ้อนทับกัน (id, created_at, status) ถ้าคุณเขียน SELECT id ฐานข้อมูลอาจโชว์ข้อผิดพลาดว่า "ambiguous column" หรือแย่กว่านั้น คุณอาจอ่านผิด id
ควรใช้ prefix ของตาราง (หรือ alias) เพื่อความชัดเจน:
SELECT c.customer_id, o.order_id
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
หลีกเลี่ยง SELECT * ในคิวรีที่มี JOIN
SELECT * จะยุ่งเหยิงเร็วเมื่อ JOIN: คุณดึงคอลัมน์ไม่จำเป็น เสี่ยงชื่อซ้ำ และทำให้มองไม่ออกว่าคิวรีสร้างอะไรขึ้นมา
แทนที่จะใช้ ให้เลือกคอลัมน์ที่ต้องการ ผลลัพธ์จะสะอาด ดูแลรักษาง่าย และมักมีประสิทธิภาพกว่า—โดยเฉพาะเมื่อตารางกว้าง
การกรองข้อมูลที่ JOIN: WHERE vs ON
เมื่อคุณ join ตาราง WHERE และ ON ต่างทำการกรองแต่ในช่วงเวลาที่ต่างกัน
- ON ตัดสินว่า แถวไหนจะจับคู่ขณะ JOIN
- WHERE กรอง ผลลัพธ์สุดท้ายหลัง JOIN เสร็จแล้ว
ความต่างของเวลาเกิดเป็นเหตุผลที่คนเปลี่ยน LEFT JOIN เป็น INNER JOIN โดยไม่ตั้งใจ
ตัวอย่าง WHERE ทำให้ LEFT JOIN หาย
ถ้าคุณต้องการ ลูกค้าทั้งหมด แม้ไม่มีคำสั่งซื้อที่ชำระล่าสุด:
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
ปัญหา: สำหรับลูกค้าที่ไม่มีคำสั่งซื้อ o.status และ o.order_date เป็น NULL เงื่อนไข WHERE จะตัดแถวนั้นทิ้ง ทำให้ LEFT JOIN ทำงานเหมือน INNER JOIN
ย้ายเงื่อนไขที่เกี่ยวกับการจับคู่ไปใส่ใน ON เพื่อเก็บแถวที่ไม่จับคู่
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
ตอนนี้ลูกค้าที่ไม่มีคำสั่งซื้อที่เข้าเงื่อนไขยังปรากฏ (พร้อมคอลัมน์คำสั่งซื้อเป็น NULL) ซึ่งมักเป็นจุดประสงค์ของ LEFT JOIN
เช็คลิสต์ด่วน: อะไรควรอยู่ใน ON vs WHERE?
- ใส่เงื่อนไขใน ON เมื่อมันอธิบายว่า แถวฝั่งขวาที่อนุญาตให้จับคู่เป็นอย่างไร (เช่น กรองสถานะ/วันที่ของตารางที่ถูก join)
- ใส่เงื่อนไขใน WHERE เมื่อมันอธิบายว่า ผลลัพธ์สุดท้ายต้องการเก็บแถวแบบใด (เช่น กรองตารางซ้าย หรือกรองหลังจากต้องการให้จับคู่แล้ว)
- ถ้าคุณต้องการ “เก็บแถวซ้ายทั้งหมด แต่จำกัดแถวขวาที่จะจับคู่” ให้ใช้ LEFT JOIN + ใส่เงื่อนไขใน ON
- ถ้าคุณต้องการแถวที่มีการจับคู่จริงๆ ให้ใช้ INNER JOIN (หรือ
WHERE o.order_id IS NOT NULLอย่างชัดเจน)
หลีกเลี่ยงแถวซ้ำและกับดัก many-to-many
JOIN ไม่ได้แค่ “เพิ่มคอลัมน์”—มันอาจ เพิ่มจำนวนแถว ซึ่งเป็นพฤติกรรมที่ถูกต้องแต่ทำให้คนสับสนเมื่อตัวเลขรวมเปลี่ยนไปอย่างไม่คาดคิด
ทำไมแถวถึงทวีคูณ
การ join คืนหนึ่งแถวผลลัพธ์สำหรับทุกคู่แถวที่จับคู่กัน
- One-to-many: ลูกค้าหนึ่งคนมีหลายคำสั่งซื้อ ถ้า join
customersกับordersลูกค้าแต่ละคนอาจปรากฏหลายครั้ง - Many-to-many: ถ้าคุณ join
ordersกับpaymentsแล้วแต่ละคำสั่งซื้อมีหลายการชำระ และคุณยัง join กับตาราง "many" อีก (เช่นorder_items) คุณอาจได้ผลคูณระหว่างpayments × items
สรุปก่อน join (pre-aggregate)
ถ้าจุดประสงค์คือ “หนึ่งแถวต่อคำสั่งซื้อ” หรือ “หนึ่งแถวต่อลูกค้า” ให้สรุปฝั่งที่เป็น many ก่อนแล้วค่อย join
-- One row per order from payments
WITH payment_totals AS (
SELECT
order_id,
SUM(amount) AS total_paid,
COUNT(*) AS payment_count
FROM payments
GROUP BY order_id
)
SELECT
o.order_id,
o.customer_id,
COALESCE(pt.total_paid, 0) AS total_paid,
COALESCE(pt.payment_count, 0) AS payment_count
FROM orders o
LEFT JOIN payment_totals pt
ON pt.order_id = o.order_id;
จะช่วยให้รูปร่างของการ join คาดเดาได้: หนึ่งคำสั่งซื้อคงเป็นหนึ่งแถว
DISTINCT เป็นทางเลือกสุดท้าย
SELECT DISTINCT อาจแก้ปัญหาแถวซ้ำได้แต่ซ่อนสาเหตุจริง:
- อาจคัดแถวที่ถูกต้องทิ้ง
- อาจซ่อนเงื่อนไข join ผิดพลาด
- อาจทำให้ผลรวมผิดพลาด (โดยเฉพาะ SUM และ COUNT)
ใช้เมื่อเข้าใจสาเหตุของการซ้ำแน่ชัดเท่านั้น
เช็ครวดเร็ว: ตรวจสอบจำนวนแถว
ก่อนเชื่อผลลัพธ์ ให้เปรียบเทียบจำนวนแถว:
- นับแถวในตารางหลัก (เช่น orders)
- นับแถวหลัง join
- ถ้าจำนวนเพิ่มขึ้นไม่คาดคิด ให้ตรวจสอบคีย์ที่ทำให้จับคู่หลายครั้ง และตัดสินใจว่าจะสรุปก่อนหรือเปลี่ยนเส้นทางการ join
พื้นฐานประสิทธิภาพและชีทสรุป JOIN
JOIN มักถูกตำหนิว่าเป็นสาเหตุคิวรีช้า แต่สาเหตุจริงมักเป็น ปริมาณข้อมูลที่คุณขอให้ฐานข้อมูลรวมกัน และ ความง่ายที่มันจะหาการจับคู่
ดัชนี (แนวคิด) และทำไมช่วย JOIN
คิดว่าดัชนีเหมือนสารบัญหนังสือ ถ้าไม่มีมัน ฐานข้อมูลอาจต้องสแกนหลายแถวเพื่อหาการจับคู่สำหรับเงื่อนไข JOIN หากมีดัชนีบนคีย์การ join (เช่น customers.customer_id และ orders.customer_id) ฐานข้อมูลจะกระโดดไปที่แถวที่เกี่ยวข้องได้เร็วขึ้น
คุณไม่ต้องรู้รายละเอียดภายในเพื่อใช้ให้ดี: ถ้าคอลัมน์ถูกใช้บ่อยในการจับคู่ (ON a.id = b.a_id) มันเป็นผู้สมัครที่ดีที่จะทำดัชนี
JOIN บนคีย์ที่เสถียร (ไม่ใช่ชื่อหรืออีเมล)
ถ้าเป็นไปได้ ให้ join บนตัวระบุที่เสถียรและไม่ซ้ำซ้อน:
- ดี:
customers.customer_id = orders.customer_id - เสี่ยง:
customers.email = orders.emailหรือcustomers.name = orders.name
ชื่อเปลี่ยนได้และซ้ำได้ อีเมลอาจเปลี่ยนหรือมีรูปแบบต่างกัน ID ออกแบบมาสำหรับการจับคู่สม่ำเสมอและมักถูกทำดัชนี
ลดงานตั้งแต่ต้น
สองนิสัยที่ทำให้ JOIN เร็วขึ้นอย่างเห็นได้ชัด:
- เลือกคอลัมน์น้อยลง หลีกเลี่ยง
SELECT *เมื่อ join หลายตาราง—คอลัมน์เกินความจำเป็นเพิ่มการใช้หน่วยความจำและเครือข่าย - จำกัดแถวก่อนหรือตอน JOIN กรองแต่เนิ่นๆ เท่าที่เป็นไปได้
ตัวอย่าง: จำกัดคำสั่งซื้อก่อนแล้วค่อย join:
SELECT c.customer_id, c.name, o.order_id, o.created_at
FROM customers c
JOIN (
SELECT order_id, customer_id, created_at
FROM orders
WHERE created_at >= DATE '2025-01-01'
) o
ON o.customer_id = c.customer_id;
ถ้าคุณทำงานซ้ำบนคิวรีเหล่านี้ภายในกระบวนการสร้างแอป (เช่น สร้างหน้ารายงานบน PostgreSQL) เครื่องมืออย่าง Koder.ai ช่วยเร่งการสร้างสเกฟโฟลด์—สคีมา endpoints UI—ในขณะที่คุณควบคุมโลจิก JOIN ที่กำหนดความถูกต้อง
ชีทสรุป JOIN ด่วน
- INNER JOIN → แถวที่จับคู่ใน ทั้งสอง ตารางเท่านั้น
- LEFT JOIN → แถวทั้งหมดจากตารางซ้าย พร้อมการจับคู่จากขวา (ไม่จับคู่จะเป็น
NULL) - RIGHT JOIN → แถวทั้งหมดจากตารางขวา พร้อมการจับคู่จากซ้าย (
NULLเมื่อหาย) - FULL OUTER JOIN → แถวทั้งหมดจากทั้งสองตาราง; แถวที่จับคู่จะรวมกัน แถวที่ไม่จับคู่จะมี
NULL - CROSS JOIN → ทุกการผสมของแถว (ขนาดถูกคูณ; ใช้อย่างระมัดระวัง)
- SELF JOIN → ตารางต่อกับตัวเอง (ใช้กับโครงสร้างลำดับชั้นและการเปรียบเทียบ)
คำถามที่พบบ่อย
What is a SQL JOIN in plain English?
การ JOIN ใน SQL รวมแถวจากสอง (หรือมากกว่า) ตารางให้เป็นชุดผลลัพธ์เดียว โดยจับคอลัมน์ที่เกี่ยวข้องเข้าด้วยกัน—บ่อยครั้งคือ primary key กับ foreign key (เช่น customers.customer_id = orders.customer_id) วิธีนี้แหละที่ช่วยให้คุณนำตารางที่ถูก normalized กลับมารวมกันเวลาต้องการรายงาน การตรวจสอบ หรือการวิเคราะห์
When should I use INNER JOIN?
ใช้ INNER JOIN เมื่อคุณต้องการเฉพาะแถวที่มีความสัมพันธ์ในทั้งสองตารางเท่านั้น
- ลูกค้าที่ไม่มีคำสั่งซื้อจะไม่ปรากฏ
- คำสั่งซื้อที่อ้างอิงลูกค้าที่หายไปจะไม่ปรากฏ
เหมาะสำหรับความสัมพันธ์ที่ “ยืนยันแล้ว” เช่น การแสดงเฉพาะลูกค้าที่เคยซื้อของ
How do I find rows that don’t have a match in another table?
ใช้ LEFT JOIN เมื่อคุณต้องการเก็บแถวทั้งหมดจากตารางหลัก (ซ้าย) และดึงข้อมูลจากตารางขวาเมื่อมีการจับคู่
ตัวอย่างการหาที่ไม่มีการจับคู่:
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Do I really need RIGHT JOIN, and how do I replace it with LEFT JOIN?
RIGHT JOIN จะเก็บแถวทั้งหมดจากตารางด้านขวาและใส่ค่า NULL ให้คอลัมน์ของตารางซ้ายเมื่อไม่มีการจับคู่ ทีมส่วนใหญ่หลีกเลี่ยงเพราะอ่านกลับด้านได้ยาก
โดยทั่วไปสามารถเขียนใหม่เป็น LEFT JOIN ได้โดยสลับลำดับตาราง เช่น:
FROM payments p
LEFT JOIN orders o ON o.order_id = p.order_id
What is FULL OUTER JOIN best used for?
ใช้ FULL OUTER JOIN เมื่อคุณต้องการผลลัพธ์ที่รวมทั้งแถวที่จับคู่และแถวที่มีเฉพาะฝั่งใดฝั่งหนึ่ง ซึ่งเหมาะกับการไกล่เกลี่ย เช่น “คำสั่งซื้อที่ไม่มีการชำระ” และ “การชำระเงินที่ไม่มีคำสั่งซื้อ” เพราะฝั่งที่ไม่มีจะมีค่า NULL ในคอลัมน์อีกฝั่ง
What if my database doesn’t support FULL OUTER JOIN?
บางฐานข้อมูล (เช่น MySQL และ SQLite) ไม่มี FULL OUTER JOIN ตรงๆ วิธีแก้คือรวมผลจาก
orders LEFT JOIN payments- กับแถวที่อยู่ใน
paymentsแต่ไม่มีในorders
โดยใช้ UNION (หรือ UNION ALL พร้อมเงื่อนไขที่ระมัดระวัง) เพื่อรักษาแถวที่อยู่ข้างเดียวทั้งสองฝั่ง
What is a CROSS JOIN, and when is it actually useful?
CROSS JOIN คืนค่าทุกการจับคู่ระหว่างสองตาราง (Cartesian product) และมีประโยชน์เมื่อต้องการสร้างสถานการณ์ เช่น ขนาด × สี หรือสร้างกริดปฏิทิน
ข้อควรระวัง: จำนวนแถวเพิ่มขึ้นเร็วมาก ดังนั้นใช้เมื่อขนาดตารางนำเข้าตัวเล็กและควบคุมได้
What is a SELF JOIN, and why do I need table aliases?
Self join คือการเชื่อมตารางกับตัวมันเองเพื่อเชื่อมความสัมพันธ์ภายในตารางเดียวกัน (เช่น พนักงาน → ผู้จัดการ)
คุณต้องใช้ alias เพื่อแยกความหมายของแต่ละสำเนา เช่น:
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id
What’s the difference between filtering in ON vs WHERE for joins?
ON กำหนดว่าแถวใดจะจับคู่กันระหว่างการ JOIN; WHERE กรองผลลัพธ์หลังการ JOIN เสร็จแล้ว
ด้วย LEFT JOIN การใส่เงื่อนไขของตารางขวาใน WHERE อาจทำให้แถวที่ไม่มีการจับคู่ถูกทิ้งไปและเปลี่ยนพฤติกรรมเป็นเหมือน INNER JOIN
ถ้าต้องการเก็บแถวซ้ายทั้งหมดแต่จำกัดว่าแถวขวาที่จะจับคู่นั้นต้องเป็นอย่างไร ให้ใส่เงื่อนไขของตารางขวาไว้ใน ON แทน
Why do joins create duplicates, and how do I prevent double-counting?
การ JOIN ทำให้แถวทวีคูณเมื่อความสัมพันธ์เป็น one-to-many หรือ many-to-many เช่น คำสั่งซื้อที่มีสองการชำระจะปรากฏสองครั้งเมื่อต่อกับตาราง payments
ป้องกันการนับซ้ำโดยสรุปฝั่งที่เป็น "many" ก่อนแล้วค่อย JOIN:
WITH payment_totals AS (
SELECT
order_id,
SUM(amount) AS total_paid,
COUNT(*) AS payment_count
FROM payments
GROUP BY order_id
)
SELECT
o.order_id,
o.customer_id,
COALESCE(pt.total_paid, 0) AS total_paid,
COALESCE(pt.payment_count, 0) AS payment_count
FROM orders o
LEFT JOIN payment_totals pt
ON pt.order_id = o.order_id;
ใช้ DISTINCT เป็นทางเลือกสุดท้ายเพราะมันอาจปกปิดปัญหาจริงและทำให้ผลรวมผิดพลาด