Relational Database & SQL (Part 1)
Database Schema · Column Types · PRIMARY KEY · SQLite · INSERT · SELECT · LIMIT
healthinfo.db บน Windows ได้CREATE TABLE · INSERT · SELECT · LIMIT กับเวชระเบียนจริงได้ (CLO2)เวชระเบียนกระจายหลายระบบ: Admission, Lab, Pharmacy, Billing — เก็บ Excel ไฟล์เดียว = ซ้ำซ้อน/ผิดพลาดง่าย ฐานข้อมูลเชิงสัมพันธ์แยกเป็นตารางย่อยแล้วเชื่อมด้วยคีย์
แก้ชื่อผู้ป่วย = ต้องแก้ทุกแถว → เสี่ยงไม่ตรงกัน (Update Anomaly)
โมเดลเดียวกับ OMOP CDM: person / condition_era / drug_exposure แยกกัน
Table = Relation · แถว = Record (ผู้ป่วย 1 คน) · คอลัมน์ = Field (ตัวแปร) · ทุกตารางมี Schema = พิมพ์เขียวชื่อ+ชนิด+กฎ
| patient_id ⭐ INTEGER · PK |
hn TEXT · UNIQUE |
gender TEXT |
birth_date TEXT ISO8601 |
systolic_bp REAL |
|---|---|---|---|---|
| 10001 | HN-54001 | ชาย | 1960-06-23 | 129.0 |
| 10002 | HN-54002 | หญิง | 1975-02-10 | 155.0 |
ก่อน CREATE TABLE ต้องตอบให้ได้: เก็บอะไร · ชนิดอะไร · คีย์คืออะไร · ห้ามว่างไหม — คำตอบชุดนี้คือ Data Dictionary
| คอลัมน์ | Type | Constraint | คำอธิบาย |
|---|---|---|---|
| patient_id | INTEGER | PRIMARY KEY | รหัสผู้ป่วย (ห้ามซ้ำ) |
| hn | TEXT | UNIQUE NOT NULL | เลข HN |
| gender | TEXT | CHECK IN ('ชาย','หญิง') | เพศ |
| birth_date | TEXT | — (ISO8601) | YYYY-MM-DD |
| systolic_bp | REAL | CHECK (>0) | mmHg |
| hba1c | REAL | — (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);
patients_data.csv ใน Excel แล้วถามตัวเอง “คอลัมน์ไหนเป็น PK? ชนิดอะไร?” ก่อนดูสไลด์ 7SQLite มี type affinity 5 แบบ — เลือกให้ตรงธรรมชาติข้อมูล ไม่งั้นเปรียบเทียบ/เรียงผิด
PK ต้อง UNIQUE + NOT NULL + Stable — ใช้ชี้ระเบียน 1 แถวได้แม่นยำ และเป็นสะพาน JOIN ข้ามตาราง (สัปดาห์ 7)
| name | dob |
|---|---|
| สมชาย | 1960-06-23 |
| สมชาย | 1960-06-23 |
UPDATE/DELETE ไม่รู้ชี้แถวไหน
| patient_id ⭐ | name |
|---|---|
| 10001 | สมชาย |
| 10002 | สมชาย |
WHERE patient_id=10001 ชัดเจน
เคล็ด: INTEGER PRIMARY KEY alias rowid → auto-increment + เร็วสุดใน SQLite
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
ไม่ต้องติดตั้งเซิร์ฟเวอร์ · ไม่มี user/password · ก๊อปปี้ไฟล์ = ก๊อปปี้ฐานข้อมูลทั้งฐาน — เหมาะกับ self-study ออฟไลน์
import sqlite3; con=sqlite3.connect("healthinfo.db")scoop install sqlitebrowser — ลากไฟล์ .db เข้า ดูตารางเหมือน Excelscoop install sqlite → sqlite3 healthinfo.dbuv run jupyter lab (ติดตั้ง scoop/uv ดูสไลด์ 14)Syntax อ่านจาก Data Dictionary สไลด์ 03 → แปลงเป็น DDL ตรง ๆ
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 เป็นนิสัยที่ดี
| Field | Type | Key/Rule |
|---|---|---|
| patient_id | INTEGER | PK ⭐ |
| hn | TEXT | UNIQUE NOT NULL |
| gender | TEXT | CHECK IN |
| birth_date | TEXT | ISO8601 |
| systolic_bp | REAL | CHECK > 0 |
| hba1c | REAL | NULL ได้ |
snake_case1 แถว: execute · หลายแถว: executemany · ละเมิด PK/UNIQUE = error ทันที (หรือ OR IGNORE ข้าม)
# 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 = ข้อมูลหาย!
Placeholder ?/:name ให้ driver escape ให้ — ปลอดภัยจาก SQL injection เสมอ
con.rollback() ย้อนได้ถ้ายังไม่ commit
เวชระเบียน: HbA1c ที่ยังไม่ได้วัด = NULL (ไม่ทราบ) — ต่างจาก '' (ทราบว่าว่าง) — ตรวจด้วย IS NULL เท่านั้น!
| ค่า | ความหมายทางคลินิก | ตรวจด้วย |
|---|---|---|
| NULL | ยังไม่ได้ตรวจ / ไม่ทราบ | IS NULL |
| '' | ตรวจแล้ว แต่บันทึกว่าง | = '' |
-- ✅ ถูก: หาคนที่ยังไม่ได้วัด SELECT hn FROM patients WHERE hba1c IS NULL; -- ❌ ผิด: ได้ 0 แถวเสมอ! SELECT hn FROM patients WHERE hba1c = NULL;
= NULL ให้เห็น 0 แถวด้วยตาอ่านต่อ: NULL values (sqlite.org)
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×
WHERE กรองแถวก่อน SELECTตารางเวชระเบียนแสนแถว — 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) # เทียบเท่า
เขียน SELECT … FROM … LIMIT — เครื่องทำ FROM → SELECT → LIMIT — เข้าใจลำดับ = ไม่งงว่าทำไม LIMIT ได้ก่อน/หลัง
-- ✅ ปกติ: FROM → SELECT → LIMIT SELECT hn FROM patients LIMIT 3;
WHERE ระหว่าง FROM กับ SELECT และเพิ่ม ORDER BY ก่อน LIMIT — ลำดับเต็มคือFROM → WHERE → SELECT → ORDER BY → LIMITจำ pipeline นี้ไว้ — ทุก query ที่เหลือของคอร์สวางบนโครงเดียวกัน
ติดตั้งครั้งเดียวด้วย scoop + uv — แล้วเลือกวิธีเปิด healthinfo.db
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
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 ได้
scoop install sqlitebrowser # Open Database → healthinfo.db # Browse Data / Execute SQL tab
ดี: เห็นตารางแบบ Excel
→ sqlitebrowser.org/aboutscoop install sqlite sqlite3 healthinfo.db sqlite> .tables sqlite> SELECT * FROM patients LIMIT 5; sqlite> .quit
ดี: เบา · ใช้ในสคริปต์
→ sqlite.org/cliเปิด week05-sql-basics.ipynb ด้วย uv run jupyter lab — รันออฟไลน์ได้ทุกเครื่อง
| # | ภารกิจ | ipynb § | สไลด์ |
|---|---|---|---|
| 1 | สร้าง DB + CREATE TABLE patients | §2 | 07 |
| 2 | INSERT 3 แถว + executemany CSV (~100 แถว) | §3–4 | 08 |
| 3 | SELECT เลือกคอลัมน์ + alias + pandas | §5 | 10 |
| 4 | LIMIT / OFFSET + เทียบ pd.read_csv | §6 | 11 |
| 5 | IS NULL vs = NULL (0 แถว!) | §7 | 09 |
| 6 | ละเมิด PK → IntegrityError + OR IGNORE | §8 | 05 |
SELECT COUNT(*) FROM patients; ≈ 103 (บางแถวถูก OR IGNORE ข้าม)SELECT sql FROM sqlite_master; เห็น PK/UNIQUE/CHECK ครบUNIQUE constraint failed และอธิบายได้.ipynb + healthinfo.db — อาจารย์ตรวจด้วยsqlite3 healthinfo.db "SELECT COUNT(*) FROM patients;"sqlite3 healthinfo.db ".schema patients"หน้าเดียวจบ — ใช้ทบทวนก่อนสอบกลางภาค (คาบ 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 | สาเหตุ/แก้ |
|---|---|
| UNIQUE constraint failed | PK/HN ซ้ำ → เปลี่ยน id หรือใช้ INSERT OR IGNORE |
| no such column | พิมพ์ชื่อผิด → PRAGMA table_info(t) |
| no such table | connect ผิดไฟล์/ลืม CREATE → ls *.db |
| ข้อมูลหายหลังรัน | ลืม con.commit() |
| WHERE x = NULL ได้ 0 | ใช้ IS NULL เสมอ |
connect → cursor → execute/executemany → commit → closeทุกลิงก์เป็นสเปก/เอกสารทางการ — อ่านต่อด้วยตนเองได้เต็มรูปแบบ
scoop install sqlitebrowserดร.วิชิต สมบัติ — staff.sci.ubu.ac.th/wichit.s · wichit.s@ubu.ac.th
สัปดาห์หน้า: Data Warehouse & SQL Queries (WHERE/ORDER BY/aggregates) — เก็บ healthinfo.db ไว้ใช้ต่อ!