สัปดาห์ที่ 08 · Data with AI

ตาราง — list / dict / CSV / JSON / SQLite

ในคอร์สเดิม สิ่งเหล่านี้สอนกระจาย 4 หัวข้อ · ในคู่มือนี้รวมเป็นเรื่องเดียว — "ทุกอย่างคือตาราง" · แค่เก็บที่ต่างกัน เลือกตามขนาด/ความถาวร/จำนวนผู้ใช้

เป้าหมายสัปดาห์นี้

🔒 ทำไม "ออกแบบ schema" คือทักษะที่ AI ทำให้คุณไม่ได้ AI เขียน read_csv ได้ใน 5 วินาที · แต่ "ตัดสินใจว่าจะเก็บ field อะไร · type อะไร · จับคู่ตารางยังไง" = งานของคุณ · ถ้าออกแบบผิดตอนแรก — แก้ทีหลังเจ็บปวด · AI ก็ไม่รู้ว่า Grading App ของคุณ จะใช้กี่ user · เก็บนานแค่ไหน · ต้อง query อะไรบ้าง · นั่นเป็น domain knowledge ของคุณ

🎯 Running Case — เก็บข้อมูล Grading App ของพี่นัท

W04-W07 เราสร้าง logic ของ Grading App แล้ว · ตอนนี้ต้องตัดสินใจว่า "ข้อมูลจริง" จะเก็บไว้ที่ไหน · พี่นัทมีข้อมูล 4 ชนิด — นักศึกษา 50 คน · drawing assignments · rubric items · คะแนน · เลือก format ผิด = ปวดหัวภายหลัง

🗺 Data Storage Ladder — เลือกตามขนาดของปัญหา

ทุก format มีจุดที่ "ดีที่สุด" · ไต่ขึ้นเมื่อปัญหาใหญ่ขึ้น · ไม่ใช่ "ใช้ SQL ตลอด"

flowchart TB L1["1. ตัวแปร (RAM)
list / dict
ทดลอง · code ตัวอย่าง · ใน function
❌ ปิดโปรแกรม = หาย"] L2["2. ไฟล์ในเครื่อง
.json / .csv / .txt
script ส่วนตัว · config · < 1000 row
❌ 2 คนเขียนพร้อมกัน = ชน"] L3["3. Database 1 ไฟล์
SQLite
app เล็ก · 1 user · query ซับซ้อน
❌ หลายคนพร้อมกัน lock ทั้งไฟล์"] L4["4. Database จริงจัง
PostgreSQL / MySQL
หลาย user · production · network
⚠️ ต้อง host server · backup"] L1 -->|"> 1000 row
ต้องคงอยู่"| L2 L2 -->|"ต้อง search/filter
หรือมี relationship"| L3 L3 -->|"หลาย user/server
พร้อมกัน"| L4 classDef ram fill:#0d3f3a,stroke:#34d399,color:#fff classDef file fill:#3d2c1a,stroke:#ffd43b,color:#fff classDef sqlite fill:#1e3a5f,stroke:#3776ab,color:#fff classDef prod fill:#5c1818,stroke:#ef4444,color:#fff class L1 ram class L2 file class L3 sqlite class L4 prod
กฎ — เริ่มล่างสุดเสมอ Grading App ใหม่ → เริ่ม list/dict ใน RAM · พอเริ่มทดลองจริง → JSON file · พอใส่จริง 50 คน → SQLite · ถ้ามี TA หลายคนใช้พร้อมกัน → PostgreSQL · 90% ของ project นักศึกษาปี 1 อยู่แค่ขั้น 1-2

🏭 ข้อมูลที่ Engineer เจอจริง

ไม่ใช่แค่ list ของนักศึกษา — ลองดูว่าแต่ละสาขาเจอข้อมูลแบบไหน

สาขาข้อมูลที่เจอจริงFormat ที่ใช้บ่อย
Mechatronics / RoboticsSensor log (อุณหภูมิ, vibration, distance)CSV (จาก data logger) · MQTT → InfluxDB
Electrical / PowerEnergy meter readings · Power qualityCSV ทุก 15 นาที · SQLite ใน gateway
Civil / Smart BuildingBuilding automation log · OccupancyCSV หรือ JSON ผ่าน BMS
ChemicalLab measurement · Batch recordsExcel/CSV · LIMS database
IndustrialProduction data · Quality controlSQL (MES) · Excel report
MechanicalPart list (BOM) · CAD propertiesExcel · JSON (config)
ทุกสาขา (vibe-coding)Form responses · API JSON · configGoogle Sheets · JSON · SQLite
สังเกต — CSV ครองโลก vendor data เกือบทุก data logger / instrument / sensor export มาเป็น CSV · ถ้าทำงานกับ hardware แทบจะเห็น CSV ตลอด · ต้องอ่าน + clean CSV ให้คล่อง = essential skill

🔑 หลักคิด — All Data is a Table

ลองดู — ทุกอย่างมีหน้าตาเหมือนตาราง · แค่ภาษา/รูปแบบต่างกัน

idnameemailyear
1Ployploy@ubu.ac.th1
2Bankbank@ubu.ac.th1
3Mintmint@ubu.ac.th2

เขียนแบบเดียวกัน — 5 รูปแบบ

1. Python list of dicts (in RAM):

students = [
    {"id": 1, "name": "Ploy", "email": "ploy@ubu.ac.th", "year": 1},
    {"id": 2, "name": "Bank", "email": "bank@ubu.ac.th", "year": 1},
    {"id": 3, "name": "Mint", "email": "mint@ubu.ac.th", "year": 2},
]

2. JSON file (in disk):

[
  {"id": 1, "name": "Ploy", "email": "ploy@ubu.ac.th", "year": 1},
  {"id": 2, "name": "Bank", "email": "bank@ubu.ac.th", "year": 1},
  {"id": 3, "name": "Mint", "email": "mint@ubu.ac.th", "year": 2}
]

3. CSV file (in disk):

id,name,email,year
1,Ploy,ploy@ubu.ac.th,1
2,Bank,bank@ubu.ac.th,1
3,Mint,mint@ubu.ac.th,2

4. Pandas DataFrame (RAM, but "table-aware"):

import pandas as pd
df = pd.read_csv("students.csv")
print(df)

5. SQLite (file but searchable):

CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name TEXT,
    email TEXT,
    year INTEGER
);
INSERT INTO students VALUES (1, 'Ploy', 'ploy@ubu.ac.th', 1);
...
เลือกอันไหน? ดูได้จากตารางใน W02 — Database Overview · สั้น ๆ: เริ่มจาก list/dict → เมื่อข้อมูลเยอะค่อยใส่ JSON/CSV → เมื่อต้อง query/multi-user ค่อยขึ้น SQLite

