การออกแบบฐานข้อมูลขั้นสูง & การเชื่อมตาราง
ER Diagram · FOREIGN KEY · INNER/LEFT/RIGHT/FULL JOIN · SELF JOIN · dot notation · UNION · GROUP BY · HAVING
Entity = ตาราง · Relationship = เส้นเชื่อม · Cardinality บอกจำนวน: 1:1 · 1:N · M:N — ใช้ crow's foot notation
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
)""")
FOREIGN KEY constraint failed-- ลบแบบ cascade (ใช้อย่างระวัง) CREATE TABLE t(… REFERENCES patients(patient_id) ON DELETE CASCADE);
แถวที่ 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'
| hn | drug | cost |
|---|---|---|
| HN-54001 | Metformin | 35 |
| HN-54001 | Lisinopril | 42 |
| HN-54002 | Metformin | 55 |
| HN-54003 | Atorva… | 68 |
| HN-54003 | Metformin | 35 |
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_id | hn | drug |
|---|---|---|
| 10001 | HN-54001 | Metformin |
| 10001 | HN-54001 | Lisinopril |
| 10004 | HN-54004 | NULL |
| 10005 | HN-54005 | NULL |
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;
-- Emulate RIGHT บน SQLite เก่า: SELECT r.*, p.* FROM prescriptions r LEFT JOIN patients p ON r.patient_id=p.patient_id; -- สลับข้าง
ใช้ alias ตั้ง 2 ชื่อให้ตารางเดียว — use case: referral chain, contact tracing, org hierarchy
| rx_from | rx_to |
|---|---|
| 1 | 2 |
| 2 | 3 |
| 3 | 1 |
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;
ตาราง.คอลัมน์ บอกที่มาชัด — จำเป็นเมื่อ 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'))
patients p, prescriptions rr.patient_id = p.patient_idJOIN r USING(patient_id) — แต่ dot ชัดกว่าต่อผลลัพธ์แนวตั้ง (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;
pd.concat([a,b]).drop_duplicates()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;
SELECT hn, drug, COUNT(*) FROM rx ไม่มี GROUP BY → "no such column"/ผลสุ่ม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
WHERE COUNT(*)>1 → misuse! COUNT ยังไม่เกิด ณ WHERESELECT 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;
week07-joins.ipynb — สร้างตาราง prescriptions/referrals แล้ว JOIN ครบทุกแบบ
| # | ภารกิจ | § | สไลด์ |
|---|---|---|---|
| 1 | PRAGMA FK + CREATE prescriptions + seed | §2 | 02–03 |
| 2 | INNER vs LEFT นับแถว + anti-join | §3–4 | 04–05 |
| 3 | RIGHT/FULL (≥3.39) เห็น orphan | §5 | 06 |
| 4 | SELF JOIN referrals → hn สองฝั่ง | §6–7 | 07–08 |
| 5 | UNION vs UNION ALL นับ | §7 | 09 |
| 6 | GROUP BY + HAVING + pipeline 8 ขั้น | §8–10 | 10–12 |
visits(visit_id PK, patient_id FK, visit_date, hba1c) + ≥5 แถว + query หาคน hba1c เฉลี่ย>7 (JOIN+GROUP+HAVING) — วาด ER ด้วย draw.io แนบภาพ| ชนิด | Syntax | ได้อะไร | Use case สุขภาพ |
|---|---|---|---|
| INNER | A JOIN B ON k | match เท่านั้น | rx + ข้อมูลผู้ป่วย (clean cohort) |
| LEFT | A LEFT JOIN B | A ครบ + B or NULL | รายงานผู้ป่วยครบทุกคน |
| anti-LEFT | LEFT … WHERE B.k IS NULL | A ที่ไม่มีใน B | ยังไม่จ่ายยา / missed follow-up |
| RIGHT | A RIGHT JOIN B | B ครบ (≥3.39) | audit orphan records |
| FULL | A FULL OUTER JOIN B | ทั้งสองฝั่ง | data quality check |
| SELF | t a JOIN t b ON … | ตาราง×2 มุม | referral chain |
| UNION | q1 UNION [ALL] q2 | stack แนวตั้ง | รวม cohorts |
PRAGMA foreign_keys=ON · dot notation เสมอ · นับ COUNT ก่อน/หลัง JOIN · non-aggregate ต้องอยู่ใน GROUP BY · filter rows=WHERE, groups=HAVING