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

【เริ่มต้นกับ SQL IN Clause】จัดการหลายเงื่อนไขอย่างชาญฉลาด! พลิกโฉมการเขียน WHERE Clause ด้วย IN และ NOT IN

ในการพัฒนาเว็บไซต์ เรามักจะเจอกับสถานการณ์ที่ต้องค้นหาข้อมูลที่ตรงกับเงื่อนไขใดเงื่อนไขหนึ่งจากหลายๆ ตัวเลือกอยู่บ่อยครั้ง เช่น "ต้องการแสดงรายการสินค้าเฉพาะหมวดหมู่ที่กำหนด" หรือ "ต้องการส่งแคมเปญโปรโมชั่นเฉพาะผู้ใช้ในกรุงเทพฯ, นนทบุรี, และปทุมธานี"

ในสถานการณ์เช่นนี้ คุณจะเขียน WHERE clause อย่างไร?

WHERE category = 'เครื่องเขียน' OR category = 'เครื่องใช้ไฟฟ้า' OR category = 'อาหาร'

แน่นอนว่าโค้ดแบบนี้ก็ทำงานได้ถูกต้อง แต่จะเกิดอะไรขึ้นถ้ามีตัวเลือกเพิ่มเป็น 5 หรือ 10 อย่าง? โค้ดจะเต็มไปด้วย `OR` ที่ยาวเหยียด ทำให้อ่านยากและแก้ไขลำบากมาก แต่ปัญหาเหล่านี้จะหมดไปในพริบตาด้วยพระเอกของงานนี้ นั่นคือ `IN` Clause

ในบทความนี้ เราจะอธิบายทุกอย่างตั้งแต่พื้นฐานการใช้งาน `IN` clause และ `NOT IN` clause ซึ่งใช้สำหรับกำหนดเงื่อนไขตรงกันข้าม ไปจนถึงเทคนิคประยุกต์ที่ใช้ร่วมกับ Subquery และ "กับดัก NULL" ที่หลายคนมักจะพลาด พร้อมตัวอย่างโค้ดที่สามารถคัดลอกไปใช้งานได้ทันที มาฝึกใช้ `IN` clause ให้เชี่ยวชาญ เพื่อพัฒนา SQL ของคุณให้ฉลาดและอ่านง่ายขึ้นกันเถอะ!


การเตรียมการ: มาสร้างข้อมูลสินค้าสำหรับค้นหากันเถอะ

ในการทดลองใช้งาน ก่อนอื่นเราต้องมีข้อมูลเสียก่อน ในครั้งนี้ เราจะสร้างตาราง `products` สำหรับจัดการรายการสินค้าอย่างง่าย และตาราง `suppliers` สำหรับจัดการข้อมูลซัพพลายเออร์ที่จัดหาสินค้านั้นๆ การมีหมวดหมู่และซัพพลายเออร์หลายรายจะช่วยให้เราเห็นถึงความสะดวกของ `IN` clause ได้ชัดเจนยิ่งขึ้น

-- ลบตารางหากมีอยู่แล้ว (เพื่อให้ทดลองซ้ำได้)
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS suppliers;

-- สร้างตารางซัพพลายเออร์
CREATE TABLE suppliers (
  id INTEGER PRIMARY KEY,
  supplier_name TEXT NOT NULL,
  region TEXT NOT NULL -- 'คันโต', 'คันไซ'
);

-- สร้างตารางสินค้า
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  product_name TEXT NOT NULL,
  category TEXT NOT NULL,
  price INTEGER,
  supplier_id INTEGER,
  notes TEXT -- คอลัมน์หมายเหตุที่อนุญาตให้มีค่า NULL ได้
);

-- เพิ่มข้อมูลซัพพลายเออร์
INSERT INTO suppliers (id, supplier_name, region) VALUES
(101, 'Tokyo Office Supply', 'คันโต'),
(102, 'Osaka Kitchenware', 'คันไซ'),
(103, 'Chiba Food Service', 'คันโต'),
(104, 'Kyoto Traditional Stationery', 'คันไซ');

-- เพิ่มข้อมูลสินค้า
INSERT INTO products (id, product_name, category, price, supplier_id, notes) VALUES
(1, 'ปากกาสุดยอด', 'เครื่องเขียน', 150, 101, 'เขียนลื่น'),
(2, 'สมุดโน้ตเวทมนตร์', 'เครื่องเขียน', 300, 104, NULL),
(3, 'หม้อหุงข้าวขั้นเทพ', 'เครื่องใช้ไฟฟ้า', 25000, 102, 'สินค้ายอดนิยม'),
(4, 'แอปเปิ้ลมหัศจรรย์', 'อาหาร', 500, 103, 'จำนวนจำกัด'),
(5, 'ฮับ USB-C', 'อุปกรณ์คอมพิวเตอร์', 4000, 101, NULL),
(6, 'พู่กันโบราณ', 'เครื่องเขียน', 1200, 104, 'งานฝีมือช่าง'),
(7, 'น้ำผลไม้รวมโอซาก้า', 'อาหาร', 350, NULL, 'ไม่ทราบซัพพลายเออร์');