🧱 list — ลำดับของอะไรก็ได้

scores = [85, 72, 91, 60, 45] # เข้าถึงสมาชิก (index เริ่มที่ 0) print("scores[0] =", scores[0]) # ตัวแรก print("scores[-1] =", scores[-1]) # ตัวสุดท้าย print("scores[1:3] =", scores[1:3]) # slice # เพิ่ม / ลบ scores.append(78) scores.insert(0, 100) print("after append+insert:", scores) # functions ที่ใช้บ่อย print("len:", len(scores), "| sum:", sum(scores)) print("max:", max(scores), "| min:", min(scores)) print("sorted:", sorted(scores))

List comprehension — เขียน loop เป็น 1 บรรทัด

scores = [85, 72, 91, 60, 45, 33] # เดิม: 3 บรรทัด doubled = [] for s in scores: doubled.append(s * 2) print("doubled (long):", doubled) # ใหม่: 1 บรรทัด doubled2 = [s * 2 for s in scores] print("doubled (comp):", doubled2) # มี filter passing = [s for s in scores if s >= 50] print("passing:", passing) # ใช้ใน data work บ่อยมาก emails = [{"id": 1, "name": "Ploy"}, {"id": 2, "name": "Bank"}] names = [s["name"] for s in emails] print("names:", names)
nums = [1, 2, 3, 4, 5]
result = [n * n for n in nums if n % 2 == 0]
print(result)
[4, 16]
Filter n % 2 == 0 = เลขคู่ → [2, 4] · แล้ว map n * n → [4, 16]

📚 dict — คีย์ → ค่า

student = { "id": "640001", "name": "Ploy", "scores": [85, 72, 91], } # เข้าถึง print("name:", student["name"]) print("phone:", student.get("phone", "N/A")) # default ถ้าไม่มี # เพิ่ม / แก้ student["email"] = "ploy@ubu.ac.th" print("\nafter add email:") for key, value in student.items(): print(f" {key}: {value}") # nested dict — common in real data drawing = { "id": "D001", "student": {"id": "640001", "name": "Ploy"}, "scores": {"R1": 28, "R2": 18, "R3": 22, "R4": 24} } print("\nstudent name:", drawing["student"]["name"]) print("R1 score: ", drawing["scores"]["R1"]) print("total: ", sum(drawing["scores"].values()))
d = {"a": [1, 2, 3], "b": [4, 5]}
print(len(d), sum(d["a"]), d.get("c", 0))
2 6 0
len(d) = จำนวน key = 2 · sum(d["a"]) = 1+2+3 = 6 · d.get("c", 0) = ไม่มี key "c" → default 0
# ทุก key / ทุก value
list(student.keys())
list(student.values())

📦 JSON — เซฟ dict/list ลงไฟล์

เปิดไฟล์ใน Cursor: open("students.json", "w", encoding="utf-8") · ใน browser นี้ Pyodide ใช้ virtual file system จึงรันได้เลย

import json # Grading App data — เก็บนักศึกษา students = [ {"id": "640001", "name": "Ploy", "year": 2, "major": "MAE"}, {"id": "640002", "name": "Bank", "year": 2, "major": "EE"}, {"id": "640003", "name": "Mint", "year": 2, "major": "MAE"}, ] # เขียน JSON — กฎทอง: utf-8 + ensure_ascii=False สำหรับภาษาไทย with open("students.json", "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2) # อ่านกลับ with open("students.json", "r", encoding="utf-8") as f: loaded = json.load(f) print("จำนวน:", len(loaded)) for s in loaded: print(f" {s['id']}: {s['name']} (year {s['year']}, {s['major']})") # ดู raw JSON ที่เขียนลงไฟล์ print("\n=== raw JSON ===") with open("students.json", encoding="utf-8") as f: print(f.read())
กฎทอง — encoding="utf-8" + ensure_ascii=False เสมอ ลองลบ ensure_ascii=False ใน runner ด้านบนแล้ว Run — ภาษาไทยจะกลายเป็น ปล.. · นี่คือสิ่งที่ทำให้ JSON ดู "ขยะ"

📊 CSV — มาตรฐานของข้อมูลจากภายนอก

import csv students = [ {"id": "640001", "name": "Ploy", "year": 2, "major": "MAE"}, {"id": "640002", "name": "Bank", "year": 2, "major": "EE"}, {"id": "640003", "name": "Mint", "year": 2, "major": "MAE"}, ] # เขียน CSV with open("students.csv", "w", encoding="utf-8", newline="") as f: fieldnames = ["id", "name", "year", "major"] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(students) # ดู raw CSV print("=== raw CSV ===") with open("students.csv", encoding="utf-8") as f: print(f.read()) # อ่านกลับ with open("students.csv", encoding="utf-8") as f: reader = csv.DictReader(f) rows = list(reader) print(f"อ่านได้ {len(rows)} แถว") print(f"row 0: {rows[0]}") print(f"name ของ row 1: {rows[1]['name']}")
CSV จาก Excel/Google Sheets ระวัง
  • encoding อาจจะเป็น cp874 (Thai Windows) ไม่ใช่ utf-8 — เปิด Excel → Save As → CSV UTF-8 (Comma delimited)
  • Excel ชอบเปลี่ยน "001234" เป็น 1234 — เก็บ ID เป็น string ใน source data
  • field ที่มี comma ต้องครอบด้วย "..."
  • CSV ไม่มี type — อ่านทุก field มาเป็น string เสมอ · ต้อง int() เอง

