[กำแพงของ SQL เบื้องต้น] พิชิต JOIN ให้สิ้นซาก! ความแตกต่างระหว่าง INNER JOIN และ LEFT JOIN คืออะไร?
"มีรายชื่อพนักงาน แต่ชื่อแผนกอยู่อีกไฟล์... อยากแสดงรายชื่อพนักงานกับชื่อแผนกพร้อมกันในที่เดียว!"
"อยากได้ลิสต์ที่ดูปุ๊บรู้ปั๊บว่าสินค้าชิ้นไหนอยู่หมวดหมู่ไหน!"
ในฐานะเว็บครีเอเตอร์ที่ทำงานกับฐานข้อมูล ปัญหาที่แทบทุกคนต้องเจอคือ "การที่อยากเห็นข้อมูลที่กระจัดกระจายอยู่หลายๆ ตารางมารวมกันในที่เดียว" ไม่ว่าจะเป็นข้อมูลลูกค้ากับประวัติการซื้อ, ตารางหลักสินค้ากับตารางสต็อก ในฐานข้อมูลที่ผ่านการทำ Normalization แล้ว เป็นเรื่องปกติที่ข้อมูลจะถูกแบ่งตามวัตถุประสงค์
อาวุธอันทรงพลังของ SQL ที่จะมาช่วยเชื่อมต่อข้อมูลที่กระจัดกระจายเหล่านี้ให้เป็นหนึ่งเดียว ราวกับต่อจิ๊กซอว์ ก็คือ "JOIN" ครับ ถ้าคุณสามารถใช้ JOIN ได้อย่างเชี่ยวชาญ ขอบเขตของข้อมูลที่คุณจะสามารถจัดการด้วย SQL ได้จะกว้างขึ้นอย่างมหาศาล และจะสามารถดึงข้อมูลที่มีคุณค่าและซับซ้อนยิ่งขึ้นออกมาได้
ในบทความนี้ เราจะเน้นไปที่ INNER JOIN และ LEFT JOIN ซึ่งเป็น JOIN ที่ใช้บ่อยที่สุด พร้อมอธิบายหน้าที่และการเลือกใช้อย่างละเอียด ด้วยภาพประกอบและโค้ดที่สามารถคัดลอกไปใช้ได้ รับรองว่าแม้แต่มือใหม่ก็เข้าใจได้อย่างแน่นอน!
การเตรียมตัว: ตารางสำหรับเรียนรู้ JOIN
เพื่อให้เข้าใจการทำงานของ JOIN อย่างชัดเจน ครั้งนี้เราจะใช้ตาราง "พนักงาน (employees)" และ "แผนก (departments)" เหมือนเดิมครับ แต่ครั้งนี้เพื่อความเข้าใจที่ง่ายขึ้น เราได้เพิ่มข้อมูลพิเศษคือ "พนักงานใหม่ที่ยังไม่ถูกบรรจุในแผนก" และ "แผนกประชาสัมพันธ์ที่เพิ่งตั้งใหม่และยังไม่มีพนักงาน" เข้าไปด้วย
ลองคัดลอกโค้ด SQL ด้านล่างไปรันในระบบฐานข้อมูลของคุณ หรือจะลองใน "สนามฝึก SQL" ที่จะกล่าวถึงต่อไปก็ได้ครับ
-- ลบตารางหากมีอยู่แล้ว
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
-- สร้างตารางแผนก
CREATE TABLE departments (
id INT PRIMARY KEY,
department_name VARCHAR(50)
);
-- เพิ่มข้อมูลแผนก
INSERT INTO departments (id, department_name) VALUES
(1, '営業部'),
(2, '開発部'),
(3, '人事部'),
(4, '広報部'); -- แผนกที่ยังไม่มีพนักงาน
-- สร้างตารางพนักงาน
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
department_id INT -- อนุญาตให้มีค่า NULL
);
-- เพิ่มข้อมูลพนักงาน
INSERT INTO employees (id, name, department_id) VALUES
(1, '山田 太郎', 1),
(2, '鈴木 花子', 2),
(3, '佐藤 次郎', 1),
(4, '高橋 三郎', 3),
(5, '田中 恵子', 2),
(6, '中村 さくら', NULL); -- พนักงานใหม่ที่ยังไม่ถูกบรรจุในแผนก
พร้อมกันรึยังครับ? เรามาออกเดินทางเพื่อเชื่อมต่อสองตารางนี้เข้าด้วยกันเลย!
1. INNER JOIN: การเชื่อมเฉพาะข้อมูลส่วนที่ซ้ำกันของสองตาราง
INNER JOIN เป็น JOIN ที่พื้นฐานและใช้บ่อยที่สุด พูดง่ายๆ ก็คือ เป็นวิธีการเชื่อมข้อมูลเพื่อ "ดึงเฉพาะข้อมูลที่มีความเกี่ยวข้องกันและมีอยู่จริงในทั้งสองตาราง"
ถ้าเปรียบเทียบกับแผนภาพเวนน์ (Venn diagram) ก็คือการเลือกเอาเฉพาะ "ส่วนที่ซ้อนทับกัน" ของวงกลมสองวง หรือพูดอีกอย่างคือ ใช้ในกรณีที่ต้องการข้อมูล "เฉพาะพนักงานที่มีสังกัดแผนก" เท่านั้น
วิธีใช้พื้นฐาน
เราจะระบุตารางแรกใน FROM clause และระบุตารางที่สองด้วย INNER JOIN จากนั้นใช้ ON clause เพื่อกำหนดว่าจะใช้คอลัมน์ไหนเป็นคีย์ (เครื่องหมาย) ในการเชื่อมตารางทั้งสองเข้าด้วยกัน
SELECT
e.name,
d.department_name
FROM
employees AS e
INNER JOIN
departments AS d ON e.department_id = d.id;
ลองดูผลลัพธ์นี้ครับ มีจุดสำคัญ 2 ประการคือ:
- คุณ "Nakamura Sakura" ที่ยังไม่มีสังกัดแผนก ไม่ได้ถูกรวมอยู่ในผลลัพธ์
- "แผนกประชาสัมพันธ์" (広報部) ที่ยังไม่มีพนักงานสักคน ก็ไม่ถูกรวมอยู่ในผลลัพธ์เช่นกัน
นี่คือคุณสมบัติที่สำคัญที่สุดของ INNER JOIN ครับ เฉพาะข้อมูลที่ตรงตามเงื่อนไขการเชื่อม ON e.department_id = d.id เท่านั้นที่จะถูกส่งกลับมาเป็นผลลัพธ์
※ การตั้งชื่อเล่น (alias) ให้กับตาราง เช่น employees AS e จะช่วยให้เขียนคิวรีได้สั้นและสะดวกขึ้นนะครับ
2. LEFT JOIN: การแสดงข้อมูลทั้งหมดจากตารางฝั่งซ้าย
LEFT JOIN คือวิธีการเชื่อมข้อมูลแบบ "แสดงข้อมูลทั้งหมดจากตารางฝั่งซ้าย (ตารางแรกที่ระบุใน FROM) และนำข้อมูลที่เกี่ยวข้องจากตารางฝั่งขวามาประกบ" บางครั้งเรียกว่า LEFT OUTER JOIN แต่คำว่า OUTER สามารถละไว้ได้
ถ้าเป็นแผนภาพเวนน์ ก็คือการรวมเอาวงกลมฝั่งซ้ายทั้งหมด และเอาเฉพาะส่วนที่ซ้อนทับของวงกลมฝั่งขวามาด้วย หากตารางฝั่งขวาไม่มีข้อมูลที่สอดคล้องกัน ส่วนนั้นก็จะมีค่าเป็น NULL (ค่าว่าง)
ตัวอย่างที่ 1: แสดงข้อมูลทั้งหมดโดยยึด "พนักงาน" เป็นหลัก
ลองพิจารณากรณีที่ว่า "อยากได้รายชื่อพนักงานทั้งหมด ถ้ามีสังกัดแผนก ก็อยากรู้ชื่อแผนกด้วย" ในกรณีนี้ เราจะวางตาราง "พนักงาน (employees)" ซึ่งเป็นแกนหลักไว้ทางซ้าย
SELECT
e.name,
d.department_name
FROM
employees AS e
LEFT JOIN
departments AS d ON e.department_id = d.id;
สังเกตที่ผลลัพธ์สิครับ! ต่างจากตอนใช้ INNER JOIN ครั้งนี้คุณ "Nakamura Sakura" ที่ยังไม่มีแผนกสังกัดได้ถูกรวมเข้ามาในผลลัพธ์ด้วย และ department_name ของเธอก็เป็น NULL
นี่คือพลังของ LEFT JOIN ครับ เนื่องจากข้อมูลทั้งหมดจากตารางฝั่งซ้าย (employees) จะถูกแสดงผล จึงเหมาะอย่างยิ่งสำหรับงานอย่าง "การค้นหาพนักงานที่ยังไม่ได้สังกัดแผนก"
ตัวอย่างที่ 2: แสดงข้อมูลทั้งหมดโดยยึด "แผนก" เป็นหลัก
แล้วถ้าเราสลับตารางฝั่งซ้ายกับขวาล่ะ จะเกิดอะไรขึ้น? ลองคิดในกรณีที่ว่า "อยากเห็นรายชื่อแผนกทั้งหมด ถ้ามีพนักงานสังกัดอยู่ ก็อยากรู้ชื่อพนักงานด้วย" ครั้งนี้เราจะวางตาราง "แผนก (departments)" ไว้ทางซ้ายดูครับ
SELECT
d.department_name,
e.name
FROM
departments AS d
LEFT JOIN
employees AS e ON d.id = e.department_id;
ผลลัพธ์ครั้งนี้แตกต่างออกไปอีกแล้ว! "แผนกประชาสัมพันธ์" (広報部) ที่ไม่มีพนักงานสังกัดอยู่เลยก็ยังแสดงผลขึ้นมาอย่างถูกต้อง และชื่อพนักงานที่สอดคล้องกัน (name) ก็เป็น NULL จะเห็นได้ว่าในการใช้ LEFT JOIN นั้น การวางตารางไหนไว้ "ฝั่งซ้าย" เป็นเรื่องที่สำคัญอย่างยิ่ง
เคล็ดลับการเลือกใช้ระหว่าง INNER JOIN vs LEFT JOIN
ถ้าหากสับสนว่าจะใช้ตัวไหนดี ให้ลองถามตัวเองว่าคุณอยากให้ข้อมูลอะไรเป็น "ตัวเอก"
-
INNER JOIN: "อยากเห็นข้อมูลเฉพาะที่มีข้อมูลครบทั้ง พนักงานและแผนก เท่านั้น!"
→ ใช้เมื่อต้องการดูความสัมพันธ์ที่ชัดเจนของสองตาราง -
LEFT JOIN: "อยากเห็น พนักงานทุกคน ส่วนข้อมูลแผนกจะมีหรือไม่มีก็ได้!"
→ ใช้เมื่อต้องการแสดงข้อมูลทั้งหมดจากตารางหนึ่งเป็นหลัก (master)
ถ้าใช้เกณฑ์นี้ในการตัดสินใจ คุณก็จะสามารถเลือก JOIN ที่เหมาะสมได้อย่างเป็นธรรมชาติครับ
ลงมือทำจริง! มาลองใช้ JOIN บนเบราว์เซอร์กันเถอะ
รอมานานแล้วใช่ไหมครับ! นี่คือสภาพแวดล้อมที่คุณสามารถลงมือทดลองใช้ INNER JOIN และ LEFT JOIN ที่เรียนมาได้ด้วยตัวเอง
ลองคัดลอกโค้ดทั้งหมดด้านล่าง แล้วบันทึกเป็นไฟล์ชื่อ join_practice.html จากนั้นเปิดไฟล์ด้วยเบราว์เซอร์ของคุณ ลองสลับตารางฝั่งซ้าย-ขวา หรือเปลี่ยนระหว่าง INNER กับ LEFT เพื่อดูว่าผลลัพธ์จะเปลี่ยนแปลงไปอย่างไร!
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<title>สนามฝึก SQL JOIN</title>
<script src="https://cdn.jsdelivr.net/npm/alasql@4"></script>
<style>
body { font-family: sans-serif; padding: 2rem; background-color: #f9f9f9; }
.container { max-width: 800px; margin: auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
h1 { color: #333; }
textarea { width: 100%; height: 180px; font-family: monospace; font-size: 16px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; margin-bottom: 1rem; }
button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background-color: #0056b3; }
#result-area { margin-top: 2rem; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.error { color: red; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>มาลองใช้ JOIN กัน!</h1>
<p>ป้อนคำสั่ง SQL ในกล่องข้อความด้านล่างแล้วกดปุ่ม "รัน SQL"</p>
<textarea id="sql-input">-- เขียนโค้ด SQL ของคุณที่นี่
SELECT
e.name,
d.department_name
FROM
employees AS e
LEFT JOIN
departments AS d ON e.department_id = d.id;</textarea>
<button onclick="executeSQL()">รัน SQL</button>
<div id="result-area"></div>
</div>
<script>
// การเริ่มต้นฐานข้อมูลและเตรียมข้อมูล
const db = new alasql.Database();
db.exec(`
CREATE TABLE departments (id INT, department_name STRING);
INSERT INTO departments VALUES (1, '営業部'), (2, '開発部'), (3, '人事部'), (4, '広報部');
CREATE TABLE employees (id INT, name STRING, department_id INT);
INSERT INTO employees VALUES (1, '山田 太郎', 1), (2, '鈴木 花子', 2), (3, '佐藤 次郎', 1), (4, '高橋 三郎', 3), (5, '田中 恵子', 2), (6, '中村 さくら', NULL);
`);
function executeSQL() {
const sql = document.getElementById('sql-input').value;
const resultArea = document.getElementById('result-area');
resultArea.innerHTML = '';
try {
const result = db.exec(sql);
if (result.length > 0) {
resultArea.appendChild(createTable(result));
} else {
resultArea.innerHTML = '<p>ไม่พบผลลัพธ์ (0 แถว)</p>';
}
} catch (e) {
resultArea.innerHTML = `<p class="error">เกิดข้อผิดพลาด: ${e.message}</p>`;
}
}
function createTable(data) {
const table = document.createElement('table');
const thead = table.createTHead();
const tbody = table.createTBody();
const headerRow = thead.insertRow();
for (const key in data[0]) {
const th = document.createElement('th');
th.textContent = key;
headerRow.appendChild(th);
}
data.forEach(rowData => {
const row = tbody.insertRow();
for (const key in rowData) {
const cell = row.insertCell();
cell.textContent = (rowData[key] === null || rowData[key] === undefined) ? 'NULL' : rowData[key];
}
});
return table;
}
// แสดงผลครั้งแรก
executeSQL();
</script>
</body>
</html>
ข้อควรระวังและเทคนิคที่เกี่ยวข้อง
ความแตกต่างระหว่าง ON และ WHERE
สิ่งที่มือใหม่มักจะสับสนคือการใช้ ON กับ WHERE ให้จำง่ายๆ ดังนี้ครับ
- ON: กำหนดกฎการเชื่อม ว่าจะใช้ "คีย์ไหน" ในการเชื่อมตารางเข้าด้วยกัน
- WHERE: กำหนดกฎการกรอง ว่าจะ "เลือกแถวไหน" จากตารางใหญ่ที่เกิดจากการเชื่อมแล้ว
ให้จำลำดับว่า "เชื่อมตารางให้ถูกต้องด้วย ON ก่อน แล้วจึงกรองข้อมูลที่ต้องการด้วย WHERE"
RIGHT JOIN และ FULL OUTER JOIN
JOIN ยังมีเพื่อนๆ อีกนะครับ
- RIGHT JOIN: เป็นเวอร์ชันตรงกันข้ามของ
LEFT JOINครับ โดยจะยึดตารางฝั่งขวาเป็นหลักในการแสดงผลทั้งหมด - FULL OUTER JOIN: แสดงข้อมูลทั้งหมดจากทั้งสองตาราง หากไม่มีข้อมูลที่สอดคล้องกันก็จะใส่ค่า
NULLแทน
ในการทำงานจริง เราจะใช้ LEFT JOIN บ่อยที่สุด แต่การรู้ว่ามีเพื่อนๆ เหล่านี้อยู่ด้วยก็เป็นเรื่องที่ดีครับ
สรุป
お疲れ様でした!テーブル結合の基本であるINNER JOINとLEFT JOINについて、その違いと使い分けを解説しました。
- INNER JOIN: ใช้เมื่อต้องการเฉพาะส่วนที่ซ้ำกันของสองตาราง ข้อมูลที่ไม่เกี่ยวข้องกันจะถูกตัดออกจากผลลัพธ์
- LEFT JOIN: ใช้เมื่อต้องการให้ตารางฝั่งหนึ่งเป็นตัวเอกและแสดงข้อมูลทั้งหมด ข้อมูลที่ไม่มีความสัมพันธ์จะถูกเติมด้วย
NULL - การเลือกใช้ JOIN ตัดสินจาก "เราอยากให้ข้อมูลอะไรเป็นตัวเอก"!
JOIN คือหัวใจสำคัญในการดึงศักยภาพของฐานข้อมูลเชิงสัมพันธ์ออกมาใช้อย่างเต็มที่ ในตอนแรกอาจจะรู้สึกว่ายากไปหน่อย แต่ยิ่งใช้บ่อยเท่าไหร่ ก็จะยิ่งเข้าใจความสะดวกของมัน และการดึงข้อมูลก็จะยิ่งสนุกขึ้นเรื่อยๆ ลองนำไปประยุกต์ใช้ในรูปแบบต่างๆ และทำให้ JOIN เป็นอาวุธของคุณนะครับ!