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

Data Warehouse & SQL Queries

คลังข้อมูล & การสืบค้นเชิงลึกด้วย SQL

WHERE · AND/OR/NOT · AS · ORDER BY · MIN/MAX/AVG/COUNT · DISTINCT · DATE · LIKE · IN · BETWEEN

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

  • แยก OLTP vs Data Warehouse และออกแบบ Star Schema ได้ (CLO1)
  • กรองด้วย WHERE + ตรรกะ AND/OR/NOT และจัดการ NULL ได้ถูกต้อง
  • ใช้ AS · ORDER BY · DISTINCT · aggregates · LIKE/IN/BETWEEN/DATE ได้ (CLO2)

🗺️ Learning Path

สไลด์ 2–3
Warehouse theory
โน้ตบุ๊ก
week06-sql-queries.ipynb
สไลด์ 4–13
ฝึก query พร้อมกัน
Lab + HW
สไลด์ 15
ใช้ healthinfo.db ต่อจากสัปดาห์ 5 — ไม่ต้องเริ่มใหม่

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

📚 สเปกตลอดคาบ: SELECT · Expressions · Aggregates · Date/Time · โน้ตบุ๊ก: week06-sql-queries.ipynb

01 · OLTP vs Data Warehouse

ระบบหน้างาน (OLTP) เน้นเขียนเร็วทีละแถว — คลังข้อมูล (DW) เน้นอ่านเร็วเชิงวิเคราะห์ย้อนหลัง

🏥 Pipeline: HIS → ETL → DW

HIS / EHR (OLTP)
INSERT 1 คน · Normalized สูง · JOIN เยอะ
↓ ETL ทุกคืน (Extract-Transform-Load)
Data Warehouse
Star Schema · สรุปยอด · query เร็ว
↓ BI / รายงานผู้บริหาร / ML

📊 เทียบมิติสำคัญ

มิติOLTPWarehouse
คำถามHN-54001 อยู่ไหน?HbA1c เฉลี่ยเดือนนี้?
สคีมา3NF normalizedStar/Snowflake
เขียนแถวเดี่ยวบ่อยbulk load
เครื่องมือSQLite/PostgresSQLite(เล็ก)/DuckDB/BigQuery

คาบนี้ใช้ healthinfo.db เป็น DW ย่อส่วน

02 · Basic DB Design — Star Schema

ตารางกลาง fact เก็บเหตุการณ์ · รอบนอก dimension อธิบายบริบท — WHERE/GROUP BY จะวิ่งบน fact

dim_patient
patient_id
gender
birth_date
fact_visits ⭐
visit_id PK
patient_id FK
date · hba1c · bp
→ วิเคราะห์ที่นี่
dim_date
date PK
month/year
dim_clinic
clinic_id
name
dim_drug
drug_id
name

Lab สัปดาห์ 5–6 ใช้ patients รวม fact+dim แบบย่อ — พอฝึก WHERE/aggregates โดยไม่ต้อง JOIN

✅ กฎ 3 ข้อก่อนเขียน SQL

  1. PK ชัดทุกตาราง — ตรวจด้วย PRAGMA table_info(patients)
  2. ชนิดตรง: วันที่ TEXT ISO8601 · ตัวเลข INTEGER/REAL
  3. ชื่อ snake_case — birth_date ไม่ใช่ 'Birth Date'
💡 ตรวจสคีมาปัจจุบัน: SELECT sql FROM sqlite_master WHERE type='table';

สเปก: CREATE TABLE · OMOP CDM v5.4

03 · WHERE — กรองแถว

WHERE ทำงานก่อน SELECT (pipeline สไลด์ 12) — ค่าข้อความต้องครอบด้วย '

-- หญิงเท่านั้น
SELECT * FROM patients WHERE gender='หญิง' LIMIT 5;

-- เบาหวานควบคุมไม่ดี
SELECT hn, hba1c FROM patients WHERE hba1c > 7.0;

-- ความดันสูง
SELECT hn, systolic_bp FROM patients WHERE systolic_bp >= 140;