🐼 Pandas — Universal Translator + Data Wrangler

Pandas คือ Excel + SQL + format converter รวมกัน · runner นี้โหลด pandas package (~20s ครั้งแรก · cache หลังจากนั้น) — แล้วคุณรัน "จริง" ได้

📥 1) อ่านได้ทุก format — เขียนได้ทุก format

นี่คือ superpower ของ Pandas — ใช้เป็น universal translator

import pandas as pd import json # เริ่มต้นจาก list of dicts students = [ {"id": "640001", "name": "Ploy", "year": 2, "major": "MAE", "gpa": 3.45}, {"id": "640002", "name": "Bank", "year": 2, "major": "EE", "gpa": 3.20}, {"id": "640003", "name": "Mint", "year": 2, "major": "MAE", "gpa": 3.78}, {"id": "640004", "name": "Tum", "year": 1, "major": "ChE", "gpa": 2.90}, ] # → DataFrame df = pd.DataFrame(students) print("=== DataFrame ===") print(df) # → CSV df.to_csv("out.csv", index=False, encoding="utf-8") # → JSON df.to_json("out.json", orient="records", force_ascii=False, indent=2) # → SQLite (built-in) import sqlite3 conn = sqlite3.connect("out.db") df.to_sql("students", conn, if_exists="replace", index=False) conn.close() # อ่านกลับจาก SQLite — แสดงว่าเขียนสำเร็จ conn = sqlite3.connect("out.db") df2 = pd.read_sql("SELECT * FROM students WHERE gpa >= 3.5", conn) print("\n=== query กลับจาก SQLite (gpa >= 3.5) ===") print(df2)

💡 ครั้งแรกใช้เวลา ~20 วินาทีโหลด pandas + sqlite3 (built-in Python) · ครั้งต่อ ๆ ไปเร็ว

👀 2) Inspect — รู้จักข้อมูลก่อนใช้งาน

import pandas as pd # Sample: drawing scores ของห้องเรียนพี่นัท data = { "student_id": ["640001", "640002", "640003", "640004", "640005"], "name": ["Ploy", "Bank", "Mint", "Tum", "Aof"], "drawing_id": ["D1", "D1", "D1", "D1", "D1"], "R1": [28, 25, 30, 20, 26], # Dimensions "R2": [18, 15, 20, 12, 18], # Line quality "R3": [22, 20, 23, 18, 21], # Annotations "R4": [24, 22, 25, 18, 24], # Title block } df = pd.DataFrame(data) print("--- head() ---") # 5 แถวแรก print(df.head()) print("\n--- shape ---") print(df.shape) # (rows, cols) print("\n--- columns ---") print(list(df.columns)) print("\n--- dtypes ---") print(df.dtypes) # type ของแต่ละ column print("\n--- describe() ---") print(df.describe()) # mean, std, min, max ของ numeric

🎯 3) Select & Filter — เลือกแถว/คอลัมน์

import pandas as pd df = pd.DataFrame({ "name": ["Ploy", "Bank", "Mint", "Tum", "Aof"], "year": [2, 2, 2, 1, 3], "gpa": [3.45, 3.20, 3.78, 2.90, 3.60], "major": ["MAE", "EE", "MAE", "ChE", "MAE"], }) # เลือก 1 column → Series print("--- df['name'] ---") print(df["name"].tolist()) # เลือกหลาย columns → DataFrame print("\n--- df[['name', 'gpa']] ---") print(df[["name", "gpa"]]) # Filter ด้วย boolean print("\n--- gpa >= 3.5 ---") print(df[df["gpa"] >= 3.5]) # Filter หลายเงื่อนไข — ใช้ & และ | print("\n--- MAE และ gpa >= 3.4 ---") print(df[(df["major"] == "MAE") & (df["gpa"] >= 3.4)]) # .loc[row, col] — เลือกตาม label print("\n--- df.loc[0, 'name'] ---") print(df.loc[0, "name"]) # .iloc[row, col] — เลือกตาม position print("\n--- df.iloc[-1] (แถวสุดท้าย) ---") print(df.iloc[-1])

📊 4) Groupby — สรุปข้อมูลตามกลุ่ม

import pandas as pd # scores ของนักศึกษา 50 คน — แต่ละคนทำ drawing 3 ชิ้น data = { "student_id": ["640001", "640001", "640001", "640002", "640002", "640002", "640003", "640003", "640003"], "drawing": ["D1", "D2", "D3"] * 3, "score": [78, 82, 85, 65, 70, 72, 90, 88, 92], } df = pd.DataFrame(data) # คะแนนเฉลี่ยของแต่ละนักศึกษา print("--- เฉลี่ยต่อคน ---") print(df.groupby("student_id")["score"].mean()) # จำนวน drawing ของแต่ละคน print("\n--- นับ drawing ต่อคน ---") print(df.groupby("student_id").size()) # หลาย aggregation พร้อมกัน print("\n--- multi agg ---") print(df.groupby("student_id")["score"].agg(["mean", "min", "max", "count"])) # Groupby + filter top = df.groupby("student_id")["score"].mean() print("\n--- ใครเฉลี่ย > 80 ---") print(top[top > 80])

🔗 5) Merge — รวมตาราง (Join)

import pandas as pd # 2 ตาราง — แยกกัน เพื่อไม่ซ้ำข้อมูล students = pd.DataFrame({ "id": ["640001", "640002", "640003"], "name": ["Ploy", "Bank", "Mint"], "year": [2, 2, 2], }) scores = pd.DataFrame({ "student_id": ["640001", "640001", "640002", "640003"], "drawing": ["D1", "D2", "D1", "D1"], "score": [85, 90, 70, 95], }) # Merge — เหมือน SQL JOIN merged = students.merge(scores, left_on="id", right_on="student_id") print("--- merged ---") print(merged) # ตอนนี้ groupby ก็ได้ชื่อแล้ว print("\n--- เฉลี่ยพร้อมชื่อ ---") print(merged.groupby("name")["score"].mean())

