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

ฐานข้อมูลเชิงสัมพันธ์ & SQL ตอนที่ 1

Relational Database & SQL (Part 1)

Database Schema · Column Types · PRIMARY KEY · SQLite · INSERT · SELECT · LIMIT

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

  • อธิบายตารางเชิงสัมพันธ์ สคีมา ชนิดคอลัมน์ และ PRIMARY KEY ได้ (CLO1)
  • สร้างฐานข้อมูล SQLite ไฟล์เดียว healthinfo.db บน Windows ได้
  • เขียน CREATE TABLE · INSERT · SELECT · LIMIT กับเวชระเบียนจริงได้ (CLO2)

🗺️ เส้นทางการเรียน (Learning Path)

อ่านสไลด์ 2–7
ทฤษฎี
เปิดโน้ตบุ๊ก
week05-sql-basics.ipynb
ฝึกสไลด์ 8–13
พร้อมโน้ตบุ๊ก
Lab + Homework
สไลด์ 15
Self-study — ทุกโค้ด copy-paste ได้ · มีปุ่มเลื่อนสไลด์ด้านบน/ล่างทุกหน้า · ลิงก์สเปกทางการในทุกสไลด์

สารบัญ — คลิกข้ามไปหัวข้อที่ต้องการ

📚 สเปกตลอดคาบ: SQLite Docs · CREATE TABLE · INSERT · SELECT · Python sqlite3 · โน้ตบุ๊ก: week05-sql-basics.ipynb

01 · ทำไมโรงพยาบาลต้องฐานข้อมูลเชิงสัมพันธ์?

เวชระเบียนกระจายหลายระบบ: Admission, Lab, Pharmacy, Billing — เก็บ Excel ไฟล์เดียว = ซ้ำซ้อน/ผิดพลาดง่าย ฐานข้อมูลเชิงสัมพันธ์แยกเป็นตารางย่อยแล้วเชื่อมด้วยคีย์

❌ Flat file — ตารางเดียว

patient_id | name | age | drug    | dose  | ward
1001 | สมชาย | 67 | Metformin | 500mg | MED
1001 | สมชาย | 67 | Lisinopril| 10mg  | MED ← ซ้ำ!

แก้ชื่อผู้ป่วย = ต้องแก้ทุกแถว → เสี่ยงไม่ตรงกัน (Update Anomaly)

✅ Relational — แยกตาราง + คีย์

patients
patient_id (PK)
name, birth_date
prescriptions
rx_id (PK)
patient_id (FK) ──┐
drug, dose
── เชื่อมด้วย patient_id ──▶ ไม่ซ้ำ · JOIN ได้ (สัปดาห์ 7)

โมเดลเดียวกับ OMOP CDM: person / condition_era / drug_exposure แยกกัน

02 · กายวิภาคของตาราง (Table Anatomy)

Table = Relation · แถว = Record (ผู้ป่วย 1 คน) · คอลัมน์ = Field (ตัวแปร) · ทุกตารางมี Schema = พิมพ์เขียวชื่อ+ชนิด+กฎ

patient_id ⭐
INTEGER · PK
hn
TEXT · UNIQUE
gender
TEXT
birth_date
TEXT ISO8601
systolic_bp
REAL
10001HN-54001ชาย1960-06-23129.0
10002HN-54002หญิง1975-02-10155.0
⭐ แถวหัวตาราง = Schema
บอกชื่อ+ชนิดทุกคอลัมน์ — เปลี่ยนยาก ต้องคิดก่อนสร้าง
แถวข้อมูล = Record
1 แถว = 1 เอนทิตี — ห้ามมี 2 แถวผู้ป่วยคนเดียวกัน (PK กันไว้)
เซลล์ = ค่าเดียว
Atomic value — ห้ามฝัง list/JSON ถ้าแยกคอลัมน์ได้
📖 สเปก: CREATE TABLE syntax · W3Schools CREATE TABLE — คีย์เวิร์ด SQL ไม่สน case แต่ชื่อตาราง/คอลัมน์ควร snake_case

03 · สคีมา & Data Dictionary

ก่อน CREATE TABLE ต้องตอบให้ได้: เก็บอะไร · ชนิดอะไร · คีย์คืออะไร · ห้ามว่างไหม — คำตอบชุดนี้คือ Data Dictionary

📋 Data Dictionary ของตาราง patients

