MySQL และ MariaDB เป็น Database Engine ยอดนิยมที่ขับเคลื่อนเว็บไซต์และแอปพลิเคชันนับล้านทั่วโลก แต่การติดตั้งด้วยค่า default ไม่ได้หมายความว่าจะทำงานได้เร็วที่สุด โดยเฉพาะบน VPS ที่มี RAM และ CPU จำกัด การจูนค่าตัวแปรใน my.cnf ให้เหมาะสมกับทรัพยากรที่มีและลักษณะ workload ของระบบ สามารถเพิ่มความเร็ว query ได้ตั้งแต่ 2 เท่าไปจนถึงหลายสิบเท่า บทความนี้รวบรวมเทคนิคการจูน MySQL/MariaDB บน VPS อย่างละเอียด พร้อมคำสั่งและค่าที่แนะนำจริงจากประสบการณ์ใช้งาน

ก่อนเริ่มจูน สิ่งสำคัญที่สุดคือการเข้าใจว่า "ไม่มีค่า one-size-fits-all" การจูนที่เหมาะสมต้องอาศัยข้อมูล เช่น ขนาด RAM บน VPS จำนวน connection พร้อมกัน และประเภท workload ว่าเน้น read หรือ write ค่าที่แนะนำในบทความนี้เป็นจุดเริ่มต้นที่ดีสำหรับ VPS ขนาดกลาง (RAM 2–8GB) ที่รัน LAMP Stack หรือ WordPress

ทำความเข้าใจไฟล์ my.cnf ก่อนจูน

ไฟล์ตั้งค่าหลักของ MySQL/MariaDB คือ /etc/mysql/my.cnf หรือ /etc/my.cnf (แล้วแต่ distro) บน Ubuntu/Debian อาจอยู่ใน /etc/mysql/mysql.conf.d/mysqld.cnf ตรวจสอบตำแหน่งที่ระบบใช้จริงด้วย:

mysql --verbose --help | grep "my.cnf"
# หรือตรวจสอบ
mysqld --verbose --help 2>/dev/null | grep -A 1 "Default options"

ก่อนแก้ไขควร backup ไฟล์เดิมเสมอ:

sudo cp /etc/mysql/my.cnf /etc/mysql/my.cnf.backup.$(date +%Y%m%d)

โครงสร้างไฟล์ my.cnf แบ่งเป็น section ต่างๆ section ที่สำคัญสำหรับ server คือ [mysqld] ซึ่งตั้งค่าพฤติกรรมของ daemon ส่วน [mysql] และ [client] ตั้งค่าของ command-line client การแก้ไขผิด section จะไม่ส่งผล ดังนั้นต้องระวังให้ดี

ตรวจสอบสถานะ MySQL ก่อนจูน

การจูนที่ดีต้องเริ่มจากการเก็บ baseline ก่อน เพื่อเปรียบเทียบผลลัพธ์หลังจูน คำสั่งต่อไปนี้ให้ข้อมูลที่จำเป็น:

# ดูค่าตัวแปรปัจจุบัน
mysql -u root -p -e "SHOW GLOBAL VARIABLES LIKE 'innodb%';"
mysql -u root -p -e "SHOW GLOBAL VARIABLES LIKE 'key_buffer%';"

# ดู Status ปัจจุบัน
mysql -u root -p -e "SHOW GLOBAL STATUS;"

# ดู Process ที่กำลังรัน
mysql -u root -p -e "SHOW PROCESSLIST;"

# ดูข้อมูล engine
mysql -u root -p -e "SHOW ENGINE INNODB STATUS\G"

นอกจากนั้นควรดูการใช้ทรัพยากรระดับ OS ด้วย:

# ดู RAM ที่ใช้ทั้งหมด
free -h

# ดู process ที่ใช้ RAM มาก
ps aux --sort=-%mem | head -10

