SQL Data Types, Index & Authorization
เลือกชนิดข้อมูลให้ถูก · index ทำงานยังไงและควรสร้างเมื่อไหร่ · GRANT / REVOKE / role
วิดีโอที่อาจารย์ให้ดูก่อนเรียนบทนี้:
10.1ทำไมชนิดข้อมูลถึงสำคัญ
- ควบคุมว่าข้อมูลแบบไหนเก็บได้ในแต่ละคอลัมน์
- ประหยัดพื้นที่จัดเก็บและหน่วยความจำ
- ให้ฐานข้อมูลตรวจสอบความถูกต้องของข้อมูลได้อัตโนมัติ
- เพิ่มประสิทธิภาพของ query ผ่าน operation ที่มีประสิทธิภาพ
ชนิดข้อมูลกับประสิทธิภาพ: ชนิดที่เล็กกว่า = query เร็วกว่า · ชนิดที่เหมาะสมทำให้ index ทำงานได้ดี · ชนิดที่ไม่ตรงกันทำให้เกิด implicit conversion · ชนิดที่ผิดทำให้ตรรกะของแอปพัง
สมมติ
customer_id ประกาศเป็น VARCHAR(10) แต่เราเขียน:
SELECT * FROM customers WHERE customer_id = 12345; -- ใส่เป็นตัวเลข ไม่ใช่ '12345'
ฐานข้อมูลแปลงคอลัมน์ทุกแถวจากข้อความเป็นตัวเลขก่อนเทียบ — กลายเป็น
WHERE CAST(customer_id AS NUMBER) = 12345ปัญหา: index ถูกสร้างไว้บนค่าข้อความเดิม ไม่ใช่ค่าที่แปลงแล้ว พอค่าที่ต้องเทียบไม่ใช่ค่าที่ index เก็บไว้ ฐานข้อมูลจึงใช้ index ไม่ได้ ต้องกลับไปสแกนทั้งตาราง — จาก 3–4 ครั้งกลายเป็นล้านครั้ง โดยที่ query ยังคืนคำตอบถูกอยู่ จึงไม่มีใครสังเกต
ทางแก้: เขียนค่าให้ตรงชนิดกับคอลัมน์ (
= '12345') และออกแบบให้คอลัมน์ที่ join กันเป็นชนิดเดียวกันตั้งแต่แรก
ชนิดข้อมูลที่ต้องรู้
- INT — จำนวนเต็ม เช่น 42, −100, 123456
- DECIMAL(p,s) — ทศนิยมตายตัว
DECIMAL(5,2): 123.45, 3.14 - FLOAT — ตัวเลขโดยประมาณ 3.14159, 6.022e23
- BIGINT — จำนวนเต็มใหญ่มาก 9223372036854775807
- CHAR(n) — ความยาวคงที่
CHAR(10): 'John' - VARCHAR(n) — ความยาวแปรผัน
VARCHAR(50) - TEXT — ข้อความยาว เช่น ย่อหน้า
- ENUM — รายการค่าที่อนุญาต
ENUM('Small','Medium','Large')
- DATE —
date '2005-7-27' - TIME —
time '09:00:30' - TIMESTAMP — วันที่+เวลา
timestamp '2005-7-27 09:00:30' - INTERVAL — ช่วงเวลา
interval '1' day
การลบ date/time/timestamp ออกจากกันได้ค่าเป็น interval และ interval บวกกลับเข้ากับวันเวลาได้
- BLOB — Binary Large Object (รูป เสียง)
- CLOB — Character Large Object (เอกสารยาว)
- BINARY / VARBINARY — ข้อมูลไบนารีคงที่ / แปรผัน
- BOOLEAN · JSON · XML · UUID
เมื่อ query คืน large object ระบบจะคืน pointer ไม่ใช่ตัว object เอง
2. ใช้ FLOAT เก็บเงิน → เกิดความคลาดเคลื่อน เพราะ FLOAT เก็บเป็นฐาน 2 ซึ่งแทนค่าทศนิยมฐาน 10 หลายค่าไม่ได้เป๊ะ
DECLARE @price FLOAT = 19.95;
SELECT @price; -- อาจแสดง: 19.950000000000003
3. ใช้ TEXT กับสตริงสั้น ๆ → เสียประสิทธิภาพ4. ลืมใส่ NOT NULL → คุณภาพข้อมูลแย่
แบบฝึกหัดออกแบบตาราง — พร้อมเฉลย
Demo Exercise 1 — ตาราง books ของร้านหนังสือออนไลน์
ฟิลด์ที่ต้องมี: Book ID, Title, Author, ISBN, Publication Date, Price, Page Count, Description
CREATE TABLE books (
book_id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author VARCHAR(100) NOT NULL,
isbn CHAR(13) UNIQUE,
publication_date DATE,
price DECIMAL(6,2),
page_count SMALLINT UNSIGNED,
description TEXT
);
| คอลัมน์ | เหตุผลที่เลือกชนิดนี้ |
|---|---|
| book_id | INT AUTO_INCREMENT ให้ระบบสร้าง id ที่ไม่ซ้ำเอง |
| title, author | VARCHAR เพราะความยาวไม่คงที่ กำหนดค่าสูงสุดที่สมเหตุสมผล |
| isbn | CHAR(13) เพราะ ISBN-13 ยาว 13 ตัวพอดีเสมอ — ใช้ CHAR ประหยัดและเร็วกว่า VARCHAR ในกรณีความยาวคงที่ |
| price | DECIMAL(6,2) เก็บเงินได้ถึง 9999.99 อย่างแม่นยำ |
| page_count | SMALLINT UNSIGNED จำนวนบวกได้ถึง 65535 — พอเหลือเฟือและประหยัดกว่า INT |
| description | TEXT สำหรับข้อความยาวที่ความยาวไม่แน่นอน |
Demo Exercise 2 — ตาราง employees
CREATE TABLE employees (
employee_id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20),
hire_date DATE NOT NULL,
salary DECIMAL(10,2),
department VARCHAR(50),
job_title VARCHAR(100)
);
จุดที่ต้องอธิบายได้: email VARCHAR(100) UNIQUE รับประกันว่าอีเมลไม่ซ้ำ ·
phone VARCHAR(20) ไม่ใช่ INT เพราะเบอร์โทรมีขีดคั่น เลขศูนย์นำหน้า และรหัสประเทศ ·
hire_date DATE NOT NULL เป็นฟิลด์บังคับ · salary DECIMAL(10,2) เก็บได้ถึง 99,999,999.99 อย่างแม่นยำ
10.2Index
Index คือโครงสร้างข้อมูลในฐานข้อมูลที่เพิ่มความเร็วในการดึงข้อมูล เปรียบเหมือนดัชนีท้ายเล่มหนังสือที่ช่วยให้หาหน้าได้เร็ว · สร้างบนคอลัมน์หนึ่งคอลัมน์หรือหลายคอลัมน์ และถูกเก็บแยกจากตัวข้อมูลในตาราง
Index ทำงานอย่างไร
- เก็บรายการค่าที่เรียงลำดับแล้วพร้อมตำแหน่งของแถวที่ตรงกัน
- ทำให้ฐานข้อมูลหาข้อมูลได้โดยไม่ต้องสแกนทั้งตาราง
- แลกกันระหว่างการอ่านที่เร็วขึ้น กับการเขียนที่ช้าลง
- ฐานข้อมูลจะใช้ index ให้เองอัตโนมัติเมื่อมันช่วยให้ query เร็วขึ้น
SELECT * FROM customers
WHERE email = 'john@example.com';
เริ่มจากแถวแรก อ่านคอลัมน์ email เทียบทีละแถว · ต้องอ่านครบทั้ง 1,000,000 แถว
แม้เจอที่แถวที่ 50 ก็ยังต้องเช็กอีก 999,950 แถวที่เหลือ เพราะอาจมีค่าซ้ำ
I/O: อ่านทุก data page จากดิสก์ · CPU: เปรียบเทียบสตริง 1,000,000 ครั้ง
Time complexity: O(n) — เชิงเส้น · ผลลัพธ์: 2.3 วินาที
CREATE INDEX idx_email ON customers(email);
SELECT * FROM customers
WHERE email = 'john@example.com';
Root: เทียบกับ 'm@example.com' → ไปทางซ้าย
Level 1: เทียบกับ 'd@gmail.com' → ไปทางขวา
Level 0: เจอค่าตรง → ได้ row pointer (47,823)
กระโดดไปที่แถว 47,823 โดยตรง → คืนข้อมูล
I/O: อ่าน index page แค่ 3–4 หน้า + data page 1 หน้า · CPU: เปรียบเทียบไม่เกิน 3–4 ครั้ง
Time complexity: O(log n) · ผลลัพธ์: 0.003 วินาที
B-tree (Balanced tree) คือโครงสร้างข้อมูลที่ปรับสมดุลตัวเอง เก็บข้อมูลเรียงลำดับไว้เสมอ และคงเวลาการค้นหาแบบลอการิทึม · คิดง่าย ๆ ว่าเป็นระบบแฟ้มเอกสารที่จัดระเบียบตัวเองอัตโนมัติเพื่อให้ค้นได้เร็วตลอด
คำสั่งสร้าง index และควรสร้างเมื่อไหร่
CREATE INDEX <name> ON <relation-name> (attribute);
CREATE INDEX studentID_index ON student(ID);
CREATE INDEX idx_email ON customers(email);
CREATE INDEX idx_city ON customers(city);
CREATE INDEX idx_order_date ON orders(order_date);
CREATE INDEX idx_customer_id ON orders(customer_id);
• คอลัมน์ที่ใช้ join ตาราง
• ตารางขนาดใหญ่ที่ถูก query บ่อย
• คอลัมน์ที่มี cardinality สูง (มีค่าที่ไม่ซ้ำเยอะ)
email มี cardinality ≈ 1,000,000 (สูง) ·
gender มี cardinality = 2 (ต่ำ)
| index บน | ค้นหนึ่งค่า เจอกี่แถว | ฐานข้อมูลทำอะไร |
|---|---|---|
| ≈ 1 แถว | ไล่ B-tree 3–4 ชั้น → กระโดดอ่าน data page เดียว → เร็วมาก | |
| city | ≈ 1,000 แถว | ยังคุ้ม — ตัดจาก 1,000,000 เหลือ 1,000 แถวที่ต้องอ่าน |
| gender | ≈ 500,000 แถว | ต้องอ่าน index บวก กระโดดอ่าน data page ครึ่งตาราง — ช้ากว่าสแกนทั้งตารางเสียอีก |
ผลคือ optimizer จะ “ไม่ใช้” index ตัวนั้นเอง ทั้งที่เราสร้างไว้ — เราเลยเสียทั้งพื้นที่ และเสียความเร็วตอน INSERT/UPDATE ฟรี ๆ
กฎคร่าว ๆ ในวงการ: ถ้า query หนึ่งต้องดึงเกินราว 10–20% ของตาราง การสแกนทั้งตารางมักเร็วกว่าใช้ index · index ที่มีค่าไม่ซ้ำน้อย ๆ จึงมักไม่คุ้ม ยกเว้นเมื่อค่าที่เราค้นนั้นหายากมาก เช่น
status='error' ที่มีแค่ 0.1% ของแถวทั้งหมด
เหตุผลเชิงทฤษฎี: query จำนวนมากอ้างถึงเรคอร์ดเพียงส่วนน้อยของตาราง การให้ระบบอ่านทุกเรคอร์ดเพื่อหาเรคอร์ดที่มีค่าเฉพาะเจาะจงจึงไม่มีประสิทธิภาพ · index บน attribute ของ relation คือโครงสร้างข้อมูลที่ช่วยให้ระบบหา tuple ที่มีค่าตามที่ระบุได้อย่างมีประสิทธิภาพโดยไม่ต้องไล่ดูทุก tuple
ตัวอย่างสถานการณ์จากสไลด์
customers (id, name, email, city, state, country)
SELECT * FROM customers WHERE email = 'user@example.com';
SELECT city, COUNT(*) FROM customers GROUP BY city;
SELECT * FROM customers WHERE city='New York' AND state='NY';
idx_email — เหมาะกับการค้นหาลูกค้ารายคน cardinality สูง จึงมีประสิทธิภาพมาก
idx_city — ช่วยทั้งการกรอง (WHERE) และการจัดกลุ่ม/รวมยอด (GROUP BY) แม้ cardinality จะต่ำกว่า แต่ยังคุ้มเพราะใช้บ่อย
index หลายตัวอยู่บนตารางเดียวกันได้ แต่ละตัวช่วย query คนละแบบ
orders (id, customer_id, order_date, total) · order_items (id, order_id, product_id, quantity)
CREATE INDEX idx_order_date ON orders(order_date);
CREATE INDEX idx_customer_id ON orders(customer_id);
CREATE INDEX idx_order_id ON order_items(order_id);
idx_order_date เร่งการค้นตามช่วงวันที่และรายงานเชิงเวลา ·
idx_customer_id เร่งการกรองตามลูกค้า · idx_order_id บน order_items เร่ง การ join ระหว่างสองตาราง
Primary key index ถูกสร้างอัตโนมัติบนคอลัมน์ id ของทั้งสองตาราง
10.3Authorization
Authorization คือกลไกควบคุมการเข้าถึงและการดำเนินการกับฐานข้อมูล กำหนดว่าผู้ใช้หรือ role ใดทำอะไรกับ object ในฐานข้อมูลได้บ้าง · รับประกันความปลอดภัยและ integrity ของข้อมูล และนำหลัก least privilege มาใช้
| คนทำงาน | ให้แค่นี้พอ |
|---|---|
| ทีมการตลาด — ทำรายงาน | GRANT SELECT ON customers เท่านั้น (อ่านอย่างเดียว) |
| นักศึกษาฝึกงาน — แก้ข้อมูลติดต่อ | GRANT UPDATE (email, phone) ON employees — แก้ได้แค่ 2 คอลัมน์ ไม่แตะเงินเดือน |
| ระบบเว็บที่ผู้ใช้ทั่วไปเรียก | อ่าน/เขียนเฉพาะตารางที่จำเป็น · ไม่ให้ DROP, ไม่ให้ ALTER |
1. จำกัดความเสียหายจากอุบัติเหตุ — คนที่ไม่มีสิทธิ์ DELETE ก็ลบข้อมูลผิดพลาดไม่ได้
2. จำกัดความเสียหายจากบัญชีที่ถูกขโมย — ถ้ารหัสผ่านของทีมการตลาดหลุด ผู้บุกรุกก็ได้แค่สิทธิ์อ่าน ไม่สามารถลบทั้งฐานข้อมูลได้
3. ตรวจสอบย้อนหลังได้ — เมื่อสิทธิ์แคบ การหาว่าใครทำอะไรก็ง่ายขึ้นมาก
ในทางปฏิบัติ: เริ่มจากไม่ให้สิทธิ์อะไรเลย แล้วค่อย
GRANT เพิ่มทีละอย่างเมื่อมีความจำเป็นจริง ๆ —
ไม่ใช่ให้ ALL PRIVILEGES ไปก่อนแล้วค่อยตามเก็บทีหลัง · และต้อง REVOKE ทันทีเมื่อคนเปลี่ยนหน้าที่หรือลาออก
- SELECT — อ่านข้อมูลจากตารางหรือ view
- INSERT — เพิ่มเรคอร์ดใหม่
- UPDATE — แก้ไขเรคอร์ดที่มีอยู่
- DELETE — ลบเรคอร์ด
- EXECUTE — รัน stored procedure หรือฟังก์ชัน
- ALL PRIVILEGES — ทางลัดสำหรับสิทธิ์ทั้งหมดที่อนุญาต
GRANT เมื่อ: มีผู้ใช้หรือแอปใหม่ต้องเข้าถึงฐานข้อมูล · หน้าที่ของผู้ใช้เปลี่ยนหรือขยาย · ต้องการสิทธิ์ชั่วคราวสำหรับโครงการ · กำลังทำ role-based access control (RBAC)
REVOKE เมื่อ: พนักงานลาออก · ผู้ใช้เปลี่ยนหน้าที่หรือแผนก · โครงการชั่วคราวจบแล้ว · การตรวจสอบความปลอดภัยพบว่ามีสิทธิ์เกินความจำเป็น
คำสั่ง GRANT และ REVOKE
GRANT <privilege list> ON <relation or view> TO <user list>;
REVOKE <privilege list> ON <relation or view> FROM <user list>;
-- ให้สิทธิ์อ่านทีมการตลาด
GRANT SELECT ON customers TO marketing_team;
-- ให้สิทธิ์เต็มกับทีม HR บนตาราง employees
GRANT SELECT, INSERT, UPDATE, DELETE ON employees TO hr_team;
-- จำกัดเฉพาะบางคอลัมน์
GRANT UPDATE (email, phone) ON employees TO intern_role;
-- ถอนสิทธิ์ทั้งหมดของพนักงานที่ลาออก (MySQL: ทั้งฐานข้อมูลเขียนเป็น db.*)
REVOKE ALL PRIVILEGES ON company_db.* FROM john_doe;
-- (PostgreSQL เขียนว่า REVOKE ALL PRIVILEGES ON DATABASE company_db FROM john_doe;)
GRANT SELECT ON instructor TO U1, U2, U3;
REVOKE SELECT ON student FROM U1, U2, U3;
ตารางที่ตัวอย่างสองบรรทัดท้ายอ้างถึง คือฐานข้อมูลมหาวิทยาลัย —
instructor(ID, name, dept_name, salary) · student(ID, name, dept_name, tot_cred) ·
takes(ID, course_id, sec_id, semester, year, grade)
(โครงสร้างเต็มอยู่ในบทที่ 5)
<user list>เป็นได้ทั้ง user-id · public (ให้สิทธิ์ผู้ใช้ที่ถูกต้องทุกคน) · หรือ role- ผู้ที่จะ grant ต้องถือสิทธิ์นั้นอยู่แล้ว (หรือเป็น DBA)
<privilege-list>ใช้ all เพื่อถอนสิทธิ์ทั้งหมดที่ผู้ถูกถอนมีอยู่- ถ้ารายชื่อผู้ถูกถอนมี public ผู้ใช้ทุกคนจะเสียสิทธิ์ยกเว้นคนที่ได้รับสิทธิ์แบบระบุตัวโดยตรง
WITH GRANT OPTION— เขียนต่อท้ายGRANT … TO u WITH GRANT OPTIONแปลว่า ให้ u ส่งต่อสิทธิ์นี้ให้คนอื่นได้อีกทอด → เกิดเป็น “โซ่การให้สิทธิ์”
เช่น DBA → คณบดี → หัวหน้าภาค → อาจารย์ · แต่ละขั้นต่อสิทธิ์กันไปเรื่อย ๆ- ถ้าสิทธิ์เดียวกันถูก grant ให้ผู้ใช้คนเดิมสองครั้งโดยผู้ให้ต่างคนกัน ผู้ใช้อาจยังคงสิทธิ์ไว้หลังการ revoke ครั้งหนึ่ง
เพราะยังมีอีกโซ่หนึ่งค้ำอยู่ — ถอนโซ่เดียวไม่พอ ต้องถอนทุกโซ่ที่ให้สิทธิ์นั้น - สิทธิ์ทั้งหมดที่ขึ้นอยู่กับสิทธิ์ที่ถูกถอน ก็จะถูกถอนไปด้วย (cascading revoke)
พอถอนต้นโซ่ สิทธิ์ที่แตกออกไปจากมันทั้งหมดถูกถอนตามอัตโนมัติ — ถอนสิทธิ์คณบดี แล้วหัวหน้าภาคกับอาจารย์ที่ได้ต่อมาจากเขาก็เสียสิทธิ์ด้วย
Roles
Role คือวิธีแยกแยะผู้ใช้ตามสิ่งที่พวกเขาเข้าถึงหรืออัปเดตได้ในฐานข้อมูล
CREATE ROLE instructor;
GRANT instructor TO Amit; -- มอบ role ให้ผู้ใช้
GRANT SELECT ON takes TO instructor; -- มอบสิทธิ์ให้ role
-- role มอบให้ role อื่นได้ → เกิดการสืบทอดสิทธิ์
CREATE ROLE teaching_assistant;
GRANT teaching_assistant TO instructor; -- instructor ได้สิทธิ์ทุกอย่างของ TA
-- ลูกโซ่ของ role
CREATE ROLE dean;
GRANT instructor TO dean;
GRANT dean TO Satoshi;
GRANT instructor TO dean แปลว่า dean ได้รับสิทธิ์ทั้งหมดของ instructorและเนื่องจาก instructor ได้รับสิทธิ์ของ teaching_assistant มาก่อนหน้า → Satoshi (ที่เป็น dean) จึงได้สิทธิ์ทั้งของ instructor และ teaching_assistant ทั้งหมด
Authorization บน View
CREATE VIEW geo_instructor AS
(SELECT * FROM instructor WHERE dept_name = 'Geology');
GRANT SELECT ON geo_instructor TO geo_staff;
-- แล้วถ้า geo_staff สั่ง...
SELECT * FROM geo_instructor;
• ไม่จำเป็นต้องมีสิทธิ์บนตารางฐาน (instructor) โดยตรง
• การเข้าถึงเกิดขึ้นผ่านสิทธิ์ที่ให้ไว้กับ view
• view ทำหน้าที่เป็นชั้นความปลอดภัย (security layer) ระหว่างผู้ใช้กับตารางฐาน
และในทางกลับกัน: การ grant สิทธิ์บน view ไม่ได้เป็นการ grant สิทธิ์ใด ๆ บน relation ที่อยู่เบื้องหลัง geo_staff จึงยัง
SELECT * FROM instructor ตรง ๆ ไม่ได้ ทำให้เห็นเฉพาะอาจารย์ภาค Geology เท่านั้น
10.4สรุปบทนี้
- ชนิดข้อมูลสำคัญต่อ integrity และการใช้พื้นที่ — เลือกตามลักษณะข้อมูลและความต้องการในอนาคต
- Index เพิ่มประสิทธิภาพ query อย่างมีนัยสำคัญ — สร้างบนคอลัมน์ที่ query บ่อยและคอลัมน์ที่ใช้ join
- ต้องชั่งน้ำหนักระหว่างความเร็วในการอ่าน กับภาระในการเขียน
- Authorization จำเป็นต่อความปลอดภัย — ใช้ GRANT และ REVOKE ควบคุมการเข้าถึง และใช้หลัก least privilege
- การใช้ทั้งสามอย่างให้ถูกต้องนำไปสู่: ประสิทธิภาพที่ดีขึ้น · integrity ที่แข็งแรงขึ้น · ความปลอดภัยและการควบคุมการเข้าถึงที่ดีขึ้น
GRANT SELECT ON geo_instructor (view ของ instructor) แต่ไม่มีสิทธิ์บน instructor · จะเกิดอะไรขึ้น