ตัวดำเนินการ: = != < <= > >=

Diagram

patients 103 แถว
↓ WHERE gender='หญิง'
~50 แถว
↓ SELECT hn, hba1c
2 × 50
กับดัก: = NULL → 0 แถว! (สไลด์ 04)

04 · AND · OR · NOT · NULL

ผสมเงื่อนไข + วงเล็บให้ชัด (ลำดับ NOT→AND→OR) · NULL ใช้ IS NULL เท่านั้น

-- หญิง + HbA1c สูง
SELECT hn,hba1c FROM patients
WHERE gender='หญิง' AND hba1c > 7 LIMIT 5;

-- ชาย หรือ ดันสูงมาก
SELECT hn,systolic_bp FROM patients
WHERE gender='ชาย' OR systolic_bp >= 180 LIMIT 5;

-- วงเล็บเปลี่ยนความหมาย!
WHERE (g='หญิง' AND a1c>7) OR bp>180
WHERE g='หญิง' AND (a1c>7 OR bp>180)
นิพจน์ผล
hba1c = NULLUNKNOWN → 0 แถว
hba1c IS NULLTRUE ✓
NOT(gender='หญิง')ไม่รวม gender IS NULL
TRUE ✓ ผ่าน · FALSE ✗ ตก · UNKNOWN ⚠️ ตกด้วย
📚 IS NULL · AND/OR · NULL

05 · AS — Alias

ตั้งชื่อใหม่ให้คอลัมน์/ตารางในผลลัพธ์ — ใช้ใน ORDER BY ได้ แต่ใช้ใน WHERE ไม่ได้

SELECT hn AS hospital_no,
       hba1c AS a1c
FROM patients LIMIT 5;

-- นิพจน์
SELECT systolic_bp AS sbp,
       hba1c*10 AS a1c_x10
FROM patients LIMIT 5;

-- ตาราง (ใช้เยอะตอน JOIN สัปดาห์ 7)
SELECT p.hn FROM patients AS p;
SELECT p.hn FROM patients p; -- ละ AS ได้

ก่อน/หลัง

ไม่มี Alias
hnhba1c
HN-540018.1
มี Alias
hospital_noa1c
HN-540018.1
❌: SELECT hn AS x … WHERE x='HN-54001' → Error: no such column (WHERE มาก่อน SELECT)

06 · ORDER BY

ASC=น้อย→มาก(default) · DESC=มาก→น้อย · เรียงหลายคอลัมน์ได้ · NULL: ASC มาก่อน / DESC มาหลัง

-- HbA1c สูงสุด 5 คน
SELECT hn,hba1c FROM patients
ORDER BY hba1c DESC LIMIT 5;

-- 2 ชั้น: เพศ ↑ แล้ว a1c ↓
SELECT gender,hn,hba1c FROM patients
ORDER BY gender ASC, hba1c DESC LIMIT 8;

-- ใช้ alias ได้
SELECT hn AS hospital_no, hba1c
FROM patients ORDER BY hospital_no LIMIT 5;

-- เลขลำดับ (สั้นแต่เปราะ)
ORDER BY 2 DESC

Diagram: DESC top-N

12.6 🥇
10.3 🥈
10.2 🥉
… ORDER BY hba1c DESC LIMIT 3
ห้าม: LIMIT โดยไม่มี ORDER BY — ลำดับไม่การันตี

ไทย: SQLite เทียบ Unicode binary ('ชาย' < 'หญิง')

07 · MIN / MAX / AVG / COUNT / DISTINCT

Aggregate ยุบหลายแถว→ค่าเดียว · ทุกตัวข้าม NULL ยกเว้น COUNT(*) · DISTINCT ตัดซ้ำ

SELECT
  COUNT(*)           AS n_total,
  COUNT(hba1c)       AS n_measured,
  AVG(hba1c)         AS avg_a1c,
  MIN(hba1c)         AS min_a1c,
  MAX(hba1c)         AS max_a1c
FROM patients;

SELECT COUNT(DISTINCT gender) AS n_genders
FROM patients;   -- → 2

