首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

從 MySQL 遷移到 PostgreSQL 完整實戰指南:資料遷移、PHP 程式碼改造、工具鏈

     編輯:MySQL 資料庫相關知識

遷移動機:為什麼選 PostgreSQL?

  • ACID 合規:完整支援序列化隔離、外鍵、檢查約束、觸發器
  • 進階資料類型:JSONB、陣列、UUID、幾何、網路地址、全文檢索、範圍類型
  • 擴展生態:PostGIS、TimescaleDB、Citus、pg_partman、pg_stat_statements
  • 平行查詢:9.6+ 平行 Seq Scan/Join/Aggregate,大幅加速分析負載
  • 邏輯複寫:10+ 發布/訂閱、雙向同步、跨版本升級
  • 社群驅動、無廠商鎖定:釋出週期可預期、功能決策透明

第一階段:資料遷移 — 結構與資料轉換

1. 導出 MySQL 結構與資料

# 結構 (含觸發器、程序、檢視表)
mysqldump -u root -p --no-data --routines --triggers --events --single-transaction mydb > mysql_schema.sql

# 資料 (分表導出利於平行匯入)
mysqldump -u root -p --no-create-info --single-transaction --quick --max_allowed_packet=1G mydb > mysql_data.sql

# 或壓縮傳輸
mysqldump -u root -p --single-transaction mydb | gzip > mydb_$(date +%F).sql.gz

2. 結構轉換關鍵對照表

MySQLPostgreSQL說明
AUTO_INCREMENTGENERATED ALWAYS AS IDENTITY 或 SERIAL/BIGSERIALPG 10+ 建議 IDENTITY
TINYINT(1) (布林)BOOLEAN
VARCHAR(n)VARCHAR(n) / TEXTPG 無長度限制效能差異,TEXT 更彈性
DATETIME / TIMESTAMPTIMESTAMP WITHOUT TIME ZONE / TIMESTAMPTZ建議統一用 TIMESTAMPTZ
ENUM('a','b')CHECK (col IN ('a','b')) 或 CREATE TYPE
ENGINE=InnoDB(無需指定,預設唯一引擎)
utf8mb4UTF8 (PG 內部固定 UTF8)
BACKTICKS \`col\`DOUBLE QUOTES "col" (區分大小寫) 或無引號 (折疊小寫)
NOW()NOW() / CURRENT_TIMESTAMP
IFNULL(a,b)COALESCE(a,b)
LIMIT n OFFSET mLIMIT n OFFSET m (相同)
REPLACE INTOINSERT ... ON CONFLICT (pk) DO UPDATEUPSERT 標準語法
INSERT IGNOREINSERT ... ON CONFLICT DO NOTHING

3. 半自動轉換工具鏈

  • pgloader:`pgloader mysql://user@host/mydb postgresql://user@host/mydb` — 直接線上遷移、支援類型映射、平行載入
  • mysql2pgsql (Python/Ruby 腳本) — 針對特定版本調優
  • 手工修正:預存程序、觸發器、動態 SQL、**必須人工重寫** (PL/pgSQL vs MySQL SP 差異極大)

4. 序列值同步 (關鍵!)

MySQL `AUTO_INCREMENT` 最大值 ≠ PG 序列當前值。遷移後必須同步:

-- 對每個有序列的表
SELECT setval(pg_get_serial_sequence('tablename', 'id'), COALESCE(max(id), 1)) FROM tablename;

第二階段:PHP 程式碼改造 — PDO 抽象層平滑過渡

1. 連線字串差異

-- MySQL
$dsn = "mysql:host=localhost;dbname=mydb;charset=utf8mb4";

-- PostgreSQL
$dsn = "pgsql:host=localhost;dbname=mydb;port=5432";

2. 統一資料庫存取層 (推薦模式)

<?php
// Database.php - 統一介面
class Database {
    private static $pdo = null;
    
