Home > Blog > tech

Database Migration คืออะไร? สอนจัดการ Schema Migration สำหรับ Backend Developer 2026

database migration schema management guide
Database Migration คืออะไร? สอนจัดการ Schema Migration สำหรับ Backend Developer 2026
2026-04-08 | tech | 3500 words

ถ้าคุณเคยทำงานกับ Database ในโปรเจกต์จริง คุณต้องเคยเจอปัญหาเหล่านี้ เช่น Schema ใน Development ไม่ตรงกับ Production หรือทีมเพิ่ม Column ใหม่แล้วลืมบอกคนอื่น หรือต้อง Rollback การเปลี่ยนแปลง Database แต่ไม่รู้ว่าเปลี่ยนอะไรไปบ้าง ปัญหาเหล่านี้แก้ได้ด้วย Database Migration

บทความนี้จะสอนทุกอย่างเกี่ยวกับ Database Migration ตั้งแต่แนวคิดพื้นฐาน เครื่องมือยอดนิยมในแต่ละภาษา ไปจนถึง Best Practices ที่ใช้ใน Production จริง รวมถึงเทคนิค Zero-downtime Migration ที่หลายคนมองข้าม

Database Migration คืออะไร?

Database Migration คือกระบวนการจัดการการเปลี่ยนแปลง Database Schema อย่างเป็นระบบ เปรียบเทียบง่ายๆ ก็เหมือน Git สำหรับ Database ถ้า Git ติดตามการเปลี่ยนแปลงของ Source Code แล้ว Migration ก็ติดตามการเปลี่ยนแปลงของ Database Structure

แต่ละ Migration คือไฟล์ที่บรรจุคำสั่ง SQL หรือ Code ที่อธิบายว่าต้องเปลี่ยนแปลง Database อย่างไร โดยทั่วไปจะมี 2 ส่วน คือ Up (ทำการเปลี่ยนแปลง) และ Down (ย้อนกลับการเปลี่ยนแปลง)

-- Migration: 001_create_users_table.sql

-- Up: สร้างตาราง users
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);

-- Down: ลบตาราง users
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;

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

1. Version Control สำหรับ Database: เหมือนกับที่ Git ช่วยติดตามว่า Source Code เปลี่ยนอะไรบ้าง Migration ช่วยติดตามว่า Database Schema เปลี่ยนอะไรบ้าง ใครเปลี่ยน เมื่อไหร่ และทำไม ทำให้สามารถ Audit ได้ทุกเมื่อ

2. ทำงานเป็นทีมได้: เมื่อทีมมีหลายคนทำงานพร้อมกัน แต่ละคนอาจต้องแก้ไข Database Schema สำหรับ Feature ของตัวเอง Migration ช่วยให้ทุกคนสามารถเพิ่ม Schema Changes ของตัวเอง แล้วระบบจะรัน Migration ตามลำดับให้อัตโนมัติ

3. Reproducible Environment: ทุกคนในทีมสามารถสร้าง Database ที่เหมือนกันทุกประการได้ แค่รันชุด Migration ทั้งหมดจากศูนย์ ไม่ต้องส่ง SQL Dump ให้กัน ไม่ต้องถามกันว่า "ใครเพิ่ม Column นี้ตอนไหร่"

4. Safe Rollback: ถ้า Migration ไหนมีปัญหา สามารถ Rollback กลับไปเวอร์ชันก่อนหน้าได้อย่างปลอดภัย เพราะมี Down Migration ที่อธิบายวิธีย้อนกลับ

5. CI/CD Integration: Migration สามารถรันอัตโนมัติเป็นส่วนหนึ่งของ Deployment Pipeline ทำให้ Database Schema อัพเดทพร้อมกับ Application Code ทุกครั้ง

Migration File Naming Convention

การตั้งชื่อไฟล์ Migration เป็นสิ่งสำคัญมาก เพราะ Migration Tool ส่วนใหญ่จะรันไฟล์ตามลำดับชื่อ มี Convention หลักๆ 2 แบบ

แบบ Sequential Number

migrations/
├── 001_create_users_table.sql
├── 002_create_products_table.sql
├── 003_add_email_to_users.sql
├── 004_create_orders_table.sql
└── 005_add_index_on_orders_date.sql

แบบ Timestamp

migrations/
├── 20260101120000_create_users_table.sql
├── 20260115143000_create_products_table.sql
├── 20260201090000_add_email_to_users.sql
├── 20260208160000_create_orders_table.sql
└── 20260215110000_add_index_on_orders_date.sql
แนะนำ: ใช้ Timestamp-based naming สำหรับทีมที่มีหลายคนทำงานพร้อมกัน เพราะจะไม่ชนกันเหมือน Sequential Number ที่อาจมีคนใช้เลขเดียวกัน Flyway ใช้ V1, V2 (sequential) ส่วน Alembic และ Prisma ใช้ Timestamp

เครื่องมือ Migration แบ่งตามภาษา

Java: Flyway

Flyway เป็น Migration Tool ที่นิยมที่สุดในโลก Java รองรับ SQL-based migrations และ Java-based migrations ทำงานร่วมกับ Spring Boot ได้อย่างดีเยี่ยม

-- V1__Create_users_table.sql (Flyway naming: V + version + __ + description)
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- V2__Create_products_table.sql
CREATE TABLE products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    category_id BIGINT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- V3__Add_phone_to_users.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
# รัน Flyway
flyway migrate           # รัน Pending migrations
flyway info              # ดูสถานะ Migration
flyway validate          # ตรวจสอบ Migration files
flyway repair            # แก้ไข Migration metadata
flyway clean             # ลบทุกอย่าง (อย่าใช้ใน Production!)
flyway baseline          # ตั้ง Baseline สำหรับ Database ที่มีอยู่แล้ว
# application.yml (Spring Boot + Flyway)
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: secret

Java: Liquibase

Liquibase เป็นอีกทางเลือกที่นิยมสำหรับ Java รองรับการเขียน Migration ในรูปแบบ XML, YAML, JSON หรือ SQL ทำให้ยืดหยุ่นกว่า Flyway

# changelog.yaml (Liquibase)
databaseChangeLog:
  - changeSet:
      id: 1
      author: somchai
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: email
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: name
                  type: VARCHAR(100)
      rollback:
        - dropTable:
            tableName: users

  - changeSet:
      id: 2
      author: somchai
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: VARCHAR(20)
      rollback:
        - dropColumn:
            tableName: users
            columnName: phone

Python: Alembic (with SQLAlchemy)

Alembic เป็น Migration Tool มาตรฐานของ Python ที่ทำงานคู่กับ SQLAlchemy ORM สามารถ Auto-generate Migration จาก Model Changes ได้

# ติดตั้ง
pip install alembic sqlalchemy

# เริ่มต้น
alembic init migrations

# สร้าง Migration อัตโนมัติจาก Model changes
alembic revision --autogenerate -m "create users table"

# รัน Migration
alembic upgrade head        # อัพเดทไปเวอร์ชันล่าสุด
alembic upgrade +1          # อัพเดท 1 step
alembic downgrade -1        # ย้อนกลับ 1 step
alembic downgrade base      # ย้อนกลับทั้งหมด
alembic history             # ดูประวัติ
alembic current             # ดูเวอร์ชันปัจจุบัน
# migrations/versions/20260408_create_users.py
"""create users table

Revision ID: a1b2c3d4e5f6
Revises:
Create Date: 2026-04-08 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa

revision = 'a1b2c3d4e5f6'
down_revision = None

def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('email', sa.String(255), nullable=False, unique=True),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('password_hash', sa.String(255), nullable=False),
        sa.Column('is_active', sa.Boolean(), default=True),
        sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
    )
    op.create_index('idx_users_email', 'users', ['email'])

def downgrade():
    op.drop_index('idx_users_email', 'users')
    op.drop_table('users')

JavaScript/TypeScript: Prisma Migrate

Prisma Migrate เป็นส่วนหนึ่งของ Prisma ORM ที่กำลังได้รับความนิยมอย่างมากในปี 2026 ใช้ Prisma Schema Language ในการ Define Schema แล้ว Generate Migration อัตโนมัติ

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())

  @@index([authorId])
}
# Prisma Migrate Commands
npx prisma migrate dev --name create_users    # สร้าง + รัน Migration (dev)
npx prisma migrate deploy                     # รัน Pending migrations (production)
npx prisma migrate reset                      # Reset database + รัน migrations ใหม่
npx prisma migrate status                     # ดูสถานะ Migration
npx prisma db push                            # Push schema โดยไม่สร้าง migration file
npx prisma generate                           # Generate Prisma Client

JavaScript/TypeScript: Drizzle Kit

Drizzle ORM เป็น ORM ที่กำลังมาแรงในปี 2026 โดดเด่นเรื่อง Type Safety ที่ดีเยี่ยมและไม่ Abstract SQL จนเกินไป Drizzle Kit เป็น Migration Tool ของมัน

// drizzle/schema.ts
import { pgTable, serial, varchar, boolean, timestamp, integer, text } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  name: varchar("name", { length: 100 }).notNull(),
  passwordHash: varchar("password_hash", { length: 255 }).notNull(),
  isActive: boolean("is_active").default(true),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 200 }).notNull(),
  content: text("content"),
  published: boolean("published").default(false),
  authorId: integer("author_id").references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});
# Drizzle Kit Commands
npx drizzle-kit generate    # Generate migration จาก schema changes
npx drizzle-kit migrate     # รัน migrations
npx drizzle-kit push        # Push schema โดยตรง (dev)
npx drizzle-kit studio      # เปิด Database GUI
npx drizzle-kit check       # ตรวจสอบ schema consistency

JavaScript: Knex.js

Knex.js เป็น SQL Query Builder ที่มี Migration System ในตัว เหมาะสำหรับคนที่ต้องการเขียน Migration เป็น JavaScript/TypeScript โดยไม่ต้องใช้ ORM เต็มตัว

// migrations/20260408100000_create_users.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.createTable("users", (table) => {
    table.increments("id").primary();
    table.string("email", 255).notNull().unique();
    table.string("name", 100).notNull();
    table.string("password_hash", 255).notNull();
    table.boolean("is_active").defaultTo(true);
    table.timestamps(true, true);
    table.index(["email"]);
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists("users");
}
# Knex Migration Commands
npx knex migrate:make create_users     # สร้างไฟล์ migration ใหม่
npx knex migrate:latest                # รัน pending migrations
npx knex migrate:rollback              # ย้อนกลับ batch ล่าสุด
npx knex migrate:status                # ดูสถานะ

Go: golang-migrate

-- 000001_create_users.up.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 000001_create_users.down.sql
DROP TABLE IF EXISTS users;
# golang-migrate Commands
migrate -path ./migrations -database "postgres://user:pass@localhost/db?sslmode=disable" up
migrate -path ./migrations -database "..." down 1
migrate -path ./migrations -database "..." version
migrate -path ./migrations -database "..." force 1

Go: Atlas (Modern Alternative)

Atlas เป็น Migration Tool รุ่นใหม่จาก Ariga ที่ใช้ Declarative Approach แทน Imperative คุณแค่ Define Schema ที่ต้องการ แล้ว Atlas จะคำนวณ Migration ให้อัตโนมัติ

# schema.hcl (Atlas Schema Language)
table "users" {
  schema = schema.public
  column "id" {
    type = bigserial
  }
  column "email" {
    type = varchar(255)
    null = false
  }
  column "name" {
    type = varchar(100)
    null = false
  }
  primary_key {
    columns = [column.id]
  }
  index "idx_users_email" {
    columns = [column.email]
    unique  = true
  }
}
# Atlas Commands
atlas schema inspect -u "postgres://..."          # Inspect current schema
atlas schema diff --from file://schema.hcl --to "postgres://..."  # Generate diff
atlas migrate diff create_users --to file://schema.hcl             # Create migration
atlas migrate apply -u "postgres://..."            # Apply migrations

เปรียบเทียบ Migration Tools

ToolภาษารูปแบบAuto-generateRollbackจุดเด่น
FlywayJavaSQL / Javaไม่มีPro onlySimple, ใช้ง่าย
LiquibaseJavaXML/YAML/SQLได้ได้ยืดหยุ่นสูง
AlembicPythonPythonได้ได้SQLAlchemy integration
Prisma MigrateJS/TSPrisma Schemaได้ได้Type-safe, DX ดี
Drizzle KitJS/TSTypeScriptได้ได้SQL-like, lightweight
Knex.jsJS/TSJS/TSไม่มีได้Query builder + migration
golang-migrateGoSQLไม่มีได้CLI + Library
AtlasGoHCL / SQLได้ได้Declarative, modern

Up/Down Migration Pattern

Migration ส่วนใหญ่ใช้ Pattern Up/Down (หรือ Forward/Backward) โดย Up คือคำสั่งที่ทำการเปลี่ยนแปลง และ Down คือคำสั่งที่ย้อนกลับ

ตัวอย่าง Up/Down ที่ดี

-- Up: เพิ่ม Column ใหม่
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);

-- Down: ลบ Column ที่เพิ่ม
ALTER TABLE users DROP COLUMN avatar_url;
-- Up: เปลี่ยนชื่อ Column
ALTER TABLE users RENAME COLUMN name TO full_name;

-- Down: เปลี่ยนชื่อกลับ
ALTER TABLE users RENAME COLUMN full_name TO name;
-- Up: เพิ่ม Index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- Down: ลบ Index
DROP INDEX idx_orders_user_date;

กรณีที่ Down Migration เป็นไปไม่ได้

บาง Migration ไม่สามารถ Rollback ได้ เช่น การลบ Column ที่มีข้อมูลอยู่ ถ้า Rollback ข้อมูลก็หายไปแล้ว หรือการเปลี่ยน Data Type ที่ทำให้ข้อมูลเสียหาย ในกรณีนี้ ควรเขียน Down Migration ที่ throw error เพื่อป้องกันการ Rollback โดยไม่ตั้งใจ

# Python/Alembic — Migration ที่ Rollback ไม่ได้
def upgrade():
    op.drop_column('users', 'legacy_field')

def downgrade():
    raise Exception("Cannot rollback: data in 'legacy_field' has been permanently deleted")

Migration Best Practices

1. Small, Focused Changes

แต่ละ Migration ควรทำสิ่งเดียว อย่ารวมหลายเรื่องในไฟล์เดียว เช่น อย่าสร้าง 5 ตารางในไฟล์เดียว แยกเป็น 5 ไฟล์ ทำให้ Debug ง่ายกว่า Rollback ง่ายกว่า และ Review ง่ายกว่า

# แบบไม่ดี — ยัดทุกอย่างในไฟล์เดียว
001_create_everything.sql

# แบบดี — แยกแต่ละเรื่อง
001_create_users_table.sql
002_create_products_table.sql
003_create_orders_table.sql
004_add_index_on_users_email.sql

2. Backward Compatible Changes

Migration ที่ดีควร Backward Compatible หมายความว่า Application เวอร์ชันเก่าควรยังทำงานได้กับ Schema ใหม่ สิ่งนี้สำคัญมากสำหรับ Zero-downtime Deployment

-- แบบดี: เพิ่ม Column ใหม่ที่มี Default value
ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL;
-- Code เก่าไม่สนใจ Column ใหม่ — ยังทำงานได้

-- แบบไม่ดี: เปลี่ยนชื่อ Column ทันที
ALTER TABLE users RENAME COLUMN name TO full_name;
-- Code เก่าที่ใช้ Column "name" จะพังทันที!

3. Never Modify Applied Migrations

อย่าแก้ไข Migration ที่รันไปแล้วเด็ดขาด ถ้าต้องการเปลี่ยนแปลง ให้สร้าง Migration ใหม่ เพราะ Migration ที่รันแล้วถูกบันทึก Checksum ไว้ ถ้าแก้ไข Tool จะแจ้ง Error

4. Test Migrations

ทดสอบ Migration ทุกครั้งก่อน Deploy โดยเฉพาะ Down Migration ให้ทดสอบ Up แล้ว Down แล้ว Up อีกครั้ง เพื่อให้แน่ใจว่าทำงานได้ทั้งสองทิศทาง

# ทดสอบ Migration Cycle
alembic upgrade head     # Up ทั้งหมด
alembic downgrade base   # Down ทั้งหมด
alembic upgrade head     # Up อีกครั้ง — ต้องผ่าน!

5. Data Migration แยกจาก Schema Migration

แยก Schema Migration (เปลี่ยนโครงสร้าง) ออกจาก Data Migration (เปลี่ยนข้อมูล) เพราะ Data Migration มักจะช้าและซับซ้อนกว่า

# แบบดี — แยก Schema กับ Data
003_add_full_name_column.sql         # Schema: เพิ่ม Column
004_populate_full_name.sql           # Data: คัดลอกข้อมูล
005_drop_first_last_name.sql         # Schema: ลบ Column เก่า

# แบบไม่ดี — รวมกัน
003_merge_name_columns.sql           # ทั้ง Schema + Data ในไฟล์เดียว

Zero-Downtime Migration Strategies

สำหรับ Production System ที่ต้องทำงาน 24/7 การเปลี่ยนแปลง Database Schema โดยไม่ทำให้ Service หยุดชะงัก เป็นเรื่องท้าทายมาก มีหลายเทคนิคที่ใช้กัน

Expand-Contract Pattern

เป็นเทคนิคที่นิยมที่สุดสำหรับ Zero-downtime Migration แบ่งการเปลี่ยนแปลงออกเป็น 3 ขั้นตอน

ขั้นตอนที่ 1: Expand (เพิ่มสิ่งใหม่)

-- เพิ่ม Column ใหม่ ยังไม่ลบของเก่า
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);

-- Application Code: เขียนข้อมูลลงทั้ง name และ full_name
-- (Dual Write)

ขั้นตอนที่ 2: Migrate Data

-- คัดลอกข้อมูลจาก Column เก่าไป Column ใหม่
UPDATE users SET full_name = name WHERE full_name IS NULL;

-- Application Code: เปลี่ยนมาอ่านจาก full_name แทน name
-- (ยังคง Dual Write)

ขั้นตอนที่ 3: Contract (ลบของเก่า)

-- Application Code: หยุดเขียน name Column
-- รอจนมั่นใจว่า Code ใหม่ Deploy ครบทุก Instance

-- ลบ Column เก่า
ALTER TABLE users DROP COLUMN name;
หลักสำคัญ: แต่ละขั้นตอนของ Expand-Contract ควรเป็น Migration แยกกัน และ Deploy แยกกัน อย่าทำทุกขั้นตอนในครั้งเดียว เพราะถ้ามีปัญหาจะ Rollback ได้ยาก

Blue-Green for Database

สำหรับ Database ที่ต้อง Migrate ขนาดใหญ่ ใช้เทคนิค Blue-Green โดยสร้าง Database Copy (Green) ทำ Migration บน Green แล้ว Switch Traffic มาที่ Green เมื่อพร้อม

ข้อจำกัดคือต้องจัดการ Data Sync ระหว่าง Blue กับ Green ในช่วงเปลี่ยนผ่าน ซึ่งซับซ้อนมาก ใน PostgreSQL สามารถใช้ Logical Replication ช่วยได้

Handling Long-Running Migrations

Migration บางอย่างใช้เวลานานมาก เช่น การเพิ่ม Index บนตารางที่มีข้อมูลหลายล้าน Row หรือการ ALTER TABLE ที่ต้อง Rewrite ทั้งตาราง มีเทคนิคจัดการดังนี้

PostgreSQL: CREATE INDEX CONCURRENTLY

-- แบบปกติ — Lock ตาราง ไม่สามารถ Write ได้
CREATE INDEX idx_orders_date ON orders(created_at);

-- แบบ Concurrent — ไม่ Lock ตาราง แต่ใช้เวลานานกว่า
CREATE INDEX CONCURRENTLY idx_orders_date ON orders(created_at);

-- หมายเหตุ: CONCURRENTLY ไม่สามารถรันภายใน Transaction ได้
-- ต้องตั้งค่า Migration Tool ให้ไม่ Wrap ใน Transaction

MySQL: Online DDL

-- MySQL 8.0+ รองรับ Online DDL ที่ไม่ Lock ตาราง
ALTER TABLE orders ADD INDEX idx_date(created_at), ALGORITHM=INPLACE, LOCK=NONE;

-- เพิ่ม Column
ALTER TABLE users ADD COLUMN avatar VARCHAR(500), ALGORITHM=INSTANT;

Batch Data Migration

# Python — Migrate ข้อมูลแบบ Batch เพื่อไม่ให้ Lock นาน
def upgrade():
    conn = op.get_bind()
    batch_size = 1000
    offset = 0

    while True:
        result = conn.execute(
            text(f"SELECT id FROM users WHERE full_name IS NULL LIMIT {batch_size} OFFSET {offset}")
        )
        rows = result.fetchall()
        if not rows:
            break

        ids = [row[0] for row in rows]
        conn.execute(
            text("UPDATE users SET full_name = name WHERE id = ANY(:ids)"),
            {"ids": ids}
        )
        offset += batch_size
        print(f"Migrated {offset} rows...")

Migration in CI/CD Pipeline

การรัน Migration อัตโนมัติเป็นส่วนหนึ่งของ Deployment Pipeline เป็นสิ่งจำเป็นสำหรับทีมที่ Deploy บ่อย

# GitHub Actions — Migration Pipeline
name: Deploy with Migration
on:
  push:
    branches: [main]

jobs:
  migrate-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Migration on Staging
        run: |
          npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}

      - name: Run Tests Against Migrated DB
        run: npm test

      - name: Run Migration on Production
        run: |
          npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}

      - name: Deploy Application
        run: |
          # Deploy app after migration succeeds
          ./deploy.sh
Best Practice: รัน Migration ก่อน Deploy Application ใหม่ เพื่อให้ Database Schema พร้อมก่อนที่ Code ใหม่จะเริ่มทำงาน และ Migration ต้อง Backward Compatible เสมอ เพื่อให้ Code เก่ายังทำงานได้ขณะที่ Migration กำลังรัน

Multi-Tenant Migration

สำหรับ Application ที่มีหลาย Tenant (ลูกค้าหลายราย) การจัดการ Migration ซับซ้อนขึ้น มี 2 แนวทางหลัก

Shared Database — แยก Schema

-- สร้าง Schema สำหรับแต่ละ Tenant
CREATE SCHEMA tenant_001;
CREATE SCHEMA tenant_002;

-- Migration ต้องรันในทุก Schema
DO $$
DECLARE
    tenant_schema TEXT;
BEGIN
    FOR tenant_schema IN
        SELECT schema_name FROM information_schema.schemata
        WHERE schema_name LIKE 'tenant_%'
    LOOP
        EXECUTE format('ALTER TABLE %I.users ADD COLUMN avatar VARCHAR(500)', tenant_schema);
    END LOOP;
END $$;

Database Per Tenant

# Python — รัน Migration ทุก Tenant Database
import asyncio
from alembic import command
from alembic.config import Config

async def migrate_all_tenants():
    tenants = await get_all_tenant_configs()

    for tenant in tenants:
        alembic_cfg = Config("alembic.ini")
        alembic_cfg.set_main_option("sqlalchemy.url", tenant.database_url)

        try:
            command.upgrade(alembic_cfg, "head")
            print(f"Migrated tenant {tenant.name} successfully")
        except Exception as e:
            print(f"Failed to migrate tenant {tenant.name}: {e}")
            # อย่าหยุด — ลอง Tenant ถัดไป

Schema Drift Detection

Schema Drift คือสถานการณ์ที่ Database Schema จริงไม่ตรงกับที่ Migration กำหนดไว้ เกิดได้จากการแก้ไข Schema ด้วยมือ โดยไม่ผ่าน Migration Tool

# Prisma — ตรวจสอบ Schema Drift
npx prisma migrate diff   --from-schema-datasource prisma/schema.prisma   --to-migrations prisma/migrations   --exit-code

# Atlas — ตรวจสอบ Drift
atlas schema diff   --from "postgres://..."   --to file://schema.hcl

# ถ้ามี Drift จะแสดง SQL ที่ต้องรันเพื่อแก้ไข
ป้องกัน Schema Drift: อย่าแก้ไข Production Database ด้วยมือเด็ดขาด ทุกการเปลี่ยนแปลงต้องผ่าน Migration เสมอ ตั้ง CI/CD ให้ตรวจสอบ Drift ก่อน Deploy ทุกครั้ง

Seeding Data

การ Seed Data คือการเพิ่มข้อมูลเริ่มต้นเข้า Database เช่น ข้อมูล Admin User, Category ต่างๆ, Country list เป็นต้น

// prisma/seed.ts — Prisma Seeding
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  // สร้าง Admin User
  await prisma.user.upsert({
    where: { email: "admin@example.com" },
    update: {},
    create: {
      email: "admin@example.com",
      name: "Admin",
      role: "ADMIN",
    },
  });

  // สร้าง Categories
  const categories = ["Technology", "Science", "Business", "Health"];
  for (const name of categories) {
    await prisma.category.upsert({
      where: { name },
      update: {},
      create: { name },
    });
  }

  console.log("Seeding completed");
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());
# รัน Seed
npx prisma db seed

Common Mistakes

1. Destructive Changes โดยไม่มี Data Backup

-- อย่าทำแบบนี้โดยไม่ Backup!
DROP TABLE users;
ALTER TABLE orders DROP COLUMN customer_email;
TRUNCATE TABLE logs;

2. Long-Running Locks

-- อันตราย: Lock ตารางใหญ่
ALTER TABLE orders ADD COLUMN total DECIMAL(10,2) NOT NULL DEFAULT 0;
-- NOT NULL + DEFAULT ทำให้ต้อง Rewrite ทั้งตาราง ใน PostgreSQL < 11

-- ปลอดภัยกว่า: เพิ่ม Column ที่ Nullable ก่อน
ALTER TABLE orders ADD COLUMN total DECIMAL(10,2);
-- แล้ว Update ข้อมูลแบบ Batch
-- แล้วค่อย SET NOT NULL

3. ไม่ Test Down Migration

หลายทีมเขียน Down Migration แต่ไม่เคยทดสอบ พอต้อง Rollback จริงๆ กลับพบว่า Down Migration ไม่ทำงาน ทำให้ต้องแก้ไขด้วยมือ

4. Running Migrations at Application Startup

// อย่าทำแบบนี้ใน Production!
// app.ts
async function bootstrap() {
  await runMigrations(); // ถ้า Migration ช้า App จะ Start ไม่ขึ้น
  await startServer();
}

// ควรรัน Migration แยกจาก Application Startup
// ใน CI/CD Pipeline หรือ Init Container (K8s)

Migration สำหรับ MongoDB

แม้ MongoDB จะเป็น Schemaless แต่ในทางปฏิบัติยังต้องจัดการ Schema Changes เช่น เปลี่ยนโครงสร้าง Document, เพิ่ม Index, เปลี่ยน Data Format

// MongoDB Migration ด้วย migrate-mongo
// migrations/20260408-add-email-index.js
module.exports = {
  async up(db) {
    await db.collection("users").createIndex(
      { email: 1 },
      { unique: true }
    );

    // Rename field
    await db.collection("users").updateMany(
      { name: { $exists: true } },
      { $rename: { name: "fullName" } }
    );
  },

  async down(db) {
    await db.collection("users").dropIndex("email_1");

    await db.collection("users").updateMany(
      { fullName: { $exists: true } },
      { $rename: { fullName: "name" } }
    );
  }
};

Database Versioning

Migration Tool ส่วนใหญ่จะสร้างตาราง Metadata เพื่อติดตามว่า Migration ไหนรันไปแล้ว

-- ตาราง Migration Metadata (ชื่อแตกต่างตาม Tool)
-- Flyway: flyway_schema_history
-- Alembic: alembic_version
-- Prisma: _prisma_migrations
-- Knex: knex_migrations

-- ตัวอย่าง Flyway schema_history
SELECT installed_rank, version, description, type, checksum, installed_on, success
FROM flyway_schema_history;

-- ตัวอย่าง Prisma _prisma_migrations
SELECT id, migration_name, started_at, finished_at, applied_steps_count
FROM _prisma_migrations;

สรุป

Database Migration เป็นทักษะที่ Backend Developer ทุกคนต้องเชี่ยวชาญ ไม่ว่าจะใช้ภาษาหรือ Framework อะไร การจัดการ Schema Changes อย่างเป็นระบบจะช่วยให้ทีมทำงานร่วมกันได้ดีขึ้น Deploy ได้มั่นใจขึ้น และลดโอกาสเกิดปัญหาใน Production

สิ่งสำคัญที่ต้องจำคือ ทุก Schema Change ต้องผ่าน Migration เสมอ อย่าแก้ Production Database ด้วยมือ ทำ Migration ให้เล็กและ Backward Compatible เสมอ ทดสอบทั้ง Up และ Down Migration ก่อน Deploy และรัน Migration แยกจาก Application Startup ใน CI/CD Pipeline

เลือก Migration Tool ที่เหมาะกับภาษาและ Ecosystem ของคุณ ถ้าใช้ Python ก็ Alembic ถ้าใช้ TypeScript ก็ Prisma Migrate หรือ Drizzle Kit ถ้าใช้ Java ก็ Flyway หรือ Liquibase เครื่องมือเหล่านี้จะช่วยให้คุณจัดการ Database ได้อย่างมืออาชีพ


Back to Blog | iCafe Forex | SiamLanCard | Siam2R