คอลัมน์TypeConstraintคำอธิบาย
patient_idINTEGERPRIMARY KEYรหัสผู้ป่วย (ห้ามซ้ำ)
hnTEXTUNIQUE NOT NULLเลข HN
genderTEXTCHECK IN ('ชาย','หญิง')เพศ
birth_dateTEXT— (ISO8601)YYYY-MM-DD
systolic_bpREALCHECK (>0)mmHg
hba1cREAL— (NULL ได้)HbA1c % — ยังไม่วัด = NULL

เทียบของจริง: MIMIC-IV patients · OMOP person

🔍 ดูสคีมาที่สร้างแล้ว

-- CLI: .schema patients
-- Python/SQL:
SELECT sql FROM sqlite_master
WHERE type='table' AND name='patients';
-- ผล:
CREATE TABLE patients(
  patient_id INTEGER PRIMARY KEY,
  hn TEXT UNIQUE NOT NULL,
  gender TEXT CHECK(gender IN ('ชาย','หญิง')),
  birth_date TEXT,
  systolic_bp REAL CHECK(systolic_bp > 0),
  hba1c REAL
);
-- ดูคอลัมน์แบบแบน:
PRAGMA table_info(patients);
💡 Self-study: เปิด patients_data.csv ใน Excel แล้วถามตัวเอง “คอลัมน์ไหนเป็น PK? ชนิดอะไร?” ก่อนดูสไลด์ 7

04 · ชนิดคอลัมน์ (Column Types)

SQLite มี type affinity 5 แบบ — เลือกให้ตรงธรรมชาติข้อมูล ไม่งั้นเปรียบเทียบ/เรียงผิด

TEXT
ข้อความ
hn, gender, ICD-10
'HN-54001'
'ชาย'
INTEGER
จำนวนเต็ม
patient_id, อายุ
10001
67
REAL
ทศนิยม
bp, HbA1c
129.5
7.3
NUMERIC
ยืดหยุ่น
เมื่อไม่แน่ใจ
เลข/ข้อความผสม
BLOB
ไบนารี
ภาพ/ไฟล์
ไม่ใช้ในคาบนี้

⚠️ กับดักที่พบบ่อย (Anti-patterns)

❌ วันที่แบบ '24/05/2025'
ORDER/BETWEEN เรียงผิด ('07/02' > '24/05')
✅ ISO8601 '2025-05-24' — เรียง/เทียบถูก
❌ เลขเก็บเป็น TEXT '67'
เทียบแบบข้อความ: '9' > '67' เป็นจริง!
✅ INTEGER/REAL — เทียบเชิงตัวเลข

05 · Primary Key — บัตรประชาชนของแถว

PK ต้อง UNIQUE + NOT NULL + Stable — ใช้ชี้ระเบียน 1 แถวได้แม่นยำ และเป็นสะพาน JOIN ข้ามตาราง (สัปดาห์ 7)

Diagram: มี/ไม่มี PK

❌ ไม่มี PK
namedob
สมชาย1960-06-23
สมชาย1960-06-23

UPDATE/DELETE ไม่รู้ชี้แถวไหน

✅ มี PK
patient_id ⭐name
10001สมชาย
10002สมชาย

WHERE patient_id=10001 ชัดเจน

เคล็ด: INTEGER PRIMARY KEY alias rowid → auto-increment + เร็วสุดใน SQLite

💻 ลองละเมิด PK ดู

CREATE TABLE t(
  patient_id INTEGER PRIMARY KEY,
  hn TEXT UNIQUE NOT NULL
);

INSERT INTO t VALUES (10001,'HN-A');   -- ok
INSERT INTO t VALUES (10001,'HN-B');
-- ▶︎ IntegrityError:
-- UNIQUE constraint failed: t.patient_id

INSERT INTO t VALUES (NULL,'HN-C');
-- ▶︎ IntegrityError: NOT NULL constraint
💡 Constraint คือระบบนิรภัย — ให้ DB ด่าเราตอน INSERT ดีกว่าข้อมูลพังภายหลัง

06 · SQLite — ฐานข้อมูลในไฟล์เดียว

ไม่ต้องติดตั้งเซิร์ฟเวอร์ · ไม่มี user/password · ก๊อปปี้ไฟล์ = ก๊อปปี้ฐานข้อมูลทั้งฐาน — เหมาะกับ self-study ออฟไลน์

📦 healthinfo.db — ไฟล์เดียวจบ

healthinfo.db
1 ไฟล์ = หลายตาราง
patients | visits | …
ส่งอาจารย์ = แนบไฟล์ .db ไปเลย
vs Server DB (Postgres/BigQuery): ต้องรัน daemon · ตั้ง user/pass · เปิด port — เกินจำเป็นตอนเรียนพื้นฐาน

