EN813304 · Midterm
บทที่ 10 / 12 · sql-types-index-authorization.pptx · 10 ส.ค.

SQL Data Types, Index & Authorization

เลือกชนิดข้อมูลให้ถูก · index ทำงานยังไงและควรสร้างเมื่อไหร่ · GRANT / REVOKE / role

SQLมี quiz ในคาบ
หัวข้อในบทนี้

วิดีโอที่อาจารย์ให้ดูก่อนเรียนบทนี้:

SQL Data Types, Indexes, and Authorization

10.1ทำไมชนิดข้อมูลถึงสำคัญ

ชนิดข้อมูลกับประสิทธิภาพ: ชนิดที่เล็กกว่า = query เร็วกว่า · ชนิดที่เหมาะสมทำให้ index ทำงานได้ดี · ชนิดที่ไม่ตรงกันทำให้เกิด implicit conversion · ชนิดที่ผิดทำให้ตรรกะของแอปพัง

Implicit conversion คืออะไร และทำไมมันทำให้ช้า Implicit conversion (การแปลงชนิดโดยปริยาย) = ตอนเราเทียบค่าสองอันที่คนละชนิด ฐานข้อมูลจะแปลงให้เองเงียบ ๆ โดยไม่แจ้ง error

สมมติ 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 กันเป็นชนิดเดียวกันตั้งแต่แรก

ชนิดข้อมูลที่ต้องรู้

Numeric
  • INT — จำนวนเต็ม เช่น 42, −100, 123456
  • DECIMAL(p,s) — ทศนิยมตายตัว DECIMAL(5,2): 123.45, 3.14
  • FLOAT — ตัวเลขโดยประมาณ 3.14159, 6.022e23
  • BIGINT — จำนวนเต็มใหญ่มาก 9223372036854775807
String
  • CHAR(n) — ความยาวคงที่ CHAR(10): 'John'
  • VARCHAR(n) — ความยาวแปรผัน VARCHAR(50)
  • TEXT — ข้อความยาว เช่น ย่อหน้า
  • ENUM — รายการค่าที่อนุญาต ENUM('Small','Medium','Large')
Date & Time
  • 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 บวกกลับเข้ากับวันเวลาได้

Large Object & Special
  • BLOB — Binary Large Object (รูป เสียง)
  • CLOB — Character Large Object (เอกสารยาว)
  • BINARY / VARBINARY — ข้อมูลไบนารีคงที่ / แปรผัน
  • BOOLEAN · JSON · XML · UUID

เมื่อ query คืน large object ระบบจะคืน pointer ไม่ใช่ตัว object เอง

4 ความผิดพลาดที่นักศึกษาทำบ่อย (จากสไลด์) 1. ใช้ VARCHAR(255) กับทุกอย่าง → เปลืองพื้นที่
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_idINT AUTO_INCREMENT ให้ระบบสร้าง id ที่ไม่ซ้ำเอง
title, authorVARCHAR เพราะความยาวไม่คงที่ กำหนดค่าสูงสุดที่สมเหตุสมผล
isbnCHAR(13) เพราะ ISBN-13 ยาว 13 ตัวพอดีเสมอ — ใช้ CHAR ประหยัดและเร็วกว่า VARCHAR ในกรณีความยาวคงที่
priceDECIMAL(6,2) เก็บเงินได้ถึง 9999.99 อย่างแม่นยำ
page_countSMALLINT UNSIGNED จำนวนบวกได้ถึง 65535 — พอเหลือเฟือและประหยัดกว่า INT
descriptionTEXT สำหรับข้อความยาวที่ความยาวไม่แน่นอน
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 — Full Table Scan
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 วินาที

มี B-tree index — Direct Lookup
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);
ควรสร้าง index เมื่อ • คอลัมน์ที่ใช้ใน WHERE บ่อย
• คอลัมน์ที่ใช้ join ตาราง
• ตารางขนาดใหญ่ที่ถูก query บ่อย
• คอลัมน์ที่มี cardinality สูง (มีค่าที่ไม่ซ้ำเยอะ)
Cardinality — ทำไม index บนคอลัมน์ที่ค่าซ้ำเยอะถึงไม่ช่วย Cardinality ของคอลัมน์ = จำนวนค่าที่ไม่ซ้ำกันในคอลัมน์นั้น · ตาราง 1,000,000 แถว: email มี cardinality ≈ 1,000,000 (สูง) · gender มี cardinality = 2 (ต่ำ)
index บนค้นหนึ่งค่า เจอกี่แถวฐานข้อมูลทำอะไร
email≈ 1 แถว ไล่ B-tree 3–4 ชั้น → กระโดดอ่าน data page เดียว → เร็วมาก
city≈ 1,000 แถว ยังคุ้ม — ตัดจาก 1,000,000 เหลือ 1,000 แถวที่ต้องอ่าน
gender≈ 500,000 แถว ต้องอ่าน index บวก กระโดดอ่าน data page ครึ่งตาราง — ช้ากว่าสแกนทั้งตารางเสียอีก
ทำไมกระโดดอ่านถึงช้ากว่าสแกนรวด — การสแกนทั้งตารางอ่านดิสก์เรียงต่อกันเป็นแถบยาว ซึ่งเร็วมาก ส่วนการตามลิงก์จาก index ต้องกระโดดไปมาแบบสุ่ม ทีละแถว · พอต้องกระโดดครึ่งล้านครั้ง มันแพงกว่าอ่านรวดเดียว
ผลคือ optimizer จะ “ไม่ใช้” index ตัวนั้นเอง ทั้งที่เราสร้างไว้ — เราเลยเสียทั้งพื้นที่ และเสียความเร็วตอน INSERT/UPDATE ฟรี ๆ