🧰 6) value_counts & apply — utility ที่ใช้บ่อยมาก

import pandas as pd df = pd.DataFrame({ "name": ["Ploy", "Bank", "Mint", "Tum", "Aof", "Som", "Pat"], "major": ["MAE", "EE", "MAE", "ChE", "MAE", "EE", "MAE"], "score": [85, 70, 92, 60, 78, 88, 75], }) # value_counts — นับ unique values print("--- จำนวนนักศึกษาแต่ละสาขา ---") print(df["major"].value_counts()) # apply — function กับทุกแถว def to_grade(s): if s >= 80: return "A" if s >= 70: return "B" if s >= 60: return "C" return "F" df["grade"] = df["score"].apply(to_grade) print("\n--- พร้อม grade column ---") print(df) # นับ grade print("\n--- กระจาย grade ---") print(df["grade"].value_counts())
6 moves นี้ครอบคลุม 80% ของ data work ในอาชีพ อ่าน → inspect → filter → groupby → merge → apply · ฝึกให้คล่อง — Final Project และทุกงานต่อ ๆ ไปจะใช้

💾 SQLite — Database 1 ไฟล์

เมื่อข้อมูลเริ่มเยอะ + ต้อง search/filter ซับซ้อน · CSV ช้า · ใช้ SQLite

import sqlite3 # สร้าง/เปิด database (สร้างไฟล์ถ้ายังไม่มี) conn = sqlite3.connect("grading.db") cur = conn.cursor() # สร้าง table — ระบุ type + constraints cur.execute(""" CREATE TABLE IF NOT EXISTS students ( id TEXT PRIMARY KEY, name TEXT NOT NULL, year INTEGER, major TEXT ) """) # เพิ่มแถว — ใช้ ? เพื่อ safe (ป้องกัน SQL injection) data = [ ("640001", "Ploy", 2, "MAE"), ("640002", "Bank", 2, "EE"), ("640003", "Mint", 2, "MAE"), ("640004", "Tum", 1, "ChE"), ] cur.executemany( "INSERT OR REPLACE INTO students VALUES (?, ?, ?, ?)", data ) conn.commit() # Query 1: SELECT ทั้งหมด print("--- ทั้งหมด ---") for row in cur.execute("SELECT * FROM students"): print(row) # Query 2: WHERE filter print("\n--- เฉพาะ MAE ---") for row in cur.execute("SELECT name, year FROM students WHERE major = ?", ("MAE",)): print(row) # Query 3: aggregation print("\n--- นับนักศึกษาแต่ละสาขา ---") for row in cur.execute("SELECT major, COUNT(*) FROM students GROUP BY major"): print(row) conn.close()

SQL พื้นฐาน 5 คำสั่ง

SELECT cols FROM tableดึงข้อมูลออก
INSERT INTO table VALUES (...)เพิ่มแถว
UPDATE table SET col=val WHERE ...แก้แถว
DELETE FROM table WHERE ...ลบแถว
WHERE / GROUP BY / ORDER BYกรอง / จัดกลุ่ม / เรียง
SQLite ใน Pandas ก็ได้
df = pd.read_sql("SELECT * FROM students WHERE year = 1", conn)
df.to_sql("students_2026", conn, if_exists="replace")
ใช้ pandas สำหรับ analyze + SQLite สำหรับ persistence — ของ Python รับกันได้หมด

📐 Schema Design — ออกแบบ "ตาราง" ก่อนเก็บ

นี่คือทักษะ human-only ของเฟส Data · ตัดสินใจว่า ตารางไหน · field อะไร · type อะไร · ความสัมพันธ์ยังไง = ส่งผลตลอด lifecycle ของ project

🔑 4 คำถามที่ต้องตอบก่อนสร้าง schema

  1. มี entity (สิ่งของ) อะไรบ้าง? — Grading App มี: Student · Drawing · Rubric · Score
  2. แต่ละ entity มี field อะไร? — Student: id, name, year, major · Drawing: id, student_id, due_date, …
  3. field ไหนเป็น Primary Key (PK)? — ID เฉพาะของแต่ละ row · ห้ามซ้ำ · เช่น student_id = "640001"
  4. มี relationship ระหว่าง entity มั้ย? — Drawing ของใคร? → ใส่ student_id เป็น Foreign Key (FK) ใน Drawing

📊 Schema ของ Grading App (ตัวอย่างเต็ม)