🛠️ เปิดใช้ 3 วิธี (Windows)

  1. แนะนำ Python sqlite3 — มากับ Python อยู่แล้ว:
    import sqlite3; con=sqlite3.connect("healthinfo.db")
    docs.python.org/sqlite3#tutorial
  2. DB Browser (GUI): scoop install sqlitebrowser — ลากไฟล์ .db เข้า ดูตารางเหมือน Excel
    sqlitebrowser.org
  3. sqlite3 CLI: scoop install sqlitesqlite3 healthinfo.db
    sqlite.org/cli
💡 แล็บนี้ใช้ Python ใน Jupyter — รันด้วย uv run jupyter lab (ติดตั้ง scoop/uv ดูสไลด์ 14)
📚 ทางการ: About SQLite · One-file DB · When to use SQLite

07 · CREATE TABLE — สร้างตาราง patients

Syntax อ่านจาก Data Dictionary สไลด์ 03 → แปลงเป็น DDL ตรง ๆ

CREATE TABLE IF NOT EXISTS ชื่อตาราง (
  คอลัมน์ ชนิด [CONSTRAINT],
  ... );
-- IF NOT EXISTS: รันซ้ำไม่ error · CONSTRAINT: PK/UNIQUE/CHECK/NOT NULL

💻 โค้ดเต็ม (ใช้ใน ipynb)

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

cur.execute("""
CREATE TABLE IF NOT EXISTS patients(
  patient_id   INTEGER PRIMARY KEY,
  hn           TEXT UNIQUE NOT NULL,
  gender       TEXT CHECK(gender IN ('ชาย','หญิง')),
  birth_date   TEXT,
  systolic_bp  REAL CHECK(systolic_bp > 0),
  hba1c        REAL
);
""")
con.commit()

DDL ไม่ต้อง commit ก็ได้ใน SQLite แต่ commit เป็นนิสัยที่ดี

🧩 ตรวจว่าได้ตามแผนไหม

FieldTypeKey/Rule
patient_idINTEGERPK ⭐
hnTEXTUNIQUE NOT NULL
genderTEXTCHECK IN
birth_dateTEXTISO8601
systolic_bpREALCHECK > 0
hba1cREALNULL ได้
ห้าม: ชื่อคอลัมน์มี space/ภาษาไทยล้วน — ใช้ snake_case

08 · INSERT — เพิ่มข้อมูล

1 แถว: execute · หลายแถว: executemany · ละเมิด PK/UNIQUE = error ทันที (หรือ OR IGNORE ข้าม)

💻 1 แถว + batch จาก CSV

# 1 แถว
cur.execute("""
INSERT INTO patients
  (patient_id,hn,gender,birth_date,systolic_bp,hba1c)
VALUES (10001,'HN-54001','ชาย','1960-06-23',129.0,8.1)
""")

# หลายแถวจาก CSV (parameterized = กัน injection)
import csv
with open('patients_data.csv', encoding='utf-8') as f:
    rows = [(int(float(r['subject_id'])),
             f"HN-{int(float(r['subject_id']))}",
             r['gender'], None,
             float(r['systolic_bp']) if r['systolic_bp'] else None,
             float(r['HbA1c_level']) if r['HbA1c_level'] else None)
            for r in csv.DictReader(f)]
cur.executemany("INSERT OR IGNORE INTO patients VALUES (?,?,?,?,?,?)", rows)

con.commit()  # 🔴 ลืม commit = ข้อมูลหาย!

🛡️ Parameterized query

❌ f-string: cur.execute(f"INSERT ... VALUES ({name})")
✅ placeholder: cur.execute("INSERT ... VALUES (?)", (name,))

Placeholder ?/:name ให้ driver escape ให้ — ปลอดภัยจาก SQL injection เสมอ

Diagram: commit

execute() → อยู่ใน transaction
↓ con.commit()
เขียนลงไฟล์ .db จริง 💾

con.rollback() ย้อนได้ถ้ายังไม่ commit

09 · NULL ≠ '' — ค่าว่าง 2 แบบ

เวชระเบียน: HbA1c ที่ยังไม่ได้วัด = NULL (ไม่ทราบ) — ต่างจาก '' (ทราบว่าว่าง) — ตรวจด้วย IS NULL เท่านั้น!

ค่าความหมายทางคลินิกตรวจด้วย
NULLยังไม่ได้ตรวจ / ไม่ทราบIS NULL
''ตรวจแล้ว แต่บันทึกว่าง= ''
-- ✅ ถูก: หาคนที่ยังไม่ได้วัด
SELECT hn FROM patients WHERE hba1c IS NULL;

