ข้ามไปยังเนื้อหา

Join

ปัญหาที่เจอจริง: ข้อมูลที่เกี่ยวข้องกันมักถูกแยกเก็บไว้คนละ table เพื่อลดความซ้ำซ้อน แต่งานจริงมักต้องเห็นข้อมูลรวมกัน — join รับสอง table มาจับคู่ row ตามกฎที่คุณเขียนใน ON clause เพราะหนังสือเก็บ id ของผู้เขียนไว้ คุณจึงสั่งให้ PostgreSQL เดินตามการเชื่อมโยงนั้นและผลิต row ที่บรรจุ column จากทั้งสอง table พร้อมกันได้ ทางเลือกเดียวที่คุณต้องตัดสินใจจริง ๆ คือจะทำอย่างไรกับ row ที่ไม่มีคู่ที่ตรงกันในอีกฝั่ง — และนั่นแหละคือสิ่งที่แยกประเภทของ join ออกจากกัน

เรายังคงใช้ table authors, books, และ sales ต่อไป

INNER JOIN คืน row ที่รวมกันหนึ่ง row สำหรับทุกคู่ที่เงื่อนไข ON เป็นจริง row ฝั่งใดก็ตามที่ไม่เจอคู่จะถูกทิ้งไปเฉย ๆ นี่คือ join ที่คุณหยิบมาใช้บ่อยที่สุด

SELECT b.title, a.name AS author
FROM books AS b
INNER JOIN authors AS a ON a.id = b.author_id
ORDER BY b.title;
title | author
------------------+---------------
Quiet Harbors | Mara Linde
The Glass Orchard| Mara Linde
Tidal Notes | Owen Pryce
(3 rows)

ผู้เขียนที่ไม่มีหนังสือ หรือหนังสือที่ author_id ไม่ตรงกับผู้เขียนคนใดเลย จะไม่มีวันปรากฏในผลลัพธ์นี้

LEFT OUTER JOIN (คีย์เวิร์ด OUTER ใส่หรือไม่ก็ได้) เก็บทุก row จาก table ฝั่งซ้ายไว้แม้จะไม่มีคู่ที่ตรงกันในฝั่งขวา column ฝั่งขวาจะถูกเติมด้วย NULL สำหรับ row ที่ไม่มีคู่เหล่านั้น นี่คือวิธีที่คุณจะแสดงรายชื่อผู้เขียนและดูว่าใครยังไม่มีหนังสือเลย

SELECT a.name AS author, b.title
FROM authors AS a
LEFT JOIN books AS b ON b.author_id = a.id
ORDER BY a.name;
author | title
---------------+-------------------
Mara Linde | Quiet Harbors
Mara Linde | The Glass Orchard
Owen Pryce | Tidal Notes
Sela Vance |
(4 rows)

Sela Vance ยังไม่ได้เขียนหนังสือเลย row ของเธอจึงยังอยู่รอดโดยมี title เป็น NULL เจ้า NULL นั้นคือ join ที่กำลังบอกว่า “row ฝั่งซ้ายมีอยู่ แต่ไม่มีอะไรตรงกันในฝั่งขวา”

RIGHT JOIN คือภาพสะท้อนกลับด้าน คือเก็บทุก row จาก table ฝั่งขวาและเติมฝั่งซ้ายด้วย NULL ตรงที่ไม่มีคู่ที่ตรงกัน A RIGHT JOIN B ผลิต row ออกมาเหมือนกับ B LEFT JOIN A หลายคนจึงเขียนแต่ LEFT JOIN เท่านั้น ส่วน FULL OUTER JOIN เก็บ row ที่ไม่มีคู่จากทั้งสองฝั่งพร้อมกัน

SELECT a.name AS author, b.title
FROM authors AS a
FULL OUTER JOIN books AS b ON b.author_id = a.id
ORDER BY a.name;
author | title
---------------+-------------------
Mara Linde | Quiet Harbors
Mara Linde | The Glass Orchard
Owen Pryce | Tidal Notes
Sela Vance |
| Unlinked Draft
(5 rows)

row สุดท้ายคือหนังสือที่ author_id ไม่ได้ชี้ไปยังผู้เขียนคนใด ฝั่งผู้เขียนจึงเป็น NULL full join เป็น join แบบเดียวที่เผย row แบบนี้ออกมาพร้อมกับผู้เขียนที่ไม่มีหนังสือ

join ทั้งสี่แบบต่างกันแค่วิธีจัดการกับ row ที่ไม่ผ่านการทดสอบ ON เท่านั้น ภาพนี้สรุปว่าใครรอด

