แผนการเรียนรู้
/
AMMS 302 | Week 7

Advanced DB Design & Joins

การออกแบบฐานข้อมูลขั้นสูง & การเชื่อมตาราง

ER Diagram · FOREIGN KEY · INNER/LEFT/RIGHT/FULL JOIN · SELF JOIN · dot notation · UNION · GROUP BY · HAVING

🎯 เป้าหมายคาบนี้

  • วาด ER diagram และประกาศ FOREIGN KEY + PRAGMA foreign_keys ได้ (CLO1)
  • เขียน JOIN ทุกชนิด / SELF JOIN / UNION พร้อม dot notation ได้ (CLO2)
  • ใช้ GROUP BY + HAVING สรุปยอดจ่ายยาต่อผู้ป่วย/ยาได้

🗺️ Learning Path

ทฤษฎี FK+ER
สไลด์ 2–3
week07-joins.ipynb
สร้างตาราง rx
JOIN ทั้ง 5 + GROUP/HAVING
สไลด์ 4–11
Lab + HW
สไลด์ 12–13
ใช้ healthinfo.db ต่อ — เราจะเพิ่มตาราง prescriptions + referrals ในโน้ตบุ๊ก

สารบัญ

📚 โน้ตบุ๊ก: week07-joins.ipynb · Specs: FK · JOIN · W3Schools JOINs

01 · ER Diagram — พิมพ์เขียวความสัมพันธ์

Entity = ตาราง · Relationship = เส้นเชื่อม · Cardinality บอกจำนวน: 1:1 · 1:N · M:N — ใช้ crow's foot notation

patients

🔑 patient_id
hn UNIQUE
gender
birth_date
1───<N
receives
prescriptions

🔑 rx_id
🔗 patient_id
drug · dose
cost_thb
ผู้ป่วย 1 คน ← ได้หลายใบสั่งยา (1:N)

Cardinality ที่เจอในโรงพยาบาล

  • 👤 patient → visits : 1:N (มาหลายครั้ง)
  • 💊 prescription → drugs : N:M (ผ่าน junction table)
  • 🩺 doctor → doctor : self (referral)
N:M → สร้างตารางกลาง
rx_items(rx_id FK, drug_id FK, qty)
💡 Homework: วาด ER ของ สปสช. Data Dictionary ด้วย draw.io (ฟรี) — เตรียม Project (สัปดาห์ 15)
🔗 ศึกษา schema จริง: MIMIC-IV hosp ERD · OMOP CDM v5.4 tables

02 · FOREIGN KEY — สายใยตาราง

FK = คอลัมน์ที่ชี้ไป PK ตารางอื่น — SQLite ปิด enforce โดย default! ต้องเปิดต่อ connection

import sqlite3
con = sqlite3.connect("healthinfo.db")
cur = con.cursor()

# 🔑 เปิด enforce — ทำ "ต่อ connection"
cur.execute("PRAGMA foreign_keys = ON")
print(cur.execute("PRAGMA foreign_keys").fetchone())

cur.execute("""
CREATE TABLE prescriptions(
  rx_id      INTEGER PRIMARY KEY,
  patient_id INTEGER NOT NULL
             REFERENCES patients(patient_id),
  drug       TEXT NOT NULL,
  dose_mg    REAL, cost_thb REAL
)""")

FK ป้องกันอะไร?

  • INSERT patient_id 99999 ที่ไม่มีจริง → error (ถ้า enforce)
  • DELETE patients ที่มี rx → FOREIGN KEY constraint failed
  • ไม่มี orphan records = รายงานไม่บิดเบี้ยว
-- ลบแบบ cascade (ใช้อย่างระวัง)
CREATE TABLE t(…
  REFERENCES patients(patient_id)
  ON DELETE CASCADE);
กับดัก: ลืม PRAGMA = FK เป็นแค่ comment! Python sqlite3 ปิด default

