当前位置: 代码网 > it编程>数据库>Mysql > 从MySQL转换到PostgreSQL的迁移过程

从MySQL转换到PostgreSQL的迁移过程

2026年04月08日 Mysql 我要评论
前言在数据库迁移项目中,从 mysql 转换到 postgresql 是一个常见但充满挑战的任务。两者在数据类型、函数实现、语法规范等方面存在显著差异,直接执行转换后的 sql 文件几乎必然会遇到大量

前言

在数据库迁移项目中,从 mysql 转换到 postgresql 是一个常见但充满挑战的任务。两者在数据类型、函数实现、语法规范等方面存在显著差异,直接执行转换后的 sql 文件几乎必然会遇到大量语法错误。

本文基于一个真实的企业级迁移项目,系统记录了从 mysql 到 postgresql 迁移过程中遇到的各类兼容性问题,并提供了完整的修复方案和自动化脚本。希望能为从事类似工作的数据库管理员和开发者提供切实可行的参考。

一、问题背景

1.1 项目概况

  • 源数据库:mysql 5.7+
  • 目标数据库:postgresql 13+
  • 数据规模:约 30.9mb 的 sql 文件,包含完整的表结构、索引、约束和数据
  • 涉及表数量:133+ 张表

1.2 核心挑战

原始 sql 文件在 postgresql 环境中执行时,遇到了以下几类主要错误:

错误类型典型表现影响范围
数据类型不兼容tinyintdatetime 等类型无法识别几乎所有表
时间戳语法差异current_timestamp(3) 报语法错误含时间字段的表
自增主键语法auto_increment 不被识别所有含自增主键的表
保留字冲突字段名为 nulltable 等关键字特定表
存储引擎/字符集engine=innodb 等 mysql 特有语法表定义部分
布尔值字面量b'0'b'1' 二进制表示法布尔类型字段
缺失列应用运行时发现字段不存在部分业务表

二、核心差异对照:mysql vs postgresql

在开始修复之前,先明确两者在关键语法上的差异:

功能点mysql 语法postgresql 语法
自增主键auto_incrementgenerated by default as identity 或 serial
当前时间戳(无精度)current_timestampcurrent_timestamp
当前时间戳(带精度)current_timestamp(3)不支持,需去除精度参数
布尔值字面量b'0' / b'1''0' / '1' 或 false / true
类型:tinyint1 字节整数int2(或 smallint
类型:datetime日期时间timestamp
类型:blob二进制大对象bytea
存储引擎engine=innodb不支持,需删除
字符集character set utf8mb4不支持,需删除
注释语法comment 'xxx' 在列定义后comment on column 独立语句

三、问题分类与解决方案

3.1 时间戳语法差异

问题描述

mysql 支持带精度参数的时间戳定义:

-- mysql
`time_stamp_` timestamp not null default current_timestamp(3)

postgresql 的 current_timestamp 不接受精度参数,且字段名中的 null 会与关键字冲突。

错误信息

错误: 语法错误 在 "null" 或附近的
line 8: `time_stamp_` null not null default current_timestamp(3)

解决方案

import re
def fix_timestamp_issues(content):
    # 1. 移除 current_timestamp 的精度参数
    content = re.sub(r'current_timestamp\(\d+\)', 'current_timestamp', content)
    # 2. 修复字段名中的 null 关键字冲突(用双引号包裹)
    content = re.sub(r'(\w+_null)\s+', r'"\1" ', content)
    return content

3.2 自增主键语法差异

问题描述

mysql 使用 auto_increment,postgresql 需要替换为 generated by default as identity 或 serial

解决方案

def fix_auto_increment(content):
    # 方案一:使用 identity(postgresql 10+ 推荐)
    content = re.sub(
        r'bigint not null auto_increment',
        'bigint not null generated by default as identity',
        content
    )
    # 方案二:使用 serial 简写(兼容旧版本)
    # content = re.sub(r'int\(11\) not null auto_increment', 'serial', content)
    return content

注意:serial 是语法糖,实际创建的是序列。identity 更符合 sql 标准,推荐使用。

3.3 数据类型映射

完整映射表

mysql 类型postgresql 类型说明
tinyintint216 位整数
smallintint216 位整数
mediumintint432 位整数
int / integerint432 位整数
bigintint864 位整数
floatfloat4单精度浮点
doublefloat8双精度浮点
decimal(p,s)numeric(p,s)精确数值
datetimetimestamp时间戳(无时区)
timestamptimestamp同上
datedate日期
timetime时间
char(n)char(n)定长字符串
varchar(n)varchar(n)变长字符串
texttext文本
blob / tinyblob / mediumblob / longblobbytea二进制数据
bit(n)bit(n)位串
boolean / boolboolean布尔值
enum('a','b')varchar + check 约束需显式转换

实现代码

def fix_data_types(content):
    type_mappings = {
        r'\btinyint\b': 'int2',
        r'\bsmallint\b': 'int2',
        r'\bmediumint\b': 'int4',
        r'\bint\b(?!4|8|eger)': 'int4',  # 避免匹配 int4/int8/integer
        r'\bbigint\b': 'int8',
        r'\bdatetime\b': 'timestamp',
        r'\bblob\b': 'bytea',
        r'\btinyblob\b': 'bytea',
        r'\bmediumblob\b': 'bytea',
        r'\blongblob\b': 'bytea',
        r'\bfloat\b': 'float4',
        r'\bdouble\b': 'float8',
    }
    for mysql_type, pg_type in type_mappings.items():
        content = re.sub(mysql_type, pg_type, content, flags=re.ignorecase)
    return content

3.4 保留字与字段名冲突

问题描述

自动转换工具可能过度使用双引号,将 sql 关键字也错误地引用了:

-- 错误:转换后的 sql
drop "table" "if" exists dual;
-- 正确:应该是
drop table if exists dual;

解决方案

def fix_reserved_words(content):
    # 修复被错误引用的 sql 关键字
    fixes = [
        (r'drop "table"', 'drop table'),
        (r'drop "if"', 'drop if'),
        (r'"if" exists', 'if exists'),
        (r'create "table"', 'create table'),
        (r'"primary" key', 'primary key'),
        (r'"foreign" key', 'foreign key'),
        (r'"not" null', 'not null'),
        (r'"default"', 'default'),
    ]
    for pattern, replacement in fixes:
        content = re.sub(pattern, replacement, content, flags=re.ignorecase)
    return content

3.5 缺失列补充

问题描述

某些业务表缺少应用运行所必需的列,需要在迁移时自动补充。

解决方案

def add_missing_columns(content):
    missing_columns_config = {
        'sys_coffee': [
            'deleted int2 default 0',
            'tenant_id int4'
        ],
        'erp_product': [
            'brand_id bigint',
            'skus text'
        ],
        'erp_product_brand': [
            'brand_category varchar(255)'
        ],
        'erp_product_reagent': [
            'skus text'
        ],
    }
    for table_name, columns in missing_columns_config.items():
        # 匹配 create table 语句的主键结束位置
        pattern = rf'(create table {table_name} \([\s\s]*?)(primary key \(id\));'
        columns_def = '\n  ' + ',\n  '.join(columns) + ','
        replacement = rf'\1{columns_def}\n\2;'
        content = re.sub(pattern, replacement, content)
    return content

3.6 布尔值语法差异

问题描述

mysql 使用 b'0' / b'1' 表示二进制字面量,postgresql 不支持此语法。

解决方案

def fix_boolean_literals(content):
    # 替换二进制字面量
    content = re.sub(r"b'0'", "'0'", content)
    content = re.sub(r"b'1'", "'1'", content)
    # 或者转换为布尔值(如果字段类型已是 boolean)
    # content = re.sub(r"b'0'", "false", content)
    # content = re.sub(r"b'1'", "true", content)
    return content

3.7 存储引擎与字符集语法

问题描述

mysql 在表定义末尾包含存储引擎、字符集、注释等信息,postgresql 不支持这些语法。

解决方案

def fix_mysql_specific_syntax(content):
    # 删除 engine、auto_increment、row_format 等
    content = re.sub(
        r'engine\s*=\s*\w+.*?row_format\s*=\s*\w+;',
        ';',
        content,
        flags=re.ignorecase
    )
    # 删除 character set / collate 子句
    content = re.sub(
        r'character\s+set\s+\w+\s+collate\s+\w+',
        '',
        content,
        flags=re.ignorecase
    )
    # 处理表注释(postgresql 需要单独处理)
    # mysql: comment 'xxx' → 需提取并转换为 comment on table
    return content

3.8 序列管理(自增主键的序列创建)

问题描述

postgresql 使用序列来实现自增主键。如果使用 serial 类型,序列会自动创建;但如果使用 identity 方式,则需要显式处理。

解决方案

def create_sequences_for_tables(content):
    # 提取所有 create table 的表名
    table_names = re.findall(r'create table (\w+)', content, re.ignorecase)
    # 排除系统表或特定前缀的表
    exclude_prefixes = ('act_', 'flw_', 'qrtz_')
    sequence_statements = []
    for table_name in table_names:
        if not table_name.startswith(exclude_prefixes):
            sequence_statements.append(f"""
-- 为表 {table_name} 创建序列
create sequence if not exists {table_name}_id_seq;
alter table {table_name} alter column id set default nextval('{table_name}_id_seq');
alter sequence {table_name}_id_seq owned by {table_name}.id;
""")
    # 将序列语句追加到文件末尾
    content += "\n\n-- 自动生成的序列定义\n" + "\n".join(sequence_statements)
    return content

3.9 索引与约束的兼容性处理

问题描述

mysql 和 postgresql 在索引语法上存在差异,特别是全文索引、前缀索引等。

解决方案

def fix_index_syntax(content):
    # 1. 移除 fulltext 索引(需替换为 postgresql 的 gin 索引)
    # mysql: fulltext index idx_name (col1, col2)
    # postgresql: create index idx_name on table_name using gin (to_tsvector('english', col1 || ' ' || col2))
    content = re.sub(
        r'fulltext\s+index\s+\w+\s*\([^)]+\)',
        '-- fulltext index removed, needs manual conversion to gin',
        content,
        flags=re.ignorecase
    )
    # 2. 处理前缀索引(postgresql 不支持)
    # mysql: index idx_name (col(10))
    # 需要移除前缀长度或替换为表达式索引
    return content

四、完整修复脚本架构

4.1 脚本文件结构

mysql_to_pg_migration/
├── fixers/
│   ├── __init__.py
│   ├── timestamp_fixer.py      # 时间戳修复
│   ├── type_fixer.py           # 数据类型映射
│   ├── auto_increment_fixer.py # 自增主键修复
│   ├── reserved_words_fixer.py # 保留字修复
│   ├── column_fixer.py         # 缺失列补充
│   ├── boolean_fixer.py        # 布尔值修复
│   ├── mysql_syntax_fixer.py   # mysql 特有语法清理
│   ├── sequence_fixer.py       # 序列管理
│   └── index_fixer.py          # 索引兼容性处理
├── complete_fix.py             # 主控脚本
├── verify_fix.py               # 验证脚本
└── config.yaml                 # 配置文件

4.2 主控脚本 complete_fix.py

#!/usr/bin/env python3
"""
mysql 到 postgresql sql 文件完整修复工具
用法: python complete_fix.py input.sql output.sql
"""
import sys
import re
import argparse
from pathlib import path
# 导入各个修复模块
from fixers import (
    fix_timestamp_issues,
    fix_auto_increment,
    fix_data_types,
    fix_reserved_words,
    add_missing_columns,
    fix_boolean_literals,
    fix_mysql_specific_syntax,
    create_sequences_for_tables,
    fix_index_syntax
)
class sqlfixer:
    def __init__(self, input_path, output_path):
        self.input_path = path(input_path)
        self.output_path = path(output_path)
        self.stats = {
            'fixes_applied': 0,
            'tables_found': 0,
            'sequences_created': 0
        }
    def read_sql(self):
        with open(self.input_path, 'r', encoding='utf-8') as f:
            return f.read()
    def write_sql(self, content):
        with open(self.output_path, 'w', encoding='utf-8') as f:
            f.write(content)
        print(f"✅ 修复完成,输出文件: {self.output_path}")
    def apply_fixes(self, content):
        """按顺序应用所有修复规则"""
        print("🔧 开始修复 sql 文件...")
        # 1. 基础语法修复
        content = fix_timestamp_issues(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 时间戳语法修复完成")
        content = fix_auto_increment(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 自增主键修复完成")
        content = fix_data_types(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 数据类型映射完成")
        content = fix_reserved_words(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 保留字冲突修复完成")
        content = add_missing_columns(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 缺失列补充完成")
        content = fix_boolean_literals(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 布尔值语法修复完成")
        content = fix_mysql_specific_syntax(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ mysql 特有语法清理完成")
        content = fix_index_syntax(content)
        self.stats['fixes_applied'] += 1
        print("  ✓ 索引语法兼容性处理完成")
        # 2. 序列管理(在表结构之后)
        content = create_sequences_for_tables(content)
        self.stats['fixes_applied'] += 1
        self.stats['sequences_created'] = content.count('create sequence')
        print(f"  ✓ 序列创建完成(共 {self.stats['sequences_created']} 个)")
        return content
    def run(self):
        print(f"📂 读取文件: {self.input_path}")
        content = self.read_sql()
        original_size = len(content)
        content = self.apply_fixes(content)
        final_size = len(content)
        self.write_sql(content)
        # 打印统计信息
        print("\n📊 修复统计:")
        print(f"  - 原始文件大小: {original_size / 1024:.2f} kb")
        print(f"  - 最终文件大小: {final_size / 1024:.2f} kb")
        print(f"  - 应用修复规则: {self.stats['fixes_applied']}")
        print(f"  - 创建序列数量: {self.stats['sequences_created']}")
def main():
    parser = argparse.argumentparser(description='mysql 到 postgresql sql 文件修复工具')
    parser.add_argument('input', help='输入 sql 文件路径')
    parser.add_argument('output', help='输出 sql 文件路径')
    args = parser.parse_args()
    fixer = sqlfixer(args.input, args.output)
    fixer.run()
if __name__ == '__main__':
    main()

4.3 验证脚本 verify_fix.py

#!/usr/bin/env python3
"""
验证修复后的 sql 文件,检测潜在问题
"""
import re
import sys
from pathlib import path
class sqlvalidator:
    def __init__(self, sql_path):
        self.sql_path = path(sql_path)
        self.content = self.read_sql()
        self.issues = []
    def read_sql(self):
        with open(self.sql_path, 'r', encoding='utf-8') as f:
            return f.read()
    def check_auto_increment(self):
        """检查是否还有未转换的 auto_increment"""
        matches = re.findall(r'auto_increment', self.content, re.ignorecase)
        if matches:
            self.issues.append(f"⚠️ 发现 {len(matches)} 处未转换的 auto_increment")
    def check_timestamp_precision(self):
        """检查是否还有带精度的时间戳"""
        matches = re.findall(r'current_timestamp\(\d+\)', self.content)
        if matches:
            self.issues.append(f"⚠️ 发现 {len(matches)} 处带精度的时间戳")
    def check_mysql_types(self):
        """检查是否还有 mysql 特有类型"""
        mysql_types = ['tinyint', 'mediumint', 'datetime', 'blob']
        for typ in mysql_types:
            matches = re.findall(rf'\b{typ}\b', self.content, re.ignorecase)
            if matches:
                self.issues.append(f"⚠️ 发现 {len(matches)} 处未转换的类型 '{typ}'")
    def check_engine_clause(self):
        """检查是否还有 engine 子句"""
        matches = re.findall(r'engine\s*=', self.content, re.ignorecase)
        if matches:
            self.issues.append(f"⚠️ 发现 {len(matches)} 处未清理的 engine 子句")
    def check_binary_literals(self):
        """检查是否还有 b'0'/b'1' 字面量"""
        matches = re.findall(r"b'[01]'", self.content)
        if matches:
            self.issues.append(f"⚠️ 发现 {len(matches)} 处未转换的二进制字面量")
    def run_all_checks(self):
        self.check_auto_increment()
        self.check_timestamp_precision()
        self.check_mysql_types()
        self.check_engine_clause()
        self.check_binary_literals()
    def report(self):
        print(f"\n🔍 验证文件: {self.sql_path}")
        print("=" * 50)
        if not self.issues:
            print("✅ 未发现明显问题,文件可以提交测试")
        else:
            print(f"⚠️ 发现 {len(self.issues)} 个潜在问题:\n")
            for issue in self.issues:
                print(f"  {issue}")
        return len(self.issues) == 0
def main():
    if len(sys.argv) != 2:
        print("用法: python verify_fix.py <sql_file>")
        sys.exit(1)
    validator = sqlvalidator(sys.argv[1])
    validator.run_all_checks()
    success = validator.report()
    sys.exit(0 if success else 1)
if __name__ == '__main__':
    main()

五、使用指南

5.1 快速开始

# 1. 克隆或创建脚本目录
mkdir mysql_to_pg_migration
cd mysql_to_pg_migration
# 2. 运行完整修复
python complete_fix.py original.sql fixed.sql
# 3. 验证修复结果
python verify_fix.py fixed.sql
# 4. 在 postgresql 中执行
psql -d target_db -f fixed.sql

5.2 分步执行(调试模式)

# 逐步应用修复,便于定位问题
python fix_timestamp.py input.sql step1.sql
python fix_type.py step1.sql step2.sql
python fix_auto_increment.py step2.sql step3.sql
python verify_fix.py step3.sql

六、修复统计(基于实际项目)

修复项修复数量说明
时间戳精度问题约 200+ 处移除 current_timestamp(n) 中的精度参数
数据类型转换约 500+ 处tinyint → int2datetime → timestamp 等
自增主键转换133 处auto_increment → identity
序列创建133 个为每个表创建独立的序列
存储引擎语法清理133 处移除 engine=innodb 等
保留字冲突修复约 50 处修复 nulltable 等关键字冲突
缺失列补充6 列为 4 张表补充业务必需的列
布尔值字面量约 30 处b'0' → '0'

最终输出:约 30.9mb 的 postgresql 兼容 sql 文件。

七、常见问题 faq

q1: 修复后仍有语法错误怎么办?

a: 按以下步骤排查:

  • 查看 postgresql 错误信息,定位具体的 sql 语句
  • 对照本文的差异对照表,检查是否有遗漏的转换规则
  • 手动修复该语句,并考虑将该模式添加到修复脚本中
  • 参考 postgresql 官方文档 确认正确语法

q2: 某些表的数据丢失了怎么办?

a: 可能原因及解决方案:

  • insert 语句格式问题:mysql 的 insert ignoreinsert ... on duplicate key update 需要转换
  • 数据格式不兼容:日期格式、转义字符等需要额外处理
  • 编码问题:确保源文件和目标数据库使用相同字符集(推荐 utf-8)

q3: 序列不工作怎么办?

a: 检查以下几点:

-- 1. 确认序列存在
select * from information_schema.sequences where sequence_name like '%_id_seq';
-- 2. 确认默认值已设置
\d table_name
-- 3. 手动设置序列值(如果需要同步现有数据)
select setval('table_name_id_seq', (select max(id) from table_name));

q4: 修复后性能比 mysql 差怎么办?

a: postgresql 和 mysql 的优化策略不同:

  • 索引类型:postgresql 支持更多索引类型(brin、gin、gist),可根据查询模式调整
  • 统计信息:执行 analyze 更新统计信息
  • 配置调优:调整 shared_bufferswork_mem 等参数
  • 查询重写:某些 mysql 特有的优化 hint 需要移除

q5: 如何处理存储过程和触发器?

a: 存储过程和触发器的转换是最复杂的部分:

  • 语法差异:mysql 使用 begin ... end,postgresql 使用 $$ ... $$ 和 language plpgsql
  • 变量声明:mysql 的 declare 位置不同
  • 错误处理:mysql 的 declare continue handler 需转换为 begin ... exception ... end
  • 建议使用 pgloader 或 aws sct 这类专业工具辅助转换

八、最佳实践总结

8.1 迁移前准备

  • ✅ 分析原始 sql:统计表数量、数据量、使用的 mysql 特性
  • ✅ 制定迁移策略:一次性迁移 vs 分批迁移
  • ✅ 准备回滚方案:保留原始 sql 备份,确保可快速回退

8.2 迁移过程

  • ✅ 分步骤执行:先迁移表结构,再迁移数据,最后迁移约束和索引
  • ✅ 自动化优先:使用正则表达式批量处理重复性问题
  • ✅ 保留手动空间:某些复杂场景(如存储过程)需要手动转换

8.3 迁移后验证

  • ✅ 语法验证:在测试环境中执行 sql,确保无语法错误
  • ✅ 数据完整性:对比源库和目标库的行数、关键字段的校验和
  • ✅ 应用兼容性:运行应用的核心功能,验证 crud 操作正常
  • ✅ 性能基准测试:对比关键查询的执行计划

8.4 工具推荐

工具用途适用场景
pgloader全量迁移自动化迁移 mysql → postgresql
aws dms / sct云迁移大规模企业级迁移
re2c + 自定义脚本语法转换处理复杂、非标准的 sql 文件
pgadmin验证调试手动执行和调试转换后的 sql

结语

mysql 到 postgresql 的迁移是一个系统工程,涉及语法、数据类型、函数、存储过程等多个层面的适配。本文基于真实项目经验,系统梳理了迁移过程中的常见问题和解决方案,并提供了一套可扩展的自动化修复脚本。

核心要点回顾:

  • 差异认知:充分理解两种数据库在语法和功能上的差异
  • 自动化优先:用脚本批量处理可重复的转换任务
  • 验证驱动:每个修复步骤后都要验证,及早发现问题
  • 分步推进:结构 → 数据 → 约束 → 应用,降低风险

以上就是从mysql转换到postgresql的迁移过程的详细内容,更多关于mysql转换到postgresql的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com