เพียงเท่านี้ เราก็พร้อมที่จะดึงข้อมูลด้วยเงื่อนไขต่างๆ แล้ว


【พื้นฐาน】การระบุหลายตัวเลือกด้วย `IN` Clause

ไวยากรณ์พื้นฐานของ `IN` clause นั้นง่ายมาก

WHERE ชื่อคอลัมน์ IN (ค่า1, ค่า2, ค่า3, ...);

เพียงแค่ใส่รายการค่าที่ต้องการค้นหาไว้ใน `()` เท่านั้น ซึ่งหมายความว่า "ขอแค่ค่าในคอลัมน์ตรงกับค่าใดค่าหนึ่งในลิสต์นี้ก็พอ" ตัวอย่างที่ใช้ `OR` เยอะๆ ในตอนต้น เมื่อใช้ `IN` clause ก็จะสามารถเขียนได้อย่างกระชับดังนี้

สถานการณ์: "ต้องการค้นหาสินค้าทั้งหมดที่มีหมวดหมู่เป็น 'เครื่องเขียน' หรือ 'อาหาร'"

SELECT * FROM products
WHERE category IN ('เครื่องเขียน', 'อาหาร');

ผลลัพธ์การทำงาน:

id | product_name         | category     | price | supplier_id | notes
---|----------------------|--------------|-------|-------------|----------------------
1  | ปากกาสุดยอด        | เครื่องเขียน     | 150   | 101         | เขียนลื่น
2  | สมุดโน้ตเวทมนตร์      | เครื่องเขียน     | 300   | 104         | NULL
4  | แอปเปิ้ลมหัศจรรย์     | อาหาร         | 500   | 103         | จำนวนจำกัด
6  | พู่กันโบราณ         | เครื่องเขียน     | 1200  | 104         | งานฝีมือช่าง
7  | น้ำผลไม้รวมโอซาก้า | อาหาร         | 350   | NULL        | ไม่ทราบซัพพลายเออร์

เป็นอย่างไรบ้าง? จะเห็นได้ว่าเข้าใจเจตนาได้ง่ายกว่าการใช้ `OR` ต่อกันหลายๆ อันมาก แนวคิด "ลิสต์" นี้คือพื้นฐานของ `IN` clause


【เงื่อนไขตรงกันข้าม】การระบุสิ่งที่ไม่ต้องการด้วย `NOT IN` Clause

แน่นอนว่า บางครั้งเราก็ต้องการค้นหาสิ่งที่ "ไม่รวมอยู่ในลิสต์นี้" ซึ่งเป็นเงื่อนไขตรงกันข้ามกับ `IN` clause ในกรณีนี้เราจะใช้ `NOT IN` clause

สถานการณ์: "ต้องการค้นหาสินค้าทั้งหมดที่ไม่ใช่หมวดหมู่ 'เครื่องเขียน' และ 'อาหาร'"

SELECT * FROM products
WHERE category NOT IN ('เครื่องเขียน', 'อาหาร');

ผลลัพธ์การทำงาน:

id | product_name     | category          | price | supplier_id | notes
---|------------------|-------------------|-------|-------------|----------------
3  | หม้อหุงข้าวขั้นเทพ | เครื่องใช้ไฟฟ้า      | 25000 | 102         | สินค้ายอดนิยม
5  | ฮับ USB-C        | อุปกรณ์คอมพิวเตอร์   | 4000  | 101         | NULL

เพียงแค่เพิ่ม `NOT` เข้าไปข้างหน้า `IN` ก็สามารถระบุเงื่อนไขปฏิเสธได้อย่างง่ายดาย ซึ่งเป็นวิธีใช้ที่สะดวกมากเช่นกัน


【ประยุกต์】การใช้ `IN` Clause ร่วมกับ Subquery

พลังที่แท้จริงของ `IN` clause จะแสดงออกมาเมื่อใช้ร่วมกับ Subquery (คำสั่งสืบค้นย่อย) Subquery คือคำสั่ง `SELECT` อีกชุดหนึ่งที่เขียนซ้อนอยู่ในคำสั่ง SQL หลัก

