Database Design กับ ScyllaDB

การทำ Data Modeling ของ ScyllaDB ทำอย่างไรกันนะ? วันนี้เราจะมาดูกันครัย

Avatar Takai
04/09/2025

Database Design กับ ScyllaDB: จัดข้อมูลยังไงให้มีประสิทธิภาพ

สวัสดีเพื่อนๆ ทุกคนอีกครั้งครับ

เคยไหมครับ เวลาเห็นแอปพลิเคชันระดับโลกอย่าง Discord ที่จัดการข้อความหลายพันล้านข้อความต่อวัน หรือแอปอื่นๆ ที่ต้องรับข้อมูลเข้า-ออกมหาศาล แล้วสงสัยว่า “เขาทำได้ยังไง?” เบื้องหลังความเร็วและความเสถียรเหล่านั้น มักจะมีฐานข้อมูลประสิทธิภาพสูงเสมอ เช่น ScyllaDB แต่การที่จะทำให้มันมีประสิทธิภาพได้นั้น เราก็ต้องรู้ว่าอะไรคือสิ่งที่ควรทำเมื่อเราใช้ ScyllaDB วันนี้เราจะมาเรียนรู้กันเพื่อให้เพื่อนๆ สามารถออกแบบฐานข้อมูล ScyllaDB ได้อย่างมีประสิทธิภาพมากขึ้นครับ

เปลี่ยนวิธีคิด เริ่มจาก “คำถาม” ไม่ใช่ “ข้อมูล” (Query-Driven Design)

นี่คือจุดที่แตกต่างที่สุด และสำคัญที่สุด ระหว่างโลกของ SQL (เช่น MySQL, PostgreSQL) กับโลกของ NoSQL อย่าง ScyllaDB ครับ

โลกของ SQL:

เรามักจะถูกสอนให้ทำ Normalization คือการออกแบบตารางให้ “สวยงาม” ลดความซ้ำซ้อนของข้อมูลให้ได้มากที่สุด

มีตาราง users, videos, tags, video_tags เพื่อเชื่อม videos กับ tags

เวลาจะดึงข้อมูล “วิดีโอทั้งหมดของ User A ที่มีแท็ก B” เราก็จะใช้คำสั่ง JOIN ไปมาเพื่อประกอบร่างข้อมูลกลับขึ้นมา จะเห็นได้ว่าเราไม่จำเป็นต้องรู้แน่ชัดว่าต้องการอะไร เราสามารถใช้การ JOIN เพื่อให้ได้ข้อมูลได้

โลกของ ScyllaDB (และ Cassandra):

ให้โยนความคิดแบบ SQL ทิ้งไปก่อน เราจะทำตรงกันข้ามเลยครับ เราจะเริ่มจากการลิสต์ “คำถาม” (Queries) ที่แอปพลิเคชันของเราจะถามฐานข้อมูลออกมาให้หมดก่อน

สมมติแอปเรามีคำถามหลักๆ คือ:

Q1: ขอดูวิดีโอทั้งหมดที่อัปโหลดโดย user_id = ‘U123’ เรียงตามวันเวลาที่อัปโหลดล่าสุด

Q2: ขอดูวิดีโอทั้งหมดที่ติด tag = ‘ScyllaDB’ เรียงตามจำนวนวิว

Q3: ขอดูคอมเมนต์ล่าสุด 10 อันของ video_id = ‘V789’

เมื่อได้คำถามมาแล้ว เราจะ สร้างตารางขึ้นมาเพื่อตอบคำถามแต่ละข้อโดยเฉพาะ

-- ตารางสำหรับตอบคำถาม Q1
CREATE TABLE videos_by_user (
    user_id uuid,
    upload_time timestamp,
    video_id uuid,
    title text,
    description text,
    PRIMARY KEY ((user_id), upload_time)
) WITH CLUSTERING ORDER BY (upload_time DESC);

-- ตารางสำหรับตอบคำถาม Q2
CREATE TABLE videos_by_tag (
    tag text,
    views counter,
    video_id uuid,
    title text,
    uploader_username text,
    PRIMARY KEY ((tag), views)
) WITH CLUSTERING ORDER BY (views DESC);

-- ตารางสำหรับตอบคำถาม Q3
CREATE TABLE comments_by_video (
    video_id uuid,
    comment_time timestamp,
    comment_id uuid,
    username text,
    comment_text text,
    PRIMARY KEY ((video_id), comment_time)
) WITH CLUSTERING ORDER BY (comment_time DESC);

สังเกตุว่าเราจะยอม “เก็บข้อมูลซ้ำซ้อน” (เช่น video_id, title) เพื่อให้การอ่านข้อมูลในแต่ละรูปแบบนั้นเร็วที่สุดเท่าที่จะเป็นไปได้ เพราะในโลกของ ScyllaDB การ JOIN ไม่มีอยู่จริง และเป็นสิ่งที่ต้องหลีกเลี่ยง