erDiagram STUDENT ||--o{ DRAWING : ส่ง DRAWING ||--o{ SCORE : ได้คะแนน RUBRIC ||--o{ SCORE : ตาม_criteria STUDENT { string id PK "เช่น 640001" string name "ชื่อ-นามสกุล" int year "1-4" string major "MAE/EE/ChE..." string email } DRAWING { string id PK "D001" string student_id FK date submitted_at string filename } RUBRIC { string id PK "R1" string criteria "Dimensions accurate" int weight "0-100" } SCORE { int id PK string drawing_id FK string rubric_id FK int score "0-weight" string note }

📝 กฎ Schema Design ที่ใช้ได้กับทุก project

กฎเพราะตัวอย่าง
ทุก table มี PK ต้องระบุได้ว่า row ไหน "ตัวเดียวกัน" id TEXT PRIMARY KEY
PK ห้ามเปลี่ยน ถ้าเปลี่ยน → FK ทุกที่พัง ใช้ UUID หรือ student_id ที่ไม่เปลี่ยน
1 field = 1 ข้อมูล (Atomic) ห้ามเก็บ "Ploy, Bank, Mint" ใน 1 cell ถ้ามีหลายค่า → ทำ table ใหม่
ห้ามทำซ้ำ (Don't Repeat) ถ้าข้อมูลซ้ำ → update ที่เดียว ไม่ใช่หลายที่ student.name เก็บที่ STUDENT table เท่านั้น
Type ที่ตรงกับธรรมชาติของข้อมูล ตัวเลข → INTEGER · วันที่ → DATE · ไม่ใช่ TEXT ทุกอัน price REAL ไม่ใช่ TEXT
NOT NULL ถ้า required ป้องกันข้อมูลขาด name TEXT NOT NULL

🚩 Schema ที่ "ผิดพลาดที่พบบ่อย"

❌ เก็บหลายค่าใน column เดียว
# ผิด — เก็บคะแนน 4 criteria ในช่องเดียว
students = [
    {"id": "640001", "name": "Ploy", "scores": "28,18,22,24"},
]
# ถูก — แยกตาราง
scores = [
    {"student_id": "640001", "rubric_id": "R1", "score": 28},
    {"student_id": "640001", "rubric_id": "R2", "score": 18},
    ...
]
❌ ทำซ้ำชื่อในหลายที่
# ผิด — เก็บชื่อนักศึกษาทุกแถวของ scores
scores = [
    {"student_id": "640001", "name": "Ploy", "score": 28},
    {"student_id": "640001", "name": "Ploy", "score": 18},
    # ถ้า Ploy เปลี่ยนชื่อ → ต้องแก้ทุกแถว
]
# ถูก — เก็บชื่อใน students แล้ว merge เมื่อต้องใช้
students = [{"id": "640001", "name": "Ploy"}]
scores = [{"student_id": "640001", "score": 28}, ...]
เคล็ดลับ — ใช้ AI สร้าง schema จาก spec ลองส่ง prompt: "ออกแบบ schema สำหรับ Grading App ที่มี Student/Drawing/Rubric/Score · แสดงเป็น Mermaid erDiagram" — AI สร้างให้ได้ดี · แต่คุณต้อง "review" ด้วยกฎ 6 ข้อด้านบน

🔗 SQL Deep Dive — JOIN · Aggregate · NULL · CASE

📌 ก่อนเริ่ม — "ไม่ต้องใช้ SQL ทุกครั้ง" สำหรับ script เล็ก ๆ · CLI tool · config · 1-user appJSON file ดีกว่า · ง่ายกว่า · อ่านได้ด้วยตา · ไม่ต้อง setup · ดู Data Storage Ladder ด้านบน · ขยับขึ้น SQL "เมื่อจำเป็น" เท่านั้น · ส่วนต่อไปนี้คือทักษะ SQL "สำหรับวันที่จำเป็น" ไม่ใช่ "ต้องใช้ตั้งแต่ Day 1"

W08 ส่วน SQLite เบื้องต้นใช้ตารางเดียว · ในความจริง "data ดี ๆ อยู่ในหลายตาราง" (ตาม Schema Design ด้านบน) · ตอนจะใช้ต้อง JOIN กลับมา · นี่คือทักษะ SQL ที่ทำเงินจริง

📊 เลือก storage ตามขนาดของ project

Project sizeเลือกเหตุผล
script ใช้ครั้งเดียว · < 10 recordslist / dict ใน RAMไม่ต้อง persist
CLI tool · config · 1 user · < 1000 recordsJSON fileง่ายที่สุด · มนุษย์อ่านได้ · gitignore ได้
Data analysis · CSV จาก vendorCSV + pandasมาตรฐาน · Excel เปิดได้
app เล็ก · multi-table · ต้อง querySQLite1 file · ไม่ต้อง server
multi-user · productionPostgreSQLconcurrent · network

👇 ถ้า project ของคุณตกหมวด "JSON ก็พอ" — "skip section นี้ได้" · กลับมาอ่านเมื่อจำเป็น

🔗 1) JOIN — รวม 2 ตารางด้วย Foreign Key

ใน Grading App: students มีข้อมูลคน · scores มีคะแนน · อยากดู "ชื่อ + คะแนน" ในรายการเดียว → ต้อง JOIN

import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() # สร้าง 2 ตาราง cur.executescript(""" CREATE TABLE students ( id TEXT PRIMARY KEY, name TEXT, major TEXT ); CREATE TABLE scores ( id INTEGER PRIMARY KEY, student_id TEXT, subject TEXT, score INTEGER ); INSERT INTO students VALUES ('640001', 'Ploy', 'MAE'), ('640002', 'Bank', 'EE'), ('640003', 'Mint', 'MAE'), ('640004', 'Tum', 'ChE'); INSERT INTO scores VALUES (1, '640001', 'Drawing', 85), (2, '640001', 'Lab', 78), (3, '640002', 'Drawing', 70), (4, '640002', 'Lab', 82), (5, '640003', 'Drawing', 92); -- 640004 (Tum) ไม่มีคะแนนเลย! """) print("=== INNER JOIN — เฉพาะคนที่มีคะแนน ===") for row in cur.execute(""" SELECT s.name, sc.subject, sc.score FROM students s INNER JOIN scores sc ON s.id = sc.student_id ORDER BY s.name """): print(row) # Tum ไม่ปรากฏ — เพราะไม่มีคะแนน print("\n=== LEFT JOIN — รวม Tum ด้วย (NULL ตรงคะแนน) ===") for row in cur.execute(""" SELECT s.name, sc.subject, sc.score FROM students s LEFT JOIN scores sc ON s.id = sc.student_id ORDER BY s.name """): print(row) # Tum ปรากฏ · subject=None · score=None
JOIN typeคืนแถวเมื่อใช้เมื่อ
INNER JOINmatch ทั้ง 2 ตารางดูเฉพาะคนที่มี scores · default ถ้าไม่ระบุ
LEFT JOINทุกแถวฝั่งซ้าย + match ฝั่งขวา (ที่ขาดเป็น NULL)"รายชื่อทั้งหมด · ใครยังไม่ส่งงาน"
RIGHT JOINตรงข้าม LEFT (SQLite ไม่มี · ใช้ swap LEFT แทน)ไม่ค่อยใช้
FULL OUTER JOINทุกแถวทั้ง 2 ตารางไม่ค่อยใช้ · SQLite ไม่รองรับ
กฎทอง — เริ่มที่ INNER · ใช้ LEFT เมื่อ "อยากเห็นคนที่ขาด" 90% ของ JOIN ในงานจริงคือ INNER · LEFT ใช้เพื่อตรวจ "ใครที่ยังไม่มีข้อมูล"

🔗 JOIN 3 ตาราง — chain ได้

import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.executescript(""" CREATE TABLE students (id TEXT PRIMARY KEY, name TEXT); CREATE TABLE drawings (id TEXT PRIMARY KEY, student_id TEXT, title TEXT); CREATE TABLE rubrics (id TEXT PRIMARY KEY, drawing_id TEXT, score INTEGER); INSERT INTO students VALUES ('640001', 'Ploy'), ('640002', 'Bank'); INSERT INTO drawings VALUES ('D1', '640001', 'House'), ('D2', '640001', 'Engine'), ('D3', '640002', 'Bridge'); INSERT INTO rubrics VALUES ('R1', 'D1', 28), ('R2', 'D1', 18), ('R3', 'D2', 25), ('R4', 'D3', 30); """) # JOIN 3 ตาราง — รายชื่อ + ภาพ + คะแนน print("=== Full chain JOIN ===") for row in cur.execute(""" SELECT s.name, d.title, r.score FROM students s INNER JOIN drawings d ON s.id = d.student_id INNER JOIN rubrics r ON d.id = r.drawing_id ORDER BY s.name, d.title """): print(row)

📊 2) Aggregate Functions — สรุปข้อมูล

Functionทำอะไรตัวอย่าง
COUNT(*)นับแถวSELECT COUNT(*) FROM students
COUNT(col)นับ non-NULL ใน columnCOUNT(email) = คนที่มี email
SUM(col)รวมSUM(score)
AVG(col)เฉลี่ยAVG(gpa)
MIN(col) / MAX(col)น้อยสุด / มากสุดMAX(score)
GROUP BYจัดกลุ่มก่อน aggregateGROUP BY major
HAVINGfilter หลัง GROUP BY (WHERE filter ก่อน)HAVING COUNT(*) > 5
import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.executescript(""" CREATE TABLE scores ( student_id TEXT, subject TEXT, score INTEGER, major TEXT ); INSERT INTO scores VALUES ('U1', 'Drawing', 85, 'MAE'), ('U2', 'Drawing', 70, 'EE'), ('U3', 'Drawing', 92, 'MAE'), ('U4', 'Drawing', 60, 'ChE'), ('U1', 'Lab', 78, 'MAE'), ('U2', 'Lab', 82, 'EE'), ('U3', 'Lab', 88, 'MAE'); """) print("=== ทั้งหมด ===") print(list(cur.execute("SELECT COUNT(*), AVG(score), MAX(score), MIN(score) FROM scores"))) print("\n=== กลุ่มตามวิชา (GROUP BY) ===") for row in cur.execute(""" SELECT subject, COUNT(*) AS n, ROUND(AVG(score), 1) AS avg, MAX(score) AS hi FROM scores GROUP BY subject """): print(row) print("\n=== กลุ่มตามสาขา · เฉพาะที่มี > 1 คน (HAVING) ===") for row in cur.execute(""" SELECT major, COUNT(*) AS n, ROUND(AVG(score), 1) AS avg FROM scores GROUP BY major HAVING COUNT(*) > 1 ORDER BY avg DESC """): print(row)

🕳 3) NULL Handling + CASE/WHEN

Real-world data มี NULL เสมอ · SQL จัดการ NULL พิเศษ (NULL ≠ 0 · NULL ≠ "")

import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.executescript(""" CREATE TABLE students ( id TEXT PRIMARY KEY, name TEXT, email TEXT, -- บางคนยังไม่มี gpa REAL -- ปี 1 ยังไม่มี ); INSERT INTO students VALUES ('U1', 'Ploy', 'ploy@ubu.ac.th', 3.45), ('U2', 'Bank', NULL, 3.20), ('U3', 'Mint', 'mint@ubu.ac.th', NULL), ('U4', 'Tum', NULL, NULL); """) # IS NULL / IS NOT NULL — ห้ามใช้ = NULL print("=== คนที่ไม่มี email ===") print(list(cur.execute("SELECT name FROM students WHERE email IS NULL"))) print("\n=== คนที่มีทั้ง email และ gpa ===") print(list(cur.execute("SELECT name FROM students WHERE email IS NOT NULL AND gpa IS NOT NULL"))) # COALESCE — แทน NULL ด้วยค่า default print("\n=== COALESCE — แสดง 'N/A' แทน NULL ===") for row in cur.execute(""" SELECT name, COALESCE(email, 'N/A') AS email, COALESCE(gpa, 0.0) AS gpa FROM students """): print(row) # CASE/WHEN — if-else ใน SQL print("\n=== CASE/WHEN — แปลง gpa เป็น grade band ===") for row in cur.execute(""" SELECT name, gpa, CASE WHEN gpa IS NULL THEN 'ยังไม่มี' WHEN gpa >= 3.5 THEN 'ดีเยี่ยม' WHEN gpa >= 3.0 THEN 'ดี' WHEN gpa >= 2.0 THEN 'พอใช้' ELSE 'อ่อน' END AS band FROM students """): print(row)
⚠️ WHERE x = NULL ไม่ทำงาน! NULL เปรียบเทียบเท่าไม่ได้ · ต้องใช้ IS NULL / IS NOT NULL · bug นี้พบบ่อยมาก · AI ก็เขียนผิดบางครั้ง

🔄 4) Transactions — All-or-Nothing