flowchart TD
  M[Matching rows on both sides] --> INNER[INNER JOIN keeps these only]
  M --> LEFT[LEFT JOIN keeps these plus unmatched left rows]
  M --> RIGHT[RIGHT JOIN keeps these plus unmatched right rows]
  M --> FULL[FULL OUTER JOIN keeps these plus unmatched rows from both sides]
join แต่ละแบบเก็บอะไรไว้บ้าง
  • การลืม ON clause จะเปลี่ยน join ให้กลายเป็น cross join (CROSS JOIN) ที่จับคู่ทุก row ฝั่งซ้ายกับทุก row ฝั่งขวา ด้วยสิบ row กับอีกสิบ row ก็ได้ผลลัพธ์ออกมาหนึ่งร้อย row — ซึ่งแทบไม่ใช่สิ่งที่คุณต้องการเลย
  • ตั้ง alias สั้น ๆ ให้แต่ละ table เช่น a และ b แล้วใส่นำหน้าทุก column วิธีนี้ทำให้ query อ่านง่ายและเลี่ยงความกำกวมเมื่อทั้งสอง table มี column ชื่อเดียวกันอย่าง id
  • NULL ในผลลัพธ์ของ outer join หมายถึง “ไม่พบ row ที่ตรงกัน” ไม่ได้หมายถึง “ค่าเป็นศูนย์หรือว่างเปล่า” ทดสอบด้วย IS NULL ห้ามใช้ = NULL เด็ดขาด
  • การใส่ตัวกรองให้ table ฝั่งขวาใน WHERE แทนที่จะเป็น ON อาจเปลี่ยน LEFT JOIN กลับให้เป็น inner join อย่างเงียบ ๆ เพราะ NULL ไม่ผ่านการเปรียบเทียบใน WHERE ส่วนใหญ่
ตัวเลือกBenefitCost
Join table ที่ normalize ไว้ตอน queryข้อมูลไม่ซ้ำ (no duplication) เสมอเป็นข้อมูลล่าสุดquery ซับซ้อนขึ้น และต้องคำนวณ join ทุกครั้งที่ query
Denormalize/คัดลอกข้อมูลไว้ล่วงหน้าอ่านเร็วกว่า ไม่ต้อง join ตอน queryเสี่ยงข้อมูลไม่ตรงกัน (stale/inconsistent) ต้องคอยซิงค์เอง
  • ลืมใส่ ON clause — PostgreSQL จะไม่เตือนว่าคุณพลาด แต่จะทำ cross join จับคู่ทุก row กับทุก row ทำให้ผลลัพธ์ระเบิด (row explosion) ตรวจสอบว่าทุก join มี ON เสมอ
  • ไม่ตั้ง alias ให้ table — เมื่อสอง table มี column ชื่อเดียวกันอย่าง id query จะโยน ambiguous column error หรือแม้ไม่ error ก็อ่านยากจนดูแลต่อไม่ได้ ใช้ alias สั้น ๆ อย่าง a, b เสมอ
  • ใส่ filter ของ table ฝั่งขวาไว้ใน WHERE แทน ON — กับ LEFT JOIN แล้วพฤติกรรมจะกลายเป็น INNER JOIN อย่างเงียบ ๆ เพราะ WHERE จะทิ้ง row ที่มีค่า NULL ทางฝั่งขวาไปด้วย ถ้าอยากกรองแต่ยังเก็บ row ฝั่งซ้ายไว้ครบ ต้องใส่เงื่อนไขนั้นใน ON

💡 ตัวอย่างจากของจริง

Discord — activity feed และหน้าประวัติข้อความอาศัย join ระหว่าง table ผู้ใช้, server และข้อความ แทนที่จะคัดลอกชื่อผู้ใช้ไปฝังไว้ในทุกข้อความ ทำให้เปลี่ยนชื่อครั้งเดียวก็อัปเดตทุกที่

GitLab — หน้าประวัติ merge request และ pipeline รวมข้อมูลจากหลาย table (users, projects, commits) ด้วย join ตอน query เพื่อให้ single source of truth อยู่ที่เดียว ไม่ต้องกังวลเรื่องข้อมูลซ้ำที่หลุดซิงค์กัน

join แบบใดคืนเฉพาะ row ที่มีคู่ที่ตรงกันทั้งสองฝั่ง?
ใน LEFT JOIN อะไรปรากฏใน column ฝั่งขวาเมื่อ row ฝั่งซ้ายไม่มีคู่ที่ตรงกัน?
จะเกิดอะไรขึ้นถ้าคุณเขียน join แต่ลืม ON clause?