การใช้ Subquery ทำให้เราสามารถใช้ ผลลัพธ์ที่ได้จากตารางอื่นมาเป็นลิสต์ แทนการเขียนค่าคงที่ตายตัวในลิสต์ของ `IN` ได้

สถานการณ์: "ต้องการค้นหาสินค้าทั้งหมดที่จัดหาโดยซัพพลายเออร์ที่อยู่ในภูมิภาคคันโต"

การค้นหานี้สามารถคิดเป็น 2 ขั้นตอนได้:

  1. ขั้นแรก ดึงลิสต์ ID ของซัพพลายเออร์ที่มี `region` เป็น 'คันโต' จากตาราง `suppliers`
  2. จากนั้น ใช้ลิสต์ ID ดังกล่าวเพื่อค้นหาสินค้าที่เกี่ยวข้องจากตาราง `products`

เราจะนำส่วนของขั้นตอนที่ 1 นี้มาใส่เป็น Subquery ไว้ใน `IN` clause

SELECT * FROM products
WHERE supplier_id IN (SELECT id FROM suppliers WHERE region = 'คันโต');

ผลลัพธ์การทำงาน:

id | product_name     | category          | price | supplier_id | notes
---|------------------|-------------------|-------|-------------|-------------
1  | ปากกาสุดยอด      | เครื่องเขียน         | 150   | 101         | เขียนลื่น
4  | แอปเปิ้ลมหัศจรรย์   | อาหาร              | 500   | 103         | จำนวนจำกัด
5  | ฮับ USB-C        | อุปกรณ์คอมพิวเตอร์   | 4000  | 101         | NULL

ฐานข้อมูลจะทำงานในส่วนของ Subquery ที่อยู่ใน `()` คือ `SELECT id FROM suppliers WHERE region = 'คันโต'` ก่อน เพื่อดึงลิสต์ของ ID (ในกรณีนี้คือ `(101, 103)`) ออกมา จากนั้น คำสั่งด้านนอกจะทำงานเสมือนเป็น `WHERE supplier_id IN (101, 103)` ข้อดีที่สุดของการใช้ร่วมกับ Subquery คือการสามารถสร้างลิสต์เงื่อนไขแบบไดนามิกได้นั่นเอง


ข้อควรระวังที่สำคัญที่สุด! กับดักอันน่ากลัวของ `NOT IN` และ `NULL`

`IN` clause นั้นใช้งานง่ายและเข้าใจได้ไม่ยาก แต่สำหรับ `NOT IN` clause นั้นมี "กับดัก" ที่ร้ายกาจซึ่งนักพัฒนาหลายคนเคยพลาดกันมาแล้ว นั่นคือ กรณีที่ในลิสต์มีค่า `NULL` รวมอยู่ด้วย

สรุปสั้นๆ คือ หากในลิสต์ของ `NOT IN` มีค่า `NULL` รวมอยู่แม้แต่ตัวเดียว คำสั่งนั้นจะไม่คืนข้อมูลกลับมาเลยแม้แต่แถวเดียว

ทำไมถึงเป็นเช่นนั้น? เพราะ `NULL` ใน SQL เป็นค่าพิเศษที่หมายถึง "ไม่มีค่า" และผลลัพธ์ของการเปรียบเทียบกับ `NULL` (เช่น `id = NULL` หรือ `id <> NULL`) จะไม่ใช่ `TRUE` หรือ `FALSE` แต่จะเป็น `UNKNOWN` (ไม่ทราบ) เสมอ

เงื่อนไข `WHERE id NOT IN (1, 2, NULL)` จะถูกประเมินในลักษณะที่คล้ายกับ `WHERE id <> 1 AND id <> 2 AND id <> NULL` แม้ว่า `id` ของแถวข้อมูลจะเป็น `3` แต่เมื่อเงื่อนไขสุดท้าย `3 <> NULL` ให้ผลเป็น `UNKNOWN` ก็จะทำให้เงื่อนไขทั้งหมดไม่เป็น `TRUE` ส่งผลให้ไม่มีแถวใดตรงกับเงื่อนไขเลย

ที่อันตรายเป็นพิเศษคือการใช้ร่วมกับ Subquery ที่เราได้ลองไปก่อนหน้านี้

สถานการณ์: "ต้องการค้นหาสินค้าทั้งหมด**ยกเว้น**สินค้าที่จัดหาโดยซัพพลายเออร์ในภูมิภาคคันไซ"

ดูเผินๆ อาจจะเหมือนใช้ได้ แต่...

-- 【กับดัก!】 คำสั่งนี้ทำงานไม่ตรงตามที่คาดหวัง!
SELECT * FROM products
WHERE supplier_id NOT IN (SELECT id FROM suppliers WHERE region = 'คันไซ');

-- Subquery ด้านบนจะคืนค่า (102, 104) แต่ถ้าหากในตาราง suppliers
-- มีแถวที่ id เป็น NULL อยู่ด้วย ผลลัพธ์จะกลายเป็น (102, 104, NULL) และ
-- คำสั่งนี้จะคืนค่ากลับมา 0 แถว

หากผลลัพธ์ของ Subquery มีโอกาสที่จะมีค่า `NULL` รวมอยู่ด้วย ผลลัพธ์จะกลายเป็น 0 แถว แม้ในข้อมูลตัวอย่างของเราจะไม่มีซัพพลายเออร์ที่มี `id` เป็น `NULL` แต่ในตาราง `products` มีแถวที่ `supplier_id` เป็น `NULL` อยู่ (สินค้า ID 7) เรามาดูกันว่าแถวนี้จะถูกจัดการอย่างไรเมื่อใช้ `NOT IN`

วิธีแก้ไข: เมื่อใช้ `NOT IN` ร่วมกับ Subquery กฎเหล็กคือ ต้องเพิ่มเงื่อนไขอย่างเช่น `WHERE id IS NOT NULL` เข้าไปใน Subquery เพื่อให้แน่ใจว่าจะไม่คืนค่า `NULL` ออกมาเด็ดขาด หรือใช้ไวยากรณ์อื่นเช่น `NOT EXISTS`


【มุมทดลอง】มาลองรัน SQL บนเบราว์เซอร์และใช้ IN Clause ให้คล่องกันเถอะ!

ถึงเวลาเปลี่ยนความรู้ให้เป็นทักษะแล้ว! คัดลอกโค้ด HTML ด้านล่างนี้ทั้งหมด บันทึกเป็นไฟล์ชื่อ `sql_in_test.html` แล้วเปิดด้วยเบราว์เซอร์ของคุณ สภาพแวดล้อมสำหรับรัน SQL ส่วนตัวของคุณจะถูกสร้างขึ้นมา พร้อมกับตาราง `products` และ `suppliers` ที่เราใช้ในบทความนี้

ลองเปลี่ยนค่าในลิสต์ของ `IN` หรือ `NOT IN` หรือเปลี่ยนเงื่อนไขใน Subquery แล้วสังเกตดูว่าผลลัพธ์จะเปลี่ยนแปลงไปอย่างไร!

