【เริ่มต้นกับ 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 ขั้นตอนได้:
- ขั้นแรก ดึงลิสต์ ID ของซัพพลายเออร์ที่มี `region` เป็น 'คันโต' จากตาราง `suppliers`
- จากนั้น ใช้ลิสต์ 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: เปลี่ยนเงื่อนไขที่ซ้ำซ้อนอย่าง `WHERE A = 1 OR A = 2 OR A = 3` ให้กลายเป็น `WHERE A IN (1, 2, 3)` ที่กระชับและอ่านง่ายขึ้นอย่างมาก
- `NOT IN` Clause: ทำงานตรงกันข้ามกับ `IN` เพื่อดึงข้อมูลที่ไม่รวมอยู่ในลิสต์
- การทำงานร่วมกับ Subquery: สามารถสร้างลิสต์ของ `IN` แบบไดนามิกจากผลลัพธ์ของคำสั่ง `SELECT` อื่นได้ ซึ่งทรงพลังอย่างยิ่ง
- กับดัก `NULL`: ข้อควรระวังที่สำคัญคือ หากลิสต์ของ `NOT IN` มีค่า `NULL` รวมอยู่ ผลลัพธ์จะกลายเป็น 0 แถว ต้องระมัดระวังเป็นพิเศษเมื่อใช้กับ Subquery
`IN` clause เป็นเทคนิคพื้นฐานที่ช่วยให้โค้ดเรียบง่ายและอ่านง่ายขึ้น โดยเฉพาะการใช้ร่วมกับ Subquery ถือเป็นสิ่งที่หลีกเลี่ยงไม่ได้ในการดึงข้อมูลที่มีเงื่อนไขซับซ้อน และความสัมพันธ์ระหว่าง `NOT IN` กับ `NULL` ก็เป็นความรู้สำคัญที่หากทราบจะช่วยลดเวลาในการดีบักได้อย่างมหาศาล ขอให้คุณนำเนื้อหาในครั้งนี้ไปฝึกฝนให้เชี่ยวชาญ เพื่อยกระดับทักษะ SQL ของคุณไปอีกขั้น!