Relational Algebra
6 operator พื้นฐาน σ Π ∪ − × ρ · การประกอบ operator เข้าด้วยกัน · equivalent queries และ query optimization
วิดีโอที่เกี่ยวข้อง:
3.1ทำไมต้องเรียน Relational Algebra
Relational algebra คือวิธีที่ฐานข้อมูล “คิด” ว่าจะไปเอามายังไง (HOW)
ทุก SQL query จะถูกแปลเป็น relational algebra ก่อนถูกรัน — เรียนพีชคณิตนี้ = เรียนวิธีอ่านใจฐานข้อมูล
| ชั้น | บทบาท |
|---|---|
| Relational Model | นิยามว่าข้อมูลหน้าตาเป็นยังไง — relation, tuple, attribute, key (คำนาม) |
| Relational Algebra | นิยามว่าทำอะไรกับข้อมูลได้บ้าง — σ, Π, ∪, −, ×, ρ (คำกริยา) |
| SQL | ประกาศว่าเราต้องการอะไร — ภาษาที่มนุษย์อ่านง่ายเพื่อสั่งให้ทำ operation เหล่านั้น |
เกิดอะไรขึ้นเมื่อเรารัน SQL
SELECT name FROM instructor WHERE salary > 90000;
↓ Parser แปลเป็น relational algebra
Π name ( σ salary > 90000 ( instructor ) )
↓ Optimizer เลือกแผนที่ถูกที่สุดจากนิพจน์ที่สมมูลกัน
↓ Execution engine คืนผลลัพธ์
พีชคณิตคือ operator tree ที่รันได้จริง ส่วนตัวหนังสือ SQL เป็นเพียงคำบรรยาย
มีกฎความสมมูลที่ทำให้ optimizerแปลง query ได้โดยรับประกันว่าผลลัพธ์เหมือนเดิม
closure property — ทุก operator รับ relation เข้า คืน relation ออก จึงต่อกันเป็น plan tree ได้
3.26 Basic Operators — ลองกดเล่นได้
1 · Select — σ (sigma)
เป็น operation ที่ใช้บ่อยที่สุด: เลือก tuple จาก relation เดียวที่ทำให้ predicate เป็นจริง
σ p (r)
pเรียกว่า selection predicate- ใช้ตัวเปรียบเทียบได้:
= ≠ > ≥ < ≤ - รวมหลายเงื่อนไขด้วย ∧ (and) · ∨ (or) · ¬ (not)
- เปรียบเทียบระหว่าง attribute สองตัวได้ เช่น
σ dept_name = building (department)= หาแผนกที่ชื่อตรงกับชื่อตึก
σ dept_name='Physics' (instructor)
σ dept_name='Physics' ∧ salary > 90000 (instructor)
2 · Project — Π (pi)
unary operation (= operation ที่รับ relation เข้าไปตัวเดียว ตรงข้ามกับ binary ที่รับสองตัวอย่าง ∪ หรือ ×) ซึ่งคืน relation เดิมโดยตัด attribute บางตัวออก
Π A₁, A₂, …, Ak (r)
เช่น
Π course_id (σ year=2026 (Enrollment)) — ถึงแม้ EN813304 จะถูกลงทะเบียนโดยนักศึกษา 3 คนในปี 2026
ผลลัพธ์ก็มี EN813304 แค่ แถวเดียวต่างจาก SQL: SELECT course_id FROM … จะคืน 3 แถว ต้องเขียน SELECT DISTINCT เองถึงจะเหมือน Π
3–5 · Set operations — ∪, −, (∩)
เงื่อนไขที่ต้องเป็นจริงก่อนใช้ set operation:
rและsต้องมี arity เท่ากัน (จำนวน attribute เท่ากัน)- domain ของ attribute ต้องเข้ากันได้ — คอลัมน์ที่ 2 ของ r ต้องเป็นข้อมูลชนิดเดียวกับคอลัมน์ที่ 2 ของ s
section = “วิชาที่เปิดสอนจริงในเทอมไหน กลุ่มไหน” (คนละอย่างกับ course ที่เป็นรายวิชาในหลักสูตร)section(course_id, sec_id, semester, year, building, room_number) — 4 ตัวแรกรวมกันเป็น primary key
| course_id | sec_id | semester | year |
|---|---|---|---|
| CS-101 | 1 | Fall | 2017 |
| CS-347 | 1 | Fall | 2017 |
| PHY-101 | 1 | Fall | 2017 |
| CS-101 | 1 | Spring | 2018 |
| CS-315 | 1 | Spring | 2018 |
Fall 2017 เปิด {CS-101, CS-347, PHY-101} · Spring 2018 เปิด {CS-101, CS-315}
→ ∪ = {CS-101, CS-347, PHY-101, CS-315} · ∩ = {CS-101} · − (Fall ลบ Spring) = {CS-347, PHY-101}
-- วิชาที่เปิดสอน Fall 2017 หรือ Spring 2018 หรือทั้งสอง
Π course_id (σ semester='Fall' ∧ year=2017 (section))
∪ Π course_id (σ semester='Spring' ∧ year=2018 (section))
-- วิชาที่เปิดสอนทั้งสองภาค
… ∩ …
-- วิชาที่เปิด Fall 2017 แต่ไม่เปิด Spring 2018
… − …
r ∩ s = r − ( r − s )
6 ตัวจริง ๆ คือ σ, Π, ∪, −, ×, ρ · natural join (⋈) ก็เป็น derived เช่นกัน
6 · Cartesian product — ×
รวมข้อมูลจาก relation สองตัว โดยสร้าง tuple ผลลัพธ์จากทุกคู่ที่เป็นไปได้ ผลลัพธ์มีคอลัมน์ของทั้งสองตารางต่อกัน และมีจำนวนแถว = (แถวของ r) × (แถวของ s)
ข้อมูลตัวอย่างที่ใช้ทั้งหัวข้อนี้ (จากสไลด์ university database):
| ID | name | dept_name | salary |
|---|---|---|---|
| 10101 | Srinivasan | Comp. Sci. | 65000 |
| 12121 | Wu | Finance | 90000 |
| 15151 | Mozart | Music | 40000 |
| ID | course_id | sec_id | semester | year |
|---|---|---|---|---|
| 10101 | CS-101 | 1 | Fall | 2017 |
| 10101 | CS-347 | 1 | Fall | 2017 |
| 12121 | FIN-201 | 1 | Spring | 2018 |
สังเกตว่า teaches.ID คือ FK ที่ชี้ไปหา instructor.ID —
Mozart (15151) ไม่มีแถวใน teaches เลย แปลว่าเทอมนี้เขาไม่ได้สอน
instructor × teaches จับคู่ ทุก อาจารย์กับ ทุก แถวการสอน → 3 × 3 = 9 แถว
แต่มีแค่ 3 แถว เท่านั้นที่ instructor.ID ตรงกับ teaches.ID จริงอีก 6 แถวคือ “Mozart สอน CS-101”, “Wu สอน CS-347” … ซึ่งไม่เป็นความจริงเลย → ไร้ความหมาย
เนื่องจาก ID เป็นชื่อคอลัมน์ที่ปรากฏในทั้งสอง relation พอเอามาต่อกันแล้วจะมีคอลัมน์ชื่อ ID สองอัน
เราจึงต้องแยกความต่างด้วยการเติมชื่อ relation นำหน้า: instructor.ID (รหัสอาจารย์จากตารางซ้าย)
และ teaches.ID (รหัสอาจารย์จากตารางขวา)
| instructor.ID | name | teaches.ID | course_id | แถวนี้จริงไหม |
|---|---|---|---|---|
| 10101 | Srinivasan | 10101 | CS-101 | จริง — ID ตรงกัน |
| 10101 | Srinivasan | 10101 | CS-347 | จริง — ID ตรงกัน |
| 10101 | Srinivasan | 12121 | FIN-201 | ขยะ — Srinivasan ไม่ได้สอน FIN-201 |
| 12121 | Wu | 10101 | CS-101 | ขยะ |
| 12121 | Wu | 10101 | CS-347 | ขยะ |
| 12121 | Wu | 12121 | FIN-201 | จริง — ID ตรงกัน |
| 15151 | Mozart | 10101 | CS-101 | ขยะ |
| 15151 | Mozart | 10101 | CS-347 | ขยะ |
| 15151 | Mozart | 12121 | FIN-201 | ขยะ |
สรุป: × ให้ “ทุกความเป็นไปได้” ซึ่งรวมทั้งที่จริงและที่มั่ว — งานของเราคือกรองเฉพาะที่จริงออกมา ซึ่งก็คือ join ในหัวข้อถัดไป
Join — ⋈ (derived)
เพื่อเก็บเฉพาะ tuple ที่อาจารย์กับวิชาที่เขาสอนตรงกันจริง เราใช้ σ กรองผลของ ×
σ instructor.ID = teaches.ID ( instructor × teaches )
นิพจน์นี้เรียกว่า equi-join — ยังไม่ใช่ instructor ⋈ teaches เป๊ะ ๆ เพราะผลลัพธ์ยังมีคอลัมน์ ID อยู่ สองอัน
· ต้อง project ทิ้งอันหนึ่งก่อนถึงจะเท่ากัน:
instructor ⋈ teaches = ΠID, name, dept_name, salary, course_id, sec_id, semester, year ( σinstructor.ID = teaches.ID ( instructor × teaches ) )
σ ตัวนี้ทำหน้าที่ “เก็บเฉพาะ 3 แถวสีเขียวในตารางข้างบน ทิ้ง 6 แถวขยะ” ผลลัพธ์คือ:
| instructor.ID | name | dept_name | teaches.ID | course_id |
|---|---|---|---|---|
| 10101 | Srinivasan | Comp. Sci. | 10101 | CS-101 |
| 10101 | Srinivasan | Comp. Sci. | 10101 | CS-347 |
| 12121 | Wu | Finance | 12121 | FIN-201 |
Mozart หายไป เพราะไม่มีแถวใน teaches ที่ ID = 15151 → เขาไม่มีคู่ให้ join ด้วย (นี่คือพฤติกรรมของ inner join)
Natural join (⋈) จับคู่ tuple ที่มีค่าเท่ากันบน attribute ร่วมทุกตัว (ในที่นี้คือ ID ตัวเดียว)
และเก็บคอลัมน์ร่วมไว้ชุดเดียว — ต่างจากบรรทัดบนที่มี ID โผล่มา 2 คอลัมน์
instructor ⋈ teaches จะเหลือ ID แค่คอลัมน์เดียว
ON a.x > b.y หรือ ON a.id = b.id
(θ คือชื่อเรียกรวม ๆ ของ predicate อะไรก็ได้) · รูปพีชคณิตคือ σθ(a × b)Equi-join = theta join กรณีพิเศษที่ θ ใช้แต่เครื่องหมาย
=Natural join (⋈) = equi-join ที่ไม่ต้องเขียนเงื่อนไข เพราะระบบจับคู่ attribute ที่ชื่อตรงกันให้เอง แล้วยุบคอลัมน์ซ้ำทิ้ง
Rename — ρ (rho) และ Assignment (←)
ผลลัพธ์ของนิพจน์พีชคณิตไม่มีชื่อให้เราอ้างถึง จึงมี rename operator
ρ x (E) -- ตั้งชื่อผลของ E ว่า x
ρ x(A₁, A₂, …, Aₙ) (E) -- ตั้งชื่อทั้ง relation และ attribute
ρ ชื่อใหม่ ← ชื่อเดิม (E) -- เปลี่ยนชื่อ "แค่คอลัมน์เดียว"
| เขียนแบบ | อ่านว่า |
|---|---|
| ρ ใหม่ ← เดิม (R) | อยู่ข้างในตัวห้อยของ ρ → “เปลี่ยนชื่อคอลัมน์ เดิม ให้เป็น ใหม่” ตารางยังเป็นตารางเดิม แค่หัวคอลัมน์เปลี่ยน |
| ชื่อ ← นิพจน์ | ยืนเดี่ยวเป็นบรรทัดของตัวเอง → assignment “คำนวณนิพจน์ฝั่งขวา แล้วเก็บผลไว้ในตัวแปรชื่อฝั่งซ้าย” เหมือน x = … ในภาษาโปรแกรม |
-- ตัวอย่าง ρ: Inventory(ISBN, StockQuantity) → Inventory(ISBN, AvailableStock)
ρ AvailableStock ← StockQuantity (Inventory)
Assignment (←) ช่วยให้เขียน query ยาว ๆ เป็นลำดับขั้นเหมือนโปรแกรม
Physics ← σ dept_name='Physics' (instructor)
Music ← σ dept_name='Music' (instructor)
Physics ∪ Music
3.3การประกอบ operator เข้าด้วยกัน
ผลลัพธ์ของทุก operation คือ relation ดังนั้นเราเอาผลลัพธ์ไปเป็น input ของ operation ถัดไปได้ แทนที่จะใส่ชื่อ relation เป็นอาร์กิวเมนต์ของ Π เราใส่นิพจน์ที่ให้ผลเป็น relation แทนได้เลย
ตัวอย่างจากสไลด์ต่อจากนี้อ้างภาค Physics ซึ่งไม่มีในตาราง 3 แถวข้างบน —
ให้คิดว่าตารางเต็มของตำรามีแถวนี้อยู่ด้วย: (22222, Einstein, Physics, 95000)
-- ชื่อของอาจารย์ทุกคนในภาควิชา Physics
Π name ( σ dept_name='Physics' (instructor) )
คุณสมบัตินี้ทำให้ต่อ operation กันได้ไม่จำกัด และรับประกันว่าผลลัพธ์ของ query มาจากข้อมูลที่มีอยู่จริงเสมอ
3.4Equivalent Queries และ Query Optimization
query หนึ่งเขียนได้หลายแบบ — ตัวอย่างจากสไลด์: หาข้อมูลอาจารย์ในภาค Physics ที่เงินเดือนเกิน 90,000
σ dept_name='Physics' ∧ salary>90000 (instructor)
σ dept_name='Physics' ( σ salary>90000 (instructor) )
สองนิพจน์นี้ ไม่เหมือนกัน (not identical) แต่ สมมูลกัน (equivalent) — คือให้ผลลัพธ์เดียวกันบนฐานข้อมูลทุกชุดที่เป็นไปได้
join ก่อนแล้วค่อยกรอง → ต้องประมวลผลข้อมูลจำนวนมากโดยไม่จำเป็น
เร็ว — ( σdept_name='Physics' ∧ salary>90000 (instructor) ) ⋈ teaches
กรองก่อนแล้วค่อย join → join กับข้อมูลน้อยลงมาก
กฎนี้เรียกว่า “push σ down” — ดัน selection ลงไปให้ใกล้ข้อมูลดิบที่สุด
คู่ที่ “ไม่” สมมูลกัน — โจทย์ Part C จากแบบฝึกหัด
ข้อพวกนี้ใช้ schema จากใบงานสัปดาห์ที่ 3:
Student(student_id, name, major, gpa) — ข้อมูลจริง 5 แถวอยู่ใน §3.5 ด้านล่าง
C1. σmajor='CoE' ∧ gpa>3.0(Student) vs σmajor='CoE'(σgpa>3.0(Student))
สมมูลกัน — การกรองด้วย ∧ ในครั้งเดียว ให้ผลเหมือนการกรองสองรอบซ้อนกัน เพราะเงื่อนไขทั้งสองต้องเป็นจริงพร้อมกันอยู่ดี และลำดับการกรองไม่ส่งผลต่อเซตผลลัพธ์
หมายเหตุ: ต่างกันแค่ประสิทธิภาพ ไม่ต่างที่ผลลัพธ์
C2. Πname(σgpa>3.0(Student)) vs σgpa>3.0(Πname(Student))
ไม่สมมูลกัน
นิพจน์ที่สอง ทำ Πname ก่อน ซึ่งตัดคอลัมน์ gpa ทิ้งไปแล้ว
จากนั้นจึงพยายามกรองด้วยเงื่อนไข gpa > 3.0 — แต่ relation ที่ได้ไม่มี attribute ชื่อ gpa เหลืออยู่เลย
นิพจน์นี้จึงไม่ถูกต้อง (invalid) เขียนแบบนี้ไม่ได้
บทเรียน: จะ push σ ลงไปใต้ Π ได้ ก็ต่อเมื่อ attribute ที่ σ ใช้ ยังอยู่ หลังการ project
3.5ฝึกจากโจทย์จริงในคาบ
ใช้ schema นี้ (จากใบงาน Relational Algebra สัปดาห์ที่ 3):
Student(student_id, name, major, gpa)
Course(course_id, title, credits, dept_name)
Enrollment(student_id, course_id, semester, year, grade)
| student_id | name | major | gpa |
|---|---|---|---|
| 6613001 | Anan | CoE | 3.25 |
| 6613002 | Beam | CoE | 2.80 |
| 6613003 | Chada | EE | 3.60 |
| 6613004 | Dao | CoE | 3.60 |
| 6613005 | Earn | ME | 2.40 |
| student_id | course_id | semester | year | grade |
|---|---|---|---|---|
| 6613001 | EN813304 | 1 | 2026 | A |
| 6613001 | EN811001 | 2 | 2025 | B+ |
| 6613002 | EN813304 | 1 | 2026 | B |
| 6613002 | EN811001 | 2 | 2025 | C+ |
| 6613003 | EN812201 | 1 | 2026 | A |
| 6613004 | EN813304 | 1 | 2026 | B+ |
Part A — อ่านนิพจน์ แล้วเขียนตารางผลลัพธ์
A1. σ major='CoE' (Student)
กรองแถวที่ major = 'CoE' · คอลัมน์ครบทุกตัว เพราะ σ ไม่ตัดคอลัมน์
| student_id | name | major | gpa |
|---|---|---|---|
| 6613001 | Anan | CoE | 3.25 |
| 6613002 | Beam | CoE | 2.80 |
| 6613004 | Dao | CoE | 3.60 |
A2. Π name, gpa ( σ gpa ≥ 3.50 (Student) )
กรอง gpa ≥ 3.50 ได้ Chada (3.60) และ Dao (3.60) → แล้ว project เหลือ name, gpa
| name | gpa |
|---|---|
| Chada | 3.60 |
| Dao | 3.60 |
ระวัง: ทั้งคู่ gpa = 3.60 เท่ากัน แต่ ชื่อต่างกัน จึงยังเป็นคนละ tuple ไม่ถูกยุบ · ถ้าโจทย์ให้ Π gpa เฉย ๆ จะเหลือแค่ 3.60 แถวเดียว
A3. Π course_id ( σ year=2026 (Enrollment) ) — ข้อกับดัก
แถวปี 2026 มี 4 แถว: EN813304 (×3) และ EN812201 (×1)
แต่ relation คือเซต → Π ลบแถวซ้ำ เหลือ 2 แถว
| course_id |
|---|
| EN813304 |
| EN812201 |
ถ้าตอบ 4 แถวคือผิด — นี่คือ hint ที่โจทย์ใส่มาให้ว่า “relation เป็นเซต ลองคิดเรื่องแถวที่ซ้ำกัน”
Part B — เขียนนิพจน์เอง
B1. หารายวิชาทั้งหมดของภาค CoE ที่มี 3 หน่วยกิต
σ dept_name='CoE' ∧ credits=3 (Course)
หรือเขียนซ้อนกันก็ได้ (สมมูลกัน): σ dept_name='CoE' ( σ credits=3 (Course) )
B2. แสดงเฉพาะชื่อและสาขาของนักศึกษาที่ gpa มากกว่า 2.X (X = เลขท้ายรหัสตัวเอง)
Π name, major ( σ gpa > 2.X (Student) )
ตัวอย่างถ้า X = 5 → gpa > 2.5 ได้ Anan (3.25), Beam (2.80), Chada (3.60), Dao (3.60) — Earn (2.40) ตกไป
| name | major |
|---|---|
| Anan | CoE |
| Beam | CoE |
| Chada | EE |
| Dao | CoE |
B3. หา course_id ของวิชาที่มีการลงทะเบียนในปี 2026 แต่ไม่มีในปี 2025
Π course_id ( σ year=2026 (Enrollment) )
−
Π course_id ( σ year=2025 (Enrollment) )
2026 = {EN813304, EN812201} · 2025 = {EN811001} · ผลต่าง = {EN813304, EN812201}
ทำไมต้อง Π ก่อนแล้วค่อย −
เพราะ − เทียบ ทั้งแถว ไม่ใช่เทียบแค่ course_id · ถ้าเอา Enrollment เต็ม ๆ มาลบกัน
แถวปี 2026 กับแถวปี 2025 มีคอลัมน์ year ต่างกันอยู่แล้ว จึงไม่มีทางหักล้างกันได้เลย — ผลลัพธ์จะเป็นแถวปี 2026 ทั้งหมด ซึ่งผิด
แม้แต่ถ้าตัด year ออกไป การเทียบทั้งแถวก็ยังพา student_id กับ grade ไปด้วย —
วิชาที่มีคนลงทั้งสองปีแต่คนละคน ก็จะรอดมาเป็นคำตอบทั้งที่ไม่ควร
ต้อง Π เหลือ course_id ก่อน ถึงจะเทียบ “วิชา” กับ “วิชา” จริง ๆ
B4. หาชื่อนักศึกษาที่ลงทะเบียนวิชา 'EN813304'
Π name ( Student ⋈ σ course_id='EN813304' (Enrollment) )
หรือเขียนแบบใช้ operator พื้นฐานล้วน:
Π name ( σ Student.student_id = Enrollment.student_id
( Student × σ course_id='EN813304' (Enrollment) ) )
ผลลัพธ์: Anan, Beam, Dao
เทคนิคทำข้อสอบ: กรอง (σ) ให้เล็กที่สุดก่อน แล้วค่อย join แล้วค่อย project เป็นขั้นสุดท้าย — ทั้งอ่านง่ายและตรงกับที่ optimizer ทำจริง
3.6ตัวอย่างจากสไลด์เพิ่มเติม
Books(ISBN, Title, Author, Price) · Inventory(ISBN, StockQuantity)
a) หนังสือราคาเกิน $20 σ Price > 20 (Books)
b) เฉพาะชื่อเรื่องกับผู้แต่ง Π Title, Author (Books)
c) ISBN ที่อยู่ใน Books หรือ Inventory
Π ISBN (Books) ∪ Π ISBN (Inventory)
d) ISBN ที่อยู่ใน Books แต่ไม่อยู่ใน Inventory
Π ISBN (Books) − Π ISBN (Inventory)
e) เปลี่ยนชื่อ StockQuantity เป็น AvailableStock
ρ AvailableStock ← StockQuantity (Inventory)
Customers(CustomerID, Name, Email, Address) · Orders(OrderID, CustomerID, OrderDate, TotalAmount)
ข้อ a ยืม LIKE กับ % ของ SQL มาใช้ (พีชคณิตแท้ ๆ ไม่มี operator นี้ แต่สไลด์เขียนแบบนี้)
% = ตัวอักษรอะไรก็ได้ กี่ตัวก็ได้ (รวม 0 ตัว) · '%New York%' จึงแปลว่า
“มีคำว่า New York อยู่ตรงไหนก็ได้ ในข้อความ” เช่น “221 Main St, New York, NY” ก็เข้าเงื่อนไข
(รายละเอียด LIKE เต็ม ๆ อยู่ในบทที่ 6)
a) ออเดอร์เกิน $100 ของลูกค้าใน New York
σ TotalAmount>100 (Orders)
⋈ σ Address LIKE '%New York%' (Customers)
b) ชื่อลูกค้ากับวันที่สั่งซื้อ
Π Name, OrderDate (Customers ⋈ Orders)
c) รวม CustomerID จากทั้งสองตาราง
Π CustomerID (Customers) ∪ Π CustomerID (Orders)
d) ลูกค้าที่ไม่เคยสั่งซื้อเลย
Π CustomerID (Customers) − Π CustomerID (Orders)
e) ทุกคู่ที่เป็นไปได้ Customers × Orders
f) เปลี่ยนชื่อ TotalAmount ρ OrderValue ← TotalAmount (Orders)
3.7ตารางแปลง SQL ↔ Relational Algebra
| SQL | Relational Algebra | หมายเหตุ |
|---|---|---|
| SELECT cols | Π | Π ตัดแถวซ้ำ · SELECT ไม่ตัด (ต้องใส่ DISTINCT) |
| WHERE p | σp | ตรงกันเป๊ะ |
| FROM a, b | a × b | Cartesian product |
| a JOIN b ON … | σθ(a × b) | theta join — join ที่เขียนเงื่อนไข (θ) เอง |
| a NATURAL JOIN b | a ⋈ b | จับคู่ทุก attribute ร่วม |
| UNION | ∪ | ทั้งคู่ตัดแถวซ้ำ |
| INTERSECT | ∩ | derived operator |
| EXCEPT / MINUS | − | |
| AS | ρ | rename |