ถ้าโปรแกรม crash กลาง INSERT 100 แถว — อะไรเกิดขึ้น? · ครึ่งหนึ่งใน · ครึ่งหนึ่งหาย · transaction ป้องกัน

import sqlite3 conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.execute("CREATE TABLE accounts (id TEXT, balance INTEGER)") cur.execute("INSERT INTO accounts VALUES ('A', 1000), ('B', 500)") conn.commit() # ตัวอย่าง — โอนเงิน A → B def transfer(amount): try: cur.execute("BEGIN") cur.execute("UPDATE accounts SET balance = balance - ? WHERE id = 'A'", (amount,)) if amount > 800: raise ValueError("จำลอง error กลาง transaction!") cur.execute("UPDATE accounts SET balance = balance + ? WHERE id = 'B'", (amount,)) conn.commit() # ✅ ทุกอย่างผ่าน → save print(f"✅ โอน {amount} สำเร็จ") except Exception as e: conn.rollback() # ❌ rollback ทุกการเปลี่ยน print(f"❌ {e} → rollback · เงินกลับเดิม") print("ก่อน:") print(list(cur.execute("SELECT * FROM accounts"))) transfer(100) # ✅ ผ่าน print("\nหลังโอน 100:") print(list(cur.execute("SELECT * FROM accounts"))) transfer(900) # ❌ error → rollback print("\nหลังพยายามโอน 900 (พังกลางทาง):") print(list(cur.execute("SELECT * FROM accounts"))) # A ยังคงมี 900 — ไม่ใช่ -800!
ACID — 4 คุณสมบัติของ Database
  • Atomic — transaction = all-or-nothing
  • Consistent — data จะ valid ตามกฎเสมอ (constraints)
  • Isolated — 2 transaction รันพร้อมกันไม่ปนกัน
  • Durable — commit แล้ว = save แน่นอน · แม้ไฟดับ
