数据库迁移 #

数据库备份 #

手动备份 #

bash
# 创建备份
heroku pg:backups:capture

# 指定数据库
heroku pg:backups:capture DATABASE_URL

# 查看备份列表
heroku pg:backups

# 输出示例
# === Backups
# ID    Created at               Status                              Size    Database
# b003  2024-01-15 15:00:00 UTC  Completed 2024-01-15 15:01:00 UTC  25.5MB  DATABASE_URL
# b002  2024-01-14 15:00:00 UTC  Completed 2024-01-14 15:01:00 UTC  24.8MB  DATABASE_URL
# b001  2024-01-13 15:00:00 UTC  Completed 2024-01-13 15:01:00 UTC  23.1MB  DATABASE_URL

自动备份 #

bash
# 设置每日自动备份
heroku pg:backups:schedule DATABASE_URL --at '02:00 America/Los_Angeles'

# 查看备份计划
heroku pg:backups:schedules

# 取消自动备份
heroku pg:backups:unschedule DATABASE_URL

备份保留策略 #

text
┌─────────────────────────────────────────────────────┐
│              备份保留策略                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Mini/Basic 计划:                                  │
│  ├── 手动备份:保留 2 个                            │
│  └── 自动备份:保留 7 天                            │
│                                                     │
│  Standard 计划:                                    │
│  ├── 手动备份:保留 10 个                           │
│  └── 自动备份:保留 30 天                           │
│                                                     │
│  Premium 计划:                                     │
│  ├── 手动备份:保留 25 个                           │
│  └── 自动备份:保留 12 个月                         │
│                                                     │
└─────────────────────────────────────────────────────┘

备份恢复 #

恢复到同一应用 #

bash
# 恢复最新备份
heroku pg:backups:restore --app myapp --confirm myapp

# 恢复指定备份
heroku pg:backups:restore b002 --app myapp --confirm myapp

# 恢复到特定数据库
heroku pg:backups:restore b002 HEROKU_POSTGRESQL_SILVER --app myapp --confirm myapp

跨应用恢复 #

bash
# 从 source-app 恢复到 target-app
heroku pg:backups:restore source-app::b002 --app target-app --confirm target-app

# 从其他应用的最新备份恢复
heroku pg:backups:restore source-app::DATABASE_URL --app target-app --confirm target-app

下载备份 #

bash
# 下载最新备份
heroku pg:backups:download

# 下载指定备份
heroku pg:backups:download b002

# 指定输出文件
heroku pg:backups:download b002 -o backup.sql

数据迁移 #

从本地迁移到 Heroku #

bash
# 方式一:使用 pg:push
heroku pg:push mylocaldb DATABASE_URL --app myapp

# 方式二:使用 pg_dump 和 psql
pg_dump mylocaldb | heroku pg:psql --app myapp

# 方式三:使用备份恢复
pg_dump mylocaldb > backup.sql
# 上传到可访问的 URL
# heroku pg:backups:restore 'https://example.com/backup.sql' --app myapp --confirm myapp

从 Heroku 迁移到本地 #

bash
# 方式一:使用 pg:pull
heroku pg:pull DATABASE_URL mylocaldb --app myapp

# 方式二:使用备份下载
heroku pg:backups:download
pg_restore -d mylocaldb backup.sql

跨应用迁移 #

bash
# 从 app-a 迁移到 app-b
heroku pg:copy app-a::DATABASE_URL DATABASE_URL --app app-b --confirm app-b

从其他云服务迁移 #

bash
# 从 AWS RDS 迁移
# 1. 导出 RDS 数据
pg_dump $RDS_URL > rds_backup.sql

# 2. 导入到 Heroku
cat rds_backup.sql | heroku pg:psql --app myapp

# 或使用 pg:push
# 先设置本地数据库指向 RDS
export DATABASE_URL=$RDS_URL
heroku pg:push mytempdb DATABASE_URL --app myapp

数据库版本升级 #

查看当前版本 #

bash
# 查看 PostgreSQL 版本
heroku pg:info

