Home > Blog > tech

Database Indexing คืออะไร? สอนสร้าง Index และ Optimize Query Performance 2026

database indexing query performance guide
Database Indexing Query Performance Guide 2026
2026-04-11 | tech | 3600 words

ถ้าคุณเคยเจอปัญหา Query ที่ทำงานช้าจนผู้ใช้ต้องรอหลายวินาที หรือ API Response Time พุ่งสูงขึ้นเรื่อยๆ ตามจำนวนข้อมูลที่เพิ่มขึ้น สาเหตุหลักมักเป็นเพราะ Database ของคุณขาด Index ที่เหมาะสม หรือ Query ถูกเขียนในแบบที่ Database Engine ไม่สามารถใช้ Index ได้อย่างมีประสิทธิภาพ

บทความนี้จะพาคุณเข้าใจ Database Indexing ตั้งแต่หลักการทำงานของ B-tree Index ไปจนถึงวิธีอ่าน EXPLAIN Plan, สร้าง Index ที่ถูกต้อง และ Optimize Query ให้เร็วขึ้นหลายสิบเท่า พร้อมตัวอย่างจริงสำหรับ PostgreSQL และ MySQL

ทำไม Index ถึงสำคัญ?

ลองนึกภาพว่าคุณมีหนังสือ 1,000 หน้า และต้องการหาคำว่า "Optimization" ถ้าไม่มีดัชนี (Index) คุณต้องเปิดอ่านทีละหน้าจากหน้าแรกจนถึงหน้าสุดท้าย ซึ่งก็คือการทำ Full Table Scan ที่มีความซับซ้อน O(n) แต่ถ้ามีดัชนีอยู่ท้ายเล่ม คุณเปิดไปดูตัวอักษร O แล้วหาคำที่ต้องการได้ทันที ซึ่งคือการทำ Index Scan ที่มีความซับซ้อนเพียง O(log n)

ตัวเลขจริงสำหรับ Table ที่มี 10 ล้าน Records ถ้าไม่มี Index อาจต้อง Scan ทั้ง 10 ล้าน Records ใช้เวลา 5-30 วินาที แต่ถ้ามี B-tree Index จะต้องเข้าถึงเพียงประมาณ 23 Nodes (log2 ของ 10 ล้าน) ใช้เวลาไม่ถึง 1 มิลลิวินาที นี่คือพลังของ Indexing ที่สามารถเปลี่ยน Query ที่ช้าจนใช้งานไม่ได้ให้กลายเป็น Query ที่เร็วจนแทบไม่รู้สึก

B-tree Index ทำงานอย่างไร?

B-tree (Balanced Tree) เป็นโครงสร้างข้อมูลที่ Database ส่วนใหญ่ใช้เป็นค่าเริ่มต้นสำหรับ Index เป็นต้นไม้แบบ Self-balancing ที่ทุก Leaf Node อยู่ในระดับเดียวกัน ทำให้การค้นหาใช้เวลาคงที่เสมอ ไม่ว่าจะหาข้อมูลที่อยู่ตำแหน่งไหนก็ตาม

โครงสร้างของ B-tree

                    [50 | 80]                    ← Root Node
                   /    |    \
          [20|35]    [60|70]    [90|95]           ← Internal Nodes
         /  |  \    /  |  \    /  |  \
       [10] [25] [40] [55] [65] [75] [85] [92] [98]  ← Leaf Nodes
        ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓
      Data  Data Data Data Data Data Data Data Data   ← Actual Rows

การค้นหาค่า 65: เริ่มจาก Root Node เห็นว่า 65 อยู่ระหว่าง 50 กับ 80 จึงไปที่ Child ตรงกลาง (Internal Node [60|70]) เห็นว่า 65 อยู่ระหว่าง 60 กับ 70 จึงไปที่ Child ตรงกลาง ซึ่งก็คือ Leaf Node [65] ที่มี Pointer ชี้ไปยังข้อมูลจริงใน Table รวมทั้งหมดแค่ 3 Steps ไม่ว่า Table จะมีกี่ล้าน Records