กฎคร่าว ๆ ในวงการ: ถ้า query หนึ่งต้องดึงเกินราว 10–20% ของตาราง การสแกนทั้งตารางมักเร็วกว่าใช้ index · index ที่มีค่าไม่ซ้ำน้อย ๆ จึงมักไม่คุ้ม ยกเว้นเมื่อค่าที่เราค้นนั้นหายากมาก เช่น status='error' ที่มีแค่ 0.1% ของแถวทั้งหมด

เหตุผลเชิงทฤษฎี: query จำนวนมากอ้างถึงเรคอร์ดเพียงส่วนน้อยของตาราง การให้ระบบอ่านทุกเรคอร์ดเพื่อหาเรคอร์ดที่มีค่าเฉพาะเจาะจงจึงไม่มีประสิทธิภาพ · index บน attribute ของ relation คือโครงสร้างข้อมูลที่ช่วยให้ระบบหา tuple ที่มีค่าตามที่ระบุได้อย่างมีประสิทธิภาพโดยไม่ต้องไล่ดูทุก tuple

ตัวอย่างสถานการณ์จากสไลด์

Scenario 1 — ฐานข้อมูลลูกค้า

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 คนละแบบ

Scenario 2 — ระบบจัดการคำสั่งซื้อ

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 มาใช้

หลัก 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 / REVOKE

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)

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;
คำตอบของคำถามในสไลด์ ใช่ — geo_staff เข้าถึง geo_instructor ได้
• ไม่จำเป็นต้องมีสิทธิ์บนตารางฐาน (instructor) โดยตรง
• การเข้าถึงเกิดขึ้นผ่านสิทธิ์ที่ให้ไว้กับ view
• view ทำหน้าที่เป็นชั้นความปลอดภัย (security layer) ระหว่างผู้ใช้กับตารางฐาน

และในทางกลับกัน: การ grant สิทธิ์บน view ไม่ได้เป็นการ grant สิทธิ์ใด ๆ บน relation ที่อยู่เบื้องหลัง geo_staff จึงยัง SELECT * FROM instructor ตรง ๆ ไม่ได้ ทำให้เห็นเฉพาะอาจารย์ภาค Geology เท่านั้น

10.4สรุปบทนี้

ทำไมจึงไม่ควรใช้ FLOAT เก็บราคาสินค้า
19.95 เก็บใน FLOAT อาจกลายเป็น 19.950000000000003 · เมื่อเอาไปบวกกันหลายพันรายการ ความคลาดเคลื่อนจะสะสมจนยอดเงินผิด · ใช้ DECIMAL(p,s) สำหรับเงินเสมอ
การเพิ่ม index ให้ตารางมีข้อเสียอะไร
ทุกครั้งที่ข้อมูลเปลี่ยน ฐานข้อมูลต้องอัปเดต index ด้วย จึงเขียนช้าลง และ index กินพื้นที่เพิ่ม · นี่คือเหตุผลที่ไม่ควรสร้าง index ทุกคอลัมน์ แต่เลือกเฉพาะคอลัมน์ที่ใช้ใน WHERE, JOIN และมี cardinality สูง
geo_staff ได้ GRANT SELECT ON geo_instructor (view ของ instructor) แต่ไม่มีสิทธิ์บน instructor · จะเกิดอะไรขึ้น
นี่คือหัวใจของการใช้ view เป็น security layer — เข้าถึงผ่าน view ได้โดยไม่ต้องมีสิทธิ์บนตารางฐาน และยังเข้าถึงตารางฐานตรง ๆ ไม่ได้ จึงเห็นแค่ข้อมูลส่วนที่ view เปิดให้
ระบบจำไว้ในเบราว์เซอร์นี้ จะได้รู้ว่าอ่านถึงไหนแล้ว