SQLite, PostgreSQL, MySQL = ACID · NoSQL ส่วนใหญ่ ไม่ ACID เต็มที่ (eventual consistency)

📇 5) Indexes — เร็วขึ้น 100 เท่า เมื่อข้อมูลเยอะ

ตาราง 10 ล้านแถว · WHERE email = 'x' = scan ทุกแถว = ช้า · สร้าง index = เหมือนสารบัญหนังสือ = เร็ว

-- สร้าง index บน column ที่ query บ่อย
CREATE INDEX idx_students_email ON students(email);
CREATE INDEX idx_scores_student_id ON scores(student_id);

-- composite index (หลาย column)
CREATE INDEX idx_scores_subj_score ON scores(subject, score);

-- ดู query plan
EXPLAIN QUERY PLAN
SELECT * FROM students WHERE email = 'ploy@ubu.ac.th';
-- → "USING INDEX idx_students_email" = เร็ว ✅
⚠️ Index ไม่ฟรี Index = ใช้ disk space · INSERT/UPDATE ช้าขึ้น (ต้อง update index ด้วย) · กฎ: สร้าง index บน column ที่ query บ่อย เท่านั้น · ไม่ใช่ทุก column · Year 1 ไม่ต้องลึก · "รู้ว่ามี" + "เมื่อช้าให้คิดถึง"

⚛️ ORM — เขียน Python · ได้ SQL

ORM (Object-Relational Mapping) = library ที่ "แปลง Python class ↔ SQL table" · ไม่ต้องเขียน SQL ดิบ · ทุก framework เว็บ (Django, FastAPI, Flask-SQLAlchemy) ใช้ ORM

📊 SQL ดิบ vs ORM

# SQL ดิบ — เขียน string import sqlite3 conn = sqlite3.connect("app.db") cur = conn.cursor() cur.execute(""" SELECT s.name, AVG(sc.score) FROM students s JOIN scores sc ON s.id = sc.student_id WHERE s.major = ? GROUP BY s.id HAVING AVG(sc.score) > 75 """, ("MAE",)) results = cur.fetchall() for name, avg in results: print(name, avg)
# SQLAlchemy ORM — เขียน Python from sqlalchemy import create_engine, func from sqlalchemy.orm import Session from models import Student, Score # class ของเรา engine = create_engine("sqlite:///app.db") with Session(engine) as session: results = ( session.query(Student.name, func.avg(Score.score)) .join(Score) .filter(Student.major == "MAE") .group_by(Student.id) .having(func.avg(Score.score) > 75) .all() ) for name, avg in results: print(name, avg)

🎯 เมื่อไหร่ใช้ ORM vs SQL ดิบ

สถานการณ์ใช้
app เล็ก · query ง่าย · 1 ตารางSQL ดิบ · ใช้ sqlite3 + pandas
app กลาง · หลาย table · CRUD เยอะORM (SQLAlchemy, Tortoise, Django ORM)
query ซับซ้อนมาก · subquery · window functionSQL ดิบ · เพราะ ORM แปลออกมาแย่
ทีมใหญ่ · ต้อง migrationORM + Alembic
เรียนรู้ครั้งแรกSQL ดิบก่อน · เข้าใจดีกว่า · ค่อยขึ้น ORM

🗂 Database Migrations (Alembic) — รู้ว่ามี

เมื่อ schema เปลี่ยน (เพิ่ม column · rename · drop) ใน production — migration = script อัพเดต database ทีละ version · เก็บใน git · rollback ได้ · ใช้ Alembic ที่จับคู่กับ SQLAlchemy

alembic init migrations
alembic revision --autogenerate -m "add email to students"
alembic upgrade head        # apply
alembic downgrade -1        # rollback

🌸 NoSQL — เมื่อ SQL ไม่เหมาะ

NoSQL = database ที่ไม่ใช่ตารางแบบ relational · มีหลายแบบ · เลือกใช้เมื่อ "data structure ไม่เหมาะกับตาราง"

NoSQLเก็บแบบเหมาะกับตัวอย่าง
MongoDB Document (JSON) schema ไม่นิ่ง · nested data · social feed · CMS Atlas free tier · pymongo
Redis Key-Value (in-memory) cache · session · rate limit · queue · pub/sub Upstash free · redis-py
Firestore Document + real-time mobile app · real-time chat · IoT live data Firebase free tier
InfluxDB Time-series sensor data · IoT logs · metrics InfluxDB Cloud free
Vector DB (Pinecone, Qdrant) Embeddings RAG · semantic search · AI memory ตัวเลือกใหม่ในยุค AI