# 输出示例
# PG Version: 16.2

升级流程 #

text
┌─────────────────────────────────────────────────────┐
│              数据库版本升级流程                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  1. 创建新版本数据库                                │
│     heroku addons:create heroku-postgresql:standard-0 --version=16 │
│                                                     │
│  2. 迁移数据                                        │
│     heroku pg:copy OLD_DB NEW_DB --app myapp        │
│                                                     │
│  3. 验证新数据库                                    │
│     heroku pg:info NEW_DB                           │
│                                                     │
│  4. 切换应用使用新数据库                            │
│     heroku pg:promote NEW_DB                        │
│                                                     │
│  5. 更新应用配置                                    │
│     heroku config:set DATABASE_URL=...              │
│                                                     │
│  6. 删除旧数据库                                    │
│     heroku addons:destroy OLD_DB                    │
│                                                     │
└─────────────────────────────────────────────────────┘

升级命令 #

bash
# 1. 创建新版本数据库
heroku addons:create heroku-postgresql:standard-0 --version=16 --app myapp

# 2. 迁移数据
heroku pg:copy DATABASE_URL HEROKU_POSTGRESQL_NEW_URL --app myapp --confirm myapp

# 3. 提升新数据库为主数据库
heroku pg:promote HEROKU_POSTGRESQL_NEW_URL --app myapp

# 4. 验证
heroku pg:info

# 5. 删除旧数据库
heroku addons:destroy heroku-postgresql-old

数据库分叉与跟随 #

创建分叉 #

bash
# 创建数据库分叉(用于测试/开发)
heroku addons:create heroku-postgresql:standard-0 --fork DATABASE_URL --app myapp-test

# 指定时间点分叉
heroku addons:create heroku-postgresql:standard-0 --fork DATABASE_URL --at "2024-01-15 10:00:00" --app myapp-test

创建跟随数据库 #

bash
# 创建只读副本
heroku addons:create heroku-postgresql:standard-0 --follow DATABASE_URL --app myapp

# 查看复制状态
heroku pg:info

# 输出示例
# === HEROKU_POSTGRESQL_FOLLOWER_URL
# Following:   DATABASE_URL
# Behind By:   0 sec

分叉 vs 跟随 #

特性 Fork Follower
数据同步 一次性复制 持续同步
读写 可读写 只读
用途 测试、开发 负载分担
成本 独立计费 独立计费

数据迁移脚本 #

Node.js 迁移脚本 #

javascript
// scripts/migrate-data.js
const { Pool } = require('pg');

async function migrateData() {
  const sourcePool = new Pool({
    connectionString: process.env.SOURCE_DATABASE_URL,
    ssl: { rejectUnauthorized: false }
  });

  const targetPool = new Pool({
    connectionString: process.env.TARGET_DATABASE_URL,
    ssl: { rejectUnauthorized: false }
  });

  try {
    // 读取源数据
    const { rows: users } = await sourcePool.query('SELECT * FROM users');
    
    // 写入目标数据库
    for (const user of users) {
      await targetPool.query(
        'INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET email = $2, name = $3',
        [user.id, user.email, user.name, user.created_at]
      );
    }
    
    console.log(`Migrated ${users.length} users`);
  } finally {
    await sourcePool.end();
    await targetPool.end();
  }
}

migrateData().catch(console.error);

Python 迁移脚本 #

python
import os
import psycopg2
from psycopg2.extras import RealDictCursor

def migrate_data():
    source_conn = psycopg2.connect(
        os.environ['SOURCE_DATABASE_URL'],
        sslmode='require'
    )
    
    target_conn = psycopg2.connect(
        os.environ['TARGET_DATABASE_URL'],
        sslmode='require'
    )
    
    try:
        source_cur = source_conn.cursor(cursor_factory=RealDictCursor)
        target_cur = target_conn.cursor()
        
        # 读取源数据
        source_cur.execute('SELECT * FROM users')
        users = source_cur.fetchall()
        
        # 写入目标数据库
        for user in users:
            target_cur.execute(
                '''
                INSERT INTO users (id, email, name, created_at)
                VALUES (%(id)s, %(email)s, %(name)s, %(created_at)s)
                ON CONFLICT (id) DO UPDATE SET
                    email = %(email)s,
                    name = %(name)s
                ''',
                user
            )
        
        target_conn.commit()
        print(f'Migrated {len(users)} users')
        
    finally:
        source_conn.close()
        target_conn.close()

