SQL Part 2 — Predicates & JOIN
เงื่อนไขใน WHERE ทั้งหมด · INNER / LEFT / RIGHT / FULL OUTER JOIN · self join
วิดีโอที่อาจารย์ให้ดูก่อนเรียนบทนี้:
6.1เงื่อนไขทั้งหมดใน WHERE clause
บทนี้รวบรวม predicate ทุกแบบที่ใช้ได้ใน WHERE — จำให้ครบเพราะข้อสอบเติมโค้ดชอบเว้นตรงนี้
subquery = คำสั่ง SELECT อีกอันที่เขียนไว้ในวงเล็บ ซ้อนอยู่ข้างใน query หลัก
ฐานข้อมูลจะรัน subquery ให้เสร็จก่อน ได้ออกมาเป็นชุดของค่า แล้ว query ข้างนอกค่อยเอาชุดนั้นไปเปรียบเทียบ
SELECT * FROM employees
WHERE department IN ( SELECT department_name FROM departments
WHERE location = 'New York' );
-- ขั้นที่ 1: subquery รันก่อน ได้ {'IT'} (ดูตาราง departments ใน 6.2 — IT อยู่ New York)
-- ขั้นที่ 2: กลายเป็น WHERE department IN ('IT')
ตาราง departments ที่อ้างถึงอยู่ที่ หัวข้อ 6.2 —
มี 4 แผนก: IT (New York) · HR (Chicago) · Marketing (Los Angeles) · Finance (Boston)แค่นี้พอสำหรับบทนี้ · รายละเอียดเต็ม ๆ อยู่บทที่ 8
| Predicate | ความหมาย | ตัวอย่าง |
|---|---|---|
| 1. Comparison | = <> หรือ != < > <= >= | salary >= 70000 |
| 2. BETWEEN | เลือกค่าในช่วงที่กำหนด รวมปลายทั้งสองข้าง | hire_date BETWEEN '2019-01-01' AND '2020-12-31' |
| 3. IN | ระบุหลายค่าใน WHERE clause | country IN ('USA','UK','Canada') |
| 4. LIKE | จับคู่ pattern · % = 0/1/หลายตัว · _ = 1 ตัว | name LIKE 'John%' |
| 5. IS NULL / IS NOT NULL | ตรวจค่าว่าง — ห้ามใช้ = NULL | shipping_date IS NULL |
| 6. AND OR NOT | ตัวดำเนินการตรรกะรวมหลายเงื่อนไข | category='Electronics' AND (price<500 OR stock>100) |
| 7. EXISTS | ตรวจว่ามีแถวที่ตรงเงื่อนไขของ subquery หรือไม่ | EXISTS (SELECT 1 FROM employees e WHERE e.department = d.department_name) |
| 8. ANY / SOME | จริงถ้าค่าใดค่าหนึ่งใน subquery ทำให้เงื่อนไขเป็นจริง | price > ANY (SELECT price FROM products WHERE category='Books') |
| 9. ALL | จริงถ้าทุกค่าใน subquery ทำให้เงื่อนไขเป็นจริง | salary > ALL (SELECT salary FROM employees WHERE department = 'HR') |
| 10. REGEXP / SIMILAR TO | จับคู่ด้วย regular expression — ขึ้นกับระบบฐานข้อมูล อ่าน pattern: ^ = ต้องอยู่ต้นสตริง · (a|b) = a หรือ b · ข้อสอบเน้น LIKE ตัวนี้แค่รู้จักไว้เทียบ | name REGEXP '^Apple (iPhone|iPad)' |
% และ _) — ใช้ได้ทุกระบบREGEXP ไม่ใช่ standard SQL แต่หลายระบบรองรับ (MySQL, PostgreSQL) ใช้ไวยากรณ์ regular expression เต็มรูปแบบ
SIMILAR TO รองรับน้อยที่สุด (มีใน PostgreSQL)
โจทย์ฝึกเขียน predicate จากสไลด์
ข้อฝึกชุดนี้ใช้ตารางสมมติของตัวเอง ไม่ใช่ฐานข้อมูล HR ในหัวข้อ 6.2 — เพื่อไม่ให้สับสน จะเรียกว่า:
emp_demo(id, name, department, salary, hire_date) ·
prod_demo(id, name, category, price, stock) ·
order_demo(id, customer_id, order_date, total_amount)
สังเกตว่าที่นี่มีคอลัมน์ name ช่องเดียว แต่ตาราง employees จริงในหัวข้อ 6.2 ใช้
first_name / last_name แยกกัน — เวลาสอบให้ดูชื่อคอลัมน์จากโจทย์เสมอ
| CURDATE() | วันที่วันนี้ |
| DATE_SUB(d, INTERVAL 5 YEAR) | เอาวันที่ d ลบไป 5 ปี · เปลี่ยน YEAR เป็น DAY, MONTH ได้ |
| DATEDIFF(a, b) | ผลต่างระหว่างสองวันที่ เป็นจำนวนวัน |
| DAYOFWEEK(d) | MySQL: 1 = อาทิตย์ … 7 = เสาร์ |
1. พนักงานที่ชื่อขึ้นต้นด้วย 'J' และอยู่กับบริษัทมาเกิน 5 ปี
SELECT * FROM emp_demo
WHERE name LIKE 'J%'
AND hire_date < DATE_SUB(CURDATE(), INTERVAL 5 YEAR);
หรือใช้ DATEDIFF(CURDATE(), hire_date) > 365*5 ก็ได้ แต่แบบ INTERVAL อ่านง่ายและถูกต้องเรื่องปีอธิกสุรทินมากกว่า
2. สินค้าที่ชื่อมีคำว่า 'phone' (ไม่สนตัวพิมพ์) และของหมดสต๊อก
SELECT * FROM prod_demo
WHERE LOWER(name) LIKE '%phone%'
AND stock = 0;
ทำไมต้อง LOWER() — pattern ของ LIKE เป็น case-sensitive จึงต้องแปลงให้เป็นตัวพิมพ์เล็กทั้งคู่ก่อนเทียบ (บาง collation ของ MySQL ไม่สนตัวพิมพ์อยู่แล้ว แต่เขียนแบบนี้ปลอดภัยกว่า)
3. ออเดอร์ใน 400 วันล่าสุดที่ยอดเกิน 1000
SELECT * FROM order_demo
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 400 DAY)
AND total_amount > 1000;4. ออเดอร์ที่สั่งในวันเสาร์อาทิตย์
SELECT * FROM order_demo
WHERE DAYOFWEEK(order_date) IN (1, 7); -- MySQL: 1 = อาทิตย์, 7 = เสาร์
หรือ WHERE WEEKDAY(order_date) IN (5, 6) (MySQL: 0 = จันทร์ … 6 = อาทิตย์)
6.2JOIN — 4 ชนิดที่ต้องแยกออก
| ชนิด | คืนอะไร |
|---|---|
| INNER JOIN | คืนเฉพาะแถวที่จับคู่ได้จากทั้งสองตาราง |
| LEFT (OUTER) JOIN | คืนทุกแถวจากตารางซ้าย พร้อมแถวที่ตรงกันจากตารางขวา (ถ้าไม่มีคู่ → NULL) |
| RIGHT (OUTER) JOIN | คืนทุกแถวจากตารางขวา พร้อมแถวที่ตรงกันจากตารางซ้าย |
| FULL (OUTER) JOIN | คืนทุกแถวจากทั้งสองตาราง — แถวที่ไม่มีคู่จะได้ NULL ในฝั่งที่ขาด |
SELECT * FROM a LEFT JOIN b ON a.id = b.a_id
UNION
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
ทำไมต้อง UNION ไม่ใช่ UNION ALL — เพราะแถวที่จับคู่ได้จะปรากฏในทั้งสอง query · UNION ลบแถวซ้ำให้ ส่วน UNION ALL จะได้แถวซ้ำ
4 ประเด็นสำคัญ (Key points จากสไลด์)
- INNER JOIN แสดงเฉพาะเรคอร์ดที่ตรงกันระหว่างตาราง
- LEFT JOIN รวมทุกเรคอร์ดจากตารางซ้าย แม้ไม่มีคู่ที่ตรงกัน
- RIGHT JOIN รวมทุกเรคอร์ดจากตารางขวา แม้ไม่มีคู่ที่ตรงกัน
- FULL OUTER JOIN (จำลอง) รวมทุกเรคอร์ดจากทั้งสองตาราง
ตัวอย่างระบบห้องสมุด
ใช้ข้อมูลชุดนี้อ่านทั้งสี่การ์ดด้านล่าง — สังเกตว่ามีหนังสือที่ไม่เคยถูกยืม (B3) และผู้ยืมที่ไม่เคยยืม (Charlie) เตรียมไว้ให้เห็นผลของ outer join
| book_id | title |
|---|---|
| B1 | Database Systems |
| B2 | Operating Systems |
| B3 | Compilers |
B3 ไม่เคยถูกยืม
| borrower_id | name |
|---|---|
| 1 | Alice Smith |
| 2 | Bob Jones |
| 3 | Charlie Brown |
Charlie ไม่เคยยืมอะไร
| loan_id | book_id | borrower_id | loan_date |
|---|---|---|---|
| L1 | B1 | 1 | 2026-07-01 |
| L2 | B2 | 2 | 2026-07-05 |
| L3 | B1 | 2 | 2026-08-02 |
SELECT b.title, br.name, l.loan_date
FROM loans l
JOIN books b ON l.book_id = b.book_id
JOIN borrowers br ON l.borrower_id = br.borrower_id;
INNER JOIN — แสดงเฉพาะหนังสือที่ถูกยืมแล้ว · ผู้ยืมที่ยังไม่เคยยืม และหนังสือที่ยังไม่เคยถูกยืม จะไม่ปรากฏ
SELECT b.title, l.loan_date, br.name
FROM books b
LEFT JOIN loans l ON b.book_id = l.book_id
LEFT JOIN borrowers br ON l.borrower_id = br.borrower_id;
LEFT JOIN — รวมหนังสือทุกเล่ม เล่มที่ไม่เคยถูกยืมจะแสดง NULL ในคอลัมน์ loan_date และ name (ชื่อผู้ยืม) · ใช้ดูว่าเล่มไหนว่างหรือไม่เคยมีคนยืม
SELECT br.name, b.title, l.loan_date
FROM borrowers br
LEFT JOIN loans l ON br.borrower_id = l.borrower_id
LEFT JOIN books b ON l.book_id = b.book_id;
รวมผู้ยืมทุกคนแม้ยังไม่เคยยืม (เช่น Charlie Brown) โดยแสดง NULL ในคอลัมน์ title และ loan_date
SELECT b.title, br.name
FROM books b
CROSS JOIN borrowers br;
CROSS JOIN = Cartesian product · ได้ (จำนวนหนังสือ × จำนวนผู้ยืม) แถว
ฐานข้อมูล HR ที่ใช้ตลอดบทนี้และบทที่ 7–8
เฉลยทุกข้อตั้งแต่หัวข้อ 6.3 เป็นต้นไป และบทที่ 7–8 อ้างข้อมูลชุดนี้ · ชื่อคอลัมน์ในตารางนี้คือชื่อจริงที่ใช้ใน query
อ่านก่อน:
ชุดข้อมูลฐานคือ พนักงาน 8 คน (id 1–8) และ 4 แผนก — ตัวเลขในเฉลยส่วนใหญ่ (เช่น COUNT(*) = 8, COUNT(DISTINCT department) = 4) มาจากชุดนี้
แถวสีเหลือง 2 แถว (Nina id 9 และแผนก Legal id 5) เพิ่มเข้ามาเฉพาะในแบบฝึกหัด Part 2 & 3 เพื่อทดสอบเรื่อง outer join กับ NULL —
ใช้เมื่อโจทย์พูดถึงมันเท่านั้น
| employee_id | first_name | last_name | department | salary | hire_date | manager_id |
|---|---|---|---|---|---|---|
| 1 | John | Doe | IT | 75000 | 2020-01-15 | NULL |
| 2 | Jane | Smith | HR | 65000 | 2019-05-11 | 1 |
| 3 | Mike | Johnson | Marketing | 70000 | 2021-03-20 | 1 |
| 4 | Emily | Brown | IT | 78000 | 2018-11-07 | 1 |
| 5 | David | Lee | Finance | 80000 | 2017-08-30 | 2 |
| 6 | Sarah | Wilson | HR | 62000 | 2022-02-14 | 2 |
| 7 | Chris | Anderson | Marketing | 69000 | 2020-07-22 | 3 |
| 8 | Lisa | Taylor | IT | 72000 | 2019-09-01 | 4 |
| 9 | Nina | Patel | Research | 60000 | 2023-06-01 | NULL |
John เป็นคนเดียวที่ manager_id เป็น NULL — เขาไม่มีหัวหน้า
แถวสีเหลือง (Nina) เพิ่มเข้ามาในแบบฝึกหัด Part 2 & 3 — แผนก Research ของเธอไม่มีในตาราง departments
| department_id | department_name | location |
|---|---|---|
| 1 | IT | New York |
| 2 | HR | Chicago |
| 3 | Marketing | Los Angeles |
| 4 | Finance | Boston |
| 5 | Legal | Seattle |
แถวสีเหลือง (Legal) เพิ่มในแบบฝึกหัด Part 2 & 3 — เป็นแผนกที่ไม่มีพนักงานสักคน
projects| project_id | project_name | start_date | department_id |
|---|---|---|---|
| 1 | Website Redesign | 2023-01-01 | 1 |
| 2 | Employee Training | 2023-03-15 | 2 |
| 3 | Product Launch | 2023-05-01 | 3 |
| 4 | Financial Upgrade | 2023-02-01 | 4 |
| 5 | Mobile App | 2023-04-01 | 1 |
| employee_id | project_id | role |
|---|---|---|
| 1 | 1 | Project Manager |
| 4 | 1 | Developer |
| 8 | 1 | Designer |
| 2 | 2 | Trainer |
| 6 | 2 | Coordinator |
| 3 | 3 | Campaign Manager |
| 7 | 3 | Content Creator |
| 5 | 4 | Financial Analyst |
| 1 | 5 | Technical Lead |
| 4 | 5 | Developer |
employees.department เก็บเป็นชื่อแผนก (ข้อความ) ไม่ใช่ id — เวลา join กับ departments จึงต้องเชื่อม
e.department = d.department_name ไม่ใช่เชื่อมด้วย id2. แต่
projects.department_id เก็บเป็นตัวเลข — เชื่อมกับ departments.department_id3. สองแถวสีเหลือง (Legal และ Nina) มีเฉพาะในแบบฝึกหัด Part 2 & 3 เพื่อเปิดเผยด้าน NULL ของ outer join:
• Legal = แผนกที่ไม่มีพนักงาน → โผล่เฉพาะเมื่อใช้
RIGHT JOIN จาก employees (หรือ LEFT JOIN จาก departments)• Nina = พนักงานที่แผนกไม่มีในตาราง departments → หายไปทันทีถ้าใช้
INNER JOINถ้าโจทย์ไม่ได้พูดถึงสองแถวนี้ ให้ใช้ข้อมูล 8 คน / 4 แผนกเดิม
6.3Self Join — join ตารางกับตัวเอง
ใช้เมื่อความสัมพันธ์อยู่ภายในตารางเดียวกัน เช่น พนักงานมีหัวหน้าซึ่งก็เป็นพนักงานเหมือนกัน
SELECT e1.first_name, e1.last_name, e1.salary AS employee_salary,
e2.first_name AS manager_first_name,
e2.last_name AS manager_last_name,
e2.salary AS manager_salary
FROM employees e1
JOIN employees e2 ON e1.manager_id = e2.employee_id
WHERE e1.salary > e2.salary;
เคล็ดลับ: ตั้ง alias สองชื่อให้ตารางเดียวกัน (e1 = ลูกน้อง, e2 = หัวหน้า) แล้วเชื่อมด้วย
e1.manager_id = e2.employee_id · ถ้าใช้ INNER JOIN คนที่ไม่มีหัวหน้า (manager_id = NULL) จะหายไป ซึ่งถูกต้องแล้วในโจทย์นี้
6.4ตะลุยโจทย์ Part 2 — พร้อมเฉลย
ใช้ข้อมูลชุดเดียวกับหัวข้อ 6.2 ด้านบน — เลื่อนกลับขึ้นไปดูตารางได้ตลอด
1–5 · WHERE predicates
-- 1. เงินเดือนเกิน 70,000
SELECT * FROM employees WHERE salary > 70000;
-- 2. เข้าทำงานระหว่าง 1 ม.ค. 2019 ถึง 31 ธ.ค. 2020
SELECT * FROM employees
WHERE hire_date BETWEEN '2019-01-01' AND '2020-12-31';
-- 3. อยู่แผนก IT หรือ Finance
SELECT * FROM employees WHERE department IN ('IT','Finance');
-- 4. นามสกุลลงท้ายด้วย 'son'
SELECT * FROM employees WHERE last_name LIKE '%son';
-- 5. IT ที่เงินเดือน > 70000 หรือ HR ที่เงินเดือน < 65000
SELECT * FROM employees
WHERE (department = 'IT' AND salary > 70000)
OR (department = 'HR' AND salary < 65000);
ข้อ 5 ต้องมีวงเล็บ — ถ้าเขียน department='IT' AND salary>70000 OR department='HR' AND salary<65000
โดยไม่ใส่วงเล็บ ก็ยังได้ผลถูกเพราะ AND มี precedence สูงกว่า OR แต่ใส่วงเล็บเสมอเพื่อความชัดเจนและกันพลาด
6–8 · JOIN
-- 6. พนักงานพร้อมสถานที่ตั้งของแผนก
SELECT e.employee_id, e.first_name, e.last_name,
d.department_name, d.location
FROM employees e
INNER JOIN departments d ON e.department = d.department_name;
-- 7. ทุกแผนกพร้อมพนักงานในแผนก (ถ้ามี)
SELECT d.department_name, e.first_name, e.last_name
FROM departments d
LEFT JOIN employees e ON d.department_name = e.department;
-- 8. พนักงานทุกคน + สถานที่แผนก + โปรเจกต์ที่ทำ (ถ้ามี)
SELECT e.first_name, e.last_name, d.location, p.project_name
FROM employees e
LEFT JOIN departments d ON e.department = d.department_name
LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id
LEFT JOIN projects p ON ep.project_id = p.project_id;
ข้อ 7 ทำไมต้องเป็น LEFT JOIN จาก departments — โจทย์บอกว่า “ทุกแผนก … (ถ้ามี)” แปลว่าแผนกที่ไม่มีพนักงานก็ต้องแสดง ถ้าใช้ INNER JOIN แผนก Legal จะหายไป
ข้อ 8 ต้องเป็น LEFT JOIN ทั้งสามที่ — โจทย์บอก “พนักงานทุกคน” จึงห้ามมีใครหายไปเลย
• LEFT JOIN departments — เพราะ Nina อยู่แผนก Research ซึ่งไม่มีในตาราง departments
ถ้าใช้ INNER JOIN เธอจะหายไปทันที ทั้งที่โจทย์ขอ “ทุกคน”
• LEFT JOIN employee_projects / projects — เพราะโจทย์บอก “ถ้ามี” คนที่ไม่มีโปรเจกต์ก็ต้องแสดง
ลำดับสำคัญ: เมื่อ LEFT JOIN ตารางแรกแล้ว ต้อง LEFT JOIN ตารางถัดไปด้วย ไม่งั้น INNER JOIN ตัวหลังจะไปตัดแถว NULL ทิ้งจนความพยายามข้างหน้าสูญเปล่า
9–13 · Self join, EXISTS, ANY, ALL
-- 9. พนักงานที่เงินเดือนสูงกว่าหัวหน้าตัวเอง
SELECT e1.first_name, e1.last_name, e1.salary AS employee_salary,
e2.first_name AS manager_first_name, e2.salary AS manager_salary
FROM employees e1
JOIN employees e2 ON e1.manager_id = e2.employee_id
WHERE e1.salary > e2.salary;
-- 10. พนักงานที่ไม่อยู่ IT และไม่ได้เข้าทำงานก่อนปี 2019
SELECT * FROM employees
WHERE NOT (department = 'IT' OR hire_date < '2019-01-01');
-- 11. พนักงานที่เป็นหัวหน้า (มีอย่างน้อย 1 คนรายงานตรง)
SELECT * FROM employees e
WHERE EXISTS (SELECT 1 FROM employees r
WHERE r.manager_id = e.employee_id);
-- 12. เงินเดือนสูงกว่าอย่างน้อยหนึ่งคนใน HR
SELECT * FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE department = 'HR');
-- 13. เงินเดือนสูงกว่าทุกคนใน HR
SELECT * FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE department = 'HR');
ข้อ 11 อ่านยังไง — EXISTS ทำงานทีละแถว
e คือชื่อเรียกแถวที่ query ข้างนอกกำลังพิจารณาอยู่ · สำหรับแต่ละแถว e
ฐานข้อมูลจะรัน query ข้างในใหม่หนึ่งรอบ เพื่อถามว่า “มีพนักงานคนไหนที่ manager_id เท่ากับ e.employee_id ไหม”
ถ้าเจออย่างน้อยหนึ่งแถว EXISTS เป็นจริง → แถว e ถูกเก็บไว้ · ที่เขียน SELECT 1 เพราะสนแค่ว่า “มีไหม” ไม่สนว่าค่าคืออะไร
แบบนี้เรียกว่า correlated subquery (query ข้างในอ้างถึงตัวแปรจากข้างนอก) — เรียนเต็ม ๆ บทที่ 8
ข้อ 12 กับ 13 คือคู่ที่ออกสอบบ่อยที่สุด
HR มีเงินเดือน 65,000 (Jane) และ 62,000 (Sarah)
> ANY = มากกว่าค่าน้อยสุด = > 62,000 → ได้ทุกคนยกเว้น Sarah
> ALL = มากกว่าค่ามากสุด = > 65,000 → ได้ John, Mike, Emily, David, Chris, Lisa
จำง่าย ๆ: ANY → MIN · ALL → MAX
ข้อ 10 กระจาย NOT ด้วยกฎเดอมอร์แกน:
NOT (A OR B) ≡ NOT A AND NOT B = department <> 'IT' AND hire_date >= '2019-01-01'
แต่ระวัง! ถ้ามีแถวที่ department เป็น NULL ทั้งสองรูปแบบจะตัดแถวนั้นทิ้ง เพราะได้ผลเป็น UNKNOWN (ดูบทที่ 7)
A LEFT JOIN B จะได้กี่แถว (สมมติจับคู่แบบ 1 ต่อ 1)ถ้าเป็น INNER JOIN จะได้ 2 แถว · ถ้าเป็น CROSS JOIN จะได้ 9 แถว
FROM employees e RIGHT JOIN departments d ก็ได้ผลเหมือนกัน แต่แบบ LEFT อ่านง่ายกว่า