<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>สนามทดลอง SQL IN Clause ออนไลน์</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: #333; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
    h1 { color: #8e44ad; }
    textarea { width: 100%; height: 180px; font-family: "SF Mono", "Consolas", monospace; font-size: 16px; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; margin-bottom: 1rem; }
    button { background-color: #9b59b6; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
    button:hover { background-color: #8e44ad; }
    button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
    #result-container { margin-top: 2rem; border: 1px solid #ddd; padding: 1rem; border-radius: 6px; background: #fdfdfd; min-height: 50px; }
    #error-message { color: #e74c3c; font-weight: bold; }
    table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
    th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
    th { background-color: #f2f2f2; }
    tr:nth-child(even) { background-color: #f9f9f9; }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
</head>
<body>

  <h1>มาลองใช้ SQL กันเถอะ!</h1>
  <p>ป้อนคำสั่ง SQL ในกล่องข้อความด้านล่างแล้วกดปุ่ม "รัน" มาลองใช้เงื่อนไขต่างๆ กันดูนะ!</p>

  <textarea id="sql-input">-- ลองค้นหาสินค้าที่มาจากซัพพลายเออร์ในภูมิภาคคันไซ
SELECT * FROM products
WHERE supplier_id IN (SELECT id FROM suppliers WHERE region = 'คันไซ');
  </textarea>
  
  <button id="execute-btn">รัน</button>
  
  <div id="result-container">
    <p id="error-message"></p>
    <div id="result-output"></div>
  </div>

  <script>
    const sqlInput = document.getElementById('sql-input');
    const executeBtn = document.getElementById('execute-btn');
    const errorMsg = document.getElementById('error-message');
    const resultOutput = document.getElementById('result-output');

    let db;

    async function initDb() {
      executeBtn.disabled = true;
      executeBtn.textContent = 'กำลังเตรียม DB...';
      try {
        const SQL = await initSqlJs({
          locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`
        });
        db = new SQL.Database();
        
        const setupSql = `
          DROP TABLE IF EXISTS products;
          DROP TABLE IF EXISTS suppliers;

          CREATE TABLE suppliers (id INTEGER PRIMARY KEY, supplier_name TEXT NOT NULL, region TEXT NOT NULL);
          CREATE TABLE products (id INTEGER PRIMARY KEY, product_name TEXT NOT NULL, category TEXT NOT NULL, price INTEGER, supplier_id INTEGER, notes TEXT);

          INSERT INTO suppliers (id, supplier_name, region) VALUES (101, 'Tokyo Office Supply', 'คันโต'), (102, 'Osaka Kitchenware', 'คันไซ'), (103, 'Chiba Food Service', 'คันโต'), (104, 'Kyoto Traditional Stationery', 'คันไซ');
          INSERT INTO products (id, product_name, category, price, supplier_id, notes) VALUES (1, 'ปากกาสุดยอด', 'เครื่องเขียน', 150, 101, 'เขียนลื่น'), (2, 'สมุดโน้ตเวทมนตร์', 'เครื่องเขียน', 300, 104, NULL), (3, 'หม้อหุงข้าวขั้นเทพ', 'เครื่องใช้ไฟฟ้า', 25000, 102, 'สินค้ายอดนิยม'), (4, 'แอปเปิ้ลมหัศจรรย์', 'อาหาร', 500, 103, 'จำนวนจำกัด'), (5, 'ฮับ USB-C', 'อุปกรณ์คอมพิวเตอร์', 4000, 101, NULL), (6, 'พู่กันโบราณ', 'เครื่องเขียน', 1200, 104, 'งานฝีมือช่าง'), (7, 'น้ำผลไม้รวมโอซาก้า', 'อาหาร', 350, NULL, 'ไม่ทราบซัพพลายเออร์');
        `;
        db.run(setupSql);
        
        executeBtn.disabled = false;
        executeBtn.textContent = 'รัน';
        resultOutput.innerHTML = '<p>เตรียมพร้อมแล้ว! ลองใช้ SQL ได้อย่างอิสระเลย</p>';

      } catch (err) {
        errorMsg.textContent = 'การเริ่มต้นฐานข้อมูลล้มเหลว: ' + err.message;
        console.error(err);
      }
    }

    function executeSql() {
      if (!db) return;
      
      const sql = sqlInput.value;
      errorMsg.textContent = '';
      resultOutput.innerHTML = '';

      try {
        const results = db.exec(sql);
        if (results.length === 0) {
          resultOutput.innerHTML = '<p>คำสั่งทำงานสำเร็จ แต่ไม่มีชุดผลลัพธ์ถูกส่งคืน</p>';
          return;
        }
        
        results.forEach(result => {
          const table = document.createElement('table');
          const thead = document.createElement('thead');
          const tbody = document.createElement('tbody');
          
          const headerRow = document.createElement('tr');
          result.columns.forEach(colName => {
            const th = document.createElement('th');
            th.textContent = colName;
            headerRow.appendChild(th);
          });
          thead.appendChild(headerRow);
          
          result.values.forEach(row => {
            const bodyRow = document.createElement('tr');
            row.forEach(cellValue => {
              const td = document.createElement('td');
              td.textContent = cellValue === null ? 'NULL' : cellValue;
              bodyRow.appendChild(td);
            });
            tbody.appendChild(bodyRow);
          });
          
          table.appendChild(thead);
          table.appendChild(tbody);
          resultOutput.appendChild(table);
        });

      } catch (err) {
        errorMsg.textContent = 'SQL ผิดพลาด: ' + err.message;
        console.error(err);
      }
    }

    executeBtn.addEventListener('click', executeSql);
    
    initDb();
  </script>
</body>
</html>

สรุป

ในครั้งนี้ เราได้เรียนรู้เกี่ยวกับ `IN` clause และ `NOT IN` clause ที่ช่วยให้การรวมหลายเงื่อนไขทำได้อย่างชาญฉลาด

`IN` clause เป็นเทคนิคพื้นฐานที่ช่วยให้โค้ดเรียบง่ายและอ่านง่ายขึ้น โดยเฉพาะการใช้ร่วมกับ Subquery ถือเป็นสิ่งที่หลีกเลี่ยงไม่ได้ในการดึงข้อมูลที่มีเงื่อนไขซับซ้อน และความสัมพันธ์ระหว่าง `NOT IN` กับ `NULL` ก็เป็นความรู้สำคัญที่หากทราบจะช่วยลดเวลาในการดีบักได้อย่างมหาศาล ขอให้คุณนำเนื้อหาในครั้งนี้ไปฝึกฝนให้เชี่ยวชาญ เพื่อยกระดับทักษะ SQL ของคุณไปอีกขั้น!