Intermediate SQL — Join Expressions & Views
natural join vs inner join · outer join ทั้งสามแบบ · CREATE VIEW · view ที่อัปเดตได้ · materialized view
วิดีโอที่อาจารย์ให้ดูก่อนเรียนบทนี้:
9.1Joined Relations
Join operation รับ relation สองตัวเข้าไป แล้วคืน relation ออกมาอีกตัวหนึ่ง
join คือ Cartesian product ที่บังคับให้ tuple จากทั้งสอง relation ต้องตรงกัน ภายใต้เงื่อนไขบางอย่าง และยังกำหนดด้วยว่า attribute ตัวไหนจะปรากฏในผลลัพธ์ · join operation มักถูกใช้เป็น subquery expression ใน from clause
| คำ | ความหมาย |
|---|---|
| Join condition | กำหนดว่า tuple ไหนใน relation ทั้งสองตรงกัน และ attribute ไหนจะอยู่ในผลลัพธ์ |
| Join type | กำหนดว่า tuple ที่ไม่ตรงกับใครเลยจะถูกจัดการอย่างไร |
Join มี 3 กลุ่ม: Natural Join · Inner Join · Outer Join
instructor และ teaches ให้ดู) —
section มีคอลัมน์เพิ่มอีก 2 ตัว ที่จะใช้บ่อยมากในเรื่อง view:
instructor (ID, name, dept_name, salary)
department (dept_name, building, budget)
course (course_id, title, dept_name, credits)
section (course_id, sec_id, semester, year, building, room_number) ← 2 คอลัมน์ท้ายเพิ่มมาในบทนี้
teaches (ID, course_id, sec_id, semester, year)
ระวังจุดที่สับสนที่สุดของบทนี้: คำว่า building โผล่สองที่ —
department.building = ตึกที่ภาควิชาตั้งอยู่ · section.building = ตึกที่ห้องเรียนอยู่
ทั้งสองไม่จำเป็นต้องเป็นตึกเดียวกัน
9.2Natural Join
Natural join จับคู่ tuple ที่มีค่าเท่ากันใน attribute ร่วมทุกตัว และเก็บสำเนาของคอลัมน์ร่วมไว้เพียงชุดเดียว
“attribute ร่วม” = คอลัมน์ที่ชื่อเหมือนกันในทั้งสองตาราง — ระบบหาให้เองโดยเราไม่ต้องเขียน ON
| ID | name | dept_name | salary |
|---|---|---|---|
| 10101 | Srinivasan | Comp. Sci. | 65000 |
| 12121 | Wu | Finance | 90000 |
| 15151 | Mozart | Music | 40000 |
| dept_name | building | budget |
|---|---|---|
| Comp. Sci. | Taylor | 100000 |
| Finance | Painter | 120000 |
| Music | Packard | 80000 |
-- แสดงชื่ออาจารย์พร้อมตึกของภาควิชา
-- คอลัมน์ร่วมคือ dept_name → ระบบจับคู่ให้เองโดยไม่ต้องเขียน ON
SELECT name, dept_name, building
FROM instructor NATURAL JOIN department;
| name | dept_name | building |
|---|---|---|
| Srinivasan | Comp. Sci. | Taylor |
| Wu | Finance | Painter |
| Mozart | Music | Packard |
dept_name ปรากฏในผลลัพธ์แค่คอลัมน์เดียว ทั้งที่มาจากสองตาราง — นี่คือสิ่งที่ทำให้ natural join ต่างจาก inner join
ต่อกันหลายตารางได้ โดยเขียนเรียงกันไปเรื่อย ๆ:
SELECT A₁, A₂, … Aₙ
FROM r₁ NATURAL JOIN r₂ NATURAL JOIN … NATURAL JOIN rₙ
WHERE P;
-- ชื่ออาจารย์ + ตึกภาค + วิชาที่สอน (คอลัมน์ร่วมกับ teaches คือ ID)
SELECT name, building, course_id
FROM instructor NATURAL JOIN department NATURAL JOIN teaches;
SELECT name, building, course_id
FROM instructor NATURAL JOIN department JOIN teaches;
ใน MySQL JOIN ที่ไม่มี ON ถือเป็น CROSS JOIN —
ระบบไม่ฟ้อง error แต่จับคู่ทุกแถวกับทุกแถว ผลลัพธ์ระเบิดและเต็มไปด้วยแถวขยะ
ซึ่งหายากกว่า error มาก เพราะไม่มีอะไรเตือน (มาตรฐาน SQL จะฟ้อง)ถูก — เขียน
ON ให้ครบทุก JOIN
SELECT name, building, course_id
FROM instructor NATURAL JOIN department
JOIN teaches ON instructor.ID = teaches.ID;
กฎง่าย ๆ: NATURAL JOIN = ห้ามมี ON (ระบบหาให้แล้ว) ·
JOIN / INNER JOIN ที่ไม่มี ON = กลายเป็น cross join เงียบ ๆ ·
LEFT JOIN / RIGHT JOIN ที่ไม่มี ON = MySQL ฟ้อง error จริงสรุป: เขียน ON ทุกครั้ง
| NATURAL JOIN | INNER JOIN … ON | |
|---|---|---|
| เงื่อนไขจับคู่ | อัตโนมัติ จากชื่อคอลัมน์ที่เหมือนกันทุกตัว | เขียนเองใน ON |
| คอลัมน์ร่วมในผลลัพธ์ | เหลือชุดเดียว | ปรากฏสองครั้ง (จากทั้งสองตาราง) |
| ความเสี่ยง | สูง — ถ้าบังเอิญมีคอลัมน์ชื่อซ้ำที่ไม่ควรจับคู่กัน (เช่น name, id, created_at) ผลลัพธ์จะผิดโดยไม่มี error | ต่ำ — ควบคุมได้เอง |
ข้อมูล course / section / teaches ที่แบบฝึกหัด 9.6 ใช้
โจทย์ข้อ 2, 4, 5, 7 ถามถึงตึก Taylor และภาระงานสอน — ใช้ข้อมูลชุดนี้ตรวจคำตอบได้
| course_id | title | dept_name |
|---|---|---|
| CS-101 | Intro to CS | Comp. Sci. |
| CS-347 | Database Sys. | Comp. Sci. |
| FIN-201 | Investments | Finance |
| course_id | sec_id | sem | year | building | room |
|---|---|---|---|---|---|
| CS-101 | 1 | Fall | 2017 | Taylor | 3128 |
| CS-347 | 1 | Fall | 2017 | Taylor | 3128 |
| FIN-201 | 1 | Spring | 2018 | Painter | 514 |
| ID | course_id | sec_id | sem | year |
|---|---|---|---|---|
| 10101 | CS-101 | 1 | Fall | 2017 |
| 10101 | CS-347 | 1 | Fall | 2017 |
| 12121 | FIN-201 | 1 | Spring | 2018 |
Mozart (15151) ไม่มีแถวเลย → ข้อ 5 ต้องได้ course_count = 0
ตอบข้อ 2 ได้ทันที: section ที่ building='Taylor' คือ CS-101 และ CS-347 ทั้งคู่สอนโดย Srinivasan
→ ผลลัพธ์ = (Intro to CS, Srinivasan), (Database Sys., Srinivasan)
ข้อ 7: อาจารย์ที่สอนที่ Taylor มีคนเดียวคือ Srinivasan (65,000) → AVG = 65,000
· ถ้าลืมใส่ DISTINCT เงินเดือนเขาจะถูกนับ 2 ครั้ง (เพราะสอน 2 วิชา) — ค่าเฉลี่ยยังได้ 65,000 ในกรณีนี้
แต่ถ้ามีอาจารย์คนที่สองที่สอนวิชาเดียว ค่าเฉลี่ยจะเพี้ยนทันที
9.3Inner Join
Inner join คืนเฉพาะแถวที่ตรงกันจากทั้งสองตารางตาม join condition
SELECT * FROM table1 INNER JOIN table2
ON table1.column = table2.column;
- ใช้เมื่อ ต้องการดึงเฉพาะข้อมูลที่มีคู่ที่ตรงกันในทั้งสองตาราง
- ประเด็นสำคัญ แถวที่ไม่มีคู่ในตารางใดตารางหนึ่งจะถูกตัดออกจากผลลัพธ์
9.4Outer Join
Outer join เป็นส่วนขยายของ join ที่เลี่ยงการสูญเสียข้อมูล — คำนวณ join ตามปกติ แล้วเติม tuple จาก relation หนึ่งที่จับคู่กับ relation อีกตัวไม่ได้ กลับเข้าไปในผลลัพธ์ โดยใช้ค่า null เติมช่องว่าง
เก็บทุกแถวจากตารางซ้าย · ที่ไม่มีคู่ → คอลัมน์ฝั่งขวาเป็น NULL
เก็บทุกแถวจากตารางขวา · ที่ไม่มีคู่ → คอลัมน์ฝั่งซ้ายเป็น NULL
เก็บทุกแถวจากทั้งสองฝั่ง
MySQL ไม่มี ต้องใช้ LEFT UNION RIGHT
9.5Views — มุมมองเสมือน
logical model / conceptual model = ภาพของตารางจริงทั้งหมดที่เก็บอยู่ในฐานข้อมูล พร้อมคอลัมน์และความสัมพันธ์ — เป็นคำที่ใช้แทนกันได้ในบทนี้ (ทบทวนสามระดับ physical / logical / view ได้ที่ บทที่ 1)
บางกรณีเราไม่ต้องการให้ผู้ใช้ทุกคนเห็น logical model ทั้งหมด ลองนึกถึงคนที่จำเป็นต้องรู้ชื่อและภาควิชาของอาจารย์ แต่ไม่ควรเห็นเงินเดือน คนคนนี้ควรเห็นแค่
SELECT ID, name, dept_name FROM instructor;
View คือกลไกที่ซ่อนข้อมูลบางส่วนจากสายตาผู้ใช้บางกลุ่ม · relation ใดก็ตามที่ไม่ได้อยู่ใน conceptual model แต่ถูกทำให้ผู้ใช้มองเห็นในฐานะ “virtual relation” เรียกว่า view
การนิยาม view
CREATE VIEW v AS <query expression>;
-- view ของอาจารย์ที่ไม่มีเงินเดือน
CREATE VIEW faculty AS
SELECT ID, name, dept_name FROM instructor;
-- ใช้ชื่อ view เหมือนตารางธรรมดา
SELECT name FROM faculty WHERE dept_name = 'Biology';
-- view ที่มีการรวมยอด และตั้งชื่อคอลัมน์เอง
CREATE VIEW departments_total_salary(dept_name, total_salary) AS
SELECT dept_name, SUM(salary)
FROM instructor
GROUP BY dept_name;
view เป็นการบันทึกนิพจน์ (expression) เอาไว้ · เมื่อมี query ที่ใช้ view นั้น ระบบจะแทนนิพจน์นั้นเข้าไปใน query แล้วรันใหม่กับข้อมูลปัจจุบัน
นี่คือเหตุผลที่ view สดเสมอ — ข้อมูลในตารางฐานเปลี่ยน view ก็เปลี่ยนตามทันที
View ที่นิยามจาก view อื่น
view หนึ่งสามารถถูกใช้ในนิพจน์ที่นิยาม view อีกตัวได้ — เหมือนต่อเลโก้ทีละชั้น
- view
v1depends directly บนv2ถ้าv2ถูกใช้ในนิพจน์ที่นิยามv1 - view
v1depends บนv2ถ้า depends directly หรือมีเส้นทางของการพึ่งพาจากv1ไปv2 - view
vเป็น recursive ถ้ามันขึ้นกับตัวมันเอง
CREATE VIEW physics_fall_2017 AS
SELECT course.course_id, sec_id, building, room_number
FROM course, section
WHERE course.course_id = section.course_id
AND course.dept_name = 'Physics'
AND section.semester = 'Fall'
AND section.year = '2017';
CREATE VIEW physics_fall_2017_watson AS
SELECT course_id, room_number
FROM physics_fall_2017 -- ใช้ view ตัวแรก
WHERE building = 'Watson';
การอัปเดตผ่าน view
view เป็นแค่ “หน้าต่าง” ที่มองไปยังตารางจริง — คำถามคือ ถ้าเราเขียนข้อมูลผ่านหน้าต่างนั้น ระบบจะเอาไปเขียนลงตารางจริงยังไง
instructor(ID, name, dept_name, salary) โดย dept_name เป็น NOT NULLview ที่ซ่อนทั้งเงินเดือนและภาควิชา:
CREATE VIEW instructor_names AS
SELECT ID, name FROM instructor; -- เห็นแค่ 2 คอลัมน์
ทีนี้สมมติผู้ใช้ที่เห็นแค่ view นี้ เพิ่มอาจารย์คนใหม่:
INSERT INTO instructor_names VALUES ('106', 'Manee Jaidee');
ระบบต้องแปลงคำสั่งนี้เป็นการ insert ลงตารางจริง instructor ซึ่งมี 4 คอลัมน์ —
แต่ผู้ใช้ให้มาแค่ 2 · แล้ว dept_name กับ salary จะเอาค่าอะไร มีสองทางเลือก:
- ปฏิเสธการ insert ไปเลย — ปลอดภัยที่สุด และเป็นสิ่งที่เกิดขึ้นจริงที่นี่ เพราะ
dept_nameเป็น NOT NULL - insert โดยเติมค่าที่เหลือเป็น NULL →
('106','Manee Jaidee', NULL, NULL)— ทำได้ก็ต่อเมื่อคอลัมน์ที่ขาดหายยอมรับ NULL ได้
นี่คือที่มาของเงื่อนไขข้อ 3 ในกล่องด้านล่าง — “attribute ที่ไม่ได้อยู่ใน SELECT clause ต้องตั้งเป็น null ได้”
ถ้า dept_name ยอมรับ NULL ได้ การ insert ผ่าน view นี้ก็จะสำเร็จ
CREATE VIEW instructor_info AS
SELECT ID, name, building
FROM instructor, department
WHERE instructor.dept_name = department.dept_name;
INSERT INTO instructor_info VALUES ('69987','White','Taylor');
view นี้ join สองตาราง แล้วโชว์ building ซึ่งเป็นคอลัมน์ของ department ไม่ใช่ของ instructorพอสั่ง insert ระบบต้องตอบให้ได้ว่า “แถวใหม่นี้ต้องไปลงที่ instructor และ/หรือ department ยังไง” ซึ่งตอบไม่ได้:
1. ตึก Taylor มีหลายภาควิชา — จะให้ White สังกัดภาคไหน?
ถ้า Taylor มีทั้ง Comp. Sci. และ Biology ระบบจะเดา
instructor.dept_name ไม่ได้เลย2. แล้วถ้าไม่มีภาควิชาไหนอยู่ตึก Taylor เลยล่ะ?
ต้องไปสร้างแถวใหม่ใน department ด้วยไหม? แล้วให้
dept_name กับ budget เป็นอะไร?ระบบไม่มีทางเดาได้ จึงเป็นเหตุผลที่มีข้อจำกัดด้านล่างนี้ — โดยเฉพาะข้อ 1: FROM ต้องมีตารางเดียว
1.
FROM clause มี relation ในฐานข้อมูลเพียงตัวเดียว2.
SELECT clause มีแต่ชื่อ attribute ของ relation นั้น — ไม่มีนิพจน์ ไม่มี aggregate ไม่มี DISTINCT3. attribute ที่ไม่ได้อยู่ใน SELECT clause ต้องตั้งเป็น null ได้
4. query ไม่มี GROUP BY หรือ HAVING
Materialized View
Materializing a view คือการสร้างตารางกายภาพจริงที่บรรจุ tuple ทั้งหมดของผลลัพธ์ของ query ที่นิยาม view นั้น
- สำเนาเชิงกายภาพถูกสร้างตอนที่นิยาม view
- ถ้า relation ที่ใช้ใน query ถูกอัปเดต ผลลัพธ์ใน materialized view จะล้าสมัย (out of date)
- จึงต้อง maintain view ด้วยการอัปเดตทุกครั้งที่ relation ต้นทางเปลี่ยน
| View | Materialized View | |
|---|---|---|
| เก็บข้อมูลจริงไหม | ไม่ — เก็บแค่นิยาม แล้วรัน query ใหม่ทุกครั้ง | ใช่ — เก็บสำเนาผลลัพธ์เป็นตารางจริง |
| ความเร็วในการอ่าน | ช้ากว่า (ต้องคำนวณใหม่) | เร็วกว่า (อ่านจากตารางตรง ๆ) |
| ความสดของข้อมูล | สดเสมอ | ล้าสมัยได้ — ต้อง refresh |
| พื้นที่จัดเก็บ | แทบไม่ใช้ | ใช้เท่าขนาดผลลัพธ์ |
| ใช้เมื่อไหร่ | ข้อมูลเปลี่ยนบ่อย ต้องการความถูกต้องเสมอ | รายงาน / dashboard ที่ query แพงและยอมรับข้อมูลล่าช้าได้ |
หมายเหตุ: MySQL ไม่มี MATERIALIZED VIEW ในตัว ต้องจำลองด้วยการ CREATE TABLE … AS SELECT …
แล้วรีเฟรชเอง (ต่างจาก PostgreSQL และ Oracle ที่รองรับโดยตรง)
การลบ view
DROP VIEW faculty;
DROP VIEW IF EXISTS faculty;
DROP VIEW IF EXISTS department_salary_totals;
DELETE ใช้ลบข้อมูล (แถว) ออกจากตาราง ไม่ใช่ลบ object อย่าง view — ใช้ DELETE ลบ view ไม่ได้
สิ่งที่ต้องคำนึงเวลา drop:
•
IF EXISTS ช่วยกัน error กรณี view ไม่มีอยู่• ลำดับสำคัญถ้ามี view ที่พึ่งพา view อื่น — ต้องลบ view ที่พึ่งพาก่อน
เพราะถ้าลบ view ฐานไปก่อน view ที่พึ่งพามันจะพังทันที: MySQL ยอมให้ลบ แล้วค่อย error ตอนมีคนเรียกใช้ (
ER_VIEW_INVALID) ซึ่งเจอยากมาก ·
PostgreSQL ปฏิเสธตั้งแต่แรก ถ้าไม่ใส่ CASCADEในตัวอย่าง 9.5b ต้อง
DROP VIEW physics_fall_2017_watson; ก่อน แล้วค่อย DROP VIEW physics_fall_2017;
9.6โจทย์แบบฝึกหัด Intermediate SQL
1–2 · Natural join และ join ข้ามตาราง
-- 1. ชื่ออาจารย์พร้อมชื่อภาควิชา ใช้ natural join
SELECT name, dept_name
FROM instructor NATURAL JOIN department;
-- 2. รายวิชาที่สอนในตึก Taylor พร้อมชื่ออาจารย์
SELECT DISTINCT c.title, i.name
FROM course c
JOIN section s ON c.course_id = s.course_id
JOIN teaches t ON s.course_id = t.course_id
AND s.sec_id = t.sec_id
AND s.semester = t.semester
AND s.year = t.year
JOIN instructor i ON t.ID = i.ID
WHERE s.building = 'Taylor';
ข้อ 2 ต้อง join ด้วย 4 คอลัมน์ เพราะ PK ของ section เป็น composite key
(course_id, sec_id, semester, year) — เชื่อมแค่ course_id อย่างเดียวจะได้คู่ผิด
3 · สร้าง view ยอดเงินเดือนรวมต่อภาค แล้วหาภาคที่รวมสูงสุด
CREATE VIEW departments_total_salary(dept_name, total_salary) AS
SELECT dept_name, SUM(salary)
FROM instructor
GROUP BY dept_name;
SELECT dept_name, total_salary
FROM departments_total_salary
ORDER BY total_salary DESC
LIMIT 1;
หรือเขียนแบบไม่ใช้ LIMIT (พกพาได้กว่า):
SELECT * FROM departments_total_salary
WHERE total_salary = (SELECT MAX(total_salary) FROM departments_total_salary);
แบบที่สองดีกว่าตรงไหน — ถ้ามีสองภาคที่ยอดรวมเท่ากันและสูงสุด จะได้ทั้งคู่ ส่วน LIMIT 1 จะได้แค่แถวเดียวแบบสุ่ม
4–5 · view ซ้อน view และ view ที่มี aggregate
-- 4. view ของวิชาที่เปิด Fall 2023 แล้วใช้หาวิชาในตึก Watson
CREATE VIEW courses_fall_2023 AS
SELECT c.course_id, c.title, s.sec_id, s.building, s.room_number
FROM course c
JOIN section s ON c.course_id = s.course_id
WHERE s.semester = 'Fall' AND s.year = 2023;
SELECT course_id, title, room_number
FROM courses_fall_2023
WHERE building = 'Watson';
-- 5. view ภาระงานสอนของอาจารย์ แล้วเรียงตามเงินเดือนมากไปน้อย
CREATE VIEW instructor_load AS
SELECT i.ID, i.name, i.dept_name, i.salary,
COUNT(t.course_id) AS course_count
FROM instructor i
LEFT JOIN teaches t ON i.ID = t.ID
GROUP BY i.ID, i.name, i.dept_name, i.salary;
SELECT * FROM instructor_load ORDER BY salary DESC;
ข้อ 5 ใช้ LEFT JOIN เพื่อให้อาจารย์ที่ยังไม่ได้สอนวิชาไหนเลยยังปรากฏ พร้อม course_count = 0 ·
(COUNT(t.course_id) คืน 0 ไม่ใช่ 1 เพราะ COUNT ของคอลัมน์เมิน NULL — ถ้าใช้ COUNT(*) จะได้ 1 ซึ่งผิด!)
6 · Materialized view ของหน่วยกิตเฉลี่ยต่อภาค
-- MySQL ไม่มี MATERIALIZED VIEW จึงจำลองด้วยตารางจริง
CREATE TABLE mv_avg_credits AS
SELECT dept_name, AVG(credits) AS avg_credits
FROM course
GROUP BY dept_name;
-- หาภาคที่หน่วยกิตเฉลี่ยสูงกว่าค่าเฉลี่ยรวม
SELECT * FROM mv_avg_credits
WHERE avg_credits > (SELECT AVG(avg_credits) FROM mv_avg_credits);
-- ต้อง refresh เองเมื่อ course เปลี่ยน
DROP TABLE mv_avg_credits;
CREATE TABLE mv_avg_credits AS SELECT …;
บรรทัดสุดท้ายคือประเด็นสำคัญของ materialized view — ต้องดูแลให้ทันสมัยเอง ต่างจาก view ธรรมดาที่สดเสมอโดยอัตโนมัติ
7–8 · Extra credit — subquery ใน FROM และ WITH
-- 7. เงินเดือนเฉลี่ยของอาจารย์ที่สอนในตึก Taylor
SELECT AVG(salary) AS avg_taylor_salary
FROM (SELECT DISTINCT i.ID, i.salary
FROM instructor i
JOIN teaches t ON i.ID = t.ID
JOIN section s ON t.course_id = s.course_id
AND t.sec_id = s.sec_id
AND t.semester = s.semester
AND t.year = s.year
WHERE s.building = 'Taylor') AS taylor_instructors;
-- 8. WITH: อาจารย์ที่เงินเดือนสูงกว่าค่าเฉลี่ย แล้ว join กับ department
WITH above_avg AS
(SELECT ID, name, dept_name, salary
FROM instructor
WHERE salary > (SELECT AVG(salary) FROM instructor))
SELECT a.name, a.salary, d.dept_name, d.building
FROM above_avg a
JOIN department d ON a.dept_name = d.dept_name
ORDER BY a.salary DESC;
ข้อ 7 ทำไมต้อง DISTINCT ใน subquery — อาจารย์ที่สอนหลายวิชาในตึก Taylor จะปรากฏหลายแถว ถ้าไม่ใส่ DISTINCT เงินเดือนของเขาจะถูกนับซ้ำใน AVG ทำให้ค่าเฉลี่ยเพี้ยน
9.7สรุปบทนี้
- Join expression เป็นเครื่องมือทรงพลังในการรวมข้อมูลจากหลายตาราง
- join แต่ละชนิด (Natural, Inner, Outer) ตอบโจทย์การดึงข้อมูลต่างกัน
- View สร้างตารางเสมือน ช่วยเรื่องความปลอดภัยของข้อมูลและทำให้ query ซับซ้อนดูง่ายขึ้น
- เข้าใจการอัปเดตผ่าน view และ materialized view เป็นเรื่องสำคัญในการบริหารฐานข้อมูล
faculty ออกจากฐานข้อมูล ควรใช้คำสั่งใดDROP VIEW และเติม IF EXISTS ได้เพื่อความปลอดภัย