03 · INNER JOIN — จุดตัด A∩B

แถวที่ key match ทั้งสองฝั่งเท่านั้น — ไม่ match หล่นทิ้ง (orphan หาย!)

SELECT p.hn, r.drug, r.cost_thb
FROM patients AS p
INNER JOIN prescriptions AS r
        ON r.patient_id = p.patient_id;

-- ON คือเงื่อนไขจับคู่
-- WHERE กรองหลัง join ได้เหมือนกัน
… WHERE r.drug = 'Metformin'
A∩B only
hndrugcost
HN-54001Metformin35
HN-54001Lisinopril42
HN-54002Metformin55
HN-54003Atorva…68
HN-54003Metformin35
💡 นับ: patients=103, rx=6 → INNER=5 (orphan rx_id=6 หาย!) — เช็ค count หลัง JOIN เสมอ

04 · LEFT JOIN — ซ้ายครบถ้วน

A ทั้งหมด + B ที่ match · ไม่ match → คอลัมน์ขวา = NULL — มาตรฐานงาน public health (ไม่ทำคนไข้หาย)

SELECT p.patient_id, p.hn, r.drug
FROM patients p
LEFT JOIN prescriptions r
       ON r.patient_id = p.patient_id
ORDER BY p.patient_id LIMIT 8;

-- ⭐ Anti-JOIN: คนที่ "ไม่มี" rx
SELECT p.hn
FROM patients p
LEFT JOIN prescriptions r
       ON r.patient_id=p.patient_id
WHERE r.rx_id IS NULL;
patient_idhndrug
10001HN-54001Metformin
10001HN-54001Lisinopril
10004HN-54004NULL
10005HN-54005NULL
✅ 103 คนไม่หาย · IS NULL row = anti-join หา “ยังไม่จ่ายยา” ได้ทันที

05 · RIGHT / FULL OUTER JOIN

SQLite รองรับตั้งแต่ v3.39 (2022) — เช็ค sqlite_version; เวอร์ชันเก่า emulate ด้วย LEFT สลับข้าง

-- RIGHT: ขวาครบ (เห็น orphan!)
SELECT r.rx_id, r.drug, p.hn
FROM patients p
RIGHT JOIN prescriptions r
        ON r.patient_id = p.patient_id;
-- rx_id=6 → hn=NULL ✓

-- FULL: สองฝั่งครบ
SELECT … FROM patients p
FULL OUTER JOIN prescriptions r
  ON r.patient_id=p.patient_id;

Venn เทียบ 3 แบบ

INNER
LEFT
A∪(A∩B)
FULL
A∪B
-- Emulate RIGHT บน SQLite เก่า:
SELECT r.*, p.*
FROM prescriptions r
LEFT JOIN patients p
  ON r.patient_id=p.patient_id; -- สลับข้าง

06 · SELF JOIN — ตารางเชื่อมตัวเอง

ใช้ alias ตั้ง 2 ชื่อให้ตารางเดียว — use case: referral chain, contact tracing, org hierarchy

referrals(rx_from, rx_to)
rx_fromrx_to
12
23
31
id ล้วน อ่านไม่รู้เรื่อง → ต้อง map เป็น hn 2 ฝั่ง
SELECT a.rx_from,
       pa.hn AS from_hn,   -- alias ฝั่งต้น
       a.rx_to,
       pb.hn AS to_hn      -- alias ฝั่งปลาย
FROM referrals a
JOIN patients pa
     ON pa.patient_id = a.rx_from
JOIN patients pb
     ON pb.patient_id = a.rx_to;
💡 patients ปรากฏ 2 ครั้ง (pa, pb) — นี่คือ SELF JOIN + dot notation ทำงานร่วมกัน

07 · Dot notation & Alias ระดับโปร

ตาราง.คอลัมน์ บอกที่มาชัด — จำเป็นเมื่อ JOIN แล้วชื่อคอลัมน์ซ้ำ (patient_id มีทั้งสองตาราง!)

