EN813304 · Midterm
บทที่ 4 / 12 · advanced-rel-concepts.pptx · สัปดาห์ที่ 3

Advanced Relational Concepts & Schema Design

การตั้งชื่อแบบ snake_case · composite attribute · multi-valued attribute · domain และ CHECK constraint

ออกแบบแบบฝึกหัดในคาบ
หัวข้อในบทนี้

4.1Naming Convention — snake_case vs CamelCase

อาจารย์ตั้งกติกาของวิชานี้ไว้ชัดเจนว่าให้ใช้ snake_case พร้อมเหตุผลที่ตอบได้ในข้อสอบแนวอธิบาย

snake_case — แนะนำ

street_address

  • Universal compatibility — ทำงานเหมือนกันทุกระบบฐานข้อมูล
  • อ่านง่ายกว่า — employee_performance_review_date แยกคำได้ทันที
  • ตรงกับมาตรฐาน SQL — เข้าชุดกับฟังก์ชันในตัวอย่าง CURRENT_TIMESTAMP
  • Case-insensitive — ไม่มีปัญหาตอน deploy หรือย้ายระบบ
  • มาตรฐานทั้งวงการและตำรา
CamelCase — มีปัญหา

streetAddress

  • ปัญหา case sensitivity — MySQL ทำงานต่างกันบน Linux กับ Windows
  • PostgreSQL แปลงเป็นตัวพิมพ์เล็กอัตโนมัติ ถ้าไม่ใส่เครื่องหมายคำพูดครอบ
  • อ่านยากกว่า — employeePerformanceReviewDate ต้องเพ่ง
  • ขัดกับธรรมเนียมของภาษา SQL เอง
  • เสี่ยงพังตอนย้ายสภาพแวดล้อม
ธรรมเนียมการตั้งชื่อที่ต้องใช้ในวิชานี้
สิ่งที่ตั้งชื่อกติกาทำไม (ตอบข้อสอบแนวอธิบายได้)
ตารางคำนามพหูพจน์ → customers, orders ตารางเก็บหลายแถว · SELECT * FROM customers จึงอ่านเป็นประโยคภาษาอังกฤษได้ตรงความหมาย
Primary keytable_name_id → customer_id เห็นชื่อคอลัมน์ก็รู้ทันทีว่ามาจากตารางไหน — สำคัญมากตอน join หลายตารางแล้วมีคอลัมน์ id โผล่มาเต็มไปหมด
Foreign keyใช้ชื่อเดียวกับ PK ที่มันอ้างถึง → orders ก็ใช้ customer_id เงื่อนไข join อ่านเป็น orders.customer_id = customers.customer_id ได้เลย และใช้ USING (customer_id) หรือ NATURAL JOIN ได้ ซึ่งจะสั้นกว่ามาก
Attribute ทั่วไปsnake_case ที่สื่อความหมาย → first_name คนอ่านโค้ดต่อไม่ต้องเปิด documentation ก็เดาได้ว่าคอลัมน์เก็บอะไร
วันที่จุดประสงค์ + _date → registration_date ตารางเดียวมักมีหลายวันที่ (created_date, shipped_date) — ถ้าตั้งว่า date เฉย ๆ จะแยกไม่ออกว่าวันอะไร
Booleanขึ้นต้นด้วย is_ / has_ → is_active อ่าน WHERE is_active ได้เป็นประโยคจริง/เท็จทันที · ถ้าตั้งว่า active เฉย ๆ จะเดาไม่ออกว่าเก็บ true/false หรือเก็บวันที่
ชื่อฐานข้อมูลตัวพิมพ์เล็ก + ขีดล่าง ไม่มีเว้นวรรค เว้นวรรคและอักขระพิเศษบังคับให้ต้องใส่เครื่องหมายคำพูดครอบทุกครั้ง และมักพังตอนเขียน script หรือ backup

4.2Composite Attribute

Composite attribute คือ attribute ที่มีส่วนประกอบย่อยซึ่งมีความหมายในตัวเอง เช่น ที่อยู่ประกอบด้วยถนน เมือง รหัสไปรษณีย์ และประเทศ ซึ่งแต่ละส่วนอาจถูกค้นหาหรือเรียงลำดับแยกกันในแอปพลิเคชันจริง