-- นับรวม NULL เป็น 0
AVG(COALESCE(hba1c,0))

Diagram

7.18.3NULL6.2
↓ AVG ข้าม NULL
(7.1+8.3+6.2)/3 = 7.2
COUNT(*)=4 · COUNT(hba1c)=3 · DISTINCT=3
ห้าม: SELECT gender, AVG(hba1c) ไม่มี GROUP BY → error/ผลสุ่ม (สัปดาห์ 7)

08 · DATE — วันที่ใน SQLite

ไม่มี DATE type — เก็บ TEXT ISO8601 แล้วใช้ date()/strftime()/julianday()

-- ปี/เดือน
SELECT hn, strftime('%Y',birth_date) y,
       strftime('%m',birth_date) m
FROM patients LIMIT 5;

-- อายุ ณ วันนี้
SELECT hn,birth_date,
 CAST((julianday('now')-julianday(birth_date))/365.25 AS INT) age
FROM patients LIMIT 5;

-- ครึ่งปีแรก 2025
WHERE birth_date BETWEEN '2025-01-01' AND '2025-06-30'
❌ '24/05/2025'
เรียงผิด · BETWEEN พัง · strftime ทำไม่ได้
✅ '2025-05-24'
เรียง/BETWEEN/strftime ครบ — มาตรฐาน FHIR/OMOP
CSV เรามี 2 รูปแบบ — กรองด้วย LIKE '____-__-__' ก่อนคำนวณเสมอ

09 · LIKE — แพทเทิร์น

% = กี่ตัวก็ได้ · _ = 1 ตัว · ใช้กับ TEXT

-- ขึ้นต้น HN-54
WHERE hn LIKE 'HN-54%'

-- เดือน 02 (ISO)
WHERE birth_date LIKE '____-02-__'

-- ตัดวันที่ผิดรูปแบบ
WHERE birth_date NOT LIKE '%/%'

-- escape % จริง
WHERE note LIKE '%100\%%' ESCAPE '\'
'HN-54%'
HN-54001 ✓ HN-54200 ✓ HN-A0001 ✗
'____-02-__'
2025-02-10 ✓ | 24/02/2025 ✗
ช้า: %…% บนตารางใหญ่ไม่มี index → scan ทั้งตาราง

10 · IN — หลายค่า

IN (a,b,c) = OR ยาวแบบอ่านง่าย · NOT IN+NULL = กับดัก

WHERE gender IN ('ชาย','หญิง')

WHERE hn IN ('HN-54001','HN-54002')

WHERE hba1c IN (7.0,7.5,8.0)

-- subquery (สัปดาห์ 8)
WHERE patient_id IN (SELECT …)

DISTINCT ช่วยสร้างลิสต์: SELECT DISTINCT gender FROM patients

OR ยาว:
gender='ชาย' OR gender='หญิง' OR gender='อื่น'
IN สั้น:
gender IN ('ชาย','หญิง','อื่น')
⚠️ NOT IN (…, NULL) → 0 แถวเสมอ — กรอง NULL ก่อน

11 · BETWEEN — ช่วงค่า (รวมขอบ)

BETWEEN a AND b>=a AND <=b — ใช้ได้ทั้งเลขและวันที่ ISO

-- เฝ้าระวัง 6.5–7.0
WHERE hba1c BETWEEN 6.5 AND 7.0

-- pre-hypertension
WHERE systolic_bp BETWEEN 120 AND 139

-- ครึ่งปีแรก
WHERE birth_date
  BETWEEN '2025-01-01' AND '2025-06-30'
6.4▶6.5 ──── 7.0◀7.1
รวมขอบทั้งสอง — ไม่รวมใช้ >/<
💡 เลขเก็บเป็น TEXT? CAST ก่อน: CAST(x AS REAL) BETWEEN 120 AND 139

12 · ลำดับการทำงานจริง (Pipeline)

เขียน SELECT…FROM…WHERE…ORDER BY…LIMIT แต่เครื่องทำตาม pipeline นี้ — จำไว้ใช้ทั้งคอร์ส