รู้จัก Primary Key ให้ทะลุปรุโปร่ง

Primary Key คือทุกสิ่งทุกอย่างใน ScyllaDB มันเป็นตัวกำหนดชะตากรรมของข้อมูล ตั้งแต่การจัดเก็บไปจนถึงความเร็วในการดึงข้อมูล มาดูกันทีละส่วนครับ

Partition Key: ผู้จัดสรรข้อมูลสู่ Server ลองนึกภาพซูเปอร์มาร์เก็ตขนาดใหญ่ที่มีเคาน์เตอร์คิดเงิน (Server/Node) หลายสิบเคาน์เตอร์ Partition Key ก็คือ “กฎ” ที่บอกว่าลูกค้าแต่ละคน (ข้อมูลแต่ละแถว) จะต้องไปจ่ายเงินที่เคาน์เตอร์ไหน ScyllaDB จะนำค่าของ Partition Key มาผ่านกระบวนการ Hashing เพื่อให้ได้ค่าตัวเลขที่ไม่ซ้ำกัน แล้วใช้ตัวเลขนั้นเป็นตัวกำหนดว่า “ข้อมูลแถวนี้ ต้องไปอยู่ Node นี้นะ”

ดังนั้น ควรเลือกคอลัมน์ที่มีค่ากระจายตัวสูง (High Cardinality) เช่น user_id, video_id, sensor_id เพื่อให้ข้อมูลกระจายไปตามเคาน์เตอร์ต่างๆ อย่างเท่าเทียมกัน ทุกเคาน์เตอร์จะได้ทำงานไม่ว่างเว้น และไม่ควรเลือกคอลัมน์ที่มีค่าซ้ำๆ กันเยอะๆ (Low Cardinality) เช่น gender (มีแค่ไม่กี่ค่า), country (ถ้าผู้ใช้ส่วนใหญ่อยู่ประเทศเดียวกัน) จะทำให้ลูกค้าวิ่งไปต่อคิวกันอยู่ที่เคาน์เตอร์เดียวจนแถวยาวเหยียด ในขณะที่เคาน์เตอร์อื่นว่าง นี่คือสาเหตุของปัญหาคอขวดที่เรียกว่า “Hotspot” ครับ

Clustering Key: ผู้จัดเรียงแฟ้มในลิ้นชัก หลังจากที่ Partition Key บอกเราแล้วว่าข้อมูลต้องไปอยู่ “ลิ้นชัก” ไหน Clustering Key ก็จะมารับหน้าที่ต่อ ในการกำหนดว่า “แฟ้ม” (ข้อมูล) ที่อยู่ในลิ้นชักนั้น จะต้องถูก จัดเรียงตามลำดับ อย่างไร ข้อมูลใน ScyllaDB จะถูกจัดเรียงบนดิสก์ตามลำดับของ Clustering Key เป๊ะๆ นี่คือเหตุผลที่ว่าทำไมการดึงข้อมูลตามลำดับ หรือการดึงเป็นช่วง (Range Scan) ถึงได้เร็วสุดๆ

การเรียงลำดับฟรีๆ: ถ้าเรา ORDER BY ด้วยคอลัมน์ที่เป็น Clustering Key ในทิศทางเดียวกับที่กำหนดไว้ (ASC/DESC) ScyllaDB แทบไม่ต้องทำอะไรเลย แค่หยิบข้อมูลตามลำดับที่มันถูกเก็บไว้อยู่แล้วมาให้เรา

การกรองข้อมูลแบบช่วง: เราสามารถใช้ > , < , >= , <= กับ Clustering Key ได้อย่างมีประสิทธิภาพ เช่น SELECT * FROM comments_by_video WHERE video_id = ‘V789’ AND comment_time > ‘2025-09-01’ เพื่อดึงคอมเมนต์ทั้งหมดของวิดีโอนี้ที่โพสต์หลังวันที่ 1 กันยายน เป็นต้น

สรุปภาพรวม Primary Key: PRIMARY KEY ((Partition Key), Clustering Key) คือสูตรสำเร็จ () วงเล็บแรกคือ Partition Key และที่ตามมาคือ Clustering Key ครับ

การทำซ้ำไม่ได้แย่เสมอไปถ้ารู้จัก Batch

อย่างที่เห็นในตัวอย่างตารางด้านบน เรายอมเก็บข้อมูลซ้ำซ้อนอย่าง title หรือ username ไว้ในหลายๆ ที่ คำถามที่หลายคนสงสัยคือ “แล้วถ้าข้อมูลต้นฉบับมันเปลี่ยนล่ะ เช่น User เปลี่ยนชื่อ จะอัปเดตยังไงให้ครบทุกที่?”