B-tree vs B+tree: Database ส่วนใหญ่ใช้ B+tree จริงๆ ไม่ใช่ B-tree ธรรมดา ความแตกต่างคือ B+tree เก็บข้อมูลจริงเฉพาะที่ Leaf Nodes และ Leaf Nodes เชื่อมกันเป็น Linked List ทำให้ Range Query (เช่น WHERE age BETWEEN 20 AND 30) เร็วมากเพราะแค่ไล่ตาม Linked List ไม่ต้องกลับขึ้นไป Root

ประเภทของ Index

PostgreSQL Index Types

ประเภทใช้เมื่อตัวอย่าง
B-treeเปรียบเทียบ =, <, >, BETWEEN, ORDER BY (ค่าเริ่มต้น)เกือบทุก Column ที่ใช้ใน WHERE
Hashเปรียบเทียบ = เท่านั้น (เร็วกว่า B-tree สำหรับ Equality)Lookup by exact value
GINFull-text search, Array, JSONB containsค้นหาข้อความ, JSONB field
GiSTGeometric data, Full-text search, Range typesค้นหาตำแหน่งใกล้เคียง (PostGIS)
BRINข้อมูลที่เรียงตามลำดับธรรมชาติ (เช่น timestamp)Log table ที่ Insert ตามเวลา

MySQL Index Types

ประเภทใช้เมื่อหมายเหตุ
B-treeค่าเริ่มต้น ใช้ได้ทั่วไปInnoDB ใช้ B+tree จริงๆ
Full-textค้นหาข้อความ MATCH AGAINSTรองรับ Natural Language Search
Spatialข้อมูลภูมิศาสตร์ (R-tree)ใช้กับ GEOMETRY types

การสร้าง Index แบบต่างๆ

Single Column Index

-- PostgreSQL / MySQL
CREATE INDEX idx_users_email ON users (email);

-- ใช้เมื่อ Query WHERE clause ใช้ Column เดียว
SELECT * FROM users WHERE email = 'test@example.com';

Composite Index (Multi-Column)

-- สร้าง Composite Index
CREATE INDEX idx_orders_user_date ON orders (user_id, created_at);

-- Query ที่ใช้ Index ได้ ✓
SELECT * FROM orders WHERE user_id = 123;
SELECT * FROM orders WHERE user_id = 123 AND created_at > '2026-01-01';
SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at;

-- Query ที่ใช้ Index ไม่ได้ ✗
SELECT * FROM orders WHERE created_at > '2026-01-01';
-- เพราะ created_at ไม่ใช่ Column แรกใน Index
Leftmost Prefix Rule: Composite Index (A, B, C) สามารถใช้ได้กับ Query ที่ค้นหาด้วย (A), (A, B) หรือ (A, B, C) แต่ไม่สามารถใช้ได้กับ (B), (C) หรือ (B, C) ลำดับของ Column ใน Composite Index สำคัญมากและส่งผลต่อประสิทธิภาพโดยตรง

Partial Index (PostgreSQL)

-- สร้าง Index เฉพาะ Records ที่ตรงเงื่อนไข
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';

-- Index นี้เล็กกว่า Full Index มาก
-- ใช้ได้กับ Query:
SELECT * FROM orders WHERE status = 'pending' AND created_at > '2026-01-01';

-- ไม่ใช้กับ:
SELECT * FROM orders WHERE status = 'completed' AND created_at > '2026-01-01';

Covering Index (Index Only Scan)

-- PostgreSQL: INCLUDE columns ที่ไม่ต้อง Search แต่ต้อง Return
CREATE INDEX idx_users_email_covering ON users (email) INCLUDE (name, phone);

-- Query นี้จะเป็น Index Only Scan (ไม่ต้องอ่าน Table เลย)
SELECT name, phone FROM users WHERE email = 'test@example.com';

