← 中文资源

简体中文版本

在线模式变更

在线模式变更在读写并发的条件下改建表结构:建影子副本、分块拷贝、回放增量,最后原子换名,而不是整段重建期间锁表。

阅读英文原文

概念定义

改建表结构曾经意味着全程锁表。在线方案解耦:先在带外建新结构,把实时变更流入其中,最后一瞬切换。重建慢慢磨,业务流量照跑。

工作机制

MySQL InnoDB 原生在线 DDL 拷贝行并回放行日志;gh-ost 改读 binlog 而不用触发器,并按副本延迟自节流。PostgreSQL 多数操作只在提交时取 MVCC 级锁,CONCURRENTLY 建索引避开独占锁。最终换名或提交仍需短暂独占窗口。

使用场景

任何大到无法在锁窗口内重建的表。要预算:影子副本的磁盘、副本延迟余量、节流阈值,以及回滚预案——切换点就是难以回头的时刻。

常见误区

“在线”不等于零影响:吞吐会降、副本会滞后、磁盘几乎翻倍。切换点的元数据锁会排在任何长事务后面,不先清理就会冻结全表;忽略主键热点的分块拷贝会打爆单页。部分 MySQL 变更已有 instant 算法,先查版本里哪些操作仍是全量重建。

代码示例

import { pgTable, text, integer, timestamp, index } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

// 初始 schema(drizzle/0000_init.sql 由 drizzle-kit generate 产出)
const invoices = pgTable('invoices', {
  id: integer('id').primaryKey(),
  customer: text('customer').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
});

// 增量迁移:新增可空列 → 回填 → 收紧约束
const ADD_COLUMN = sql`
  ALTER TABLE invoices
    ADD COLUMN due_date timestamp;
`;

const BACKFILL = sql`
  UPDATE invoices
    SET due_date = created_at + interval '30 days'
    WHERE due_date IS NULL;
`;

const TIGHTEN = sql`
  ALTER TABLE invoices
    ALTER COLUMN due_date SET NOT NULL;
`;

async function applySteps(db, steps: ReturnType<typeof sql>[]) {
  for (const step of steps) {
    await db.execute(step);
  }
  await db.execute(sql`
    CREATE INDEX IF NOT EXISTS invoices_due_date_idx
      ON invoices (due_date);
  `);
}

console.log('three-step migration prepared: add, backfill, tighten');
CREATE TABLE employees (
    employee_id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    hire_date DATE NOT NULL DEFAULT CURRENT_DATE,
    salary NUMERIC(10, 2) NOT NULL,
    department_id INTEGER NOT NULL,
    manager_id INTEGER,
    employment_status VARCHAR(20) NOT NULL DEFAULT 'active',

    -- Unique constraint
    CONSTRAINT uk_employees_email UNIQUE (email),

    -- Check constraints for data validation
    CONSTRAINT chk_salary_positive CHECK (salary > 0),
    CONSTRAINT chk_salary_range CHECK (salary <= 1000000),
    CONSTRAINT chk_hire_date CHECK (hire_date <= CURRENT_DATE),
    CONSTRAINT chk_status_valid CHECK (
        employment_status IN ('active', 'on_leave', 'terminated', 'suspended')
    ),
    CONSTRAINT chk_phone_format CHECK (
        phone IS NULL OR phone ~ '^\+?[0-9]{10,15}$'
    ),

    -- Foreign key constraints
    CONSTRAINT fk_employee_department
        FOREIGN KEY (department_id)
        REFERENCES departments(department_id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE,

    CONSTRAINT fk_employee_manager
        FOREIGN KEY (manager_id)
        REFERENCES employees(employee_id)
        ON DELETE SET NULL
) TABLESPACE pg_default;

-- Add a deferred constraint
ALTER TABLE employees
    ADD CONSTRAINT chk_no_self_reference
    CHECK (manager_id IS NULL OR manager_id != employee_id);