[SQL ระดับกลาง] พิชิต Subquery! คู่มือฉบับสมบูรณ์สำหรับ SELECT ซ้อน SELECT
"อยากรู้ข้อมูลเฉพาะพนักงานที่ได้รับเงินเดือนสูงกว่าเงินเดือนเฉลี่ยของพนักงานทั้งหมด..."
"ใครคือพนักงานที่ได้รับเงินเดือนสูงสุดในแต่ละแผนก?"
ในฐานะเว็บครีเอเตอร์ที่ทำงานกับฐานข้อมูล คุณเคยมีความคิดแบบนี้ไหมครับ "อยากจะใช้ผลลัพธ์ของคิวรีหนึ่ง มาเป็นเงื่อนไขของอีกคิวรีหนึ่ง" การดึงข้อมูลที่ซับซ้อนเล็กน้อยซึ่งไม่สามารถทำได้ในคิวรีเดียว สิ่งที่จะมาช่วยแก้ปัญหานี้ได้อย่างสวยงามก็คือ Subquery (คิวรีย่อย) ที่เราจะมาแนะนำกันในวันนี้
Subquery พูดง่ายๆ ก็คือ "คิวรีซ้อนคิวรี" ครับ เป็นเทคนิคการฝังคำสั่ง SELECT หนึ่งเข้าไปในคำสั่ง SELECT อีกอันหนึ่ง เหมือนกับตุ๊กตาแม่ลูกดกของรัสเซีย (Matryoshka) ถ้าคุณใช้เทคนิคนี้ได้คล่องแล้วล่ะก็ คุณจะสามารถรวมขั้นตอนการดึงข้อมูลที่เคยต้องทำหลายขั้นตอนให้เหลือเพียงคิวรีเดียวได้ และทักษะ SQL ของคุณก็จะเพิ่มขึ้นอย่างก้าวกระโดด!
ในบทความนี้ เราได้เตรียมโค้ดตัวอย่างมากมายที่แม้แต่มือใหม่ก็สามารถคัดลอกไปใช้ได้ทันที เพื่อให้คุณได้สัมผัสกับประสบการณ์ "โค้ดรันได้!" และเข้าใจถึงความทรงพลังและความสะดวกสบายของ Subquery อย่างแท้จริง
การเตรียมตัว: ตารางสำหรับฝึกฝนในครั้งนี้
เพื่อให้ได้สัมผัสกับพลังของ Subquery อย่างเต็มที่ ในครั้งนี้เราจะเตรียมตาราง 2 ตารางคือ ตาราง "พนักงาน (employees)" และ "แผนก (departments)" ครับ ลองคัดลอกโค้ด SQL ด้านล่างแล้วนำไปรันในระบบฐานข้อมูลของคุณได้เลย
แน่นอนว่าถ้ายังไม่มีสภาพแวดล้อมสำหรับทดลองก็ไม่ต้องกังวล! ในช่วงท้ายของบทความ เราได้เตรียม "สนามฝึก SQL" ที่ให้คุณสามารถลองเขียน SQL บนเบราว์เซอร์ได้เลย
-- ลบตารางหากมีอยู่แล้ว
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
-- สร้างตารางแผนก (departments)
CREATE TABLE departments (
id INT PRIMARY KEY,
department_name VARCHAR(50)
);
-- เพิ่มข้อมูลแผนก
INSERT INTO departments (id, department_name) VALUES
(1, '営業部'),
(2, '開発部'),
(3, '人事部');
-- สร้างตารางพนักงาน (employees)
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
department_id INT,
salary INT,
FOREIGN KEY (department_id) REFERENCES departments(id)
);
-- เพิ่มข้อมูลพนักงาน
INSERT INTO employees (id, name, department_id, salary) VALUES
(1, '山田 太郎', 1, 600000), -- แผนกขาย
(2, '鈴木 花子', 2, 550000), -- แผนกพัฒนา
(3, '佐藤 次郎', 1, 400000), -- แผนกขาย
(4, '高橋 三郎', 3, 650000), -- แผนกบุคคล
(5, '田中 恵子', 2, 500000), -- แผนกพัฒนา
(6, '伊藤 四郎', 1, 700000), -- แผนกขาย
(7, '渡辺 久美', 2, 750000), -- แผนกพัฒนา
(8, '山本 五郎', 3, 380000); -- แผนกบุคคล
พร้อมแล้วใช่ไหมครับ? งั้นเรามาเข้าสู่โลกอันทรงพลังของ Subquery กันเลย!
1. พื้นฐานสำคัญ: Subquery ใน WHERE clause
วิธีการใช้ Subquery ที่พื้นฐานและเข้าใจง่ายที่สุดคือการใช้เป็นเงื่อนไขใน WHERE clause ครับ ให้ภาพว่าเรากำลังสร้างเงื่อนไขการกรองของคิวรีด้านนอก (คิวรีหลัก) ขึ้นมาแบบไดนามิกด้วยผลลัพธ์ของคิวรีด้านใน (Subquery)
กฎก็คือ Subquery จะต้องอยู่ภายในเครื่องหมาย () (วงเล็บ) เสมอ SQL จะทำการรัน Subquery ในวงเล็บก่อน แล้วจึงนำผลลัพธ์ที่ได้ไปใช้เป็นเงื่อนไขของคิวรีหลักต่อไป
ตัวอย่างที่ 1: ดึงข้อมูลพนักงานที่มีเงินเดือนสูงกว่าค่าเฉลี่ย
ลองมาหา "พนักงานที่ได้รับเงินเดือนสูงกว่าเงินเดือนเฉลี่ยของพนักงานทั้งหมด" ที่เรายกตัวอย่างไว้ตอนต้นกันครับ การจะทำเช่นนี้ได้ต้องมี 2 ขั้นตอน:
- คำนวณ "เงินเดือนเฉลี่ยของพนักงานทั้งหมด" ก่อน
- จากนั้นใช้เงินเดือนเฉลี่ยนั้นเป็นเงื่อนไขในการดึงข้อมูลพนักงาน
ถ้าใช้ Subquery เราสามารถทำทั้งหมดนี้ได้ในคิวรีเดียว
SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
เป็นอย่างไรบ้างครับ? ขั้นแรก (SELECT AVG(salary) FROM employees) จะถูกรันเพื่อคำนวณเงินเดือนเฉลี่ย (ในตัวอย่างนี้คือ 547500) จากนั้นผลลัพธ์ที่ได้จะถูกนำมาใช้เป็นเงื่อนไขในคิวรีหลัก คือ WHERE salary > 547500 เห็นไหมครับว่าเราสามารถบรรลุเป้าหมายได้ในคิวรีเดียวอย่างสวยงาม
ตัวอย่างที่ 2: ดึงข้อมูลพนักงานในแผนกที่ต้องการ (ร่วมกับ IN)
Subquery ไม่เพียงแต่จะส่งค่ากลับมาเป็นค่าเดียวได้เท่านั้น แต่ยังสามารถส่งกลับมาเป็นรายการของหลายๆ ค่าได้อีกด้วย การนำรายการนั้นมาใช้ร่วมกับตัวดำเนินการ IN จะทำให้สามารถกำหนดเงื่อนไขได้อย่างยืดหยุ่นมาก
ตัวอย่างเช่น สมมติว่าเราต้องการดึงข้อมูลพนักงานทุกคนที่สังกัด "แผนกขาย" หรือ "แผนกบุคคล" เราจะใช้ Subquery เพื่อดึงรายการ ID ของแผนกจากชื่อแผนก แล้วนำไปใช้เป็นเงื่อนไขในคิวรีหลักกันครับ
SELECT *
FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE department_name IN ('営業部', '人事部')
);
ในคิวรีนี้ Subquery จะถูกรันก่อน และส่งคืนรายการ ID ของ '営業部' (แผนกขาย) และ '人事部' (แผนกบุคคล) ซึ่งก็คือ (1, 3) จากนั้นคิวรีหลักจะถูกตีความเหมือนกับ WHERE department_id IN (1, 3) และทำการดึงข้อมูลพนักงานที่เกี่ยวข้องออกมา ซึ่งสะดวกมากเพราะเราไม่จำเป็นต้องแก้ไข SQL แม้ว่าชื่อแผนกจะเปลี่ยนไป
2. ขั้นสูง: Subquery ใน SELECT และ FROM clause
Subquery ไม่ได้มีประโยชน์แค่ใน WHERE clause เท่านั้น แต่ยังสามารถใช้ใน SELECT และ FROM clause ได้อย่างยอดเยี่ยม ซึ่งจะช่วยขยายขอบเขตการวิเคราะห์ข้อมูลของคุณให้กว้างขึ้นมาก
Subquery ใน SELECT clause (Scalar Subquery)
เมื่อใช้ Subquery ใน SELECT clause เราสามารถดึงค่าที่เกี่ยวข้องสำหรับแต่ละแถวของคิวรีหลักมาแสดงเป็นคอลัมน์ใหม่ได้ แต่มีข้อแม้ว่า Subquery นี้จะต้องส่งคืนค่าเป็นค่าเดียว (Scalar value) จาก 1 แถว 1 คอลัมน์เท่านั้น
ตัวอย่างเช่น ลองแสดงข้อมูลของพนักงานแต่ละคน พร้อมกับ "เงินเดือนเฉลี่ยของแผนกที่พนักงานคนนั้นสังกัดอยู่"
SELECT
name,
salary,
(SELECT department_name FROM departments WHERE id = e.department_id) AS department_name,
(SELECT AVG(salary) FROM employees WHERE department_id = e.department_id) AS department_avg_salary
FROM
employees AS e;
อาจจะดูซับซ้อนขึ้นมาหน่อยนะครับ นี่คือสิ่งที่เรียกว่า Correlated Subquery ซึ่ง Subquery (ด้านใน) จะอ้างอิงข้อมูลจากแต่ละแถวของคิวรีหลัก (ด้านนอก) ในที่นี้คือ e.department_id เพื่อทำงาน ทุกครั้งที่คิวรีหลักเลื่อนไปทีละแถว ชื่อแผนกและเงินเดือนเฉลี่ยของแผนกนั้นๆ จะถูกคำนวณโดยใช้ ID แผนกของแถวนั้น แล้วเพิ่มเข้าไปในคอลัมน์ผลลัพธ์
Subquery ใน FROM clause (Inline View)
เมื่อใช้ Subquery ใน FROM clause เราสามารถใช้ผลลัพธ์ของ Subquery นั้นเหมือนกับเป็น "ตารางเสมือนชั่วคราว (Inline View)" ได้ ซึ่งมีประโยชน์มากสำหรับการประมวลผลแบบสองขั้นตอน คือใช้ Subquery เพื่อสรุปหรือประมวลผลข้อมูลก่อน แล้วจึงใช้คิวรีอีกอันหนึ่งมาจัดการกับผลลัพธ์นั้น
ตัวอย่างเช่น ลอง "คำนวณเงินเดือนเฉลี่ยของแต่ละแผนก แล้วแสดงเฉพาะแผนกที่มีเงินเดือนเฉลี่ยตั้งแต่ 550,000 ขึ้นไป"
SELECT
d.department_name,
dept_avg.avg_salary
FROM
(
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) AS dept_avg
JOIN
departments AS d ON dept_avg.department_id = d.id
WHERE
dept_avg.avg_salary >= 550000;
ในคิวรีนี้ Subquery ใน FROM clause จะถูกรันก่อนเพื่อสร้างตารางชั่วคราวชื่อ dept_avg ซึ่งคำนวณเงินเดือนเฉลี่ยของแต่ละ department_id ไว้ จากนั้นคิวรีหลักจะนำตาราง dept_avg นี้ไป JOIN กับตาราง departments เพื่อแสดงชื่อแผนกและเงินเดือนเฉลี่ยที่ตรงตามเงื่อนไข เป็นเทคนิคที่มีประสิทธิภาพมากเมื่อต้องการใช้ WHERE clause กับผลลัพธ์ของ GROUP BY
ลงมือปฏิบัติ! มาลองรัน SQL บนเบราว์เซอร์กันเถอะ
รอมานานแล้วใช่ไหมครับ! นี่คือสภาพแวดล้อมที่คุณสามารถลงมือทดลอง Subquery ที่เรียนมาทั้งหมดได้ด้วยตัวเอง
ลองคัดลอกโค้ดทั้งหมดด้านล่าง แล้วบันทึกเป็นไฟล์ชื่อ subquery_practice.html จากนั้นเปิดไฟล์ด้วยเบราว์เซอร์ของคุณ ลองใช้ Subquery รูปแบบต่างๆ ที่แนะนำในบทความนี้เพื่อสัมผัสการทำงานของมันด้วยตัวเอง!
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<title>สนามฝึก Subquery SQL</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>มาลองใช้ Subquery กันเถอะ!</h1>
<p>ป้อนคำสั่ง SQL ในช่องข้อความด้านล่างแล้วกดปุ่ม "รัน SQL"</p>
<textarea id="sql-input">-- เขียนโค้ด SQL ของคุณที่นี่
SELECT
d.department_name,
dept_avg.avg_salary
FROM
(
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) AS dept_avg
JOIN
departments AS d ON dept_avg.department_id = d.id
WHERE
dept_avg.avg_salary >= 550000;</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 PRIMARY KEY, department_name VARCHAR(50));
INSERT INTO departments VALUES (1, '営業部'), (2, '開発部'), (3, '人事部');
CREATE TABLE employees (id INT PRIMARY KEY, name VARCHAR(50), department_id INT, salary INT);
INSERT INTO employees VALUES
(1, '山田 太郎', 1, 600000), (2, '鈴木 花子', 2, 550000), (3, '佐藤 次郎', 1, 400000),
(4, '高橋 三郎', 3, 650000), (5, '田中 恵子', 2, 500000), (6, '伊藤 四郎', 1, 700000),
(7, '渡辺 久美', 2, 750000), (8, '山本 五郎', 3, 380000);
`);
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];
}
});
return table;
}
// แสดงผลครั้งแรก
executeSQL();
</script>
</body>
</html>
ข้อควรระวังและเทคนิคที่เกี่ยวข้อง
Subquery นั้นทรงพลังมาก แต่ก็มีข้อควรระวังอยู่บ้าง การรู้สิ่งเหล่านี้จะช่วยให้คุณเขียนคิวรีที่ดียิ่งขึ้นได้
ปัญหาด้านประสิทธิภาพ
Subquery โดยเฉพาะอย่างยิ่ง Correlated Subquery ที่ได้แนะนำไปนั้น จะถูกรันในทุกๆ แถวของคิวรีหลัก ซึ่งอาจทำให้ประสิทธิภาพลดลงอย่างมากเมื่อมีข้อมูลจำนวนมาก ในหลายกรณี Subquery สามารถเขียนใหม่โดยใช้ JOIN ซึ่งจะทำงานได้เร็วกว่า
ค่าที่ Subquery ส่งกลับ
รูปแบบของข้อมูลที่ Subquery ส่งกลับมานั้นมีกฎอยู่ครับ
- เมื่อใช้กับตัวดำเนินการเปรียบเทียบเช่น
=,>,<Subquery จะต้องส่งคืนค่าเดียว (Scalar value) เท่านั้น - เมื่อใช้กับตัวดำเนินการ
INสามารถส่งคืนรายการของค่าใน 1 คอลัมน์ได้ - เมื่อใช้ใน
SELECTclause ก็ต้องเป็นค่าเดียวเช่นกัน
การฝ่าฝืนกฎเหล่านี้จะทำให้เกิดข้อผิดพลาด ดังนั้นการตระหนักอยู่เสมอว่า Subquery ของคุณจะส่งผลลัพธ์แบบใดกลับมาจึงเป็นเรื่องสำคัญ
เทคนิคที่เกี่ยวข้อง: การเขียนใหม่ด้วย `JOIN`
จากมุมมองด้านประสิทธิภาพ ขอแนะนำให้เขียน Subquery หลายๆ แบบใหม่โดยใช้ JOIN ครับ ลองมาเขียนคิวรี "ดึงข้อมูลพนักงานในแผนกที่ต้องการ" ที่ผ่านมาด้วย JOIN กัน
SELECT
e.*
FROM
employees AS e
JOIN
departments AS d ON e.department_id = d.id
WHERE
d.department_name IN ('営業部', '人事部');
คิวรีที่ใช้ JOIN นี้จะให้ผลลัพธ์เหมือนกับที่ใช้ Subquery โดยทั่วไปแล้ว ตัวปรับปรุงประสิทธิภาพของฐานข้อมูล (Optimizer) สามารถประมวลผล JOIN ได้อย่างมีประสิทธิภาพมากกว่า ดังนั้นควรพิจารณาใช้ JOIN โดยเฉพาะเมื่อมีข้อมูลจำนวนมาก
สรุป
お疲れ様でした!今回は、SQLの強力なテクニックであるサブクエリについて、その使い方を深く掘り下げてきました。
- Subquery ใน
WHEREclause: วิธีใช้พื้นฐานที่สุดในการสร้างเงื่อนไขของคิวรีแบบไดนามิก - Subquery ใน
SELECTclause: ดึงค่าที่เกี่ยวข้องสำหรับแต่ละแถวและเพิ่มเป็นคอลัมน์ใหม่ (Correlated Subquery) - Subquery ใน
FROMclause: ใช้ผลลัพธ์ของคิวรีเป็นตารางชั่วคราวเพื่อทำการสืบค้นที่ซับซ้อนยิ่งขึ้น - ประสิทธิภาพ: Subquery สะดวกสบาย แต่การพิจารณาเขียนใหม่ด้วย
JOINก็เป็นสิ่งสำคัญเสมอ
เมื่อคุณใช้ Subquery ได้อย่างคล่องแคล่วแล้ว การดึงข้อมูลที่เคยต้องทำหลายครั้งจะเสร็จสิ้นได้ในครั้งเดียว ทำให้ประสิทธิภาพในการทำงานดีขึ้นอย่างก้าวกระโดด อย่าลืมใช้สนามฝึก SQL ที่แนะนำในครั้งนี้เพื่อลองรูปแบบต่างๆ และทำให้ Subquery เป็นของคุณเองนะครับ!