-- MySQL: ใส่ทุก Column ที่ Query ต้องการใน Index
CREATE INDEX idx_users_email_name ON users (email, name, phone);
-- Covering Index ใน MySQL ต้องรวม SELECT columns เข้าไปด้วย

Unique Index

-- สร้าง Unique Index (ป้องกันข้อมูลซ้ำ)
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);

-- Partial Unique Index — Unique เฉพาะ Active Records
CREATE UNIQUE INDEX idx_users_active_email ON users (email)
WHERE deleted_at IS NULL;

EXPLAIN / EXPLAIN ANALYZE Deep Dive

EXPLAIN เป็นเครื่องมือที่สำคัญที่สุดสำหรับการ Optimize Query เพราะมันบอกว่า Database วางแผนจะ Execute Query อย่างไร ใช้ Index หรือไม่ และแต่ละ Step ใช้เวลาเท่าไหร่

PostgreSQL EXPLAIN

-- ดูแค่ Plan (ไม่ Execute จริง)
EXPLAIN SELECT * FROM orders WHERE user_id = 123;

-- ดู Plan + Execute จริง (ได้เวลาจริง)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;

-- ดู Plan แบบละเอียดพร้อม Buffer Statistics
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';

-- Output ตัวอย่าง:
-- Index Scan using idx_orders_user_id on orders
--   Index Cond: (user_id = 123)
--   Filter: (status = 'pending')
--   Rows Removed by Filter: 15
--   Buffers: shared hit=4
--   Planning Time: 0.085 ms
--   Execution Time: 0.042 ms

MySQL EXPLAIN

-- MySQL EXPLAIN
EXPLAIN SELECT * FROM orders WHERE user_id = 123;

-- MySQL EXPLAIN ANALYZE (MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;

-- ดูด้วย FORMAT=JSON สำหรับรายละเอียดเพิ่ม
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 123;

อ่าน Execution Plan ให้เป็น

การอ่าน Execution Plan เป็นทักษะสำคัญที่แยก DBA มือสมัครเล่นออกจากมืออาชีพ มาดูประเภทของ Scan แต่ละแบบ

Scan Types ใน PostgreSQL

Scan Typeคำอธิบายดี/ไม่ดี
Seq Scanอ่านทุก Row ใน Table ตั้งแต่ต้นจนจบไม่ดี สำหรับ Table ใหญ่
Index Scanใช้ Index หา Row แล้วไปอ่าน Tableดี สำหรับ Query ที่ Return น้อย Row
Index Only Scanใช้ Index อย่างเดียว ไม่ต้องอ่าน Tableดีมาก เร็วที่สุด
Bitmap Index Scanสร้าง Bitmap จาก Index แล้วอ่าน Table เป็น Blockดี สำหรับ Query ที่ Return หลาย Row
-- Seq Scan (ไม่มี Index หรือ Index ไม่ถูกใช้)
EXPLAIN SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
-- Seq Scan on users  (cost=0.00..25000.00 rows=1 width=120)
--   Filter: (lower(email) = 'test@example.com')
-- แก้: สร้าง Expression Index
CREATE INDEX idx_users_email_lower ON users (LOWER(email));

-- Index Scan (ใช้ Index + อ่าน Table)
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- Index Scan using idx_users_email on users  (cost=0.42..8.44 rows=1 width=120)
--   Index Cond: (email = 'test@example.com')

-- Index Only Scan (ใช้ Index อย่างเดียว เร็วสุด)
EXPLAIN SELECT email FROM users WHERE email = 'test@example.com';
-- Index Only Scan using idx_users_email on users  (cost=0.42..4.44 rows=1 width=40)
--   Index Cond: (email = 'test@example.com')

Common Slow Query Patterns

มาดูรูปแบบ Query ที่พบบ่อยว่าทำให้ช้า พร้อมวิธีแก้ไขแต่ละกรณี

1. Missing Index

-- ช้า: ไม่มี Index บน user_id
SELECT * FROM orders WHERE user_id = 123;
-- Seq Scan on orders  (cost=0.00..500000.00 rows=500 ...)
-- Execution Time: 2500ms

-- แก้: สร้าง Index
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- Index Scan using idx_orders_user_id  (cost=0.42..50.00 rows=500 ...)
-- Execution Time: 0.5ms

2. Wrong Index (ลำดับ Column ผิด)

-- มี Index (status, user_id) แต่ Query ค้นหาด้วย user_id ก่อน
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
-- Index ถูกใช้แบบไม่เต็มประสิทธิภาพ

-- แก้: สร้าง Index ที่ลำดับ Column ตรงกับ Query
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
-- หลักการ: Column ที่มี High Selectivity (กรองได้เยอะ) ไว้ก่อน

3. N+1 Query Problem

-- ช้ามาก: ดึง Users แล้ววนลูปดึง Orders ทีละคน
-- Query 1: SELECT * FROM users LIMIT 100;
-- Query 2-101: SELECT * FROM orders WHERE user_id = ?;  (x100 ครั้ง)
-- รวม 101 Queries!

-- แก้: ใช้ JOIN หรือ Subquery
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id IN (SELECT id FROM users LIMIT 100);
-- รวมแค่ 1-2 Queries

4. SELECT * (ดึงทุก Column)

-- ช้า: ดึงทุก Column แม้ต้องการแค่ 2 Column
SELECT * FROM users WHERE email = 'test@example.com';
-- ต้องอ่าน Table (Index Scan) เพราะ Index ไม่มีทุก Column

-- เร็ว: ระบุ Column ที่ต้องการ
SELECT id, name FROM users WHERE email = 'test@example.com';
-- ถ้ามี Covering Index จะเป็น Index Only Scan

5. ไม่ใส่ LIMIT

-- ช้า: Return ทั้งหมด 100,000 Records
SELECT * FROM logs WHERE created_at > '2026-01-01' ORDER BY created_at DESC;

-- เร็ว: ใส่ LIMIT
SELECT * FROM logs WHERE created_at > '2026-01-01'
ORDER BY created_at DESC LIMIT 50;
-- Database สามารถหยุดได้ทันทีเมื่อได้ 50 Records

6. Function บน Indexed Column

-- ช้า: ใช้ Function ทำให้ Index ไม่ถูกใช้
SELECT * FROM users WHERE YEAR(created_at) = 2026;  -- MySQL
SELECT * FROM users WHERE EXTRACT(YEAR FROM created_at) = 2026;  -- PostgreSQL

-- เร็ว: เขียนเป็น Range Query แทน
SELECT * FROM users WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- ใช้ B-tree Index ได้เต็มที่

Composite Index: ลำดับ Column สำคัญมาก

การเลือกลำดับ Column ใน Composite Index เป็นศิลปะที่ต้องเข้าใจหลักการ ไม่ใช่แค่ใส่ Column เข้าไปแล้วจะได้ผลดี

-- ตัวอย่าง: Table orders มี 10 ล้าน Records
-- user_id มี 100,000 Unique Values (Selectivity: 100)
-- status มี 5 Unique Values (Selectivity: 2,000,000)

-- Index A: (user_id, status) — ดีกว่า
-- ค้น user_id = 123 → เหลือ ~100 Records → กรอง status → ผลลัพธ์

-- Index B: (status, user_id) — แย่กว่าสำหรับ Query ที่มี user_id
-- ค้น status = 'pending' → เหลือ ~2,000,000 Records → กรอง user_id → ผลลัพธ์

-- หลักการ: Column ที่มี Selectivity สูง (กรองได้เยอะ) ไว้ก่อน
-- ยกเว้น: ถ้า Query ส่วนใหญ่ค้นด้วย status อย่างเดียว
--          ให้สร้าง Index แยกสำหรับ status

Composite Index สำหรับ ORDER BY

-- Query: SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC;

-- Index ที่ดี:
CREATE INDEX idx_orders_user_date_desc ON orders (user_id, created_at DESC);
-- Database ไม่ต้อง Sort เพราะ Index เรียงไว้แล้ว

-- Index ที่ไม่ดี:
CREATE INDEX idx_orders_date_user ON orders (created_at, user_id);
-- ต้อง Scan created_at ทั้งหมด แล้วค่อยกรอง user_id

Index Bloat และ Maintenance

เมื่อเวลาผ่านไป Index อาจ "บวม" (Bloat) ขึ้นเพราะ PostgreSQL ใช้ MVCC ที่เก็บ Dead Tuples ไว้ใน Index จนกว่า VACUUM จะทำความสะอาด ถ้าปล่อยไว้นาน Index จะใหญ่กว่าที่ควรจะเป็นมากและทำให้ Query ช้าลง

-- ตรวจสอบ Index Bloat (PostgreSQL)
SELECT
    schemaname || '.' || tablename AS table_name,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
    idx_scan AS times_used,
    idx_tup_read AS tuples_read
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;

-- REINDEX — สร้าง Index ใหม่ (Lock Table)
REINDEX INDEX idx_users_email;
REINDEX TABLE users;

-- REINDEX CONCURRENTLY (PostgreSQL 12+) — ไม่ Lock Table
REINDEX INDEX CONCURRENTLY idx_users_email;

-- pg_repack — Alternative ที่ดีกว่า REINDEX
-- ติดตั้ง: CREATE EXTENSION pg_repack;
-- ใช้: pg_repack -d mydb -t users

VACUUM และ ANALYZE

-- VACUUM — ทำความสะอาด Dead Tuples
VACUUM users;
VACUUM FULL users;  -- Reclaim พื้นที่ (Lock Table)

-- ANALYZE — อัปเดต Statistics สำหรับ Query Planner
ANALYZE users;

-- ทั้งสองอย่าง
VACUUM ANALYZE users;

-- ดู autovacuum settings
SHOW autovacuum;
SHOW autovacuum_vacuum_threshold;
SHOW autovacuum_analyze_threshold;

เมื่อไหร่ไม่ควรสร้าง Index

Index ไม่ใช่ยาวิเศษที่สร้างเท่าไหร่ก็ดี การสร้าง Index มากเกินไปมีผลเสียจริงๆ

-- ตรวจสอบ Index ที่ไม่ถูกใช้ (PostgreSQL)
SELECT
    schemaname || '.' || relname AS table_name,
    indexrelname AS index_name,
    pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
    idx_scan AS number_of_scans
FROM pg_stat_user_indexes ui
JOIN pg_index i ON ui.indexrelid = i.indexrelid
WHERE idx_scan = 0
  AND NOT indisunique
ORDER BY pg_relation_size(i.indexrelid) DESC;

-- พิจารณาลบ Index ที่ idx_scan = 0 มานานกว่า 30 วัน
-- DROP INDEX CONCURRENTLY idx_unused_index;

Index Monitoring Queries

PostgreSQL: pg_stat_user_indexes

-- ดู Index Usage Statistics
SELECT
    relname AS table_name,
    indexrelname AS index_name,
    idx_scan AS times_used,
    idx_tup_read AS rows_read_from_index,
    idx_tup_fetch AS rows_fetched_from_table,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;

-- ดู Table ที่มี Seq Scan มาก (อาจขาด Index)
SELECT
    relname AS table_name,
    seq_scan,
    seq_tup_read,
    idx_scan,
    CASE WHEN seq_scan + idx_scan > 0
         THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1)
         ELSE 0 END AS idx_scan_pct
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_tup_read DESC
LIMIT 20;

