Spaces:
Running
Running
Download index.html from thanthamky/sql-crashcourse: direct link, hf CLI and curl.
- Browser
- Download file 84.3 kB
-
https://huggingface.co/spaces/thanthamky/sql-crashcourse/resolve/main/index.html
- Command line
-
hf download hf://spaces/thanthamky/sql-crashcourse/index.html
-
curl -L -o index.html https://huggingface.co/spaces/thanthamky/sql-crashcourse/resolve/main/index.html
84.3 kB
| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>คู่มือ SQL ฉบับเร่งรัด — จากศูนย์สู่มืออาชีพ</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #0f1117; | |
| --surface: #1a1d27; | |
| --surface2: #22263a; | |
| --border: #2e3350; | |
| --accent: #5b8dee; | |
| --accent2: #7c5cbf; | |
| --green: #3ecf8e; | |
| --yellow: #f5a623; | |
| --red: #e05252; | |
| --text: #d0d6f0; | |
| --muted: #7a83a8; | |
| --sidebar-w: 260px; | |
| --code-bg: #141720; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| font-family: 'Segoe UI', 'Noto Sans Thai', system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.7; | |
| font-size: 15px; | |
| } | |
| /* ── Sidebar ── */ | |
| #sidebar { | |
| position: fixed; top: 0; left: 0; | |
| width: var(--sidebar-w); | |
| height: 100vh; | |
| background: var(--surface); | |
| border-right: 1px solid var(--border); | |
| overflow-y: auto; | |
| z-index: 100; | |
| padding-bottom: 2rem; | |
| } | |
| #sidebar .logo { | |
| padding: 1.25rem 1rem; | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| letter-spacing: .08em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| #sidebar nav { padding: .5rem 0; } | |
| #sidebar .nav-section { | |
| padding: .6rem 1rem .2rem; | |
| font-size: .68rem; | |
| font-weight: 700; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| #sidebar a { | |
| display: block; | |
| padding: .38rem 1rem .38rem 1.25rem; | |
| color: var(--muted); | |
| text-decoration: none; | |
| font-size: .82rem; | |
| border-left: 2px solid transparent; | |
| transition: all .15s; | |
| } | |
| #sidebar a:hover, #sidebar a.active { | |
| color: var(--text); | |
| border-left-color: var(--accent); | |
| background: rgba(91,141,238,.08); | |
| } | |
| /* ── Main ── */ | |
| #main { | |
| margin-left: var(--sidebar-w); | |
| padding: 2.5rem 3rem; | |
| max-width: 960px; | |
| } | |
| /* ── Hero ── */ | |
| .hero { | |
| background: linear-gradient(135deg, #1c2240 0%, #1a1d27 100%); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 2.5rem; | |
| margin-bottom: 3rem; | |
| } | |
| .hero h1 { | |
| font-size: 2rem; | |
| font-weight: 800; | |
| background: linear-gradient(90deg, var(--accent), #a78bfa); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: .5rem; | |
| } | |
| .hero p { color: var(--muted); font-size: .95rem; } | |
| .hero .badges { display: flex; gap: .5rem; margin-top: 1rem; flex-wrap: wrap; } | |
| .badge { | |
| padding: .25rem .7rem; | |
| border-radius: 20px; | |
| font-size: .72rem; | |
| font-weight: 600; | |
| letter-spacing: .04em; | |
| } | |
| .badge-blue { background: rgba(91,141,238,.15); color: var(--accent); border: 1px solid rgba(91,141,238,.3); } | |
| .badge-green { background: rgba(62,207,142,.15); color: var(--green); border: 1px solid rgba(62,207,142,.3); } | |
| .badge-purple{ background: rgba(124,92,191,.15); color: #a78bfa; border: 1px solid rgba(124,92,191,.3); } | |
| /* ── Section headings ── */ | |
| .part-header { | |
| display: flex; align-items: center; gap: 1rem; | |
| margin: 3rem 0 1.5rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .part-number { | |
| width: 42px; height: 42px; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 1.1rem; font-weight: 800; color: #fff; | |
| flex-shrink: 0; | |
| } | |
| .part-header h2 { font-size: 1.4rem; font-weight: 700; color: #fff; } | |
| h3 { | |
| font-size: 1.05rem; font-weight: 700; | |
| color: #fff; margin: 2rem 0 .75rem; | |
| padding-left: .75rem; | |
| border-left: 3px solid var(--accent); | |
| } | |
| h4 { | |
| font-size: .9rem; font-weight: 600; | |
| color: var(--accent); margin: 1.5rem 0 .5rem; | |
| text-transform: uppercase; letter-spacing: .06em; | |
| } | |
| p { margin-bottom: .9rem; color: var(--text); } | |
| /* ── Code blocks ── */ | |
| pre { | |
| background: var(--code-bg) ; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1.1rem 1.25rem ; | |
| margin: .75rem 0 1.25rem; | |
| overflow-x: auto; | |
| font-size: .82rem ; | |
| line-height: 1.6 ; | |
| } | |
| pre code { background: none ; padding: 0 ; font-size: inherit ; } | |
| code { | |
| background: var(--surface2); | |
| color: #e2b96a; | |
| padding: .15em .45em; | |
| border-radius: 4px; | |
| font-size: .83em; | |
| font-family: 'Fira Code', 'Consolas', monospace; | |
| } | |
| /* ── Tables ── */ | |
| .table-wrap { overflow-x: auto; margin: .75rem 0 1.5rem; } | |
| table { width: 100%; border-collapse: collapse; font-size: .85rem; } | |
| th { | |
| background: var(--surface2); | |
| color: var(--accent); | |
| padding: .6rem .9rem; | |
| text-align: left; | |
| font-weight: 600; | |
| font-size: .78rem; | |
| letter-spacing: .04em; | |
| text-transform: uppercase; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| td { padding: .55rem .9rem; border-bottom: 1px solid var(--border); vertical-align: top; } | |
| tr:last-child td { border-bottom: none; } | |
| tr:hover td { background: rgba(255,255,255,.025); } | |
| /* ── Callout boxes ── */ | |
| .callout { | |
| border-radius: 8px; | |
| padding: .9rem 1.1rem; | |
| margin: 1rem 0; | |
| font-size: .88rem; | |
| border-left: 3px solid; | |
| } | |
| .callout-info { background: rgba(91,141,238,.1); border-color: var(--accent); } | |
| .callout-warn { background: rgba(245,166,35,.1); border-color: var(--yellow); } | |
| .callout-tip { background: rgba(62,207,142,.1); border-color: var(--green); } | |
| .callout-title { font-weight: 700; margin-bottom: .3rem; } | |
| .callout-info .callout-title { color: var(--accent); } | |
| .callout-warn .callout-title { color: var(--yellow); } | |
| .callout-tip .callout-title { color: var(--green); } | |
| /* ── Quiz ── */ | |
| .quiz-box { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 1.5rem; | |
| margin: 2rem 0; | |
| } | |
| .quiz-box h3 { | |
| border: none; padding: 0; margin: 0 0 1rem; | |
| font-size: 1rem; color: var(--yellow); | |
| } | |
| .quiz-q { margin-bottom: .8rem; font-size: .9rem; } | |
| .quiz-q strong { color: var(--accent); } | |
| details { margin: .5rem 0; } | |
| summary { | |
| cursor: pointer; | |
| color: var(--green); | |
| font-size: .82rem; | |
| padding: .4rem .6rem; | |
| background: rgba(62,207,142,.07); | |
| border-radius: 5px; | |
| border: 1px solid rgba(62,207,142,.2); | |
| list-style: none; | |
| user-select: none; | |
| } | |
| summary::-webkit-details-marker { display: none; } | |
| summary::before { content: '▶ '; font-size: .7rem; } | |
| details[open] summary::before { content: '▼ '; } | |
| .answer { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-top: none; | |
| border-radius: 0 0 5px 5px; | |
| padding: .9rem 1rem; | |
| font-size: .85rem; | |
| } | |
| /* ── Cheatsheet ── */ | |
| .cs-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); | |
| gap: 1rem; | |
| margin: 1rem 0 2rem; | |
| } | |
| .cs-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .cs-card-header { | |
| padding: .6rem 1rem; | |
| background: var(--surface2); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: .6rem; | |
| } | |
| .cs-tag { | |
| font-size: .68rem; font-weight: 700; | |
| padding: .15rem .5rem; border-radius: 4px; | |
| letter-spacing: .05em; text-transform: uppercase; | |
| } | |
| .cs-tag-ddl { background: rgba(124,92,191,.25); color: #c4a8ff; } | |
| .cs-tag-dml { background: rgba(62,207,142,.2); color: var(--green); } | |
| .cs-tag-dql { background: rgba(91,141,238,.2); color: var(--accent); } | |
| .cs-tag-adv { background: rgba(245,166,35,.2); color: var(--yellow); } | |
| .cs-card-title { font-size: .88rem; font-weight: 700; color: #fff; } | |
| .cs-card pre { | |
| margin: 0 ; | |
| border: none ; | |
| border-radius: 0 ; | |
| font-size: .78rem ; | |
| } | |
| .cs-note { | |
| padding: .5rem 1rem; | |
| font-size: .77rem; | |
| color: var(--muted); | |
| border-top: 1px solid var(--border); | |
| } | |
| /* ── TOC progress line ── */ | |
| #progress-bar { | |
| position: fixed; top: 0; left: 0; right: 0; | |
| height: 2px; background: var(--border); z-index: 200; | |
| } | |
| #progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), #a78bfa); | |
| width: 0%; | |
| transition: width .1s; | |
| } | |
| /* ── Divider ── */ | |
| hr { border: none; border-top: 1px solid var(--border); margin: 2.5rem 0; } | |
| /* ── Data example table ── */ | |
| .data-preview { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin: .75rem 0 1.25rem; | |
| font-size: .8rem; | |
| } | |
| .data-preview table { margin: 0; } | |
| .data-preview th { background: #1e2235; font-size: .73rem; } | |
| .data-preview td { font-family: 'Fira Code', monospace; font-size: .78rem; } | |
| @media (max-width: 900px) { | |
| #sidebar { display: none; } | |
| #main { margin-left: 0; padding: 1.5rem; } | |
| .cs-grid { grid-template-columns: 1fr; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="progress-bar"><div id="progress-fill"></div></div> | |
| <!-- ══ SIDEBAR ══ --> | |
| <nav id="sidebar"> | |
| <div class="logo">SQL Tutorial TH</div> | |
| <nav> | |
| <div class="nav-section">เนื้อหา</div> | |
| <a href="#part1">ส่วนที่ 1 — พื้นฐาน</a> | |
| <a href="#p1-what">SQL คืออะไร?</a> | |
| <a href="#p1-concepts">แนวคิดหลัก</a> | |
| <a href="#p1-types">ประเภทคำสั่ง</a> | |
| <a href="#p1-datatypes">ประเภทข้อมูล</a> | |
| <a href="#p1-keys">Key คืออะไร?</a> | |
| <a href="#p1-null">NULL คืออะไร?</a> | |
| <a href="#q1">แบบทดสอบ 1</a> | |
| <div class="nav-section">สร้างตาราง</div> | |
| <a href="#part2">ส่วนที่ 2 — CREATE TABLE</a> | |
| <a href="#p2-syntax">โครงสร้างคำสั่ง</a> | |
| <a href="#p2-examples">ตัวอย่างตาราง</a> | |
| <a href="#p2-insert">INSERT ข้อมูล</a> | |
| <a href="#p2-alter">แก้ไขตาราง</a> | |
| <a href="#q2">แบบทดสอบ 2</a> | |
| <div class="nav-section">Query ข้อมูล</div> | |
| <a href="#part3">ส่วนที่ 3 — SELECT</a> | |
| <a href="#p3-select">SELECT พื้นฐาน</a> | |
| <a href="#p3-where">WHERE & เงื่อนไข</a> | |
| <a href="#p3-order">ORDER BY & LIMIT</a> | |
| <a href="#p3-agg">Aggregate Functions</a> | |
| <a href="#p3-group">GROUP BY & HAVING</a> | |
| <a href="#p3-join">JOIN</a> | |
| <a href="#p3-sub">Subquery</a> | |
| <a href="#p3-fn">ฟังก์ชันที่ใช้บ่อย</a> | |
| <a href="#q3">แบบทดสอบ 3</a> | |
| <div class="nav-section">ขั้นสูง</div> | |
| <a href="#part4">ส่วนที่ 4 — Advanced</a> | |
| <a href="#p4-update">UPDATE & DELETE</a> | |
| <a href="#p4-tx">Transaction</a> | |
| <a href="#p4-view">View</a> | |
| <a href="#p4-index">Index</a> | |
| <a href="#p4-window">Window Functions</a> | |
| <a href="#p4-cte">CTE</a> | |
| <a href="#p4-practical">Practical Queries</a> | |
| <a href="#q4">แบบทดสอบ 4</a> | |
| <div class="nav-section">Reference</div> | |
| <a href="#cheatsheet">Cheatsheet ครบทุกคำสั่ง</a> | |
| <a href="#cs-ddl">DDL — สร้าง/แก้/ลบโครงสร้าง</a> | |
| <a href="#cs-dml">DML — จัดการข้อมูล</a> | |
| <a href="#cs-dql">DQL — ดึงข้อมูล</a> | |
| <a href="#cs-adv">Advanced SQL</a> | |
| <a href="#cs-fn">Functions Reference</a> | |
| </nav> | |
| </nav> | |
| <!-- ══ MAIN ══ --> | |
| <main id="main"> | |
| <!-- Hero --> | |
| <div class="hero"> | |
| <h1>คู่มือ SQL ฉบับเร่งรัด</h1> | |
| <p>จากศูนย์สู่มืออาชีพ — เนื้อหาครบทั้งทฤษฎีและปฏิบัติ พร้อม Cheatsheet อ้างอิงครบทุกคำสั่ง</p> | |
| <div class="badges"> | |
| <span class="badge badge-blue">4 บทเรียน</span> | |
| <span class="badge badge-green">SQLite / MySQL / PostgreSQL</span> | |
| <span class="badge badge-purple">ภาษาไทย</span> | |
| </div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PART 1 — FUNDAMENTALS | |
| ══════════════════════════════════════ --> | |
| <div id="part1" class="part-header"> | |
| <div class="part-number">1</div> | |
| <div> | |
| <h2>พื้นฐาน SQL คืออะไร?</h2> | |
| <p style="color:var(--muted);font-size:.85rem;margin:0">แนวคิดหลัก, ประเภทข้อมูล, Key, NULL</p> | |
| </div> | |
| </div> | |
| <h3 id="p1-what">SQL คืออะไร?</h3> | |
| <p><strong>SQL</strong> ย่อมาจาก <strong>Structured Query Language</strong> — ภาษาสำหรับ "คุยกับฐานข้อมูล" ลองนึกว่าฐานข้อมูลคือ Excel ขนาดยักษ์ที่มีหลายแผ่น (หลายตาราง) SQL คือสิ่งที่ช่วยให้เรา:</p> | |
| <div class="table-wrap"><table> | |
| <tr><th>สิ่งที่ทำได้</th><th>ตัวอย่าง</th></tr> | |
| <tr><td>ดึงข้อมูล</td><td>แสดงรายชื่อลูกค้าทั้งหมด</td></tr> | |
| <tr><td>เพิ่มข้อมูล</td><td>เพิ่มลูกค้าใหม่เข้าระบบ</td></tr> | |
| <tr><td>แก้ไขข้อมูล</td><td>เปลี่ยนที่อยู่ของลูกค้า</td></tr> | |
| <tr><td>ลบข้อมูล</td><td>ลบออเดอร์ที่ยกเลิกแล้ว</td></tr> | |
| <tr><td>วิเคราะห์ข้อมูล</td><td>ยอดขายรวมเดือนนี้เท่าไร?</td></tr> | |
| </table></div> | |
| <div class="callout callout-info"><div class="callout-title">ใช้กับฐานข้อมูลอะไรได้บ้าง?</div>SQL ใช้ได้กับ MySQL, PostgreSQL, SQLite, SQL Server, Oracle — ไวยากรณ์เหมือนกันมากกว่า 90% คู่มือนี้ใช้ SQLite เป็นหลัก</div> | |
| <h3 id="p1-concepts">แนวคิดหลัก — ตาราง, แถว, คอลัมน์</h3> | |
| <p>ข้อมูลใน SQL จัดเก็บในรูป <strong>ตาราง (Table)</strong> เหมือน Excel:</p> | |
| <div class="data-preview"><table> | |
| <tr><th>employee_id</th><th>name</th><th>department</th><th>salary</th></tr> | |
| <tr><td>1</td><td>สมชาย ใจดี</td><td>IT</td><td>55000</td></tr> | |
| <tr><td>2</td><td>สมหญิง รัก</td><td>HR</td><td>48000</td></tr> | |
| <tr><td>3</td><td>วิทยา เก่ง</td><td>IT</td><td>62000</td></tr> | |
| </table></div> | |
| <div class="table-wrap"><table> | |
| <tr><th>องค์ประกอบ</th><th>อธิบาย</th><th>ตัวอย่าง</th></tr> | |
| <tr><td><strong>ตาราง (Table)</strong></td><td>กล่องเก็บข้อมูล</td><td><code>employees</code></td></tr> | |
| <tr><td><strong>คอลัมน์ (Column)</strong></td><td>หัวข้อ / ประเภทข้อมูล</td><td><code>name</code>, <code>salary</code></td></tr> | |
| <tr><td><strong>แถว (Row)</strong></td><td>ข้อมูล 1 รายการ</td><td>พนักงาน 1 คน</td></tr> | |
| </table></div> | |
| <h3 id="p1-types">ประเภทคำสั่ง SQL</h3> | |
| <div class="table-wrap"><table> | |
| <tr><th>กลุ่ม</th><th>ย่อว่า</th><th>คำสั่งหลัก</th><th>ใช้ทำอะไร</th></tr> | |
| <tr><td>Data Query Language</td><td><span class="badge badge-blue">DQL</span></td><td><code>SELECT</code></td><td>ดึงข้อมูล</td></tr> | |
| <tr><td>Data Definition Language</td><td><span class="badge badge-purple">DDL</span></td><td><code>CREATE, ALTER, DROP</code></td><td>สร้าง/แก้/ลบโครงสร้าง</td></tr> | |
| <tr><td>Data Manipulation Language</td><td><span class="badge badge-green">DML</span></td><td><code>INSERT, UPDATE, DELETE</code></td><td>เพิ่ม/แก้/ลบข้อมูล</td></tr> | |
| <tr><td>Data Control Language</td><td><span class="badge badge-purple">DCL</span></td><td><code>GRANT, REVOKE</code></td><td>จัดการสิทธิ์</td></tr> | |
| </table></div> | |
| <h3 id="p1-datatypes">ประเภทข้อมูล (Data Types)</h3> | |
| <div class="table-wrap"><table> | |
| <tr><th>ประเภท</th><th>ตัวอย่าง</th><th>ใช้เก็บอะไร</th></tr> | |
| <tr><td><code>INTEGER</code></td><td>1, 42, -5</td><td>เลขจำนวนเต็ม (รหัส, จำนวน)</td></tr> | |
| <tr><td><code>REAL / FLOAT</code></td><td>3.14, 99.99</td><td>เลขทศนิยม (ราคา, น้ำหนัก)</td></tr> | |
| <tr><td><code>TEXT / VARCHAR(n)</code></td><td>"สมชาย", "BKK"</td><td>ข้อความ</td></tr> | |
| <tr><td><code>DATE</code></td><td>2024-01-15</td><td>วันที่</td></tr> | |
| <tr><td><code>DATETIME</code></td><td>2024-01-15 09:30:00</td><td>วันที่และเวลา</td></tr> | |
| <tr><td><code>BOOLEAN</code></td><td>TRUE / FALSE (หรือ 1/0)</td><td>ค่าจริง/เท็จ</td></tr> | |
| </table></div> | |
| <h3 id="p1-keys">Key — ตัวระบุข้อมูล</h3> | |
| <p><strong>Primary Key (PK)</strong> — ระบุตัวตนของแต่ละแถวอย่างเฉพาะเจาะจง ห้ามซ้ำ ห้ามว่าง เช่น รหัสพนักงาน</p> | |
| <pre><code class="language-sql">employee_id INTEGER PRIMARY KEY AUTOINCREMENT</code></pre> | |
| <p><strong>Foreign Key (FK)</strong> — เชื่อมโยงตารางหนึ่งไปยังอีกตาราง ป้องกันข้อมูลอ้างอิงที่ไม่มีอยู่จริง</p> | |
| <pre><code class="language-sql">customer_id INTEGER REFERENCES customers(customer_id)</code></pre> | |
| <h3 id="p1-null">NULL คืออะไร?</h3> | |
| <p><code>NULL</code> หมายถึง "ไม่มีค่า" หรือ "ยังไม่รู้" — <strong>ไม่เหมือน</strong> เลขศูนย์ (0) หรือสตริงว่าง ("")</p> | |
| <div class="callout callout-warn"> | |
| <div class="callout-title">ระวัง!</div> | |
| เมื่อเปรียบเทียบกับ NULL ต้องใช้ <code>IS NULL</code> หรือ <code>IS NOT NULL</code> เท่านั้น — <strong>ห้ามใช้ <code>= NULL</code></strong> | |
| </div> | |
| <pre><code class="language-sql">-- ถูก | |
| WHERE phone IS NULL | |
| -- ผิด (ไม่ได้ผลลัพธ์ที่ต้องการ) | |
| WHERE phone = NULL</code></pre> | |
| <!-- Quiz 1 --> | |
| <div id="q1" class="quiz-box"> | |
| <h3>🎯 แบบทดสอบส่วนที่ 1</h3> | |
| <div class="quiz-q"><strong>ข้อ 1.</strong> SQL ย่อมาจากอะไร? ใช้ทำอะไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 2.</strong> ตาราง, คอลัมน์, แถว — แต่ละอย่างคืออะไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 3.</strong> PRIMARY KEY คืออะไร? ต่างจาก UNIQUE อย่างไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 4.</strong> ต้องการเก็บ "ยอดขาย 12500.50 บาท" ควรใช้ประเภทข้อมูลใด?<br><em>(ก) TEXT (ข) INTEGER (ค) REAL (ง) DATE</em></div> | |
| <div class="quiz-q"><strong>ข้อ 5.</strong> คำสั่งใดถูกต้อง?<br><em>(ก) WHERE phone = NULL (ข) WHERE phone IS NULL (ค) WHERE phone == NULL</em></div> | |
| <details><summary>ดูเฉลย</summary><div class="answer"> | |
| <p><strong>1.</strong> Structured Query Language — ภาษาสำหรับสื่อสารกับฐานข้อมูล ทั้งดึง เพิ่ม แก้ไข และลบข้อมูล</p> | |
| <p><strong>2.</strong> ตาราง = กล่องเก็บข้อมูล, คอลัมน์ = หัวข้อ/ประเภท, แถว = ข้อมูล 1 รายการ</p> | |
| <p><strong>3.</strong> PRIMARY KEY ระบุตัวตนเฉพาะ ห้ามซ้ำ ห้าม NULL มีได้คอลัมน์เดียว / UNIQUE ห้ามซ้ำแต่อนุญาต NULL มีได้หลายคอลัมน์</p> | |
| <p><strong>4.</strong> (ค) REAL</p> | |
| <p><strong>5.</strong> (ข) WHERE phone IS NULL</p> | |
| </div></details> | |
| </div> | |
| <hr> | |
| <!-- ══════════════════════════════════════ | |
| PART 2 — CREATE TABLE | |
| ══════════════════════════════════════ --> | |
| <div id="part2" class="part-header"> | |
| <div class="part-number">2</div> | |
| <div> | |
| <h2>สร้างตารางข้อมูล (CREATE TABLE)</h2> | |
| <p style="color:var(--muted);font-size:.85rem;margin:0">ฐานข้อมูลร้านอาหาร — 6 ตารางพร้อมข้อมูลตัวอย่าง</p> | |
| </div> | |
| </div> | |
| <h3 id="p2-syntax">โครงสร้างคำสั่ง CREATE TABLE</h3> | |
| <pre><code class="language-sql">CREATE TABLE ชื่อตาราง ( | |
| ชื่อคอลัมน์1 ประเภทข้อมูล [ข้อจำกัด], | |
| ชื่อคอลัมน์2 ประเภทข้อมูล [ข้อจำกัด], | |
| ... | |
| );</code></pre> | |
| <h4>ข้อจำกัด (Constraints)</h4> | |
| <div class="table-wrap"><table> | |
| <tr><th>Constraint</th><th>ความหมาย</th></tr> | |
| <tr><td><code>PRIMARY KEY</code></td><td>ระบุตัวตนเฉพาะ ห้ามซ้ำ ห้ามว่าง</td></tr> | |
| <tr><td><code>NOT NULL</code></td><td>ห้ามว่าง ต้องมีค่าเสมอ</td></tr> | |
| <tr><td><code>UNIQUE</code></td><td>ห้ามซ้ำ (อนุญาต NULL ได้)</td></tr> | |
| <tr><td><code>DEFAULT value</code></td><td>ค่าเริ่มต้นถ้าไม่ระบุ</td></tr> | |
| <tr><td><code>CHECK (เงื่อนไข)</code></td><td>ตรวจสอบค่าที่ใส่เข้ามา</td></tr> | |
| <tr><td><code>REFERENCES table(col)</code></td><td>Foreign Key — เชื่อมกับตารางอื่น</td></tr> | |
| <tr><td><code>AUTOINCREMENT</code></td><td>เพิ่มตัวเลขอัตโนมัติ</td></tr> | |
| </table></div> | |
| <h3 id="p2-examples">ตัวอย่างตาราง — ระบบร้านอาหาร</h3> | |
| <h4>ตาราง 1 — พนักงาน (employees)</h4> | |
| <pre><code class="language-sql">CREATE TABLE employees ( | |
| employee_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| first_name TEXT NOT NULL, | |
| last_name TEXT NOT NULL, | |
| email TEXT UNIQUE NOT NULL, | |
| phone TEXT, -- ไม่บังคับ → อนุญาต NULL | |
| department TEXT NOT NULL DEFAULT 'General', | |
| position TEXT NOT NULL, | |
| salary REAL NOT NULL CHECK (salary > 0), | |
| hire_date DATE NOT NULL, | |
| is_active INTEGER DEFAULT 1 -- 1=ทำงานอยู่, 0=ลาออก | |
| );</code></pre> | |
| <h4>ตาราง 2 — เมนูอาหาร (menu_items)</h4> | |
| <pre><code class="language-sql">CREATE TABLE menu_items ( | |
| item_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| item_name TEXT NOT NULL, | |
| category TEXT NOT NULL, | |
| price REAL NOT NULL CHECK (price >= 0), | |
| cost REAL NOT NULL CHECK (cost >= 0), | |
| is_available INTEGER DEFAULT 1, | |
| description TEXT | |
| );</code></pre> | |
| <h4>ตาราง 3 — ลูกค้า (customers)</h4> | |
| <pre><code class="language-sql">CREATE TABLE customers ( | |
| customer_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| first_name TEXT NOT NULL, | |
| last_name TEXT NOT NULL, | |
| email TEXT UNIQUE, | |
| phone TEXT, | |
| birth_date DATE, | |
| member_since DATE NOT NULL, | |
| member_level TEXT DEFAULT 'Bronze' | |
| CHECK (member_level IN ('Bronze', 'Silver', 'Gold')), | |
| total_spent REAL DEFAULT 0 | |
| );</code></pre> | |
| <h4>ตาราง 4 — ออเดอร์ (orders) พร้อม Foreign Key</h4> | |
| <pre><code class="language-sql">CREATE TABLE orders ( | |
| order_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| customer_id INTEGER NOT NULL REFERENCES customers(customer_id), | |
| employee_id INTEGER NOT NULL REFERENCES employees(employee_id), | |
| order_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| table_number INTEGER, | |
| status TEXT DEFAULT 'Pending' | |
| CHECK (status IN ('Pending','Preparing','Served','Cancelled')), | |
| total_amount REAL DEFAULT 0, | |
| notes TEXT | |
| );</code></pre> | |
| <h4>ตาราง 5 — รายการในออเดอร์ (order_items)</h4> | |
| <pre><code class="language-sql">CREATE TABLE order_items ( | |
| order_item_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| order_id INTEGER NOT NULL REFERENCES orders(order_id), | |
| item_id INTEGER NOT NULL REFERENCES menu_items(item_id), | |
| quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0), | |
| unit_price REAL NOT NULL CHECK (unit_price >= 0) | |
| );</code></pre> | |
| <h3 id="p2-insert">INSERT — เพิ่มข้อมูล</h3> | |
| <pre><code class="language-sql">-- รูปแบบมาตรฐาน (แนะนำ — ระบุคอลัมน์ชัดเจน) | |
| INSERT INTO ชื่อตาราง (คอลัมน์1, คอลัมน์2, ...) | |
| VALUES (ค่า1, ค่า2, ...); | |
| -- เพิ่มหลายแถวพร้อมกัน | |
| INSERT INTO ชื่อตาราง (คอลัมน์1, คอลัมน์2) | |
| VALUES | |
| (ค่า1a, ค่า2a), | |
| (ค่า1b, ค่า2b), | |
| (ค่า1c, ค่า2c);</code></pre> | |
| <h4>เพิ่มพนักงานตัวอย่าง</h4> | |
| <pre><code class="language-sql">INSERT INTO employees (first_name, last_name, email, phone, department, position, salary, hire_date) | |
| VALUES | |
| ('สมชาย', 'ใจดี', 'somchai@rest.com', '081-111-1111', 'Kitchen', 'เชฟ', 35000, '2020-03-15'), | |
| ('สมหญิง', 'รักงาน', 'somying@rest.com', '082-222-2222', 'Service', 'พนักงานเสิร์ฟ', 22000, '2021-06-01'), | |
| ('วิทยา', 'เก่งมาก', 'wittaya@rest.com', '083-333-3333', 'Kitchen', 'ผู้ช่วยเชฟ', 28000, '2021-09-10'), | |
| ('นิดา', 'สวยงาม', 'nida@rest.com', '084-444-4444', 'Service', 'พนักงานเสิร์ฟ', 22000, '2022-01-20'), | |
| ('มานะ', 'บากบั่น', 'mana@rest.com', NULL, 'Management', 'ผู้จัดการ', 55000, '2019-07-01'), | |
| ('กมล', 'เหนียวแน่','kamol@rest.com', '086-666-6666', 'Service', 'แคชเชียร์', 25000, '2022-04-15'), | |
| ('ประภา', 'ฉลาด', 'prapa@rest.com', '087-777-7777', 'Kitchen', 'เชฟ', 38000, '2020-11-01');</code></pre> | |
| <h4>เพิ่มเมนูอาหาร</h4> | |
| <pre><code class="language-sql">INSERT INTO menu_items (item_name, category, price, cost, description) | |
| VALUES | |
| ('ข้าวผัดกุ้ง', 'อาหารจานหลัก', 120, 45, 'ข้าวผัดกุ้งสดกับไข่ดาว'), | |
| ('ต้มยำกุ้ง', 'อาหารจานหลัก', 180, 70, 'ต้มยำน้ำข้นรสเข้มข้น'), | |
| ('ผัดไทยกุ้งสด', 'อาหารจานหลัก', 150, 55, 'ผัดไทยเส้นเล็กกุ้งสด'), | |
| ('ส้มตำไทย', 'อาหารเรียกน้ำย่อย', 60, 20, 'ส้มตำรสจัดจ้าน'), | |
| ('น้ำเปล่า', 'เครื่องดื่ม', 15, 3, NULL), | |
| ('น้ำอัดลม', 'เครื่องดื่ม', 35, 12, NULL), | |
| ('ชาเย็น', 'เครื่องดื่ม', 45, 15, 'ชาไทยใส่นมข้นหวาน'), | |
| ('ไอศกรีมกะทิ', 'ของหวาน', 65, 22, 'ไอศกรีมกะทิราดน้ำตาลมะพร้าว'), | |
| ('มะม่วงข้าวเหนียว', 'ของหวาน', 120, 45, 'มะม่วงน้ำดอกไม้ข้าวเหนียวมูน'), | |
| ('ลาบหมู', 'อาหารจานหลัก', 110, 42, 'ลาบหมูสับสมุนไพรหอม');</code></pre> | |
| <h3 id="p2-alter">แก้ไขโครงสร้างตาราง (ALTER TABLE)</h3> | |
| <pre><code class="language-sql">-- เพิ่มคอลัมน์ใหม่ | |
| ALTER TABLE customers ADD COLUMN line_id TEXT; | |
| -- เปลี่ยนชื่อคอลัมน์ | |
| ALTER TABLE employees RENAME COLUMN phone TO phone_number; | |
| -- เปลี่ยนชื่อตาราง | |
| ALTER TABLE old_name RENAME TO new_name; | |
| -- ลบตาราง (ระวัง! ถาวร) | |
| DROP TABLE IF EXISTS table_name; | |
| -- ล้างข้อมูลแต่เก็บโครงสร้าง | |
| DELETE FROM table_name;</code></pre> | |
| <!-- Quiz 2 --> | |
| <div id="q2" class="quiz-box"> | |
| <h3>🎯 แบบทดสอบส่วนที่ 2</h3> | |
| <div class="quiz-q"><strong>ข้อ 1.</strong> เขียนคำสั่งสร้างตาราง <code>suppliers</code> ที่มี: supplier_id (PK, auto), company_name (ไม่ซ้ำ, ห้ามว่าง), contact_name, phone, rating (1–5, default 3)</div> | |
| <div class="quiz-q"><strong>ข้อ 2.</strong> UNIQUE ต่างจาก PRIMARY KEY อย่างไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 3.</strong> เขียน INSERT เพิ่มเมนู "กาแฟร้อน" ราคา 55 บาท ต้นทุน 18 บาท หมวด "เครื่องดื่ม"</div> | |
| <div class="quiz-q"><strong>ข้อ 4.</strong> ทำไม orders ต้องมี REFERENCES customers(customer_id)?</div> | |
| <details><summary>ดูเฉลย</summary><div class="answer"> | |
| <pre><code class="language-sql">-- ข้อ 1 | |
| CREATE TABLE suppliers ( | |
| supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| company_name TEXT NOT NULL UNIQUE, | |
| contact_name TEXT, | |
| phone TEXT, | |
| rating INTEGER DEFAULT 3 CHECK (rating BETWEEN 1 AND 5) | |
| );</code></pre> | |
| <p><strong>ข้อ 2.</strong> UNIQUE ห้ามซ้ำแต่อนุญาต NULL ได้หลายแถว, มีได้หลายคอลัมน์ต่อตาราง / PRIMARY KEY ห้ามซ้ำและห้าม NULL เด็ดขาด มีได้คอลัมน์เดียว</p> | |
| <pre><code class="language-sql">-- ข้อ 3 | |
| INSERT INTO menu_items (item_name, category, price, cost) | |
| VALUES ('กาแฟร้อน', 'เครื่องดื่ม', 55, 18);</code></pre> | |
| <p><strong>ข้อ 4.</strong> เพื่อป้องกันสร้างออเดอร์ให้ลูกค้าที่ไม่มีในระบบ — Foreign Key บังคับให้ customer_id ใน orders ต้องมีอยู่จริงในตาราง customers</p> | |
| </div></details> | |
| </div> | |
| <hr> | |
| <!-- ══════════════════════════════════════ | |
| PART 3 — SELECT & QUERY | |
| ══════════════════════════════════════ --> | |
| <div id="part3" class="part-header"> | |
| <div class="part-number">3</div> | |
| <div> | |
| <h2>ดึงและวิเคราะห์ข้อมูล (SELECT)</h2> | |
| <p style="color:var(--muted);font-size:.85rem;margin:0">WHERE, JOIN, GROUP BY, HAVING, Subquery, Functions</p> | |
| </div> | |
| </div> | |
| <h3 id="p3-select">SELECT พื้นฐาน</h3> | |
| <pre><code class="language-sql">-- ดึงทุกคอลัมน์ | |
| SELECT * FROM employees; | |
| -- เลือกเฉพาะคอลัมน์ | |
| SELECT first_name, last_name, position, salary | |
| FROM employees; | |
| -- ตั้งชื่อเล่น (Alias) | |
| SELECT | |
| first_name AS ชื่อ, | |
| last_name AS นามสกุล, | |
| salary AS เงินเดือน | |
| FROM employees; | |
| -- คำนวณในคอลัมน์ | |
| SELECT | |
| first_name, | |
| salary, | |
| salary * 12 AS เงินเดือนต่อปี, | |
| salary * 0.05 AS โบนัส5เปอร์เซ็น | |
| FROM employees;</code></pre> | |
| <h3 id="p3-where">WHERE — กรองข้อมูล</h3> | |
| <div class="table-wrap"><table> | |
| <tr><th>ตัวดำเนินการ</th><th>ความหมาย</th><th>ตัวอย่าง</th></tr> | |
| <tr><td><code>=</code></td><td>เท่ากับ</td><td><code>department = 'Kitchen'</code></td></tr> | |
| <tr><td><code><></code> หรือ <code>!=</code></td><td>ไม่เท่ากับ</td><td><code>status != 'Cancelled'</code></td></tr> | |
| <tr><td><code>> < >= <=</code></td><td>เปรียบเทียบ</td><td><code>salary > 30000</code></td></tr> | |
| <tr><td><code>IN (...)</code></td><td>อยู่ใน list</td><td><code>department IN ('IT','HR')</code></td></tr> | |
| <tr><td><code>BETWEEN a AND b</code></td><td>อยู่ในช่วง</td><td><code>salary BETWEEN 25000 AND 50000</code></td></tr> | |
| <tr><td><code>LIKE 'pattern'</code></td><td>ค้นหาด้วยรูปแบบ</td><td><code>name LIKE 'สม%'</code></td></tr> | |
| <tr><td><code>IS NULL / IS NOT NULL</code></td><td>ตรวจ NULL</td><td><code>phone IS NULL</code></td></tr> | |
| <tr><td><code>AND / OR / NOT</code></td><td>รวมเงื่อนไข</td><td><code>dept='IT' AND salary>50000</code></td></tr> | |
| </table></div> | |
| <pre><code class="language-sql">-- AND — เงื่อนไขทั้งหมดต้องเป็นจริง | |
| SELECT first_name, department, salary | |
| FROM employees | |
| WHERE department = 'Kitchen' AND salary > 30000; | |
| -- OR — อย่างน้อยหนึ่งเงื่อนไข | |
| SELECT first_name, department | |
| FROM employees | |
| WHERE department = 'Kitchen' OR department = 'Management'; | |
| -- IN — เขียนสั้นกว่า OR หลายตัว | |
| SELECT first_name, department | |
| FROM employees | |
| WHERE department IN ('Kitchen', 'Management'); | |
| -- BETWEEN — ช่วงค่า (รวมค่าขอบเขตทั้งสอง) | |
| SELECT first_name, salary | |
| FROM employees | |
| WHERE salary BETWEEN 25000 AND 40000; | |
| -- LIKE — % = อักษรใดก็ได้กี่ตัวก็ได้, _ = 1 ตัว | |
| SELECT item_name FROM menu_items WHERE item_name LIKE '%กุ้ง%'; | |
| SELECT first_name FROM employees WHERE first_name LIKE 'สม%'; | |
| SELECT email FROM employees WHERE email LIKE '%@rest.com';</code></pre> | |
| <h3 id="p3-order">ORDER BY & LIMIT</h3> | |
| <pre><code class="language-sql">-- เรียงจากมากไปน้อย (DESC) หรือน้อยไปมาก (ASC) | |
| SELECT first_name, salary | |
| FROM employees | |
| ORDER BY salary DESC; | |
| -- เรียงหลายคอลัมน์ | |
| SELECT first_name, department, salary | |
| FROM employees | |
| ORDER BY department ASC, salary DESC; | |
| -- จำกัดจำนวนผลลัพธ์ | |
| SELECT item_name, price | |
| FROM menu_items | |
| ORDER BY price DESC | |
| LIMIT 5; | |
| -- ข้าม N แถวแรก (Pagination) | |
| SELECT item_name, price | |
| FROM menu_items | |
| ORDER BY price DESC | |
| LIMIT 5 OFFSET 5; -- หน้า 2: แถวที่ 6-10</code></pre> | |
| <h3 id="p3-agg">Aggregate Functions — สรุปข้อมูล</h3> | |
| <div class="table-wrap"><table> | |
| <tr><th>ฟังก์ชัน</th><th>ใช้ทำอะไร</th></tr> | |
| <tr><td><code>COUNT(*)</code></td><td>นับจำนวนแถวทั้งหมด</td></tr> | |
| <tr><td><code>COUNT(col)</code></td><td>นับแถวที่ไม่ใช่ NULL</td></tr> | |
| <tr><td><code>SUM(col)</code></td><td>รวมทั้งหมด</td></tr> | |
| <tr><td><code>AVG(col)</code></td><td>ค่าเฉลี่ย</td></tr> | |
| <tr><td><code>MAX(col)</code></td><td>ค่าสูงสุด</td></tr> | |
| <tr><td><code>MIN(col)</code></td><td>ค่าต่ำสุด</td></tr> | |
| </table></div> | |
| <pre><code class="language-sql">SELECT | |
| COUNT(*) AS จำนวนพนักงาน, | |
| AVG(salary) AS เงินเดือนเฉลี่ย, | |
| MAX(salary) AS เงินเดือนสูงสุด, | |
| MIN(salary) AS เงินเดือนต่ำสุด, | |
| SUM(salary) AS ค่าใช้จ่ายรวม | |
| FROM employees;</code></pre> | |
| <h3 id="p3-group">GROUP BY & HAVING</h3> | |
| <p>GROUP BY แยกข้อมูลเป็นกลุ่ม แล้ว Aggregate แต่ละกลุ่ม — เหมือน Pivot Table ใน Excel</p> | |
| <div class="callout callout-info"> | |
| <div class="callout-title">ลำดับประมวลผล SQL (สำคัญมาก!)</div> | |
| <code>FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT</code> | |
| </div> | |
| <pre><code class="language-sql">-- จำนวนและเงินเดือนเฉลี่ยแต่ละแผนก | |
| SELECT | |
| department, | |
| COUNT(*) AS จำนวนพนักงาน, | |
| AVG(salary) AS เงินเดือนเฉลี่ย, | |
| SUM(salary) AS ค่าใช้จ่ายรวม | |
| FROM employees | |
| GROUP BY department | |
| ORDER BY เงินเดือนเฉลี่ย DESC; | |
| -- ยอดขายแต่ละวัน | |
| SELECT | |
| DATE(order_date) AS วันที่, | |
| COUNT(*) AS จำนวนออเดอร์, | |
| SUM(total_amount) AS ยอดขายรวม | |
| FROM orders | |
| WHERE status = 'Served' | |
| GROUP BY DATE(order_date) | |
| ORDER BY วันที่; | |
| -- HAVING — กรองหลัง GROUP BY (ต่างจาก WHERE ที่กรองก่อน) | |
| SELECT department, COUNT(*) AS จำนวนพนักงาน | |
| FROM employees | |
| GROUP BY department | |
| HAVING COUNT(*) > 1; -- เฉพาะแผนกที่มีมากกว่า 1 คน</code></pre> | |
| <h3 id="p3-join">JOIN — รวมข้อมูลหลายตาราง</h3> | |
| <pre><code class="language-sql">-- INNER JOIN — เฉพาะแถวที่มีคู่ทั้งสองตาราง | |
| SELECT | |
| o.order_id, | |
| c.first_name || ' ' || c.last_name AS ชื่อลูกค้า, | |
| o.order_date, | |
| o.total_amount, | |
| o.status | |
| FROM orders AS o | |
| INNER JOIN customers AS c ON o.customer_id = c.customer_id; | |
| -- JOIN สามตาราง | |
| SELECT | |
| o.order_id, | |
| c.first_name AS ลูกค้า, | |
| m.item_name AS เมนู, | |
| oi.quantity AS จำนวน, | |
| oi.quantity * oi.unit_price AS ยอดรวม | |
| FROM order_items AS oi | |
| INNER JOIN orders AS o ON oi.order_id = o.order_id | |
| INNER JOIN customers AS c ON o.customer_id = c.customer_id | |
| INNER JOIN menu_items AS m ON oi.item_id = m.item_id; | |
| -- LEFT JOIN — ทุกแถวจากซ้าย แม้ไม่มีคู่ในตารางขวา | |
| SELECT | |
| c.first_name, | |
| c.last_name, | |
| COUNT(o.order_id) AS จำนวนออเดอร์ | |
| FROM customers AS c | |
| LEFT JOIN orders AS o ON c.customer_id = o.customer_id | |
| GROUP BY c.customer_id, c.first_name, c.last_name | |
| ORDER BY จำนวนออเดอร์ DESC;</code></pre> | |
| <h3 id="p3-sub">Subquery — Query ซ้อน Query</h3> | |
| <pre><code class="language-sql">-- ใน WHERE | |
| SELECT first_name, salary | |
| FROM employees | |
| WHERE salary > (SELECT AVG(salary) FROM employees); | |
| -- ใน WHERE ด้วย IN | |
| SELECT item_name, price | |
| FROM menu_items | |
| WHERE item_id IN ( | |
| SELECT item_id FROM order_items | |
| GROUP BY item_id | |
| HAVING COUNT(*) > 2 | |
| ); | |
| -- เป็น Derived Table ใน FROM | |
| SELECT dept, avg_sal | |
| FROM ( | |
| SELECT department AS dept, AVG(salary) AS avg_sal | |
| FROM employees | |
| GROUP BY department | |
| ) AS dept_summary | |
| WHERE avg_sal > 30000;</code></pre> | |
| <h3 id="p3-fn">ฟังก์ชันที่ใช้บ่อย</h3> | |
| <h4>ข้อความ</h4> | |
| <pre><code class="language-sql">SELECT | |
| UPPER(first_name) AS ตัวพิมพ์ใหญ่, | |
| LOWER(email) AS ตัวพิมพ์เล็ก, | |
| LENGTH(first_name) AS ความยาว, | |
| first_name || ' ' || last_name AS ชื่อเต็ม, | |
| SUBSTR(phone, 1, 3) AS รหัสต้น, | |
| TRIM(' text ') AS ตัดช่องว่าง, | |
| REPLACE(department, 'Kitchen', 'ครัว') AS แปลคำ | |
| FROM employees;</code></pre> | |
| <h4>ตัวเลข</h4> | |
| <pre><code class="language-sql">SELECT | |
| ROUND(salary * 1.1, 2) AS หลังขึ้นเงิน, | |
| ABS(-500) AS ค่าสมบูรณ์, | |
| CEIL(3.2) AS ปัดขึ้น, -- 4 | |
| FLOOR(3.9) AS ปัดลง -- 3 | |
| FROM employees;</code></pre> | |
| <h4>วันที่ (SQLite)</h4> | |
| <pre><code class="language-sql">SELECT | |
| DATE('now') AS วันนี้, | |
| DATE(order_date) AS วันที่ออเดอร์, | |
| strftime('%Y', order_date) AS ปี, | |
| strftime('%m', order_date) AS เดือน, | |
| strftime('%Y-%m', order_date) AS ปี_เดือน, | |
| DATE('now', '-30 days') AS 30วันที่แล้ว | |
| FROM orders;</code></pre> | |
| <h4>CASE WHEN — เงื่อนไขแบบ if-else</h4> | |
| <pre><code class="language-sql">SELECT | |
| first_name, | |
| salary, | |
| CASE | |
| WHEN salary >= 50000 THEN 'สูง' | |
| WHEN salary >= 30000 THEN 'กลาง' | |
| ELSE 'ต่ำ' | |
| END AS ระดับเงินเดือน | |
| FROM employees; | |
| -- นับจำนวนแบบมีเงื่อนไข (Conditional Aggregation) | |
| SELECT | |
| COUNT(*) AS ออเดอร์ทั้งหมด, | |
| COUNT(CASE WHEN status = 'Served' THEN 1 END) AS สำเร็จ, | |
| COUNT(CASE WHEN status = 'Cancelled' THEN 1 END) AS ยกเลิก, | |
| SUM(CASE WHEN status = 'Served' THEN total_amount ELSE 0 END) AS ยอดขายรวม | |
| FROM orders;</code></pre> | |
| <!-- Quiz 3 --> | |
| <div id="q3" class="quiz-box"> | |
| <h3>🎯 แบบทดสอบส่วนที่ 3</h3> | |
| <div class="quiz-q"><strong>ข้อ 1.</strong> เขียน query ดูชื่อและราคาเมนูหมวด "ของหวาน" เรียงจากราคามากไปน้อย</div> | |
| <div class="quiz-q"><strong>ข้อ 2.</strong> เขียน query หาพนักงานที่เงินเดือน 20,000–35,000 บาท ในแผนก Service หรือ Kitchen</div> | |
| <div class="quiz-q"><strong>ข้อ 3.</strong> WHERE vs HAVING ต่างกันอย่างไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 4.</strong> เขียน query แสดงชื่อพนักงานพร้อมจำนวนออเดอร์ที่รับผิดชอบ (รวมคนที่ยังไม่มีออเดอร์)</div> | |
| <div class="quiz-q"><strong>ข้อ 5.</strong> INNER JOIN vs LEFT JOIN ต่างกันอย่างไร?</div> | |
| <details><summary>ดูเฉลย</summary><div class="answer"> | |
| <pre><code class="language-sql">-- ข้อ 1 | |
| SELECT item_name, price FROM menu_items | |
| WHERE category = 'ของหวาน' ORDER BY price DESC; | |
| -- ข้อ 2 | |
| SELECT first_name, department, salary FROM employees | |
| WHERE salary BETWEEN 20000 AND 35000 | |
| AND department IN ('Service', 'Kitchen'); | |
| -- ข้อ 4 | |
| SELECT e.first_name, e.last_name, COUNT(o.order_id) AS จำนวนออเดอร์ | |
| FROM employees AS e | |
| LEFT JOIN orders AS o ON e.employee_id = o.employee_id | |
| GROUP BY e.employee_id, e.first_name, e.last_name | |
| ORDER BY จำนวนออเดอร์ DESC;</code></pre> | |
| <p><strong>ข้อ 3.</strong> WHERE กรองแถวก่อน GROUP BY (กรองข้อมูลดิบ) / HAVING กรองหลัง GROUP BY (กรองผลสรุป)</p> | |
| <p><strong>ข้อ 5.</strong> INNER JOIN แสดงเฉพาะแถวที่มีคู่ทั้งสองตาราง / LEFT JOIN แสดงทุกแถวจากตารางซ้าย แม้ไม่มีคู่ในตารางขวา (ค่าจากขวาจะเป็น NULL)</p> | |
| </div></details> | |
| </div> | |
| <hr> | |
| <!-- ══════════════════════════════════════ | |
| PART 4 — ADVANCED | |
| ══════════════════════════════════════ --> | |
| <div id="part4" class="part-header"> | |
| <div class="part-number">4</div> | |
| <div> | |
| <h2>จัดการข้อมูลขั้นสูง</h2> | |
| <p style="color:var(--muted);font-size:.85rem;margin:0">UPDATE, DELETE, Transaction, View, Index, Window Functions, CTE</p> | |
| </div> | |
| </div> | |
| <h3 id="p4-update">UPDATE & DELETE</h3> | |
| <div class="callout callout-warn"> | |
| <div class="callout-title">คำเตือน!</div> | |
| ถ้าลืม <code>WHERE</code> ใน UPDATE หรือ DELETE จะแก้ไข/ลบ <strong>ทุกแถว</strong> ในตาราง ควรทำ SELECT ดูก่อนเสมอ | |
| </div> | |
| <pre><code class="language-sql">-- UPDATE — แก้ไขข้อมูล | |
| UPDATE employees | |
| SET salary = salary * 1.10 | |
| WHERE department = 'Kitchen'; | |
| -- อัปเดตหลายคอลัมน์พร้อมกัน | |
| UPDATE customers | |
| SET member_level = 'Gold', | |
| total_spent = total_spent + 500 | |
| WHERE customer_id = 1; | |
| -- DELETE — ลบข้อมูล | |
| DELETE FROM orders | |
| WHERE status = 'Cancelled'; | |
| -- ลบด้วย Subquery | |
| DELETE FROM customers | |
| WHERE customer_id NOT IN ( | |
| SELECT DISTINCT customer_id FROM orders | |
| );</code></pre> | |
| <h3 id="p4-tx">Transaction — ทำหลายคำสั่งเป็นชุดเดียว</h3> | |
| <p>Transaction มัดรวมหลายคำสั่งให้ "สำเร็จทั้งหมดหรือยกเลิกทั้งหมด" เหมือนโอนเงิน — ถ้าหักบัญชีต้นทางแล้วแต่โอนไม่สำเร็จ ต้องคืนเงิน</p> | |
| <pre><code class="language-sql">BEGIN; | |
| UPDATE customers | |
| SET total_spent = total_spent + 425 | |
| WHERE customer_id = 1; | |
| UPDATE orders | |
| SET status = 'Served' | |
| WHERE order_id = 1; | |
| COMMIT; -- ยืนยันทุกอย่าง | |
| -- หากเกิดข้อผิดพลาด: | |
| ROLLBACK; -- ยกเลิกทุกอย่างกลับสู่สถานะก่อน BEGIN</code></pre> | |
| <h3 id="p4-view">View — ตารางเสมือน</h3> | |
| <p>View คือการบันทึก Query ที่ซับซ้อนไว้เป็นชื่อ เรียกใช้ง่ายเหมือนตารางปกติ แต่ไม่ได้เก็บข้อมูลจริง (คำนวณใหม่ทุกครั้งที่เรียก)</p> | |
| <pre><code class="language-sql">-- สร้าง View | |
| CREATE VIEW vw_order_summary AS | |
| SELECT | |
| o.order_id, | |
| c.first_name || ' ' || c.last_name AS ลูกค้า, | |
| e.first_name || ' ' || e.last_name AS พนักงาน, | |
| o.order_date, | |
| o.total_amount, | |
| o.status | |
| FROM orders AS o | |
| INNER JOIN customers AS c ON o.customer_id = c.customer_id | |
| INNER JOIN employees AS e ON o.employee_id = e.employee_id; | |
| -- ใช้งาน View เหมือนตารางปกติ | |
| SELECT * FROM vw_order_summary WHERE status = 'Served'; | |
| -- ลบ View | |
| DROP VIEW IF EXISTS vw_order_summary;</code></pre> | |
| <h3 id="p4-index">Index — เพิ่มความเร็วการค้นหา</h3> | |
| <p>Index คือ "สารบัญ" ของตาราง ช่วยให้ค้นข้อมูลเร็วขึ้นมาก แต่ใช้พื้นที่เพิ่มเล็กน้อยและช้าลงเล็กน้อยตอน INSERT/UPDATE/DELETE</p> | |
| <pre><code class="language-sql">-- สร้าง Index บนคอลัมน์ที่ค้นหาบ่อย | |
| CREATE INDEX idx_orders_customer ON orders(customer_id); | |
| CREATE INDEX idx_orders_status ON orders(status); | |
| CREATE INDEX idx_emp_dept ON employees(department); | |
| -- Index หลายคอลัมน์ (Composite Index) | |
| CREATE INDEX idx_orders_date_status ON orders(order_date, status); | |
| -- ลบ Index | |
| DROP INDEX IF EXISTS idx_orders_status;</code></pre> | |
| <div class="callout callout-tip"> | |
| <div class="callout-title">เมื่อไรควรสร้าง Index</div> | |
| คอลัมน์ที่ใช้ใน WHERE, JOIN, ORDER BY บ่อยๆ — <strong>อย่าสร้างทุกคอลัมน์</strong> เพราะช้าตอน write | |
| </div> | |
| <h3 id="p4-window">Window Functions — วิเคราะห์ข้อมูลขั้นสูง</h3> | |
| <p>คำนวณข้อมูลโดยยังเก็บแถวเดิมไว้ (ต่างจาก GROUP BY ที่รวมเป็นแถวเดียว)</p> | |
| <pre><code class="language-sql">-- ROW_NUMBER() — ลำดับที่ไม่ซ้ำ | |
| SELECT | |
| first_name, department, salary, | |
| ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS อันดับในแผนก | |
| FROM employees; | |
| -- RANK() — อันดับ (ค่าซ้ำข้ามอันดับ: 1,1,3) | |
| -- DENSE_RANK() — อันดับ (ค่าซ้ำไม่ข้าม: 1,1,2) | |
| SELECT | |
| first_name, salary, | |
| RANK() OVER (ORDER BY salary DESC) AS rank, | |
| DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank | |
| FROM employees; | |
| -- SUM() OVER — ยอดสะสม (Running Total) | |
| SELECT | |
| DATE(order_date) AS วันที่, | |
| SUM(total_amount) AS ยอดวันนี้, | |
| SUM(SUM(total_amount)) OVER (ORDER BY DATE(order_date)) AS ยอดสะสม | |
| FROM orders | |
| WHERE status = 'Served' | |
| GROUP BY DATE(order_date); | |
| -- LAG() / LEAD() — ค่าแถวก่อนหน้า / ถัดไป | |
| SELECT | |
| DATE(order_date) AS วันที่, | |
| SUM(total_amount) AS ยอดขาย, | |
| LAG(SUM(total_amount)) OVER (ORDER BY DATE(order_date)) AS ยอดเมื่อวาน | |
| FROM orders WHERE status='Served' | |
| GROUP BY DATE(order_date);</code></pre> | |
| <h3 id="p4-cte">CTE — Common Table Expression</h3> | |
| <p>ตั้งชื่อ Query ชั่วคราวด้วย <code>WITH</code> ทำให้โค้ดอ่านง่ายและนำกลับมาใช้ใหม่ได้ในคำสั่งเดียวกัน</p> | |
| <pre><code class="language-sql">WITH high_value_customers AS ( | |
| SELECT customer_id, first_name, last_name, total_spent | |
| FROM customers | |
| WHERE total_spent >= 10000 | |
| ), | |
| their_orders AS ( | |
| SELECT customer_id, COUNT(*) AS จำนวนออเดอร์, SUM(total_amount) AS ยอดสั่งรวม | |
| FROM orders | |
| WHERE status = 'Served' | |
| GROUP BY customer_id | |
| ) | |
| SELECT | |
| hvc.first_name, | |
| hvc.last_name, | |
| hvc.total_spent, | |
| COALESCE(to2.จำนวนออเดอร์, 0) AS จำนวนออเดอร์, | |
| COALESCE(to2.ยอดสั่งรวม, 0) AS ยอดสั่งรวม | |
| FROM high_value_customers AS hvc | |
| LEFT JOIN their_orders AS to2 ON hvc.customer_id = to2.customer_id;</code></pre> | |
| <h3 id="p4-practical">Practical Queries — ใช้จริงในธุรกิจ</h3> | |
| <h4>Dashboard KPI ประจำวัน</h4> | |
| <pre><code class="language-sql">SELECT | |
| COUNT(*) AS ออเดอร์ทั้งหมด, | |
| COUNT(CASE WHEN status='Served' THEN 1 END) AS สำเร็จ, | |
| COUNT(CASE WHEN status='Cancelled' THEN 1 END) AS ยกเลิก, | |
| COUNT(CASE WHEN status IN ('Pending','Preparing') THEN 1 END) AS รอดำเนินการ, | |
| COALESCE(SUM(CASE WHEN status='Served' THEN total_amount END), 0) AS ยอดขายวันนี้ | |
| FROM orders | |
| WHERE DATE(order_date) = DATE('now');</code></pre> | |
| <h4>วิเคราะห์กำไรแต่ละเมนู</h4> | |
| <pre><code class="language-sql">SELECT | |
| m.item_name, | |
| m.category, | |
| SUM(oi.quantity) AS จำนวนที่ขาย, | |
| SUM(oi.quantity * oi.unit_price) AS รายได้รวม, | |
| SUM(oi.quantity * m.cost) AS ต้นทุนรวม, | |
| SUM(oi.quantity * (oi.unit_price - m.cost)) AS กำไรรวม | |
| FROM order_items AS oi | |
| INNER JOIN orders AS o ON oi.order_id = o.order_id | |
| INNER JOIN menu_items AS m ON oi.item_id = m.item_id | |
| WHERE o.status = 'Served' | |
| GROUP BY m.item_id, m.item_name, m.category | |
| ORDER BY กำไรรวม DESC;</code></pre> | |
| <h4>ลูกค้าที่ไม่มาใช้บริการ > 30 วัน</h4> | |
| <pre><code class="language-sql">SELECT | |
| c.first_name, c.last_name, c.member_level, | |
| MAX(o.order_date) AS ออเดอร์ล่าสุด | |
| FROM customers AS c | |
| LEFT JOIN orders AS o ON c.customer_id = o.customer_id AND o.status = 'Served' | |
| GROUP BY c.customer_id, c.first_name, c.last_name, c.member_level | |
| HAVING MAX(o.order_date) < DATE('now', '-30 days') | |
| OR MAX(o.order_date) IS NULL | |
| ORDER BY ออเดอร์ล่าสุด;</code></pre> | |
| <!-- Quiz 4 --> | |
| <div id="q4" class="quiz-box"> | |
| <h3>🎯 แบบทดสอบส่วนที่ 4</h3> | |
| <div class="quiz-q"><strong>ข้อ 1.</strong> เขียน query ขึ้นราคาเมนูทุกรายการในหมวด "เครื่องดื่ม" 5 บาท</div> | |
| <div class="quiz-q"><strong>ข้อ 2.</strong> Transaction คืออะไร? ยกตัวอย่างสถานการณ์ที่ต้องใช้</div> | |
| <div class="quiz-q"><strong>ข้อ 3.</strong> View vs Table ต่างกันอย่างไร?</div> | |
| <div class="quiz-q"><strong>ข้อ 4.</strong> เขียน query แสดงอันดับพนักงานตามเงินเดือน (มากไปน้อย) ในแต่ละแผนก</div> | |
| <div class="quiz-q"><strong>ข้อ 5.</strong> CTE มีประโยชน์อะไรเหนือ Subquery?</div> | |
| <details><summary>ดูเฉลย</summary><div class="answer"> | |
| <pre><code class="language-sql">-- ข้อ 1 | |
| UPDATE menu_items SET price = price + 5 WHERE category = 'เครื่องดื่ม'; | |
| -- ข้อ 4 | |
| SELECT first_name, department, salary, | |
| RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS อันดับในแผนก | |
| FROM employees;</code></pre> | |
| <p><strong>ข้อ 2.</strong> Transaction มัดรวมหลายคำสั่งให้ทำสำเร็จทั้งหมดหรือยกเลิกทั้งหมด ตัวอย่าง: โอนเงินต้องหักต้นทาง+เพิ่มปลายทางพร้อมกัน ถ้าขั้นตอนใดล้มเหลวต้อง ROLLBACK ทั้งชุด</p> | |
| <p><strong>ข้อ 3.</strong> View ไม่เก็บข้อมูลจริง คำนวณใหม่ทุกครั้งที่เรียก ข้อมูลเสมอ up-to-date / Table เก็บข้อมูลจริงในดิสก์</p> | |
| <p><strong>ข้อ 5.</strong> CTE ตั้งชื่อได้และนำกลับมาใช้ซ้ำได้ในคำสั่งเดียวกัน อ่านง่ายกว่า Subquery ซ้อนกันหลายชั้น และสามารถมีหลาย CTE ในคำสั่งเดียว</p> | |
| </div></details> | |
| </div> | |
| <hr> | |
| <!-- ══════════════════════════════════════ | |
| CHEATSHEET | |
| ══════════════════════════════════════ --> | |
| <div id="cheatsheet" class="part-header"> | |
| <div class="part-number" style="background:linear-gradient(135deg,#f5a623,#e05252)">★</div> | |
| <div> | |
| <h2>Cheatsheet — ครบทุกคำสั่ง SQL</h2> | |
| <p style="color:var(--muted);font-size:.85rem;margin:0">Template + โครงสร้าง + ตัวอย่างอ้างอิงเร็ว</p> | |
| </div> | |
| </div> | |
| <!-- ── DDL ── --> | |
| <h3 id="cs-ddl">DDL — สร้าง/แก้ไข/ลบโครงสร้าง</h3> | |
| <div class="cs-grid"> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">CREATE TABLE</span></div> | |
| <pre><code class="language-sql">CREATE TABLE [IF NOT EXISTS] table_name ( | |
| col1 TYPE [CONSTRAINT], | |
| col2 TYPE [CONSTRAINT], | |
| ... | |
| [TABLE CONSTRAINT] | |
| );</code></pre> | |
| <div class="cs-note">สร้างตารางใหม่ — IF NOT EXISTS ป้องกัน error ถ้ามีอยู่แล้ว</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">Column Constraints</span></div> | |
| <pre><code class="language-sql">col INTEGER PRIMARY KEY AUTOINCREMENT | |
| col TEXT NOT NULL | |
| col TEXT UNIQUE | |
| col TEXT UNIQUE NOT NULL | |
| col REAL DEFAULT 0 | |
| col TEXT DEFAULT 'value' | |
| col REAL CHECK (col > 0) | |
| col TEXT CHECK (col IN ('a','b','c')) | |
| col INTEGER REFERENCES other(id)</code></pre> | |
| <div class="cs-note">ข้อจำกัดใส่ต่อท้ายประเภทข้อมูลของคอลัมน์</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">ALTER TABLE</span></div> | |
| <pre><code class="language-sql">-- เพิ่มคอลัมน์ | |
| ALTER TABLE t ADD COLUMN col TYPE [CONSTRAINT]; | |
| -- เปลี่ยนชื่อคอลัมน์ | |
| ALTER TABLE t RENAME COLUMN old TO new; | |
| -- เปลี่ยนชื่อตาราง | |
| ALTER TABLE old_name RENAME TO new_name;</code></pre> | |
| <div class="cs-note">SQLite ไม่รองรับ DROP COLUMN (ก่อน v3.35) และ MODIFY COLUMN</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">DROP & TRUNCATE</span></div> | |
| <pre><code class="language-sql">-- ลบตาราง (ถาวร รวมข้อมูล) | |
| DROP TABLE [IF EXISTS] table_name; | |
| -- ลบ View | |
| DROP VIEW [IF EXISTS] view_name; | |
| -- ลบ Index | |
| DROP INDEX [IF EXISTS] index_name; | |
| -- ล้างข้อมูลทั้งตาราง (SQLite ใช้ DELETE) | |
| DELETE FROM table_name;</code></pre> | |
| <div class="cs-note">IF EXISTS ป้องกัน error หากไม่มีอยู่จริง</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">CREATE INDEX</span></div> | |
| <pre><code class="language-sql">-- Index คอลัมน์เดียว | |
| CREATE INDEX idx_name ON table(col); | |
| -- Index ไม่ซ้ำ | |
| CREATE UNIQUE INDEX idx_name ON table(col); | |
| -- Index หลายคอลัมน์ | |
| CREATE INDEX idx_name ON table(col1, col2); | |
| -- ลบ Index | |
| DROP INDEX [IF EXISTS] idx_name;</code></pre> | |
| <div class="cs-note">ใช้กับคอลัมน์ใน WHERE, JOIN, ORDER BY ที่ query บ่อย</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-ddl">DDL</span><span class="cs-card-title">CREATE VIEW</span></div> | |
| <pre><code class="language-sql">-- สร้าง View | |
| CREATE VIEW view_name AS | |
| SELECT ...; | |
| -- สร้างหรือแทนที่ (MySQL/PostgreSQL) | |
| CREATE OR REPLACE VIEW view_name AS | |
| SELECT ...; | |
| -- ลบ View | |
| DROP VIEW [IF EXISTS] view_name;</code></pre> | |
| <div class="cs-note">View ไม่เก็บข้อมูลจริง คำนวณใหม่ทุกครั้งที่เรียก</div> | |
| </div> | |
| </div><!-- /cs-grid DDL --> | |
| <!-- ── DML ── --> | |
| <h3 id="cs-dml">DML — เพิ่ม/แก้ไข/ลบข้อมูล</h3> | |
| <div class="cs-grid"> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dml">DML</span><span class="cs-card-title">INSERT INTO</span></div> | |
| <pre><code class="language-sql">-- แถวเดียว (ระบุคอลัมน์) | |
| INSERT INTO table (col1, col2, col3) | |
| VALUES (val1, val2, val3); | |
| -- หลายแถวพร้อมกัน | |
| INSERT INTO table (col1, col2) | |
| VALUES | |
| (v1a, v2a), | |
| (v1b, v2b), | |
| (v1c, v2c); | |
| -- แทรกจาก SELECT | |
| INSERT INTO table (col1, col2) | |
| SELECT col1, col2 FROM other_table | |
| WHERE condition; | |
| -- แทรกหรืออัปเดตถ้ามีอยู่แล้ว | |
| INSERT OR REPLACE INTO table (col1, col2) | |
| VALUES (v1, v2);</code></pre> | |
| <div class="cs-note">แนะนำให้ระบุชื่อคอลัมน์เสมอ ป้องกันปัญหาเมื่อโครงสร้างตารางเปลี่ยน</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dml">DML</span><span class="cs-card-title">UPDATE</span></div> | |
| <pre><code class="language-sql">-- พื้นฐาน | |
| UPDATE table | |
| SET col1 = val1 | |
| WHERE condition; | |
| -- อัปเดตหลายคอลัมน์ | |
| UPDATE table | |
| SET col1 = val1, | |
| col2 = val2, | |
| col3 = col3 + 1 | |
| WHERE condition; | |
| -- อัปเดตด้วยค่าจากตารางอื่น (Subquery) | |
| UPDATE employees | |
| SET salary = salary * 1.1 | |
| WHERE department = ( | |
| SELECT department FROM departments | |
| WHERE department_name = 'Kitchen' | |
| );</code></pre> | |
| <div class="cs-note">⚠️ ลืม WHERE = แก้ทุกแถว ทำ SELECT ดูก่อนเสมอ</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dml">DML</span><span class="cs-card-title">DELETE</span></div> | |
| <pre><code class="language-sql">-- ลบแถวที่ตรงเงื่อนไข | |
| DELETE FROM table | |
| WHERE condition; | |
| -- ลบด้วย Subquery | |
| DELETE FROM table | |
| WHERE col IN (SELECT col FROM other WHERE ...); | |
| -- ลบทุกแถว (เก็บโครงสร้าง) | |
| DELETE FROM table;</code></pre> | |
| <div class="cs-note">⚠️ ลืม WHERE = ลบทุกแถว ไม่มี undo! ใช้ Transaction ห่อไว้</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dml">DML</span><span class="cs-card-title">TRANSACTION</span></div> | |
| <pre><code class="language-sql">BEGIN; -- เริ่ม Transaction | |
| INSERT INTO ...; | |
| UPDATE ...; | |
| DELETE FROM ...; | |
| COMMIT; -- ยืนยันทุกคำสั่ง | |
| -- หรือยกเลิกถ้าเกิดข้อผิดพลาด | |
| ROLLBACK; -- คืนสู่สถานะก่อน BEGIN | |
| -- SAVEPOINT — จุดบันทึกย่อย | |
| SAVEPOINT sp1; | |
| -- ... คำสั่ง ... | |
| ROLLBACK TO sp1; -- ยกเลิกถึงจุดนี้เท่านั้น | |
| RELEASE sp1;</code></pre> | |
| <div class="cs-note">ใช้เมื่อคำสั่งหลายตัวต้องสำเร็จพร้อมกัน เช่น โอนเงิน, สร้างออเดอร์+อัปเดตสต็อก</div> | |
| </div> | |
| </div><!-- /cs-grid DML --> | |
| <!-- ── DQL ── --> | |
| <h3 id="cs-dql">DQL — ดึงข้อมูล (SELECT)</h3> | |
| <div class="cs-grid"> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">SELECT โครงสร้างเต็ม</span></div> | |
| <pre><code class="language-sql">SELECT [DISTINCT] col1 [AS alias], col2, ... | |
| FROM table1 [AS t1] | |
| [JOIN table2 AS t2 ON t1.id = t2.fk] | |
| [WHERE condition] | |
| [GROUP BY col1, col2] | |
| [HAVING group_condition] | |
| [ORDER BY col [ASC|DESC]] | |
| [LIMIT n [OFFSET m]];</code></pre> | |
| <div class="cs-note">ลำดับประมวลผล: FROM→JOIN→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">WHERE Operators</span></div> | |
| <pre><code class="language-sql">WHERE col = 'value' | |
| WHERE col <> 'value' -- ไม่เท่ากับ | |
| WHERE col > 100 | |
| WHERE col BETWEEN 10 AND 20 -- รวม 10 และ 20 | |
| WHERE col IN ('a', 'b', 'c') | |
| WHERE col NOT IN ('x', 'y') | |
| WHERE col LIKE 'prefix%' -- ขึ้นต้นด้วย | |
| WHERE col LIKE '%suffix' -- ลงท้ายด้วย | |
| WHERE col LIKE '%contains%' -- มีคำนี้อยู่ | |
| WHERE col LIKE 'a_c' -- _ = 1 ตัวอักษร | |
| WHERE col IS NULL | |
| WHERE col IS NOT NULL | |
| WHERE cond1 AND cond2 | |
| WHERE cond1 OR cond2 | |
| WHERE NOT condition</code></pre> | |
| <div class="cs-note">ใช้วงเล็บ () เมื่อรวม AND และ OR เพื่อควบคุมลำดับ</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">JOIN Types</span></div> | |
| <pre><code class="language-sql">-- INNER JOIN (ค่าเริ่มต้น) | |
| FROM a INNER JOIN b ON a.id = b.fk | |
| FROM a JOIN b ON a.id = b.fk -- เหมือนกัน | |
| -- LEFT JOIN — ทุกแถวจาก a | |
| FROM a LEFT JOIN b ON a.id = b.fk | |
| FROM a LEFT OUTER JOIN b ON ... -- เหมือนกัน | |
| -- CROSS JOIN — ทุกคู่ผสม (Cartesian Product) | |
| FROM a CROSS JOIN b | |
| -- Self JOIN — ตารางกับตัวเอง | |
| FROM employees AS e | |
| JOIN employees AS mgr ON e.manager_id = mgr.employee_id</code></pre> | |
| <div class="cs-note">SQLite ไม่รองรับ RIGHT JOIN และ FULL OUTER JOIN (ใช้ LEFT JOIN + UNION แทน)</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">GROUP BY & HAVING</span></div> | |
| <pre><code class="language-sql">-- GROUP BY พื้นฐาน | |
| SELECT col, COUNT(*), SUM(val), AVG(val) | |
| FROM table | |
| GROUP BY col; | |
| -- GROUP BY หลายคอลัมน์ | |
| SELECT col1, col2, COUNT(*) | |
| FROM table | |
| GROUP BY col1, col2; | |
| -- HAVING กรองหลัง GROUP BY | |
| SELECT col, COUNT(*) AS cnt | |
| FROM table | |
| GROUP BY col | |
| HAVING cnt > 5; | |
| -- WHERE + GROUP BY + HAVING ร่วมกัน | |
| SELECT dept, AVG(salary) AS avg_sal | |
| FROM employees | |
| WHERE is_active = 1 -- กรองก่อน group | |
| GROUP BY dept | |
| HAVING avg_sal > 30000; -- กรองหลัง group</code></pre> | |
| <div class="cs-note">HAVING ใช้ชื่อ alias จาก SELECT ได้ใน SQLite</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">Aggregate Functions</span></div> | |
| <pre><code class="language-sql">COUNT(*) -- นับทุกแถว | |
| COUNT(col) -- นับแถวที่ไม่ใช่ NULL | |
| COUNT(DISTINCT col) -- นับค่าไม่ซ้ำ | |
| SUM(col) -- รวม | |
| AVG(col) -- เฉลี่ย | |
| MAX(col) -- สูงสุด | |
| MIN(col) -- ต่ำสุด | |
| -- Conditional Aggregation | |
| COUNT(CASE WHEN cond THEN 1 END) | |
| SUM(CASE WHEN cond THEN col ELSE 0 END) | |
| AVG(CASE WHEN cond THEN col END)</code></pre> | |
| <div class="cs-note">Aggregate ใช้ใน SELECT และ HAVING เท่านั้น ไม่ใช้ใน WHERE</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">DQL</span><span class="cs-card-title">Subquery</span></div> | |
| <pre><code class="language-sql">-- ใน WHERE (Scalar subquery) | |
| WHERE col > (SELECT AVG(col) FROM t) | |
| -- ใน WHERE ด้วย IN | |
| WHERE id IN (SELECT id FROM t WHERE ...) | |
| -- ใน WHERE ด้วย EXISTS | |
| WHERE EXISTS (SELECT 1 FROM t WHERE t.fk = outer.pk) | |
| -- เป็น Derived Table ใน FROM | |
| FROM (SELECT ... FROM t WHERE ...) AS alias | |
| -- Correlated Subquery (อ้างอิงตารางนอก) | |
| SELECT * FROM employees AS e | |
| WHERE salary > ( | |
| SELECT AVG(salary) FROM employees | |
| WHERE department = e.department -- อ้าง e จากนอก | |
| )</code></pre> | |
| <div class="cs-note">Correlated subquery ช้ากว่า JOIN — ใช้ CTE หรือ JOIN แทนเมื่อเป็นไปได้</div> | |
| </div> | |
| </div><!-- /cs-grid DQL --> | |
| <!-- ── ADVANCED ── --> | |
| <h3 id="cs-adv">Advanced SQL</h3> | |
| <div class="cs-grid"> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">CTE — WITH</span></div> | |
| <pre><code class="language-sql">-- CTE เดียว | |
| WITH cte_name AS ( | |
| SELECT ... | |
| ) | |
| SELECT * FROM cte_name WHERE ...; | |
| -- หลาย CTE | |
| WITH | |
| cte1 AS (SELECT ...), | |
| cte2 AS (SELECT ... FROM cte1) | |
| SELECT * FROM cte2; | |
| -- Recursive CTE (สำหรับ hierarchy) | |
| WITH RECURSIVE cte AS ( | |
| SELECT id, name, parent_id FROM categories WHERE parent_id IS NULL | |
| UNION ALL | |
| SELECT c.id, c.name, c.parent_id | |
| FROM categories AS c | |
| JOIN cte ON c.parent_id = cte.id | |
| ) | |
| SELECT * FROM cte;</code></pre> | |
| <div class="cs-note">CTE อ่านง่ายกว่า Subquery ซ้อนกัน และนำกลับมาใช้ซ้ำในคำสั่งเดียวกันได้</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">Window Functions</span></div> | |
| <pre><code class="language-sql">func() OVER ( | |
| [PARTITION BY col1, col2] -- แบ่งกลุ่ม | |
| [ORDER BY col [ASC|DESC]] -- เรียงลำดับ | |
| [ROWS/RANGE BETWEEN ...] -- frame | |
| ) | |
| -- Ranking Functions | |
| ROW_NUMBER() OVER (...) -- 1,2,3,4 (ไม่ซ้ำ) | |
| RANK() OVER (...) -- 1,1,3,4 (ซ้ำข้ามอันดับ) | |
| DENSE_RANK() OVER (...) -- 1,1,2,3 (ซ้ำไม่ข้าม) | |
| NTILE(n) OVER (...) -- แบ่งเป็น n กลุ่ม | |
| -- Value Functions | |
| LAG(col, n, default) OVER (...) -- ค่าแถว n ก่อนหน้า | |
| LEAD(col, n, default) OVER (...) -- ค่าแถว n ถัดไป | |
| FIRST_VALUE(col) OVER (...) -- ค่าแรกในกลุ่ม | |
| LAST_VALUE(col) OVER (...) -- ค่าสุดท้ายในกลุ่ม | |
| -- Aggregate Window | |
| SUM(col) OVER (ORDER BY col2) -- Running total | |
| AVG(col) OVER (PARTITION BY g) -- เฉลี่ยในกลุ่ม | |
| COUNT(*) OVER () -- นับทั้งหมด</code></pre> | |
| <div class="cs-note">Window Functions ต่างจาก GROUP BY ตรงที่ไม่รวมแถว — แต่ละแถวยังคงอยู่</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">CASE WHEN</span></div> | |
| <pre><code class="language-sql">-- Simple CASE | |
| CASE col | |
| WHEN 'a' THEN 'result_a' | |
| WHEN 'b' THEN 'result_b' | |
| ELSE 'other' | |
| END | |
| -- Searched CASE (ยืดหยุ่นกว่า) | |
| CASE | |
| WHEN col >= 90 THEN 'A' | |
| WHEN col >= 80 THEN 'B' | |
| WHEN col >= 70 THEN 'C' | |
| ELSE 'F' | |
| END AS grade | |
| -- ใน Aggregate (Conditional Count/Sum) | |
| COUNT(CASE WHEN status='Active' THEN 1 END) | |
| SUM(CASE WHEN type='income' THEN amount | |
| WHEN type='expense' THEN -amount | |
| ELSE 0 END) AS net</code></pre> | |
| <div class="cs-note">CASE WHEN ใช้ได้ในทุกที่ที่ expression ใช้ได้: SELECT, WHERE, ORDER BY, GROUP BY</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">SET Operations</span></div> | |
| <pre><code class="language-sql">-- UNION — รวม ตัดซ้ำออก | |
| SELECT col FROM t1 | |
| UNION | |
| SELECT col FROM t2; | |
| -- UNION ALL — รวม เก็บซ้ำ (เร็วกว่า) | |
| SELECT col FROM t1 | |
| UNION ALL | |
| SELECT col FROM t2; | |
| -- INTERSECT — เฉพาะที่มีทั้งสองชุด | |
| SELECT col FROM t1 | |
| INTERSECT | |
| SELECT col FROM t2; | |
| -- EXCEPT / MINUS — อยู่ใน t1 แต่ไม่อยู่ใน t2 | |
| SELECT col FROM t1 | |
| EXCEPT | |
| SELECT col FROM t2;</code></pre> | |
| <div class="cs-note">ทุก SELECT ต้องมีจำนวนและชนิดคอลัมน์เหมือนกัน</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">EXISTS & ANY / ALL</span></div> | |
| <pre><code class="language-sql">-- EXISTS — ตรวจว่ามีผลลัพธ์หรือไม่ | |
| SELECT * FROM customers AS c | |
| WHERE EXISTS ( | |
| SELECT 1 FROM orders AS o | |
| WHERE o.customer_id = c.customer_id | |
| ); | |
| -- NOT EXISTS | |
| WHERE NOT EXISTS (SELECT 1 FROM t WHERE ...); | |
| -- ANY / SOME — ตรงกับค่าใดค่าหนึ่ง | |
| WHERE salary > ANY (SELECT salary FROM t WHERE dept='IT') | |
| -- ALL — ตรงกับทุกค่า | |
| WHERE salary > ALL (SELECT salary FROM t WHERE dept='IT')</code></pre> | |
| <div class="cs-note">EXISTS มักเร็วกว่า IN สำหรับ subquery ขนาดใหญ่</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">NULL Handling</span></div> | |
| <pre><code class="language-sql">-- ตรวจ NULL | |
| WHERE col IS NULL | |
| WHERE col IS NOT NULL | |
| -- แทน NULL ด้วยค่าอื่น | |
| COALESCE(col, 'ค่าแทน') -- ค่าแรกที่ไม่ใช่ NULL | |
| IFNULL(col, 'ค่าแทน') -- SQLite specific | |
| NULLIF(col, 0) -- คืน NULL ถ้า col = 0 | |
| -- NULL ใน Aggregate (ถูก ignore อัตโนมัติ) | |
| -- COUNT(col) ไม่นับ NULL แต่ COUNT(*) นับทุกแถว | |
| -- AVG(col) คำนวณเฉพาะ non-NULL | |
| -- Conditional NULL | |
| CASE WHEN col IS NULL THEN 'N/A' ELSE col END</code></pre> | |
| <div class="cs-note">NULL ไม่เท่ากับ NULL — ทุก NULL เปรียบเทียบกันจะได้ UNKNOWN ไม่ใช่ TRUE</div> | |
| </div> | |
| </div><!-- /cs-grid ADV --> | |
| <!-- ── FUNCTIONS REFERENCE ── --> | |
| <h3 id="cs-fn">Functions Reference</h3> | |
| <div class="cs-grid"> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">FN</span><span class="cs-card-title">String Functions</span></div> | |
| <pre><code class="language-sql">LENGTH(str) -- ความยาว | |
| UPPER(str) -- ตัวพิมพ์ใหญ่ | |
| LOWER(str) -- ตัวพิมพ์เล็ก | |
| TRIM(str) -- ตัดช่องว่างหัวท้าย | |
| LTRIM(str) / RTRIM(str) -- ตัดซ้าย / ขวา | |
| SUBSTR(str, start, len) -- ตัดสตริง | |
| REPLACE(str, old, new) -- แทนที่ | |
| INSTR(str, substr) -- หาตำแหน่ง (0 = ไม่พบ) | |
| str1 || str2 -- ต่อสตริง (SQLite) | |
| CONCAT(str1, str2) -- ต่อสตริง (MySQL) | |
| LIKE 'pattern' -- จับคู่รูปแบบ | |
| GLOB 'pattern' -- จับคู่ case-sensitive (SQLite)</code></pre> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">FN</span><span class="cs-card-title">Numeric Functions</span></div> | |
| <pre><code class="language-sql">ROUND(n, decimals) -- ปัดเศษ | |
| CEIL(n) / CEILING(n) -- ปัดขึ้น | |
| FLOOR(n) -- ปัดลง | |
| ABS(n) -- ค่าสมบูรณ์ | |
| MOD(n, m) -- เศษจากหาร (หรือ n % m) | |
| POWER(n, exp) -- n ยกกำลัง exp | |
| SQRT(n) -- รากที่สอง | |
| MAX(a, b) -- ค่าสูงสุดในหลายค่า | |
| MIN(a, b) -- ค่าต่ำสุดในหลายค่า | |
| CAST(val AS TYPE) -- แปลงประเภทข้อมูล</code></pre> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">FN</span><span class="cs-card-title">Date Functions (SQLite)</span></div> | |
| <pre><code class="language-sql">DATE('now') -- วันนี้ YYYY-MM-DD | |
| TIME('now') -- เวลาตอนนี้ HH:MM:SS | |
| DATETIME('now') -- วันที่+เวลา | |
| DATE('now', '+7 days') -- บวก 7 วัน | |
| DATE('now', '-1 month') -- ลบ 1 เดือน | |
| DATE('now', 'start of month') -- วันแรกของเดือน | |
| DATE('now', 'start of year') -- วันแรกของปี | |
| strftime('%Y', date) -- ปี | |
| strftime('%m', date) -- เดือน (01-12) | |
| strftime('%d', date) -- วัน (01-31) | |
| strftime('%H', datetime) -- ชั่วโมง | |
| strftime('%Y-%m', date) -- ปี-เดือน | |
| strftime('%w', date) -- วันในสัปดาห์ (0=อาทิตย์)</code></pre> | |
| <div class="cs-note">SQLite เก็บวันที่เป็น TEXT — ใช้รูปแบบ ISO 8601: YYYY-MM-DD</div> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">FN</span><span class="cs-card-title">Type Conversion</span></div> | |
| <pre><code class="language-sql">-- CAST — แปลงประเภท | |
| CAST('123' AS INTEGER) -- TEXT → INTEGER | |
| CAST(123 AS TEXT) -- INTEGER → TEXT | |
| CAST('3.14' AS REAL) -- TEXT → REAL | |
| CAST(price AS INTEGER) -- ตัดทศนิยม | |
| -- SQLite type affinity (อัตโนมัติ) | |
| '123' + 0 -- ได้ 123 (integer) | |
| 123 || '' -- ได้ '123' (text) | |
| -- TYPEOF — ตรวจประเภท | |
| TYPEOF(col) -- 'integer', 'real', 'text', 'null', 'blob'</code></pre> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-dql">FN</span><span class="cs-card-title">Conditional Functions</span></div> | |
| <pre><code class="language-sql">-- COALESCE — ค่าแรกที่ไม่ใช่ NULL | |
| COALESCE(col1, col2, 'default') | |
| -- IFNULL — แทน NULL (SQLite / MySQL) | |
| IFNULL(col, 'ค่าแทน') | |
| -- NULLIF — คืน NULL ถ้าเท่ากัน | |
| NULLIF(col, 0) -- ถ้า col=0 คืน NULL | |
| -- IIF — if-else สั้น (SQLite 3.32+) | |
| IIF(condition, true_val, false_val) | |
| -- CASE WHEN (ใช้ได้ทุกที่) | |
| CASE WHEN cond THEN val ELSE other END</code></pre> | |
| </div> | |
| <div class="cs-card"> | |
| <div class="cs-card-header"><span class="cs-tag cs-tag-adv">ADV</span><span class="cs-card-title">Useful Patterns</span></div> | |
| <pre><code class="language-sql">-- Pagination | |
| SELECT * FROM t ORDER BY id LIMIT 10 OFFSET (page-1)*10; | |
| -- Upsert (Insert or Update) | |
| INSERT INTO t (id, val) VALUES (1, 'x') | |
| ON CONFLICT(id) DO UPDATE SET val = excluded.val; | |
| -- Top-N ต่อกลุ่ม | |
| SELECT * FROM ( | |
| SELECT *, ROW_NUMBER() OVER | |
| (PARTITION BY dept ORDER BY salary DESC) AS rn | |
| FROM employees | |
| ) WHERE rn <= 3; | |
| -- Running Total | |
| SUM(amount) OVER (ORDER BY date | |
| ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) | |
| -- 7-Day Moving Average | |
| AVG(amount) OVER (ORDER BY date | |
| ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)</code></pre> | |
| <div class="cs-note">ON CONFLICT รองรับใน SQLite 3.24+ และ PostgreSQL</div> | |
| </div> | |
| </div><!-- /cs-grid FN --> | |
| <hr> | |
| <p style="text-align:center;color:var(--muted);font-size:.82rem;padding:1rem 0 2rem"> | |
| คู่มือ SQL ฉบับเร่งรัด — ครอบคลุม SQLite / MySQL / PostgreSQL<br> | |
| ขั้นตอนต่อไป: ฝึกกับ <strong>DB Browser for SQLite</strong> หรือ <strong>sqliteonline.com</strong> | |
| </p> | |
| </main> | |
| <script> | |
| hljs.highlightAll(); | |
| // Progress bar | |
| window.addEventListener('scroll', () => { | |
| const el = document.documentElement; | |
| const pct = (el.scrollTop / (el.scrollHeight - el.clientHeight)) * 100; | |
| document.getElementById('progress-fill').style.width = pct + '%'; | |
| }); | |
| // Active nav link | |
| const sections = document.querySelectorAll('[id]'); | |
| const links = document.querySelectorAll('#sidebar a'); | |
| const obs = new IntersectionObserver((entries) => { | |
| entries.forEach(e => { | |
| if (e.isIntersecting) { | |
| links.forEach(l => l.classList.remove('active')); | |
| const active = document.querySelector(`#sidebar a[href="#${e.target.id}"]`); | |
| if (active) active.classList.add('active'); | |
| } | |
| }); | |
| }, { rootMargin: '-20% 0px -70% 0px' }); | |
| sections.forEach(s => obs.observe(s)); | |
| </script> | |
| </body> | |
| </html> | |