1. FROM patients (103)
↓ 2. WHERE hba1c > 7 (~40)
3. SELECT hn, hba1c AS a1c (+alias)
↓ 4. ORDER BY a1c DESC (alias ใช้ได้)
5. LIMIT 5
↓ ส่งกลับ Python
-- ✅ alias ใน ORDER BY
SELECT hn AS hospital_no, hba1c
FROM patients WHERE hba1c > 7
ORDER BY hospital_no LIMIT 5;

-- ❌ alias ใน WHERE
WHERE hospital_no = 'HN-54001'
-- → no such column
📅 สัปดาห์ 7 จะแทรก GROUP BY → HAVING หลัง WHERE

13 · Workflow Windows — ใช้ healthinfo.db ต่อ

ไม่สร้าง DB ใหม่ — เปิดไฟล์จากสัปดาห์ 5 (ถ้าลบ รัน week05 ipynb ใหม่ก่อน)

⭐ A: Jupyter

uv run jupyter lab
import sqlite3, pandas as pd
con=sqlite3.connect("healthinfo.db")
pd.read_sql("""
 SELECT hn,hba1c FROM patients
 WHERE hba1c BETWEEN 6.5 AND 7
 ORDER BY hba1c DESC LIMIT 5""",con)

B: DB Browser

scoop install sqlitebrowser
Open Database → healthinfo.db
Execute SQL → Run
sqlitebrowser.org/about

C: CLI

scoop install sqlite
sqlite3 healthinfo.db
sqlite> SELECT COUNT(*)
   ...> FROM patients WHERE hba1c>7;
sqlite> .quit
sqlite.org/cli

14 · Lab สัปดาห์ที่ 6 🧪

เปิด week06-sql-queries.ipynb ด้วย uv run jupyter lab

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

#ภารกิจ§สไลด์
1WHERE + AND/OR§1–203–04
2AS + alias ใน ORDER BY / WHERE§305
3ORDER BY + NULL order§406
4COUNT/AVG/MIN/MAX/DISTINCT§507
5DATE strftime/julianday + ตัด format ผิด§608
6LIKE / IN / BETWEEN§7–909–11

✅ Self-check + HW

  • IS NULL + IS NOT NULL รวม = 103
  • COUNT(DISTINCT gender) = 2
  • ORDER BY hba1c DESC LIMIT 3 ตรงกับ pd.read_sql
  • อธิบายได้ทำไม = NULL ได้ 0 แถว
Homework 6: ส่ง .ipynb — ตรวจด้วย sqlite3 healthinfo.db "SELECT COUNT(*) FROM patients WHERE hba1c BETWEEN 6.5 AND 7;"

15 · Cheat Sheet — Week 6

งานSQL
กรองWHERE x>7 AND (y='a' OR z IS NULL)
เรียงORDER BY a DESC, b ASC
สรุปCOUNT/AVG/MIN/MAX(col)
ตัดซ้ำSELECT DISTINCT gender …
แพทเทิร์นLIKE 'HN-54%' / '____-02-__'
หลายค่าIN (…) / NOT IN (กรอง NULL ก่อน)
ช่วงBETWEEN 120 AND 139 / '2025-01-01' AND …
วันที่strftime('%Y',d) · julianday(d)

🚫 กับดักซ้ำ ๆ

  • = NULL → 0 แถว (ใช้ IS NULL)
  • LIMIT ไม่มี ORDER BY → ลำดับสุ่ม
  • alias ใช้ใน WHERE ไม่ได้
  • aggregate คู่ non-aggregate ไม่มี GROUP BY → error
  • วันที่ format ผสม → กรอง LIKE '____-__-__' ก่อน
  • NOT IN มี NULL → 0 แถว
Pipeline: FROM → WHERE → SELECT → ORDER BY → LIMIT
(สัปดาห์ 7 เพิ่ม GROUP BY → HAVING หลัง WHERE)

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

📘 SQLite

🐍 Python

🛠️ เสริม

📬 ติดต่อ

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