# ดูขนาด database
du -sh /var/lib/mysql/*

ค่าสำคัญที่ต้องจูน — InnoDB Buffer Pool

ค่าที่ส่งผลมากที่สุดต่อประสิทธิภาพ InnoDB คือ innodb_buffer_pool_size ซึ่งกำหนดขนาดหน่วยความจำที่ MySQL ใช้แคช data และ index ของ InnoDB ค่า default ต่ำมาก (128MB) ซึ่งไม่เพียงพอสำหรับฐานข้อมูลขนาดกลางถึงใหญ่

หลักการตั้งค่า innodb_buffer_pool_size:

ตัวอย่างการตั้งค่าสำหรับ VPS RAM 4GB ที่รัน LAMP:

[mysqld]
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2   # แบ่งเป็น 2 instance (แนะนำ 1 instance ต่อ 1GB)
innodb_log_file_size = 256M        # ยิ่งใหญ่ยิ่ง write performance ดี แต่ recovery นานขึ้น
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2  # 1=ปลอดภัยสูงสุด, 2=เร็วขึ้น (อาจเสีย 1 วินาทีสุดท้าย)
innodb_flush_method = O_DIRECT     # ลด double-buffering กับ OS page cache

เคล็ดลับ: หลังเปลี่ยน innodb_log_file_size ต้อง stop MySQL แล้วลบไฟล์ ib_logfile0 และ ib_logfile1 ออกก่อน restart มิฉะนั้น MySQL จะ start ไม่ได้ด้วย error "The innodb_system tablespace is larger than innodb_log_file_size" — รัน sudo rm /var/lib/mysql/ib_logfile* ก่อน start เสมอ

ค่า Connection และ Thread — รองรับผู้ใช้พร้อมกันได้มากขึ้น

ค่า default ของ max_connections ใน MySQL คือ 151 ซึ่งเพียงพอสำหรับเว็บขนาดเล็ก แต่ถ้าเว็บไซต์มี traffic สูง หรือรัน WordPress หลายไซต์ อาจพบ error "Too many connections" ได้ อย่างไรก็ตาม การเพิ่ม max_connections ไม่ใช่แค่ตัวเลข เพราะแต่ละ connection ใช้ RAM ประมาณ 1–2MB ดังนั้นต้องคำนวณให้พอดีกับ RAM ที่มี

[mysqld]
max_connections = 200              # สำหรับ VPS RAM 4GB
thread_cache_size = 50             # cache thread เพื่อลด overhead การสร้าง thread ใหม่
thread_stack = 192K
back_log = 128                     # queue รอ connection เมื่อ thread เต็ม

# Per-connection memory (ใช้ทุก connection)
sort_buffer_size = 2M              # ใช้เมื่อ sort ข้อมูล
read_buffer_size = 1M
read_rnd_buffer_size = 2M
join_buffer_size = 2M

คำนวณ memory สูงสุดที่ MySQL อาจใช้แบบคร่าวๆ:

# Total max memory ≈ innodb_buffer_pool_size + (max_connections × per_connection_memory)
# ตัวอย่าง: 2048M + (200 × (2M + 2M + 1M + 2M)) = 2048M + 1400M = ~3.4GB
# ต้องไม่เกิน RAM ทั้งหมดลบ ~1GB สำหรับ OS

เปิดและวิเคราะห์ Slow Query Log

Slow Query Log เป็นเครื่องมือสำคัญในการหา query ที่เป็นปัญหา ก่อนจูน query ต้องรู้ว่า query ไหนช้า จึงจะแก้ได้ตรงจุด

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1               # บันทึก query ที่ใช้เวลา > 1 วินาที
log_queries_not_using_indexes = 1 # บันทึก query ที่ไม่ใช้ index ด้วย
min_examined_row_limit = 100      # ไม่ log query ที่อ่านแถวน้อยกว่า 100 แถว

หลังจาก restart MySQL และรอให้มีข้อมูลสักพัก ใช้เครื่องมือ mysqldumpslow วิเคราะห์:

# ดู top 10 query ที่ช้าที่สุด
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# ดู query ที่รันบ่อยที่สุด
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

# ดู query ที่ lock นานที่สุด
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

เมื่อพบ query ที่ช้า ให้ใช้ EXPLAIN เพื่อดูว่า MySQL วาง execution plan อย่างไร:

EXPLAIN SELECT * FROM orders
  WHERE user_id = 123
  AND status = 'pending'
  ORDER BY created_at DESC;

-- ถ้า type=ALL หรือ rows สูงมาก แสดงว่าขาด index
-- เพิ่ม index:
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status, created_at);

ตารางเปรียบเทียบค่าตัวแปรสำคัญ

ตารางต่อไปนี้สรุปค่าแนะนำสำหรับ VPS ขนาดต่างๆ เพื่อใช้เป็นจุดเริ่มต้น ก่อนปรับให้เหมาะกับ workload จริงของคุณ:

ตัวแปร VPS RAM 1GB VPS RAM 2GB VPS RAM 4GB VPS RAM 8GB
innodb_buffer_pool_size 256M 768M 2G 5G
innodb_log_file_size 64M 128M 256M 512M
max_connections 50 100 200 400
thread_cache_size 8 16 32 64
table_open_cache 200 400 800 2000

การใช้ Query Cache และ Performance Schema

Query Cache เป็นฟีเจอร์ที่แคช result ของ SELECT query ไว้ใน RAM เพื่อให้ query เดิมที่รันซ้ำตอบสนองได้ทันที อย่างไรก็ตาม Query Cache มีข้อเสียที่สำคัญคือ MySQL ต้อง invalidate cache ทุกครั้งที่ตาราง update หรือ insert แม้จะเป็นแถวอื่นก็ตาม ทำให้บน site ที่มีการ write สูงมาก Query Cache อาจลดประสิทธิภาพแทนที่จะเพิ่ม

ใน MySQL 8.0 ได้ถอด Query Cache ออกไปแล้ว แต่ MariaDB ยังมีอยู่ ถ้าใช้ MariaDB และ workload เน้น read:

[mysqld]
# สำหรับ workload ที่ read >> write
query_cache_type = 1              # 0=off, 1=on สำหรับ query ที่ไม่มี SQL_NO_CACHE
query_cache_size = 64M            # ไม่ควรเกิน 128M (fragmentation ปัญหา)
query_cache_limit = 2M            # result สูงสุดที่จะ cache

สำหรับ MySQL 8.0+ หรือ workload write-heavy ให้ปิด query cache และใช้ Performance Schema แทนในการ monitor:

[mysqld]
query_cache_type = 0
query_cache_size = 0

# Performance Schema (ใช้ monitor ประสิทธิภาพ)
performance_schema = ON
performance_schema_events_statements_history_size = 100

จูน Table Cache และ Temporary Tables

เมื่อ MySQL เปิด table ทุกครั้ง จะใช้ file descriptor และ RAM สำหรับ table definition cache ถ้า table_open_cache ต่ำเกินไป MySQL จะต้องปิดและเปิด file ซ้ำๆ ซึ่งช้า โดยเฉพาะ WordPress multisite หรือ database ที่มีตารางมาก

[mysqld]
table_open_cache = 800            # ปรับตาม Opened_tables ใน SHOW STATUS
table_definition_cache = 1400    # cache table schema definition

# Temporary tables — ใช้เมื่อ GROUP BY, ORDER BY ไม่มี index
tmp_table_size = 64M              # ขนาด temp table ใน RAM ก่อนเขียน disk
max_heap_table_size = 64M         # ต้องเท่ากับ tmp_table_size

# Temp directory ให้อยู่ใน tmpfs ถ้าเป็นไปได้
# tmpdir = /tmp

ตรวจสอบว่าต้องเพิ่ม tmp_table_size หรือไม่:

mysql -u root -p -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
# ถ้า Created_tmp_disk_tables / Created_tmp_tables > 5% ควรเพิ่ม tmp_table_size

การสร้าง Index ที่ถูกต้อง — หัวใจของ Query Optimization

การจูน my.cnf ช่วยได้มาก แต่ไม่มี buffer pool ขนาดไหนที่จะแก้ query ที่ขาด index ได้ การสร้าง index ที่ถูกต้องมักให้ผลลัพธ์มากกว่าการเพิ่ม RAM ในระยะยาว

ตรวจหา Table ที่ควรมี Index

-- ดู table ที่ถูก full scan มากที่สุด
SELECT * FROM sys.schema_tables_with_full_table_scans
ORDER BY rows_full_scanned DESC LIMIT 10;

-- ดู index ที่ไม่ได้ใช้งานเลย (MariaDB/MySQL 5.6+)
SELECT * FROM sys.schema_unused_indexes;

-- ดู index ที่ซ้ำซ้อน
SELECT * FROM sys.schema_redundant_indexes;

หลักการสร้าง Composite Index

-- สำหรับ query: WHERE user_id=? AND status=? ORDER BY created_at DESC
-- Index ที่ถูกต้อง: ใส่ column ที่ filter ก่อน ตามด้วย sort column
ALTER TABLE orders ADD INDEX idx_user_status_date (user_id, status, created_at);

-- ตรวจว่า index ถูกใช้หรือไม่
EXPLAIN SELECT * FROM orders
  WHERE user_id=1 AND status='pending'
  ORDER BY created_at DESC;

ข้อควรระวัง: index ทุกตัวเพิ่ม overhead ของ INSERT/UPDATE/DELETE เพราะ MySQL ต้อง update index ทุกครั้ง ดังนั้น index ควรสร้างเฉพาะตัวที่ใช้จริงเท่านั้น

ตัวอย่าง my.cnf ฉบับสมบูรณ์สำหรับ VPS RAM 4GB

ตัวอย่างนี้เหมาะสำหรับ VPS ที่รัน WordPress หรือ PHP application ทั่วไปบน RAM 4GB โดยมี Apache/Nginx และ PHP-FPM ทำงานพร้อมกัน:

[mysqld]
# === Basic Settings ===
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
bind-address = 127.0.0.1

# === Character Set ===
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

# === InnoDB Settings ===
default_storage_engine = InnoDB
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
innodb_log_file_size = 256M
innodb_log_buffer_size = 64M
innodb_file_per_table = 1
innodb_open_files = 400
innodb_io_capacity = 400
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2

# === Connection Settings ===
max_connections = 200
max_allowed_packet = 64M
thread_cache_size = 50
thread_stack = 192K

# === Cache Settings ===
table_open_cache = 800
table_definition_cache = 1400
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 2M
join_buffer_size = 2M

# === Logging ===
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

# === Binary Log (ถ้าไม่ได้ทำ replication ปิดได้เพื่อประหยัด disk) ===
# skip-log-bin

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

ตรวจสอบและวัดผลหลังจูน

หลังจาก restart MySQL ด้วยค่าใหม่ ควรรอให้ระบบ warm up สักพัก (อย่างน้อย 1–2 ชั่วโมง) แล้วตรวจสอบผลลัพธ์:

# ตรวจ Buffer Pool Hit Rate (ควรสูงกว่า 99%)
mysql -u root -p -e "
SELECT
  (Innodb_buffer_pool_read_requests /
   (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_reads)) * 100
   AS buffer_pool_hit_rate
FROM (
  SELECT
    VARIABLE_VALUE AS Innodb_buffer_pool_read_requests
  FROM performance_schema.global_status
  WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests'
) a,
(
  SELECT
    VARIABLE_VALUE AS Innodb_buffer_pool_reads
  FROM performance_schema.global_status
  WHERE VARIABLE_NAME='Innodb_buffer_pool_reads'
) b;"

# ตรวจ Thread Cache Hit Rate (ควรสูงกว่า 90%)
mysql -u root -p -e "SHOW GLOBAL STATUS LIKE 'Threads_created';"
mysql -u root -p -e "SHOW GLOBAL STATUS LIKE 'Connections';"
# hit_rate = 1 - (Threads_created / Connections)

เครื่องมือที่ช่วยวิเคราะห์และแนะนำค่าอัตโนมัติ:

# MySQLTuner — script วิเคราะห์ config และแนะนำการจูน
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
perl mysqltuner.pl --user root --pass yourpassword

# Percona Toolkit — pt-query-digest วิเคราะห์ slow log
sudo apt install percona-toolkit
pt-query-digest /var/log/mysql/slow.log | head -100

คำถามที่พบบ่อย (FAQ)

ค่า innodb_buffer_pool_size ควรตั้งเท่าไรบน VPS RAM 2GB

สำหรับ VPS ที่ใช้ InnoDB เป็นหลัก แนะนำตั้ง innodb_buffer_pool_size ที่ 50–70% ของ RAM ทั้งหมด บน VPS RAM 2GB ควรตั้งไว้ที่ 1G (1024M) หากรันเฉพาะ MySQL ผู้เดียว แต่ถ้ามี Web Server และ PHP อยู่ด้วย ให้ลดเหลือ 512M–768M เพื่อเหลือ RAM ให้ส่วนอื่นทำงาน การตั้งค่าเกิน RAM จริงจะทำให้ระบบ swap ซึ่งช้ากว่า RAM หลายเท่า

Slow Query Log คืออะไร และควรเปิดไว้ตลอดเวลาหรือไม่

Slow Query Log คือ log ที่ MySQL/MariaDB บันทึก query ที่ใช้เวลานานกว่าค่า long_query_time ที่กำหนด (ค่า default คือ 10 วินาที แต่แนะนำให้ตั้ง 1–2 วินาทีเพื่อจับ query ที่ช้ากว่าปกติ) การเปิดไว้ตลอดเวลาบน production server มี overhead เล็กน้อย แต่ยอมรับได้ ควรเปิดไว้เพื่อวิเคราะห์ปัญหาและปิดได้เมื่อระบบเสถียรดีแล้ว

MyISAM กับ InnoDB ต่างกันอย่างไร ควรใช้แบบไหน

InnoDB รองรับ Transactions, Foreign Keys, Row-level Locking ทำให้เหมาะกับงาน OLTP (write-heavy เช่น e-commerce, CMS) และ crash-safe กว่ามาก ส่วน MyISAM เร็วกว่าสำหรับงาน read-only (full-text search บางกรณี) แต่ไม่รองรับ Transactions และ lock ทั้งตารางเมื่อ write ทำให้ concurrent users สูงๆ ช้า ปัจจุบันแนะนำใช้ InnoDB เป็นค่าเริ่มต้นสำหรับทุกตาราง ยกเว้นมีเหตุผลเฉพาะเจาะจง

วิธีดูว่า MySQL ใช้ memory มากแค่ไหน และ tuning ได้ผลหรือเปล่า

ใช้คำสั่ง SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%' เพื่อดูสัดส่วนการใช้งาน buffer pool ถ้า Innodb_buffer_pool_pages_free เป็น 0 เกือบตลอดแสดงว่า buffer pool เต็มและควรเพิ่ม ดู Innodb_buffer_pool_read_requests vs Innodb_buffer_pool_reads เพื่อคำนวณ hit rate (ควรสูงกว่า 99%) นอกจากนี้ใช้ SHOW PROCESSLIST ดู query ที่รันอยู่ และ mysqladmin status เพื่อดู Queries per second รวม

VPS KVM ประสิทธิภาพสูงของ AsiaGB

AsiaGB VPS Root Access เต็มรูปแบบ Docker MySQL Python Node.js เริ่มต้น 500 บาท/เดือน

ดูแพ็กเกจ VPS