-- ❌ ambiguous:
SELECT patient_id FROM patients p
JOIN prescriptions r USING(patient_id);
-- → Error: ambiguous column

-- ✅ dot notation:
SELECT p.patient_id, p.hn,
       r.rx_id, r.drug
FROM patients p
JOIN prescriptions r
     ON r.patient_id = p.patient_id;

-- suffixes แบบ pandas (เทียบ)
pd.merge(p, r, on='patient_id',
         suffixes=('_p','_r'))

Convention ที่นิยม

  • ตารางยาว → alias ตัวเดียว: patients p, prescriptions r
  • SELECT ระบุ dot เสมอ — อ่านรู้ทันทีว่าคอลัมน์ไหนมาจากไหน
  • ON ใช้ dot ทั้งคู่: r.patient_id = p.patient_id
💡 USING(col) ย่อได้เมื่อชื่อเหมือน: JOIN r USING(patient_id) — แต่ dot ชัดกว่า

08 · UNION / UNION ALL

ต่อผลลัพธ์แนวตั้ง (stack rows) — จำนวนคอลัมน์+type ต้องตรง · UNION ตัดซ้ำ / ALL คงซ้ำ(เร็วกว่า)

SELECT hn, 'มียา' AS status
FROM patients p
JOIN prescriptions r
  ON r.patient_id=p.patient_id
UNION                       -- ตัดซ้ำ
SELECT hn, 'ไม่มียา'
FROM patients p
LEFT JOIN prescriptions r
       ON r.patient_id=p.patient_id
WHERE r.rx_id IS NULL;
query A: 5 แถว
↓ UNION ↓
query B: 98 แถว
↓ dedupe
ผล ≤ 103 (unique)
💡 แยกกลุ่ม cohort แล้ว stack — เทียบ pandas: pd.concat([a,b]).drop_duplicates()

09 · GROUP BY — Split-Apply-Combine

non-aggregate ทุกคอลัมน์ต้องอยู่ใน GROUP BY — ไม่งั้น error

-- ยอดต่อผู้ป่วย (JOIN + GROUP BY)
SELECT p.hn,
       COUNT(r.rx_id)   AS n_rx,
       SUM(r.cost_thb)  AS spend,
       AVG(r.cost_thb)  AS avg_cost
FROM patients p
LEFT JOIN prescriptions r
       ON r.patient_id=p.patient_id
GROUP BY p.patient_id
ORDER BY spend DESC NULLS LAST;

-- ต่อชนิดยา
SELECT drug, COUNT(*) n, SUM(cost_thb) s
FROM prescriptions GROUP BY drug;

Diagram

10001|3510001|4210002|5510003|6810003|35
Split by patient_id ↓ Apply SUM/COUNT ↓ Combine
10001→77 | 10002→55 | 10003→103
❌: SELECT hn, drug, COUNT(*) FROM rx ไม่มี GROUP BY → "no such column"/ผลสุ่ม
📚 GROUP BY · W3Schools — เทียบ pandas groupby().agg() สัปดาห์ 2

10 · HAVING — กรองหลังจัดกลุ่ม

WHERE = กรองแถวดิบ (ก่อน group) · HAVING = กรองผล aggregate (หลัง group)

-- ผู้ป่วยที่จ่าย > 1 ใบ
SELECT p.hn, COUNT(r.rx_id) n_rx,
       SUM(r.cost_thb) total
FROM patients p
LEFT JOIN prescriptions r
  ON r.patient_id=p.patient_id
GROUP BY p.patient_id
HAVING COUNT(r.rx_id) > 1
ORDER BY total DESC;

-- WHERE + HAVING ร่วม
WHERE drug='Metformin'   -- pre-group
GROUP BY drug
HAVING AVG(cost_thb)>30  -- post-group

Pipeline ตำแหน่ง HAVING

