EN813304 · Midterm
บทที่ 9 / 12 · intermediate-sql-part1.pptx · สัปดาห์ที่ 7

Intermediate SQL — Join Expressions & Views

natural join vs inner join · outer join ทั้งสามแบบ · CREATE VIEW · view ที่อัปเดตได้ · materialized view

SQL 60%Views
หัวข้อในบทนี้

วิดีโอที่อาจารย์ให้ดูก่อนเรียนบทนี้:

Intermediate SQL (Part 1)

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

schema ที่ใช้ทั้งบทนี้ — เปิดค้างไว้ บทนี้ใช้ฐานข้อมูลมหาวิทยาลัยชุดเดียวกับ บทที่ 5 และ บทที่ 3 (ที่นั่นมีข้อมูลจริงของ 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

instructor
IDnamedept_namesalary
10101SrinivasanComp. Sci.65000
12121WuFinance90000
15151MozartMusic40000
department
dept_namebuildingbudget
Comp. Sci.Taylor100000
FinancePainter120000
MusicPackard80000
-- แสดงชื่ออาจารย์พร้อมตึกของภาควิชา
-- คอลัมน์ร่วมคือ dept_name → ระบบจับคู่ให้เองโดยไม่ต้องเขียน ON
SELECT name, dept_name, building
FROM   instructor NATURAL JOIN department;
namedept_namebuilding
SrinivasanComp. Sci.Taylor
WuFinancePainter
MozartMusicPackard

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;
ผสม NATURAL JOIN กับ JOIN … ON ในประโยคเดียวได้ แต่ต้องเขียน ON ให้ครบ ผิด — ขาด ON แต่ ไม่ error ซึ่งอันตรายกว่า
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 vs Inner Join — ต่างกันตรงไหน
NATURAL JOININNER JOIN … ON
เงื่อนไขจับคู่อัตโนมัติ จากชื่อคอลัมน์ที่เหมือนกันทุกตัวเขียนเองใน ON
คอลัมน์ร่วมในผลลัพธ์เหลือชุดเดียวปรากฏสองครั้ง (จากทั้งสองตาราง)
ความเสี่ยงสูง — ถ้าบังเอิญมีคอลัมน์ชื่อซ้ำที่ไม่ควรจับคู่กัน (เช่น name, id, created_at) ผลลัพธ์จะผิดโดยไม่มี errorต่ำ — ควบคุมได้เอง
ในงานจริงจึงนิยม INNER JOIN … ON มากกว่า แต่ในข้อสอบต้องอ่านออกทั้งสองแบบ

ข้อมูล course / section / teaches ที่แบบฝึกหัด 9.6 ใช้

โจทย์ข้อ 2, 4, 5, 7 ถามถึงตึก Taylor และภาระงานสอน — ใช้ข้อมูลชุดนี้ตรวจคำตอบได้

course
course_idtitledept_name
CS-101Intro to CSComp. Sci.
CS-347Database Sys.Comp. Sci.
FIN-201InvestmentsFinance
section
course_idsec_idsemyearbuildingroom
CS-1011Fall2017Taylor3128
CS-3471Fall2017Taylor3128
FIN-2011Spring2018Painter514
teaches
IDcourse_idsec_idsemyear
10101CS-1011Fall2017
10101CS-3471Fall2017
12121FIN-2011Spring2018

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 เติมช่องว่าง

Left Outer Join

เก็บทุกแถวจากตารางซ้าย · ที่ไม่มีคู่ → คอลัมน์ฝั่งขวาเป็น NULL

Right Outer Join

เก็บทุกแถวจากตารางขวา · ที่ไม่มีคู่ → คอลัมน์ฝั่งซ้ายเป็น NULL

Full Outer Join

เก็บทุกแถวจากทั้งสองฝั่ง
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 ไม่เหมือนกับการสร้าง relation ใหม่จากผลของ query
view เป็นการบันทึกนิพจน์ (expression) เอาไว้ · เมื่อมี query ที่ใช้ view นั้น ระบบจะแทนนิพจน์นั้นเข้าไปใน query แล้วรันใหม่กับข้อมูลปัจจุบัน

นี่คือเหตุผลที่ view สดเสมอ — ข้อมูลในตารางฐานเปลี่ยน view ก็เปลี่ยนตามทันที

View ที่นิยามจาก view อื่น

view หนึ่งสามารถถูกใช้ในนิพจน์ที่นิยาม view อีกตัวได้ — เหมือนต่อเลโก้ทีละชั้น

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 เป็นแค่ “หน้าต่าง” ที่มองไปยังตารางจริง — คำถามคือ ถ้าเราเขียนข้อมูลผ่านหน้าต่างนั้น ระบบจะเอาไปเขียนลงตารางจริงยังไง

ตั้งฉากก่อน — ตารางจริงกับ view ที่จะใช้ในตัวอย่างนี้ ตารางจริง instructor(ID, name, dept_name, salary) โดย dept_name เป็น NOT NULL
view ที่ซ่อนทั้งเงินเดือนและภาควิชา:
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 จะเอาค่าอะไร มีสองทางเลือก:

นี่คือที่มาของเงื่อนไขข้อ 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 ต้องมีตารางเดียว
เงื่อนไขของ view ที่อัปเดตได้ (updatable view) SQL ส่วนใหญ่ยอมให้ update ได้เฉพาะ view แบบง่าย ซึ่งต้องเป็นไปตามทุกข้อ:
1. FROM clause มี relation ในฐานข้อมูลเพียงตัวเดียว
2. SELECT clause มีแต่ชื่อ attribute ของ relation นั้น — ไม่มีนิพจน์ ไม่มี aggregate ไม่มี DISTINCT
3. attribute ที่ไม่ได้อยู่ใน SELECT clause ต้องตั้งเป็น null ได้
4. query ไม่มี GROUP BY หรือ HAVING

Materialized View

Materializing a view คือการสร้างตารางกายภาพจริงที่บรรจุ tuple ทั้งหมดของผลลัพธ์ของ query ที่นิยาม view นั้น

ViewMaterialized 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;
DROP VIEW ไม่ใช่ DELETE DROP VIEW คือคำสั่งที่ถูกต้องสำหรับลบ view ออกจาก schema
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สรุปบทนี้

ข้อใดไม่เป็นเงื่อนไขของ view ที่อัปเดตได้ในระบบ SQL ส่วนใหญ่
view ไม่จำเป็นต้องมีคอลัมน์ครบ — attribute ที่ไม่ได้อยู่ใน SELECT clause เพียงแค่ต้องตั้งเป็น null ได้ เท่านั้น (ถ้าคอลัมน์ที่ขาดหายเป็น NOT NULL ก็จะ insert ผ่าน view ไม่ได้)
ต้องการลบ view ชื่อ faculty ออกจากฐานข้อมูล ควรใช้คำสั่งใด
DELETE ใช้ลบแถวข้อมูล ไม่ใช่ลบ object · DROP TABLE ใช้กับตารางจริง ไม่ใช่ view · คำตอบที่ถูกคือ DROP VIEW และเติม IF EXISTS ได้เพื่อความปลอดภัย
ระบบจำไว้ในเบราว์เซอร์นี้ จะได้รู้ว่าอ่านถึงไหนแล้ว