# 結構 (含觸發器、程序、檢視表)
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
| MySQL | PostgreSQL | 說明 |
|---|---|---|
AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY 或 SERIAL/BIGSERIAL | PG 10+ 建議 IDENTITY |
TINYINT(1) (布林) | BOOLEAN | |
VARCHAR(n) | VARCHAR(n) / TEXT | PG 無長度限制效能差異,TEXT 更彈性 |
DATETIME / TIMESTAMP | TIMESTAMP WITHOUT TIME ZONE / TIMESTAMPTZ | 建議統一用 TIMESTAMPTZ |
ENUM('a','b') | CHECK (col IN ('a','b')) 或 CREATE TYPE | |
ENGINE=InnoDB | (無需指定,預設唯一引擎) | |
utf8mb4 | UTF8 (PG 內部固定 UTF8) | |
BACKTICKS \`col\` | DOUBLE QUOTES "col" (區分大小寫) 或無引號 (折疊小寫) | |
NOW() | NOW() / CURRENT_TIMESTAMP | |
IFNULL(a,b) | COALESCE(a,b) | |
LIMIT n OFFSET m | LIMIT n OFFSET m (相同) | |
REPLACE INTO | INSERT ... ON CONFLICT (pk) DO UPDATE | UPSERT 標準語法 |
INSERT IGNORE | INSERT ... ON CONFLICT DO NOTHING |
MySQL `AUTO_INCREMENT` 最大值 ≠ PG 序列當前值。遷移後必須同步:
-- 對每個有序列的表
SELECT setval(pg_get_serial_sequence('tablename', 'id'), COALESCE(max(id), 1)) FROM tablename;
-- MySQL
$dsn = "mysql:host=localhost;dbname=mydb;charset=utf8mb4";
-- PostgreSQL
$dsn = "pgsql:host=localhost;dbname=mydb;port=5432";
<?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 指定序列名
| 場景 | MySQL | PostgreSQL |
|---|---|---|
| 字串連接 | CONCAT(a,b) / a || b (需 PIPES_AS_CONCAT) | a || b (標準) |
| 日期運算 | DATE_ADD(d, INTERVAL 1 DAY) | d + INTERVAL '1 day' |
| 分頁 | LIMIT 10 OFFSET 20 | LIMIT 10 OFFSET 20 (相同) |
| UPSERT | ON DUPLICATE KEY UPDATE | ON CONFLICT (pk) DO UPDATE SET ... |
| JSON 操作 | JSON_EXTRACT(col, '$.path') | col->>'path' (JSONB) |
| 全文搜尋 | MATCH(col) AGAINST('word') | to_tsvector('english', col) @@ plainto_tsquery('word') |
| 最後插入 ID | LAST_INSERT_ID() | currval('seq_name') 或 RETURNING id |
PG `lastInsertId()` 需指定序列名。解法:
RETURNING id:INSERT ... VALUES (...) RETURNING id → `$stmt->fetchColumn()`'users' => 'users_id_seq'MySQL → PostgreSQL 遷移是**一次性投資、長期受益**的架構升級。關鍵在於:資料遷移工具化 (pgloader) + 程式碼抽象層隔離方言差異 + 充分測試驗證。建議採用**雙寫/陰影表驗證**策略:新舊資料庫並行寫入一段時間,比對一致性後再全面切換。