FROM + JOIN
↓ WHERE (rows)
GROUP BY
↓ HAVING (groups)
SELECT
↓ ORDER BY → LIMIT
❌: WHERE COUNT(*)>1 → misuse! COUNT ยังไม่เกิด ณ WHERE

11 · Pipeline ฉบับเต็ม 8 ขั้น

1 FROM patients p
↓ 2 JOIN prescriptions r ON …
3 WHERE birth_date LIKE '____-__-__'
4 GROUP BY gender
↓ 5 HAVING SUM(cost)>0
6 SELECT gender, COUNT(DISTINCT…) …
↓ 7 ORDER BY spend DESC
8 LIMIT 5
SELECT p.gender,
  COUNT(DISTINCT p.patient_id) n_patients,
  SUM(r.cost_thb) spend
FROM patients p
LEFT JOIN prescriptions r
  ON r.patient_id=p.patient_id
WHERE p.birth_date LIKE '____-__-__'
GROUP BY p.gender
HAVING SUM(r.cost_thb)>0
ORDER BY spend DESC LIMIT 5;
🎯 query นี้ครบทั้ง 8 clause — ถ้าอ่านออกทีละขั้น = พร้อมสอบกลางภาค (คาบ 9)

12 · Lab สัปดาห์ที่ 7 🧪

week07-joins.ipynb — สร้างตาราง prescriptions/referrals แล้ว JOIN ครบทุกแบบ

📋 ภารกิจ ↔ § ↔ สไลด์

#ภารกิจ§สไลด์
1PRAGMA FK + CREATE prescriptions + seed§202–03
2INNER vs LEFT นับแถว + anti-join§3–404–05
3RIGHT/FULL (≥3.39) เห็น orphan§506
4SELF JOIN referrals → hn สองฝั่ง§6–707–08
5UNION vs UNION ALL นับ§709
6GROUP BY + HAVING + pipeline 8 ขั้น§8–1010–12

✅ Self-check + HW

  • INNER=5, LEFT=6 (orphan ต่างกัน)
  • anti-join เจอคนไม่มียา (NULL row)
  • DELETE patient มี rx + FK on → IntegrityError
  • HAVING COUNT>1 ได้ 2 คน (HN-54001, HN-54003)
Homework 7: สร้าง visits(visit_id PK, patient_id FK, visit_date, hba1c) + ≥5 แถว + query หาคน hba1c เฉลี่ย>7 (JOIN+GROUP+HAVING) — วาด ER ด้วย draw.io แนบภาพ

13 · Cheat Sheet — JOINs

ชนิดSyntaxได้อะไรUse case สุขภาพ
INNERA JOIN B ON kmatch เท่านั้นrx + ข้อมูลผู้ป่วย (clean cohort)
LEFTA LEFT JOIN BA ครบ + B or NULLรายงานผู้ป่วยครบทุกคน
anti-LEFTLEFT … WHERE B.k IS NULLA ที่ไม่มีใน Bยังไม่จ่ายยา / missed follow-up
RIGHTA RIGHT JOIN BB ครบ (≥3.39)audit orphan records
FULLA FULL OUTER JOIN Bทั้งสองฝั่งdata quality check
SELFt a JOIN t b ON …ตาราง×2 มุมreferral chain
UNIONq1 UNION [ALL] q2stack แนวตั้งรวม cohorts
Golden rules: เปิด PRAGMA foreign_keys=ON · dot notation เสมอ · นับ COUNT ก่อน/หลัง JOIN · non-aggregate ต้องอยู่ใน GROUP BY · filter rows=WHERE, groups=HAVING

14 · แหล่งอ้างอิงทางการ

🛠️ เสริม

📬 ติดต่อ

ดร.วิชิต สมบัติ — wichit.s@ubu.ac.th

คาบหน้า: สอบกลางภาค — ทบทวน wk05–07 + cheat sheets!