วิธีจัดเก็บส่งผลโดยตรงต่อความสามารถในการ query — ถ้าเก็บ "123 Main St, Bangkok, 10110" เป็นช่องเดียว เราจะค้นตามเมืองไม่ได้ และเรียงตามรหัสไปรษณีย์ไม่ได้อย่างมีประสิทธิภาพ

หลักตัดสินว่าจะแตกหรือไม่แตก แตกออก เมื่อต้อง query หรือ manipulate ส่วนย่อยแยกกัน → employees(name_title, first_name, middle_name, last_name, name_suffix)
เก็บรวมได้ เมื่อแทบไม่ query แยก ใช้แค่แสดงผล → contacts(contact_id, full_name) เก็บ "Dr. John Smith Jr." ทั้งก้อน

ข้อควรคำนึงระดับสากล: รูปแบบที่อยู่ต่างกันไปในแต่ละประเทศ การออกแบบ composite attribute จึงต้องรองรับโครงสร้างที่อยู่ตามวัฒนธรรมและกฎหมายที่ต่างกัน

4.3Multi-valued Attribute

Multi-valued attribute ละเมิด First Normal Form — relational model บังคับว่าค่าต้องเป็น atomic การที่พนักงานหนึ่งคนมีเบอร์โทรได้หลายเบอร์จึงเป็นโจทย์ออกแบบพื้นฐานที่ต้องแก้ให้ถูก

สองวิธีแก้ที่ผิด และเหตุผล 1. ยัดหลายค่าในช่องเดียว phone_numbers VARCHAR(500) = '555-1234,555-5678'
→ ค้นหาเบอร์ใดเบอร์หนึ่งยากมาก, สร้าง index ไม่ได้, ลบเบอร์เดียวออกต้องเขียนโค้ดตัดสตริง, ตรวจความถูกต้องไม่ได้
index = โครงสร้างค้นหาที่ฐานข้อมูลสร้างไว้ล่วงหน้าบนคอลัมน์หนึ่ง เหมือนดัชนีท้ายเล่มหนังสือ ทำให้หาแถวได้โดยไม่ต้องไล่อ่านทุกแถว — แต่มันทำงานกับค่าทั้งช่อง ไม่ใช่ “ส่วนหนึ่งของสตริง” พอเก็บเป็น '555-1234,555-5678' ก็ index ไม่ได้ (รายละเอียดในบทที่ 10)

2. ทำคอลัมน์ phone_1, phone_2, phone_3
→ เปลืองที่ (ส่วนใหญ่ว่าง), จำกัดจำนวนแบบไร้เหตุผล (แล้วถ้ามีเบอร์ที่ 4?), และตรรกะ query ซับซ้อนเมื่อต้องค้นข้ามทุกคอลัมน์เบอร์

ทางแก้ที่ถูกคือ normalization — สร้างตารางแยกที่มี employee_id และ phone_number ซึ่งรองรับเบอร์โทรได้ไม่จำกัดต่อพนักงานหนึ่งคน

และตารางแบบนี้มักมี attribute เพิ่มเติมนอกเหนือจากความสัมพันธ์พื้นฐาน เช่น phone_type (mobile/office/home) หรือธง is_primary เพื่อรองรับความต้องการทางธุรกิจให้ครบ

4.4Integrity Constraint ที่ซับซ้อนขึ้น

เป้าหมายคือการบังคับกฎทางธุรกิจที่ระดับฐานข้อมูล ไม่ใช่ฝากไว้ในโค้ดแอปพลิเคชัน