-- ดู Cache Hit Ratio
SELECT
    sum(heap_blks_read) AS heap_read,
    sum(heap_blks_hit) AS heap_hit,
    round(sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read))::numeric * 100, 2) AS cache_hit_ratio
FROM pg_statio_user_tables;

MySQL: Slow Query Log

-- เปิด Slow Query Log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- Query ที่ใช้เวลามากกว่า 1 วินาที
SET GLOBAL log_queries_not_using_indexes = 'ON';

-- ดู Slow Query Log Location
SHOW VARIABLES LIKE 'slow_query_log_file';

-- ใช้ mysqldumpslow วิเคราะห์ Log
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

-- ดู Index Usage Statistics (MySQL 8.0+)
SELECT
    OBJECT_SCHEMA,
    OBJECT_NAME,
    INDEX_NAME,
    COUNT_STAR AS times_used
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'mydb'
ORDER BY COUNT_STAR DESC;

Query Optimization Checklist

เมื่อเจอ Query ที่ช้า ให้ทำตาม Checklist นี้เป็นลำดับ

  1. EXPLAIN ANALYZE: รันก่อนเสมอ ดูว่าใช้ Scan Type อะไร ถ้าเห็น Seq Scan บน Table ใหญ่ แสดงว่าขาด Index
  2. ตรวจ WHERE Clause: Column ที่ใช้ใน WHERE มี Index หรือไม่ ถ้าใช้ Function บน Column ให้เขียนใหม่เป็น Range Query
  3. ตรวจ JOIN: Column ที่ใช้ JOIN มี Index หรือไม่ โดยเฉพาะ Foreign Key Column
  4. ลำดับ Composite Index: ตรวจว่า Column ที่มี High Selectivity อยู่ก่อน Column ที่มี Low Selectivity
  5. SELECT เฉพาะ Column ที่ต้องการ: หลีกเลี่ยง SELECT * เพื่อเปิดโอกาสให้เป็น Index Only Scan
  6. ใส่ LIMIT: ถ้า Query ไม่ต้องการทุก Row ให้ใส่ LIMIT เสมอ
  7. ตรวจ N+1: ดูใน Application Log ว่ามี Query ซ้ำๆ หลายร้อย Query ต่อ Request หรือไม่
  8. ดู Buffer/Cache Hit: ถ้า Cache Hit Ratio ต่ำกว่า 99% อาจต้องเพิ่ม shared_buffers

