🇯🇵 日本語 | 🇺🇸 English | 🇪🇸 Español | 🇵🇹 Português | 🇹🇭 ไทย | 🇨🇳 中文

[กำแพงของ 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 ประการคือ:

นี่คือคุณสมบัติที่สำคัญที่สุดของ 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

ถ้าหากสับสนว่าจะใช้ตัวไหนดี ให้ลองถามตัวเองว่าคุณอยากให้ข้อมูลอะไรเป็น "ตัวเอก"

ถ้าใช้เกณฑ์นี้ในการตัดสินใจ คุณก็จะสามารถเลือก 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"


RIGHT JOIN และ FULL OUTER JOIN

JOIN ยังมีเพื่อนๆ อีกนะครับ

ในการทำงานจริง เราจะใช้ LEFT JOIN บ่อยที่สุด แต่การรู้ว่ามีเพื่อนๆ เหล่านี้อยู่ด้วยก็เป็นเรื่องที่ดีครับ


สรุป

お疲れ様でした!テーブル結合の基本であるINNER JOINLEFT JOINについて、その違いと使い分けを解説しました。

JOIN คือหัวใจสำคัญในการดึงศักยภาพของฐานข้อมูลเชิงสัมพันธ์ออกมาใช้อย่างเต็มที่ ในตอนแรกอาจจะรู้สึกว่ายากไปหน่อย แต่ยิ่งใช้บ่อยเท่าไหร่ ก็จะยิ่งเข้าใจความสะดวกของมัน และการดึงข้อมูลก็จะยิ่งสนุกขึ้นเรื่อยๆ ลองนำไปประยุกต์ใช้ในรูปแบบต่างๆ และทำให้ JOIN เป็นอาวุธของคุณนะครับ!