0

0

从 MySQL 迁移到 PostgreSQL

王林

王林

发布时间:2024-07-12 08:34:19

|

1420人浏览过

|

来源于dev.to

转载

从 mysql 迁移到 postgresql

将数据库从 mysql 迁移到 postgres 是一个具有挑战性的过程。

虽然 mysql 和 postgres 执行类似的工作,但它们之间存在一些根本差异,这些差异可能会产生需要解决才能成功迁移的问题。

从哪儿开始?

pg loader 是一个可以用来将数据移动到 postgresql 的工具,但是,它并不完美,但在某些情况下可以很好地工作。值得一看,看看这是否是你想要走的方向。

另一种方法是创建自定义脚本。

自定义脚本提供了更大的灵活性和范围来解决特定于您的数据集的问题。

在本文中,构建了自定义脚本来处理迁移过程。

导出数据

数据如何导出对于迁移的顺利进行至关重要。在默认设置中使用 mysqldump 将导致更困难的过程。

使用 --兼容=ansi 选项以 postgresql 需要的格式导出数据。

为了使迁移更容易处理,请将架构和数据转储分开,以便可以单独处理它们。每个文件的处理要求都非常不同,为每个文件创建一个脚本将使其更易于管理。

架构差异

数据类型

mysql 和 postgresql 中可用的数据类型存在差异,这意味着在处理架构时,您需要确定哪些字段数据类型最适合您的数据。

类别 mysql postgresql
数字 int、tinyint、smallint、mediumint、bigint、float、double、decimal 整数、smallint、bigint、数字、实数、双精度、串行、小串行、大串行
字符串 char、varchar、tinytext、text、mediumtext、longtext char、varchar、文本
日期和时间 日期、时间、日期时间、时间戳、年份 日期、时间、时间戳、间隔、时间戳
二进制 二进制、varbinary、tinyblob、blob、mediumblob、longblob 字节茶
布尔值 布尔值(tinyint(1)) 布尔值
枚举和集合 枚举,设置 enum(没有等效的 set)
json json json、jsonb
几何 几何、点、线、多边形 点、线、lseg、框、路径、多边形、圆
网络地址 没有内置类型 cidr、inet、macaddr
uuid 没有内置类型(可以使用char(36)) uuid
数组 没有内置支持 支持任何数据类型的数组
xml 没有内置类型 xml
范围类型 没有内置支持 int4range、int8range、numrange、tsrange、tstzrange、daterange
复合类型 没有内置支持 用户定义的复合类型

tinyint 字段类型

tinyint 在 postgresql 中不存在。您可以选择使用smallint 或boolean 来替换它。选择与当前数据集最相似的数据类型。

 $line =~ s/\btinyint(?:\(\d+\))?\b/smallint/gi;

枚举字段类型

枚举字段稍微复杂一些,虽然 postgresql 中存在枚举,但它们需要创建自定义类型。

为了避免重复自定义类型,最好规划出需要哪些枚举类型,并创建架构所需的最少数量的自定义类型。自定义类型不是特定于表的,一种自定义类型可以在多个表上使用。

create type color_enum as enum ('blue', 'green');

...
"shirt_color" color_enum not null default 'blue',
"pant_color" color_enum not null default 'green',
...

类型的创建需要在导入 sql 之前完成。然后可以调整脚本以使用已创建的自定义类型。

如果有多个字段使用 enum('blue','green'),这些字段都应该使用相同的 enum 自定义类型。为每个单独的字段创建自定义类型并不是好的数据库设计。

if ( $line =~ /"([^"]+)"\s+enum\(([^)]+)\)/ ) {
    my $column_name = $1;
    my $enum_values = $2;
    if ( $enum_values !~ /''/ ) {
        $enum_values .= ",''";
    }

    my @items = $enum_values =~ /'([^']*)'/g;

    my $sorted_enum_values = join( ',', sort @items );

    my $enum_type_name;
    if ( exists $enum_types{$sorted_enum_values} ) {
        $enum_type_name = $enum_types{$sorted_enum_values};
    }
    else {
        $enum_type_name = create_enum_type_name($sorted_enum_values);
        $enum_types{$sorted_enum_values} = $enum_type_name;

        # add create type statement to post-processing
        push @enum_lines,
        "create type $enum_type_name as enum ($enum_values);\n";
    }

    # replace the line with the new enum type
    $line =~ s/enum\([^)]+\)/$enum_type_name/;
}

索引

索引的创建方式存在差异。索引有两种变体:有字符限制的索引和无字符限制的索引。这两个都需要处理并从 sql 中删除,并放入一个单独的 sql 文件中,以便在导入完成后运行 (run_after.sql)。

if ($line =~ /^\s*key\s+/i) {
    if ($line =~ /key\s+"([^"]+)"\s+\("([^"]+)"\)/) {
        my $index_name = $1;
        my $column_name = $2;
        push @post_process_lines, "create index idx_${current_table}_$index_name on \"$current_table\" (\"$column_name\");\n";
    } elsif ($line =~ /key\s+"([^"]+)"\s+\("([^"]+)"\((\d+)\)\)/i) {
        my $index_name = $1;
        my $column_name = $2;
        my $prefix_length = $3;
        push @post_process_lines, "create index idx_${current_table}_$index_name on \"$current_table\" (left(\"$column_name\", $prefix_length));\n";
    }
    next;
}

全文索引在 postgresql 中的工作方式完全不同。要创建全文索引,索引必须将数据转换为向量。

然后可以对向量进行索引。索引向量时有两种索引类型可供选择。 gin 和 gist。两者都有优点和缺点。一般来说,gin 优于 gist。虽然 gin 构建索引的速度较慢,但​​查找速度更快。

if ( $line =~ /^\s*fulltext\s+key\s+"([^"]+)"\s+\("([^"]+)"\)/i ) {
    my $index_name  = $1;
    my $column_name = $2;
    push @post_process_lines,
    "create index idx_fts_${current_table}_$index_name on \"$current_table\" using gin (to_tsvector('english', \"$column_name\"));\n";
    next;
}

自动递增

postgresql 不使用 autoincrment 关键字,而是使用 generated always as identity。

导入数据时使用 generated always as identity 有一个问题。 generated always as identity不是为导入id而设计的,当向表中插入行时,不能指定id字段。 id 值将自动生成。尝试将您自己的 id 插入该行将会产生错误。

要解决此问题,可以将 id 字段设置为 serial 类型,而不是 int generated always as identity。 serial 对于导入来说更加灵活,但不建议将该字段保留为 serial。

使用此方法的另一种方法是将 overriding system value 添加到插入查询中。

insert into table (id, name)
overriding system value
values (100, 'a name');

如果您使用 serial,则需要将一些查询写入 run_after.sql,以将 serial 更改为 generated always as identity,并在创建 schema 并插入数据后重置内部计数器。

if ( $line =~ /^\s*"(\w+)"\s+(int|bigint)\s+not\s+null\s+auto_increment\s*,/i ) {
    my $column_name = $1;
    $line =~ s/^\s*"$column_name"\s+(int|bigint)\s+not\s+null\s+auto_increment\s*,/"$column_name" serial,/;

    push @post_process_lines, "alter table \"$current_table\" alter column \"$column_name\" drop default;\n";

    push @post_process_lines, "drop sequence ${current_table}_${column_name}_seq;\n";

    push @post_process_lines, "alter table \"$current_table\" alter column \"$column_name\" add generated always as identity;\n";

    push @post_process_lines, "select setval('${current_table}_${column_name}_seq', (select coalesce(max(\"$column_name\"), 1) from \"$current_table\"));\n\n";

}

架构结果

从mysql导出后的原始模式

drop table if exists "address_book";
/*!40101 set @saved_cs_client     = @@character_set_client */;
/*!40101 set character_set_client = utf8 */;
create table "address_book" (
  "id" int not null auto_increment,
  "user_id" varchar(50) not null,
  "common_name" varchar(50) not null,
  "display_name" varchar(50) not null,
  primary key ("id"),
  key "user_id" ("user_id")
);

处理的主要 sql 文件

drop table if exists "address_book";
create table "address_book" (
  "id" serial,
  "user_id" varchar(85) not null,
  "common_name" varchar(85) not null,
  "display_name" varchar(85) not null,
  primary key ("id")
);

运行后.sql

alter table "address_book" alter column "id" drop default;
drop sequence address_book_id_seq;
alter table "address_book" alter column "id" add generated always as identity;
select setval('address_book_id_seq', (select coalesce(max("id"), 1) from "address_book"));
create index idx_address_book_user_id on "address_book" ("user_id");

值得注意的是迁移中使用的索引命名约定。索引名称包括表名和字段名。 索引名称必须是唯一的,不仅在添加索引的表中,而且在整个数据库中,添加表名称和列名称可以减少脚本中出现重复的机会。

数据处理

迁移数据库的最大障碍是将数据转换为 postgresql 接受的格式。 postgresql 存储数据的方式存在一些差异,需要额外注意。