-- ❌ ผิด: ได้ 0 แถวเสมอ!
SELECT hn FROM patients WHERE hba1c = NULL;
ทำไม? NULL เทียบกับอะไรก็ได้ UNKNOWN (three-valued logic) — WHERE คัดเฉพาะ TRUE

Diagram: three-valued logic

TRUE → แถวผ่าน ✓
FALSE → ตก ✗
UNKNOWN (มี NULL) → ตกเช่นกัน ⚠️
💡 ในโน้ตบุ๊ก section 7 มีเซลล์ทดลอง = NULL ให้เห็น 0 แถวด้วยตา

อ่านต่อ: NULL values (sqlite.org)

📚 สเปก: IS NULL · W3Schools NULL

10 · SELECT — สืบค้นข้อมูล

SELECT คอลัมน์ FROM ตาราง;*=ทุกคอลัมน์ · ระบุชื่อ = เร็วกว่า/ชัดกว่า · ใช้ AS ตั้งชื่อผลลัพธ์ได้

💻 ตัวอย่าง

SELECT * FROM patients;                    -- ทุกคอลัมน์
SELECT hn, hba1c FROM patients;            -- เลือกบางคอลัมน์
SELECT hn AS hospital_no,                  -- alias
       hba1c * 10 AS a1c_x10
FROM patients;
SELECT COUNT(*) FROM patients;             -- นับแถว (สไลด์ 08)

-- ใน Jupyter: เป็น DataFrame ได้เลย
pd.read_sql("SELECT hn, hba1c FROM patients", con)

งานจริงเลี่ยง * กับตารางใหญ่ — transfer น้อยลง 10–100×

Diagram: SELECT ไหลอย่างไร

patients
103 แถว × 6 คอลัมน์
↓ FROM patients
SELECT hn, hba1c
103 แถว × 2 คอลัมน์
↓ ส่งกลับ Python
DataFrame / rows
💡 สัปดาห์ 6 จะเพิ่ม WHERE กรองแถวก่อน SELECT

11 · LIMIT / OFFSET — ดูบางส่วน

ตารางเวชระเบียนแสนแถว — LIMIT n ดูตัวอย่าง n แถว · LIMIT 5 OFFSET 5 ข้าม 5 แล้วเอา 5 (หน้า 2)

💻 ตัวอย่าง

-- 5 แถวแรก — ตรวจหลัง INSERT เสมอ
SELECT * FROM patients LIMIT 5;

-- หน้า 2 (แถว 6-10)
SELECT patient_id, hn FROM patients LIMIT 5 OFFSET 5;

-- เทียบ pandas
pd.read_sql("SELECT * FROM patients LIMIT 5", con)
df.head(5)  # เทียบเท่า
💡 DB Browser มีปุ่ม Browse Data ใส่ LIMIT ให้เอง — แต่เขียน SQL เองควบคุมได้แม่นกว่า

Pagination Diagram

Row 1-5| Row 6-10 ◀ OFFSET 5| Row 11-15
LIMIT 5 → เอาแค่ 5 แถว
ห้าม: LIMIT โดยไม่มี ORDER BY — ลำดับไม่การันตี รันใหม่ได้คนละแถว! (ORDER BY สัปดาห์ 6)

12 · ลำดับการทำงานจริง (Week-5 subset)

เขียน SELECT … FROM … LIMIT — เครื่องทำ FROM → SELECT → LIMIT — เข้าใจลำดับ = ไม่งงว่าทำไม LIMIT ได้ก่อน/หลัง

Diagram: Pipeline

1. FROM patients (103 แถว)
2. SELECT hn, hba1c (ตัดคอลัมน์, ตั้ง alias)
3. LIMIT 5 (ตัดแถวท้าย)
5 แถวส่งกลับ Python

💻 ผลของลำดับ

-- ✅ ปกติ: FROM → SELECT → LIMIT
SELECT hn FROM patients LIMIT 3;
📅 สัปดาห์ 6 จะแทรก WHERE ระหว่าง FROM กับ SELECT และเพิ่ม ORDER BY ก่อน LIMIT — ลำดับเต็มคือ
FROM → WHERE → SELECT → ORDER BY → LIMIT

จำ pipeline นี้ไว้ — ทุก query ที่เหลือของคอร์สวางบนโครงเดียวกัน

📚 สเปก: SELECT syntax order

13 · Workflow บน Windows (ออฟไลน์)

ติดตั้งครั้งเดียวด้วย scoop + uv — แล้วเลือกวิธีเปิด healthinfo.db