ORM-Generated Query Pitfalls

ORM เช่น Prisma, TypeORM, Sequelize, Django ORM หรือ ActiveRecord ช่วยให้เขียนโค้ดง่ายขึ้น แต่บ่อยครั้ง ORM สร้าง Query ที่ไม่มีประสิทธิภาพโดยที่คุณไม่รู้ตัว

# Python Django — N+1 Problem
# ช้า: ดึง Users แล้ว Access orders ทีละคน
users = User.objects.all()[:100]
for user in users:
    print(user.orders.count())  # 100 SQL Queries!

# เร็ว: ใช้ select_related / prefetch_related
users = User.objects.prefetch_related('orders').all()[:100]
# 2 SQL Queries เท่านั้น

# Node.js Prisma — Eager Loading
// ช้า:
const users = await prisma.user.findMany();
for (const user of users) {
  const orders = await prisma.order.findMany({ where: { userId: user.id } });
}

// เร็ว:
const users = await prisma.user.findMany({
  include: { orders: true }
});
เปิด Query Logging: ในทุก ORM ให้เปิด Query Log ใน Development เพื่อดู SQL ที่ ORM Generate ออกมา ใน Prisma ใช้ log: ['query'], ใน Django ใช้ django.db.connection.queries คุณจะตกใจว่า ORM สร้าง Query กี่ตัวต่อ Request

Connection Pooling Impact

นอกจาก Indexing แล้ว Connection Pooling ก็เป็นอีกปัจจัยสำคัญที่ส่งผลต่อ Query Performance โดยเฉพาะเมื่อ Application มีหลาย Concurrent Users