字符集

本文使用的数据集早于utf8mb4,并使用旧的默认latin1,该字符集与postgresql默认字符集utf8不兼容,需要注意的是,postgresql utf8也与mysql的utf8mb4不同。

从 latin1 迁移到 utf8 的问题是数据的存储方式。在 latin1 中每个字符都是一个字节,而在 utf8 中字符可以是多字节,最多 4 个字节。

咖啡馆这个词就是一个例子

在 latin1 中数据存储为 4 个字节,在 utf8 中存储为 5 个字节。在字符集迁移期间,会考虑字节值,并且可能会导致 utf8 中的数据被截断。 postgresql 将在此截断时出错。

Beautiful.ai
Beautiful.ai

AI在线创建幻灯片

下载

为避免截断,请向受影响的 varchar 字段添加填充。

值得注意的是,如果您更改 mysql 中的字符集,也可能会发生同样的截断问题。

字符转义

在数据库中看到反斜杠转义单引号的情况并不少见。

但是,postgresql 默认不支持这一点。相反,使用使用双单引号的 ansi sql 标准方法。

如果 varchar 字段包含 it's 则需要更改为 it's

 $line =~ s/\\'/\'\'/g;

表锁定

在 sql 转储中,每次插入之前都会有表锁定调用。

lock tables "address_book" write;

postgresql 中一般不需要手动锁定表。

postgresql 使用多版本并发控制(mvcc)来处理事务。当更新一行时,它会创建一个新版本。一旦旧版本不再使用,它​​将被删除。这意味着通常不需要表锁定。 postgresql 将与 mvcc 一起使用锁来提高并发性。手动设置锁会对并发性产生负面影响。

因此,从 sql 转储中删除手动锁并让 postgresql 根据需要处理锁是更好的选择。

导入数据

迁移过程的下一步是运行脚本生成的 sql 文件。如果前面的步骤正确完成,这部分应该是一个顺利的动作。实际发生的情况是,导入发现了前面步骤中未发现的问题,需要返回并调整脚本并重试。

要运行 sql 文件,请使用 psql 登录 postgres 数据库并运行导入功能

\i /path/to/converted_schema.sql

需要注意的两个主要错误:

错误:对于类型字符变化来说值太长(50)

这可以通过增加前面提到的 varchar 字段字符长度来解决。

错误:无效命令 n

此错误可能是由杂散转义单引号或其他不兼容的数据值引起的。要修复这些问题,可能需要将正则表达式添加到数据处理脚本中以针对特定问题区域。

其中一些错误需要更仔细地查看插入语句以找到问题所在。这在大型 sql 文件中可能具有挑战性。为了解决这个问题,请将出错的 insert 语句写到一个单独的、更小的 sql 文件中,这样可以更轻松地研究该文件以找到问题。

my %lines_to_debug = map { $_ => 1 } (1148, 1195); 
 ...
if (exists $lines_to_debug{$current_line_number}) {
    print $debug_data "$line";  
}

数据分块

无论您选择使用哪种脚本语言进行迁移,分块数据对于大型 sql 文件都非常重要。

对于此脚本,数据被分成 1mb 的块,这有助于保持脚本的效率。您应该选择对您的数据集有意义的块大小。

my $bytes_read = read( $original_data, $chunk, $chunk_size );

验证数据

有几种验证数据的方法

行数

进行行计数是确保至少插入所有行的简单方法。计算旧数据库中的行数并将其与新数据库中的行进行比较。

select count(*) from address_book

校验和

跨列运行校验和可能会有所帮助,但请记住,某些字段,尤其是 varchar 字段,可能已更改为 ansi 标准格式。因此,虽然这适用于某些领域,但它不会在所有领域都准确。

对于mysql

select md5(group_concat(coalesce(user_id, '') order by id)) from address_book

对于 postgresql

SELECT MD5(STRING_AGG(COALESCE(user_id, ''), '' ORDER BY id)) FROM address_book

手动数据检查

您还需要通过手动过程验证数据。运行一些有意义的查询,这些查询可能会发现导入问题。

最后的想法

迁移数据库是一项艰巨的任务,但只要仔细规划并充分了解您的数据集以及两个数据库系统之间的差异,就可以成功完成。

迁移到新数据库不仅仅是导入,但是可靠的数据集迁移将使您在其余的过渡过程中处于有利位置。


为此迁移创建的脚本可以在 git hub 上找到。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

686

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

326

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

348

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1159

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

359

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

758

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

577

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

420

2024.04.29

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

58

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 1.9万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 810人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号