🔧 ติดตั้งครั้งแรก (PowerShell)

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
scoop install git uv sqlite sqlitebrowser
D: ; mkdir health-informatics ; cd health-informatics
uv init ; uv add jupyterlab pandas
uv run jupyter lab   # → http://localhost:8888

คู่มือ: scoop.sh · uv installation · uv projects

⭐ A: Python + Jupyter

uv run jupyter lab
# เปิด week05-sql-basics.ipynb
import sqlite3, pandas as pd
con = sqlite3.connect("healthinfo.db")
pd.read_sql("SELECT * FROM patients LIMIT 5", con)

ดี: รวม pandas · ส่ง .ipynb ได้

B: DB Browser (GUI)

scoop install sqlitebrowser
# Open Database → healthinfo.db
# Browse Data / Execute SQL tab

ดี: เห็นตารางแบบ Excel

→ sqlitebrowser.org/about

C: sqlite3 CLI

scoop install sqlite
sqlite3 healthinfo.db
sqlite> .tables
sqlite> SELECT * FROM patients LIMIT 5;
sqlite> .quit

ดี: เบา · ใช้ในสคริปต์

→ sqlite.org/cli

14 · Lab สัปดาห์ที่ 5 🧪 — ทำตามในโน้ตบุ๊ก

เปิด week05-sql-basics.ipynb ด้วย uv run jupyter lab — รันออฟไลน์ได้ทุกเครื่อง

📋 6 ภารกิจ ↔ เซลล์ในโน้ตบุ๊ก

#ภารกิจipynb §สไลด์
1สร้าง DB + CREATE TABLE patients§207
2INSERT 3 แถว + executemany CSV (~100 แถว)§3–408
3SELECT เลือกคอลัมน์ + alias + pandas§510
4LIMIT / OFFSET + เทียบ pd.read_csv§611
5IS NULL vs = NULL (0 แถว!)§709
6ละเมิด PK → IntegrityError + OR IGNORE§805
ไฟล์: week05-sql-basics.ipynb · ข้อมูล patients_data.csv · ผลลัพธ์ healthinfo.db

✅ Self-check + Homework

  • SELECT COUNT(*) FROM patients; ≈ 103 (บางแถวถูก OR IGNORE ข้าม)
  • SELECT sql FROM sqlite_master; เห็น PK/UNIQUE/CHECK ครบ
  • INSERT PK ซ้ำ → UNIQUE constraint failed และอธิบายได้
Homework 5: ส่ง .ipynb + healthinfo.db — อาจารย์ตรวจด้วย
sqlite3 healthinfo.db "SELECT COUNT(*) FROM patients;"
sqlite3 healthinfo.db ".schema patients"
💡 ทำไม่ทันในคาบ? โน้ตบุ๊กทุกเซลล์มีเฉลย inline — รันตามแล้วอ่านคำอธิบายทีละเซลล์

15 · Cheat Sheet — สรุปทั้งคาบ

หน้าเดียวจบ — ใช้ทบทวนก่อนสอบกลางภาค (คาบ 9)

⌨️ คำสั่ง

งานSQL
สร้างตารางCREATE TABLE IF NOT EXISTS t(col TYPE RULE,…)
เพิ่มแถวINSERT [OR IGNORE] INTO t VALUES(?)
หลายแถวcur.executemany(sql, rows)
อ่านSELECT cols FROM t [LIMIT n]
บันทึกcon.commit()
ดูสคีมา.schema t / PRAGMA table_info(t)
นับSELECT COUNT(*) FROM t

🚫 Error ที่เจอบ่อย & วิธีแก้

Errorสาเหตุ/แก้
UNIQUE constraint failedPK/HN ซ้ำ → เปลี่ยน id หรือใช้ INSERT OR IGNORE
no such columnพิมพ์ชื่อผิด → PRAGMA table_info(t)
no such tableconnect ผิดไฟล์/ลืม CREATE → ls *.db
ข้อมูลหายหลังรันลืม con.commit()
WHERE x = NULL ได้ 0ใช้ IS NULL เสมอ
Python side: connect → cursor → execute/executemany → commit → close

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

ทุกลิงก์เป็นสเปก/เอกสารทางการ — อ่านต่อด้วยตนเองได้เต็มรูปแบบ

🛠️ Tools & เสริม

📬 ติดต่อ

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

สัปดาห์หน้า: Data Warehouse & SQL Queries (WHERE/ORDER BY/aggregates) — เก็บ healthinfo.db ไว้ใช้ต่อ!

เก่งมาก! — ต่อไป: เปิด week06-sql-queries.ipynb / สไลด์สัปดาห์ 6