นี่คือ Trade-off ที่เราต้องยอมรับครับ โลกของ ScyllaDB ถูกปรับแต่งมาเพื่อ การอ่านที่เร็วสุด (Blazing Fast Reads) ดังนั้นภาระจึงตกมาอยู่ที่ การเขียน (Writes) แทน

แนวทางการจัดการ คือ เวลาที่มีการเปลี่ยนแปลงข้อมูล เราจะต้องรับผิดชอบในการอัปเดตข้อมูลนั้นในทุกๆ ตารางที่มันถูกทำซ้ำไว้ด้วยตัวเอง ซึ่ง ScyllaDB ก็มีเครื่องมือช่วยคือ BATCH

BEGIN BATCH
    UPDATE videos_by_user SET title = 'New Title' WHERE ... ;
    UPDATE videos_by_tag SET title = 'New Title' WHERE ... ;
APPLY BATCH;

การใช้ LOGGED BATCH จะช่วยรับประกันว่าการเขียนทั้งหมดในชุดคำสั่งนั้น จะสำเร็จทั้งหมดหรือไม่ก็ล้มเหลวทั้งหมด (Atomicity) ช่วยรักษาความถูกต้องของข้อมูลได้ในระดับหนึ่ง แต่ก็ต้องจำไว้เสมอว่าการเขียนจะซับซ้อนและช้าลงเล็กน้อยเพื่อแลกกับการอ่านที่เร็วขึ้นมหาศาล

สิ่งที่ต้องระวัง Wide Partitions (Partition ที่ใหญ่เกินไป)

คือหนึ่งในปัญหาที่เจอบ่อยที่สุดในการออกแบบ ScyllaDB ครับ Wide Partition คือ Partition (ลิ้นชัก) ที่มีข้อมูล (แฟ้ม) ถูกยัดเข้าไปมากเกินไปจนผิดปกติ

ลองนึกภาพเซ็นเซอร์วัดอุณหภูมิที่ส่งข้อมูลทุกๆ วินาที ถ้าเราใช้ sensor_id เป็น Partition Key เพียงอย่างเดียว ข้อมูลของเซ็นเซอร์ตัวนั้นทั้งหมดตั้งแต่เริ่มติดตั้งจนถึงปัจจุบัน ก็จะถูกเก็บไว้ใน Partition เดียวกัน! พอเวลาผ่านไปเป็นปีๆ Partition นั้นอาจจะใหญ่เป็นหลาย Gigabytes หรือ Terabytes ได้เลย

ผลกระทบ:

ประสิทธิภาพการอ่านลดลง: การอ่านข้อมูลจาก Partition ที่ใหญ่มากๆ จะช้าลง

เกิดภาระหนักกับ Node: Node ที่ดูแล Partition นั้นจะทำงานหนักกว่าเพื่อน

การซ่อมแซม (Repair) และดูแลรักษาระบบทำได้ยาก

วิธีแก้ไข (Partition Key Bucketing):

เราต้องหาทาง “ซอย” Partition นั้นให้เล็กลง โดยการเพิ่มคอลัมน์อื่นเข้ามาใน Partition Key เทคนิคที่นิยมที่สุดคือการใช้ “เวลา” เข้ามาช่วย หรือที่เรียกว่า Time Bucketing

จากเดิม: PRIMARY KEY ((sensor_id), timestamp) (Partition ใหญ่มาก)

แบบใหม่: PRIMARY KEY ((sensor_id, year_month_day), timestamp) (สร้าง Partition ใหม่ทุกวัน)

ถ้าข้อมูลเยอะมาก: PRIMARY KEY ((sensor_id, year_month_day_hour), timestamp) (สร้าง Partition ใหม่ทุกชั่วโมง)

การทำแบบนี้จะช่วยจำกัดขนาดของแต่ละ Partition ให้อยู่ในระดับที่จัดการได้ (โดยทั่วไปแนะนำว่าไม่ควรเกิน 10-100MB)

โดยสรุปแล้ว การออกแบบฐานข้อมูลสำหรับ ScyllaDB คือการเปลี่ยนมุมมองจากการพยายามสร้าง “แหล่งความจริงเพียงแหล่งเดียว (Single Source of Truth)” ไปสู่การสร้าง “ชุดคำตอบที่ปรับแต่งมาอย่างดีที่สุด” สำหรับแต่ละคำถามที่เราต้องการ มันคือการยอมรับความซับซ้อนในการเขียน เพื่อแลกกับอิสรภาพและความเร็วในการอ่านข้อมูลในสเกลที่ใหญ่มากๆ ครับ