if __name__ == '__main__':
    migrate_data()

数据验证 #

数据一致性检查 #

bash
# 检查表数量
heroku pg:psql -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'"

# 检查行数
heroku pg:psql -c "SELECT schemaname, relname, n_live_tup FROM pg_stat_user_tables"

# 检查数据完整性
heroku pg:psql -c "SELECT table_name, pg_size_pretty(pg_total_relation_size(table_name::text)) FROM information_schema.tables WHERE table_schema = 'public'"

迁移后验证脚本 #

javascript
// scripts/verify-migration.js
const { Pool } = require('pg');

async function verifyMigration() {
  const sourcePool = new Pool({
    connectionString: process.env.SOURCE_DATABASE_URL,
    ssl: { rejectUnauthorized: false }
  });

  const targetPool = new Pool({
    connectionString: process.env.TARGET_DATABASE_URL,
    ssl: { rejectUnauthorized: false }
  });

  try {
    const tables = ['users', 'posts', 'comments'];
    
    for (const table of tables) {
      const sourceCount = (await sourcePool.query(`SELECT COUNT(*) FROM ${table}`)).rows[0].count;
      const targetCount = (await targetPool.query(`SELECT COUNT(*) FROM ${table}`)).rows[0].count;
      
      if (sourceCount === targetCount) {
        console.log(`✓ ${table}: ${sourceCount} rows`);
      } else {
        console.log(`✗ ${table}: source=${sourceCount}, target=${targetCount}`);
      }
    }
  } finally {
    await sourcePool.end();
    await targetPool.end();
  }
}

verifyMigration().catch(console.error);

最佳实践 #

迁移前检查清单 #

markdown
## 迁移前检查
- [ ] 备份源数据库
- [ ] 验证目标数据库配置
- [ ] 检查数据大小和预估时间
- [ ] 通知相关团队
- [ ] 准备回滚计划

## 迁移中监控
- [ ] 监控迁移进度
- [ ] 检查错误日志
- [ ] 验证数据完整性

## 迁移后验证
- [ ] 验证数据数量
- [ ] 验证数据完整性
- [ ] 测试应用功能
- [ ] 更新文档

零停机迁移 #

bash
# 1. 创建跟随数据库
heroku addons:create heroku-postgresql:standard-0 --follow DATABASE_URL

# 2. 等待同步完成
heroku pg:info

# 3. 提升跟随数据库为主数据库
heroku pg:promote HEROKU_POSTGRESQL_FOLLOWER_URL

# 4. 应用自动切换到新数据库

大数据量迁移 #

bash
# 分批迁移
# 使用 LIMIT 和 OFFSET

# 或使用游标
heroku pg:psql -c "DECLARE cur CURSOR FOR SELECT * FROM large_table"
heroku pg:psql -c "FETCH 1000 FROM cur"

故障排查 #

迁移失败 #

bash
# 检查错误日志
heroku logs --tail | grep migration

# 检查数据库状态
heroku pg:info

# 常见问题
# 1. 连接超时 - 增加超时时间
# 2. 磁盘空间不足 - 升级计划
# 3. 权限问题 - 检查用户权限

数据不一致 #

bash
# 重新迁移特定表
heroku pg:psql -c "TRUNCATE TABLE users"
heroku pg:psql < users_backup.sql

# 或使用 COPY
heroku pg:psql -c "COPY users FROM STDIN WITH CSV" < users.csv

下一步 #

数据库迁移掌握后,接下来学习 Heroku Redis 了解缓存配置!

最后更新:2026-03-28