📊 SQL vs NoSQL — เมื่อไหร่เลือก

ดี ๆ กับSQL (Postgres/SQLite)NoSQL (Mongo/Redis/Firestore)
Schema นิ่ง · structured
Schema เปลี่ยนบ่อย · flexible
JOIN หลายตาราง⚠️ ลำบาก
ACID transactions⚠️ ส่วนใหญ่ eventual
Real-time updates⚠️✅ (Firestore)
Cache / session✅ Redis
Time-series sensor data⚠️✅ InfluxDB
AI embeddings · semantic search⚠️ pgvector✅ Vector DB
📌 สำหรับ Final Project เริ่มที่ SQLite ก่อนเสมอ · ถ้ามี IoT/sensor + real-time → ลอง Firestore · ถ้าทำ AI chatbot ที่ต้อง "จำเรื่องที่คุยไปแล้ว" → ลอง vector DB (W12 LLM section) · อย่ารีบไป NoSQL ถ้า SQL พอ

🧪 Workshop — Grading App End-to-End Data Pipeline

ใน 1 notebook — เริ่มจาก list of dict → แปลง JSON → CSV → SQLite → query กลับ · ใช้ข้อมูล Grading App ของพี่นัทเป็น "ของจริง"

import pandas as pd import json, csv, sqlite3 # ==== STEP 1: สร้างข้อมูลจาก list of dicts ==== students = [ {"id": "640001", "name": "Ploy", "year": 2, "major": "MAE", "gpa": 3.45}, {"id": "640002", "name": "Bank", "year": 2, "major": "EE", "gpa": 3.20}, {"id": "640003", "name": "Mint", "year": 2, "major": "MAE", "gpa": 3.78}, {"id": "640004", "name": "Tum", "year": 1, "major": "ChE", "gpa": 2.90}, {"id": "640005", "name": "Aof", "year": 3, "major": "MAE", "gpa": 3.60}, ] print(f"✅ Step 1: สร้าง {len(students)} students ใน RAM") # ==== STEP 2: เซฟลง JSON ==== with open("students.json", "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2) print("✅ Step 2: เขียน students.json") # ==== STEP 3: แปลงเป็น CSV ผ่าน Pandas ==== df = pd.DataFrame(students) df.to_csv("students.csv", index=False, encoding="utf-8") print("✅ Step 3: แปลงเป็น students.csv") print(df) # ==== STEP 4: ใส่ลง SQLite ==== conn = sqlite3.connect("grading.db") df.to_sql("students", conn, if_exists="replace", index=False) print("\n✅ Step 4: ใส่ลง SQLite (table 'students')") # ==== STEP 5: ตอบ 3 คำถามด้วย SQL ==== print("\n--- Q1: นักศึกษาในแต่ละสาขา ---") print(pd.read_sql("SELECT major, COUNT(*) as cnt FROM students GROUP BY major", conn)) print("\n--- Q2: gpa เฉลี่ยของแต่ละชั้นปี ---") print(pd.read_sql("SELECT year, AVG(gpa) as avg_gpa FROM students GROUP BY year", conn)) print("\n--- Q3: top 3 GPA ---") print(pd.read_sql("SELECT name, gpa FROM students ORDER BY gpa DESC LIMIT 3", conn)) conn.close() print("\n🎉 ครบ pipeline · ลองแก้ข้อมูล หรือคำถาม แล้ว Run ใหม่")

📝 ส่วนต่อยอด (ใน Cursor — งานบ้าน)

  1. copy code นี้ลง Jupyter notebook (.ipynb) — แยกเป็น 5 cell ตาม step
  2. หา data จริงของคุณ — Google Forms ที่เคยทำ · sensor log · expense tracking · datase ที่ data.go.th
  3. ออกแบบ schema ตาม 4 คำถามด้านบน · วาด ER diagram ใน Mermaid
  4. แทนข้อมูล Grading App ด้วยของคุณ — รันทั้ง pipeline ใหม่
  5. ตอบ 3 คำถามใหม่ด้วย groupby / merge / value_counts
  6. commit + push GitHub — README.md อธิบาย data source + schema + คำถาม

📌 ตัดสินใจ — ใช้อันไหนเมื่อไหร่

สถานการณ์ใช้เหตุผล
ตัวแปรในโปรแกรม < 1000 recordslist of dictเร็ว · simple
เซฟ config / state ของ CLI toolJSONมนุษย์อ่านได้ · มี structure
ส่งออกให้ ExcelCSV หรือ .xlsxมาตรฐาน · เปิดที่ไหนก็ได้
analyze ข้อมูล + plotpandas DataFramefunction สำหรับ data ครบ
app ที่ต้อง search/filter ซับซ้อนSQLiteSQL เร็ว · index
หลาย user · productionPostgreSQL (W12)concurrent · network · backup

ข้อผิดที่พบบ่อย

กระโดดไป SQL ตั้งแต่แรก — โปรเจกต์ส่วนใหญ่ของนักศึกษาปี 1 ใช้ JSON ก็เพียงพอ · เริ่ม simple ก่อน
เซฟ JSON โดยไม่มี encoding="utf-8" ภาษาไทยพังทันที · กฎทอง: UTF-8 ทุกไฟล์
ลืม index=False เวลา to_csv Pandas จะใส่ column ใหม่ "Unnamed: 0" · กฎ: df.to_csv(..., index=False) เสมอ

ส่งงานสัปดาห์นี้

Reference จาก slide เดิม

ครอบคลุม Topic เดิมที่กระจายอยู่ — list/dict (จาก Topic 7), CSV จาก Topic 10 (Pandas), และขยายไป JSON + SQLite ที่ slide เดิมไม่ครอบคลุม