# PostgreSQL: ใช้ PgBouncer สำหรับ Connection Pooling
# pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = md5
pool_mode = transaction      # แนะนำสำหรับ Web App
max_client_conn = 1000       # Client connections
default_pool_size = 20       # Actual DB connections
min_pool_size = 5
reserve_pool_size = 5

# MySQL: ใช้ ProxySQL หรือ Built-in Connection Pool ของ ORM
# Prisma connection pooling
DATABASE_URL="postgresql://user:pass@localhost:6432/mydb?pgbouncer=true"

ทำไม Connection Pooling ช่วย: การสร้าง Database Connection ใหม่แต่ละครั้งใช้เวลา 20-50 มิลลิวินาที ถ้ามี 100 Requests พร้อมกัน แต่ละ Request สร้าง Connection ใหม่ Database จะต้อง Handle 100 Connections พร้อมกัน ซึ่งใช้ Memory มากและอาจถึง Max Connections ได้ Connection Pool จะเก็บ Connection ที่เปิดไว้แล้วนำกลับมาใช้ซ้ำ ทำให้ไม่ต้องสร้างใหม่ทุกครั้ง

PostgreSQL vs MySQL: Index Features Comparison

FeaturePostgreSQLMySQL (InnoDB)
Partial Indexรองรับ (WHERE clause)ไม่รองรับ
Expression Indexรองรับ (LOWER(col))รองรับ (MySQL 8.0+)
Covering IndexINCLUDE syntaxใส่ Column ใน Index
BRIN Indexรองรับไม่รองรับ
GIN/GiST Indexรองรับไม่รองรับ (มี Full-text แยก)
Concurrent Index CreationCREATE INDEX CONCURRENTLYALTER TABLE ... ALGORITHM=INPLACE
Index Only Scanรองรับ (ต้อง VACUUM ก่อน)รองรับ (Covering Index)

Advanced: BRIN Index สำหรับ Time-Series Data

-- BRIN Index เหมาะสำหรับ Table ที่ข้อมูลเรียงตามลำดับธรรมชาติ
-- เช่น Log Table ที่ INSERT ตามเวลา

-- Table logs มี 100 ล้าน Records
CREATE TABLE logs (
    id BIGSERIAL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    level TEXT,
    message TEXT
);

-- B-tree Index: ~2 GB
CREATE INDEX idx_logs_created_btree ON logs (created_at);

-- BRIN Index: ~2 MB (เล็กกว่า 1000 เท่า!)
CREATE INDEX idx_logs_created_brin ON logs USING BRIN (created_at);

-- BRIN ทำงานได้ดีเพราะ created_at เรียงตามลำดับ Insert
-- แต่ถ้าข้อมูลถูก UPDATE ให้ created_at สลับลำดับ BRIN จะทำงานไม่ดี

สรุป

Database Indexing เป็นทักษะพื้นฐานที่สำคัญที่สุดอย่างหนึ่งสำหรับนักพัฒนา Backend ความแตกต่างระหว่าง Query ที่ใช้เวลา 5 วินาที กับ Query ที่ใช้เวลา 5 มิลลิวินาที มักอยู่ที่ว่ามี Index ที่เหมาะสมหรือไม่ และ Query ถูกเขียนในรูปแบบที่ Database สามารถใช้ Index ได้หรือไม่

เริ่มต้นด้วยการเปิด Slow Query Log เพื่อหา Query ที่ช้า จากนั้นใช้ EXPLAIN ANALYZE วิเคราะห์แต่ละ Query แล้วสร้าง Index ที่ตรงกับ Access Pattern ของ Application ที่สำคัญคือ ตรวจสอบเป็นประจำว่า Index ที่สร้างไว้ยังถูกใช้งานอยู่หรือไม่ และลบ Index ที่ไม่จำเป็นออกเพื่อลดภาระของ Write Operations การทำ Query Optimization เป็นกระบวนการที่ต้องทำอย่างต่อเนื่อง ไม่ใช่ทำครั้งเดียวแล้วจบ


Back to Blog | iCafe Forex | SiamLanCard | Siam2R