ตัวอย่างเต็มจากสไลด์
CREATE TABLE students (
    student_id      INT PRIMARY KEY,
    first_name      VARCHAR(50) NOT NULL,
    last_name       VARCHAR(50) NOT NULL,
    student_age     INT CHECK (student_age >= 16 AND student_age <= 100),
    current_grade   ENUM('A', 'B', 'C', 'D', 'F'),
    grade_point_average DECIMAL(3,2)
        CHECK (grade_point_average >= 0.0 AND grade_point_average <= 4.0),
    email_address   VARCHAR(100) CHECK (email_address LIKE '%@%.%'),
    enrollment_date DATE NOT NULL,
    graduation_date DATE,
    CONSTRAINT check_graduation_after_enrollment
        CHECK (graduation_date IS NULL OR graduation_date > enrollment_date)
);
ถอดรหัสไวยากรณ์ 3 อย่างในโค้ดข้างบน
เขียนแบบแปลว่า
ENUM('A','B','C','D','F') ชนิดข้อมูลที่ยอมรับได้แค่ค่าในลิสต์เท่านั้น — ใส่ 'E' ลงไปจะถูกปฏิเสธ (SQLite ไม่มี ENUM ต้องใช้ CHECK (col IN (…)) แทน)
email_address LIKE '%@%.%' LIKE คือการเทียบรูปแบบของข้อความ · % = “ตัวอักษรอะไรก็ได้ กี่ตัวก็ได้ รวมทั้งไม่มีเลย”
อ่านทีละส่วน: % อะไรก็ได้ → @ ต้องมี @ → % อะไรก็ได้ → . ต้องมีจุด → % อะไรก็ได้
ผ่าน: a@kku.ac.th · ไม่ผ่าน: akku.ac.th (ไม่มี @) และ a@kku (ไม่มีจุดหลัง @)
เป็นการตรวจแบบหยาบ ๆ เท่านั้น — @. ก็ยังผ่าน แต่ดีกว่าไม่ตรวจเลย
CONSTRAINT ชื่อ CHECK (…) constraint ระดับตาราง แบบตั้งชื่อเอง — ต้องเขียนแยกไว้ท้ายตาราง เพราะเงื่อนไขนี้อ้างถึง คอลัมน์ 2 ตัวพร้อมกัน (graduation_date กับ enrollment_date) จึงเขียนต่อท้ายคอลัมน์ใดคอลัมน์หนึ่งไม่ได้
การตั้งชื่อ (check_graduation_after_enrollment) ทำให้ข้อความ error บอกได้ว่ากฎข้อไหนพัง และสั่ง ALTER TABLE … DROP CONSTRAINT ชื่อ ทีหลังได้ — ถ้าไม่ตั้งชื่อ ระบบจะตั้งให้เองเป็นชื่อมั่ว ๆ อย่าง students_chk_1
เขียนไว้ตรงไหนได้บ้าง: column-level = ต่อท้ายคอลัมน์เดียว ใช้ได้เมื่อเงื่อนไขอ้างถึงคอลัมน์นั้นตัวเดียว · table-level = เขียนเป็นบรรทัดของตัวเองท้าย CREATE TABLE ใช้เมื่อเงื่อนไขข้ามหลายคอลัมน์ (หรือเมื่ออยากตั้งชื่อ)
สังเกต 2 จุดที่ออกแบบอย่างละเอียด 1. DECIMAL(3,2) สำหรับ GPA — 3 หลักรวม ทศนิยม 2 ตำแหน่ง เก็บ 4.00 ได้พอดี และไม่มีปัญหาความคลาดเคลื่อนแบบ FLOAT
2. graduation_date IS NULL OR graduation_date > enrollment_date — ต้องมี IS NULL นำหน้า ไม่งั้นนักศึกษาที่ยังไม่จบ (graduation_date เป็น NULL) จะ insert ไม่ผ่าน เพราะ NULL > date ให้ผลเป็น UNKNOWN ไม่ใช่ TRUE

4.5หลักการออกแบบ Schema ที่ดี

Clarity — ความชัดเจนป้องกันความสับสน

ตั้งชื่อตาราง คอลัมน์ และ constraint ให้สม่ำเสมอและสื่อความหมาย ทำให้ฐานข้อมูลอธิบายตัวเองได้ และลดข้อผิดพลาดตอนดูแลรักษา

Simplicity — เรียบง่ายแต่ครบ

เก็บข้อมูลที่จำเป็นให้ครบ แต่อย่า over-engineer จนงานที่ทำบ่อยกลายเป็นเรื่องยุ่งยาก

Consistency — ทีมทำงานร่วมกันได้

มาตรฐานการตั้งชื่อ ชนิดข้อมูล และรูปแบบการออกแบบที่เป็นแบบเดียวกัน ทำให้นักพัฒนาหลายคนทำงานบนฐานข้อมูลเดียวกันได้อย่างมีประสิทธิภาพ

Flexibility — รองรับอนาคต

การออกแบบที่ดีคาดการณ์เส้นทางการเติบโตที่เป็นไปได้ ทำให้เพิ่มฟีเจอร์ใหม่ได้โดยไม่ต้องรื้อโครงสร้างใหญ่

การเลือกชนิดข้อมูล

ประเด็นหลักการ
ความยาวสตริงVARCHAR(50) กับ VARCHAR(255) ส่งผลต่อทั้งพื้นที่และประสิทธิภาพ — ประเมินค่าสูงสุดที่เป็นไปได้จริงบวกเผื่อการเติบโต
ความแม่นยำเชิงตัวเลขDECIMAL รับประกันความถูกต้องของการคำนวณเงิน · FLOAT ให้ประสิทธิภาพดีกว่าสำหรับงานวิทยาศาสตร์ที่ยอมรับความคลาดเคลื่อนเล็กน้อยได้
ความละเอียดของวันเวลาDATE สำหรับวันเกิด · DATETIME สำหรับ timestamp ของธุรกรรม · พิจารณา time zone สำหรับแอประดับสากล
ค่าที่มีชุดจำกัดENUM หรือ lookup table + FK บังคับค่าสถานะที่ถูกต้อง พร้อมช่วยเรื่องประสิทธิภาพ
ขนาดของจำนวนเต็มINT รองรับได้ราว 2 พันล้าน · BIGINT รองรับมากกว่ามากแต่กินพื้นที่มากกว่า

Design pattern ที่เจอบ่อย

Lookup tables

ประเทศ จังหวัด หมวดหมู่สินค้า และข้อมูลอ้างอิงที่ค่อนข้างนิ่ง ควรแยกออกเป็นตารางอ้างอิงต่างหาก

Audit trail

เพิ่ม created_by, created_date, modified_by, modified_date เพื่อติดตามการเปลี่ยนแปลง

Soft delete

ใช้ธง is_deleted แทนการลบจริง เพื่อรักษา referential integrity พร้อมซ่อนเรคอร์ดที่ไม่ใช้แล้ว

Status tracking

กระบวนการอย่างการประมวลผลคำสั่งซื้อหรือการอนุมัติ ต้องออกแบบฟิลด์สถานะและกฎการเปลี่ยนสถานะอย่างรอบคอบ

ออกแบบเผื่อประสิทธิภาพตั้งแต่ต้น

4.6แบบฝึกหัดในคาบ — KKU Bookstore

โจทย์: นักพัฒนามือใหม่ออกแบบฐานข้อมูลร้านหนังสือ KKU ไว้แบบนี้ ให้หาข้อบกพร่อง 6 ข้อ แล้วบอกว่าเป็นปัญหาเรื่องอะไร

CREATE TABLE Customer (
    ID INT PRIMARY KEY,
    FullName VARCHAR(100),           -- "นายสมชาย ใจดี"
    CompleteAddress TEXT,            -- "123 ถ.มิตรภาพ, ขอนแก่น, 40002"
    PhoneNumbers VARCHAR(200),       -- "081-111-2222,043-333-444"
    MemberStatus VARCHAR(50),        -- พิมพ์อะไรลงไปก็ได้
    Age INT,                         -- ตอนนี้มีแถวที่ Age = 250
    RegDate VARCHAR(20)              -- "22/6/2026" เก็บเป็นข้อความ
);
เฉลย Part A — ข้อบกพร่อง 6 ข้อ พร้อมชื่อ concept
#ปัญหาConcept
1Customer ใช้เอกพจน์และ ID, FullName เป็น CamelCasenaming convention — ควรเป็น customers, customer_id, full_name
2FullName รวมคำนำหน้า ชื่อ นามสกุลไว้ช่องเดียวcomposite attribute — ค้นหา/เรียงตามนามสกุลไม่ได้
3CompleteAddress รวมถนน เมือง รหัสไปรษณีย์composite attribute — ค้นตามเมืองหรือเรียงตามรหัสไปรษณีย์ไม่ได้
4PhoneNumbers เก็บหลายเบอร์คั่นด้วยจุลภาคmulti-valued attribute — ละเมิด 1NF ต้องแยกตาราง
5MemberStatus ไม่มีการจำกัดค่า พิมพ์อะไรก็ได้ · Age รับค่า 250 ได้domain constraint — ขาด CHECK / ENUM
6RegDate เก็บวันที่เป็น VARCHARdata type — เรียงลำดับผิด คำนวณช่วงวันไม่ได้ ตรวจความถูกต้องไม่ได้
เฉลย Part B — schema ที่แก้แล้ว
CREATE TABLE customers (
    customer_id     INT PRIMARY KEY,
    name_title      VARCHAR(10),
    first_name      VARCHAR(50) NOT NULL,
    last_name       VARCHAR(50) NOT NULL,
    street_address  VARCHAR(200),
    city            VARCHAR(50),
    postal_code     VARCHAR(10),
    age             INT  CHECK (age >= 16 AND age <= 100),
    member_status   VARCHAR(10) NOT NULL DEFAULT 'regular'
                    CHECK (member_status IN ('regular','silver','gold')),
    registration_date DATE NOT NULL
);

CREATE TABLE customer_phone_numbers (
    phone_id     INT PRIMARY KEY,
    customer_id  INT NOT NULL,
    phone_number VARCHAR(20) NOT NULL,
    phone_type   VARCHAR(10) CHECK (phone_type IN ('mobile','home','office')),
    is_primary   BOOLEAN DEFAULT FALSE,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

หมายเหตุเรื่อง portability: SQLite ไม่รองรับ ENUM จึงต้องใช้ CHECK (col IN (...)) แทน — ซึ่งเป็นบทเรียนที่ดีว่าการเขียนแบบ standard SQL ย้ายระบบได้ง่ายกว่า

เฉลย Part C — พิสูจน์ว่า constraint ทำงานจริง
-- 1. แถวที่ถูกต้อง → สำเร็จ
INSERT INTO customers VALUES
 (1,'นาย','สมชาย','ใจดี','123 ถ.มิตรภาพ','ขอนแก่น','40002',
  22,'gold','2026-06-22');

-- 2. age = 250 → ต้องล้มเหลว
INSERT INTO customers VALUES
 (2,'นาง','มาลี','สุขใจ','1 ถ.ศรีจันทร์','ขอนแก่น','40000',
  250,'regular','2026-06-22');
-- CHECK constraint failed: age

-- 3. member_status = 'platinum' → ต้องล้มเหลว
INSERT INTO customers VALUES
 (3,'นาย','กฤต','ตั้งใจ','9 ถ.กลางเมือง','ขอนแก่น','40000',
  30,'platinum','2026-06-22');
-- CHECK constraint failed: member_status

4.7สรุปบทนี้

การเก็บ phone_numbers VARCHAR(500) = '081-111-2222,043-333-444' ละเมิดหลักการใด
relational model บังคับว่าค่าของ attribute ต้อง atomic คือแบ่งย่อยไม่ได้ การยัดหลายเบอร์ไว้ช่องเดียวจึงละเมิด 1NF · ทางแก้คือแยกเป็นตาราง employee_phone_numbers ที่ผูกด้วย FK
ทำไม CHECK (graduation_date IS NULL OR graduation_date > enrollment_date) ต้องมีส่วน IS NULL
NULL > '2026-01-01' ให้ผลเป็น UNKNOWN ไม่ใช่ TRUE · CHECK จะปฏิเสธแถวเมื่อผลไม่เป็นจริง จึงต้องเผื่อกรณี NULL ไว้ด้วย OR — นี่คือตัวอย่างที่ตรรกะ 3 ค่าโผล่มาในเรื่องออกแบบ (ดูบทที่ 7)
ระบบจำไว้ในเบราว์เซอร์นี้ จะได้รู้ว่าอ่านถึงไหนแล้ว