    public static function getConnection(): PDO {
        if (self::$pdo === null) {
            $driver = getenv('DB_DRIVER') ?: 'pgsql'; // pgsql | mysql
            $host = getenv('DB_HOST');
            $db = getenv('DB_NAME');
            $user = getenv('DB_USER');
            $pass = getenv('DB_PASS');
            $port = getenv('DB_PORT') ?: ($driver === 'pgsql' ? 5432 : 3306);
            
            $dsn = "$driver:host=$host;dbname=$db;port=$port";
            if ($driver === 'mysql') $dsn .= ';charset=utf8mb4';
            
            self::$pdo = new PDO($dsn, $user, $pass, [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false, // 真實預處理
            ]);
        }
        return self::$pdo;
    }
    
    // 通用查詢方法
    public static function query(string $sql, array $params = []): array {
        $stmt = self::getConnection()->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
    
    public static function execute(string $sql, array $params = []): int {
        $stmt = self::getConnection()->prepare($sql);
        $stmt->execute($params);
        return $stmt->rowCount();
    }
    
    public static function lastInsertId(string $seq = null): string {
        return self::getConnection()->lastInsertId($seq); // PG 需序列名
    }
}

// 使用範例
$users = Database::query("SELECT * FROM users WHERE status = ? AND created_at > ?", ['active', '2024-01-01']);
Database::execute("INSERT INTO logs (msg, level) VALUES (?, ?)", ['User login', 'info']);
$id = Database::lastInsertId('users_id_seq'); // PG 指定序列名

3. SQL 方言差異重寫重點

場景MySQLPostgreSQL
字串連接CONCAT(a,b) / a || b (需 PIPES_AS_CONCAT)a || b (標準)
日期運算DATE_ADD(d, INTERVAL 1 DAY)d + INTERVAL '1 day'
分頁LIMIT 10 OFFSET 20LIMIT 10 OFFSET 20 (相同)
UPSERTON DUPLICATE KEY UPDATEON CONFLICT (pk) DO UPDATE SET ...
JSON 操作JSON_EXTRACT(col, '$.path')col->>'path' (JSONB)
全文搜尋MATCH(col) AGAINST('word')to_tsvector('english', col) @@ plainto_tsquery('word')
最後插入 IDLAST_INSERT_ID()currval('seq_name') 或 RETURNING id

4. `lastInsertId()` 陷阱與解法

PG `lastInsertId()` 需指定序列名。解法:

  • 在 `INSERT` 使用 RETURNING id:INSERT ... VALUES (...) RETURNING id → `$stmt->fetchColumn()`
  • 或維護對應表:'users' => 'users_id_seq'

第三階段:管理工具與驗證

  • pgAdmin 4 / DBeaver / DataGrip:圖形化管理
  • pg_dump / pg_restore:邏輯備份還原 (平行 `-j 4`)
  • pg_stat_statements:啟用後查詢 `SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 20;` 找慢查詢
  • EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON):深度執行計畫分析

遷移核對清單

  1. 結構轉換:所有表、索引、外鍵、檢視表、序列、類型
  2. 資料完整性:列數、檢查和、關鍵欄位抽樣對比
  3. 序列同步:所有 IDENTITY/SERIAL 表
  4. 應用程式:連線池、SQL 方言、最後插入 ID、JSON/日期/全文搜尋
  5. 效能基線:同樣負載測試、對比 P99 延遲、吞吐量
  6. 監控告警:pg_stat_activity、複寫延遲、检查點頻率、膨脹度
  7. 備份策略:pg_dump (邏輯) + pg_basebackup/WAL-G (物理/PITR)

總結

MySQL → PostgreSQL 遷移是**一次性投資、長期受益**的架構升級。關鍵在於:資料遷移工具化 (pgloader) + 程式碼抽象層隔離方言差異 + 充分測試驗證。建議採用**雙寫/陰影表驗證**策略:新舊資料庫並行寫入一段時間,比對一致性後再全面切換。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved