pgloader 官方教程全解:从 CSV、定宽文件到 SQLite/MySQL 的一站式 PostgreSQL 迁移实战

原创2026-10-03 09:07:181,202 阅读
文章标签:数据工程ETL数据集成数据库

pgloader 官方教程全解:从 CSV、定宽文件到 SQLite/MySQL 的一站式 PostgreSQL 迁移实战

本文以仓库内 docs/tutorial/tutorial.rst 官方教程为主体,系统讲解 pgloader 如何用一条命令把 CSV、定宽文本、dBase(DBF)、SQLite 和 MySQL 数据迁入 PostgreSQL。读完你将掌握 pgloader 命令文件的完整语法、LOAD CSV / LOAD FIXED / LOAD DBF / LOAD ARCHIVE 等核心子句的写法,以及类型转换、物化视图、归档下载与解压等实战技巧,并能对照仓库中的真实 .load 文件与源码理解其底层实现。

教程一览:pgloader 教的是什么

pgloader 的官方教程由 6 个独立小节组成(在 docs/tutorial/tutorial.rst 中通过 .. include:: 依次引入),每一节都是一个可独立运行的完整场景:

小节文件 主题 核心命令
csv.rst 按自述规范加载 CSV 文件 LOAD CSV ... WITH ...
fixed.rst 从 ZIP 归档中加载定宽文本 LOAD ARCHIVE ... LOAD FIXED ...
geolite.rst 下载并加载 MaxMind GeoLite 数据 LOAD ARCHIVE ... LOAD CSV ... AND LOAD CSV ...
dBase.rst 加载 dBase 文件并自动建表 LOAD DBF ... WITH truncate, create table
sqlite.rst 将 SQLite 数据库整体迁移 load database ... into ...
mysql.rst 从 MySQL 迁移完整数据库 load database ... into ... CAST ...

贯穿全教程的核心思想是:无论数据来自哪种格式,pgloader 都要求你先在一个"命令文件"中把加载操作描述清楚。教程中的每个例子都遵循同一个套路——先给出完整可复制的命令,再展示输入数据样例,然后给出 pgloader <file>.load 的运行输出,最后用数据库查询验证结果。

加载 CSV 数据:用命令描述文件规格

CSV(comma separated values)在实际生产中规格千差万别:分隔符、引号、转义方式、是否带表头各不相同。pgloader 允许你在命令中把这些规格逐一描述出来。

完整命令示例

以下是教程给出的 CSV 加载命令(对应仓库测试文件 test/csv.load 的简化版):

LOAD CSV
     FROM 'path/to/file.csv' (x, y, a, b, c, d)
     INTO postgresql:///pgloader?csv (a, b, d, c)

     WITH truncate,
          skip header = 1,
          fields optionally enclosed by '"',
          fields escaped by double-quote,
          fields terminated by ','

      SET client_encoding to 'latin1',
          work_mem to '12MB',
          standard_conforming_strings to 'on'

   BEFORE LOAD DO
    $$ drop table if exists csv; $$,
    $$ create table csv (
        a bigint,
        b bigint,
        c char(2),
        d text
       );
    $$;

逐段解读:

  • LOAD CSV:声明本次加载的数据源格式。
  • FROM 'path/to/file.csv' (x, y, a, b, c, d):指定输入文件路径,并列出源文件中各列的名字(用于后面做列映射)。FROM inline (...) 还可以直接内联数据,仓库中的 test/csv.load 正是这么做的——文件末尾直接跟着数据行。
  • INTO postgresql:///pgloader?csv (a, b, d, c):目标为 pgloader 数据库中的 csv 表,并给出目标列的顺序。注意目标列顺序 (a, b, d, c) 与源列 (x, y, a, b, c, d) 不一致——pgloader 按名字而非位置映射,这就是教程最后"只导入部分列"效果(x、y 两列被丢弃)的原因。
  • WITH 子句:描述 CSV 规格。
    • truncate:加载前清空目标表;
    • skip header = 1:跳过 1 行表头(示例数据第一行 Header, with a © sign 即被跳过);
    • fields optionally enclosed by '"':字段可选用双引号包裹;
    • fields escaped by double-quote:转义字符为双引号(CSV 中 "" 表示内嵌的 ");
    • fields terminated by ',':字段以逗号分隔。
  • SET 子句:设置 PostgreSQL 会话参数,如 client_encoding、work_mem、standard_conforming_strings。教程中 client_encoding to 'latin1' 说明源文件是 latin1 编码——这解释了示例数据里的 © 符号。
  • BEFORE LOAD DO:加载前执行若干 SQL 语句,这里用 $$ ... $$ 美元引用的 drop table if exists 与 create table 完成目标表重建。

输入数据与加载效果

教程使用的数据文件内容(前几行):

Header, with a © sign
"2.6.190.56","2.6.190.63","33996344","33996351","GB","United Kingdom"
"3.0.0.0","4.17.135.31","50331648","68257567","US","United States"
"4.17.135.32","4.17.135.63","68257568","68257599","CA","Canada"
"4.17.135.64","4.17.142.255","68257600","68259583","US","United States"

运行 $ pgloader csv.load 后,pgloader 输出阶段汇总表:

       table name       read   imported     errors            time
-----------------  ---------  ---------  ---------  --------------
      before load          2          2          0          0.039s
-----------------  ---------  ---------  ---------  --------------
              csv          6          6          0          0.019s
-----------------  ---------  ---------  ---------  --------------
Total import time          6          6          0          0.058s

before load 的 2 次操作对应 BEFORE LOAD DO 中的两条 SQL。查询目标表验证列过滤与重排:

pgloader# table csv;
    a     |    b     | c  |       d
----------+----------+----+----------------
 33996344 | 33996351 | GB | United Kingdom
 ...
(6 rows)

x、y(两个 IP 地址列)被丢弃,d(国家名)被排到了 c(国家代码)前面,正是 INTO 子句中目标列顺序 (a, b, d, c) 的结果。

加载定宽数据:LOAD FIXED 与归档下载

一些数据提供商至今仍使用"定宽"格式:每一列由起始下标和长度唯一确定,数据不足时通常以空格填充。教程以美国人口普查局(US Census)的 places2k 数据集为例。

从 ZIP 归档加载定宽文件

LOAD ARCHIVE
   FROM http://www2.census.gov/geo/docs/maps-data/data/gazetteer/places2k.zip
   INTO postgresql:///pgloader

   BEFORE LOAD DO
     $$ drop table if exists places; $$,
     $$ create table places
       (
          usps      char(2)  not null,
          fips      char(2)  not null,
          fips_code char(5),
          loc_name  varchar(64)
       );
     $$

   LOAD FIXED
        FROM FILENAME MATCHING ~/places2k.txt/
             WITH ENCODING latin1
             (
                usps           from   0 for  2,
                fips           from   2 for  2,
                fips_code      from   4 for  5,
                "LocationName" from   9 for 64 [trim right whitespace],
                p              from  73 for  9,
                h              from  82 for  9,
                land           from  91 for 14,
                water          from 105 for 14,
                ldm            from 119 for 14,
                wtm            from 131 for 14,
                lat            from 143 for 10,
                long           from 153 for 11
             )
        INTO postgresql:///pgloader?places
             (
                usps, fips, fips_code, "LocationName"
             );

要点解析:

  • LOAD ARCHIVE:pgloader 会先下载远程归档,再解压,然后对归档内的文件执行子命令。这正是教程运行日志里出现 Fetching '...places2k.zip' 与 Extracting files from archive '...' 的原因。
  • LOAD FIXED:定宽数据加载子句。
  • FROM FILENAME MATCHING ~/places2k.txt/:在解压后的文件里用正则匹配归档内文件名。这里的 ~.../ 是 pgloader 的正则字面量语法(对应 command-regexp.lisp 的解析)。
  • WITH ENCODING latin1:指明文件编码。
  • 列定义语法 col from N for M:第 N 个字符(0 起)开始,长度为 M。"LocationName" from 9 for 64 [trim right whitespace] 中的方括号部分是字段处理规则,trim right whitespace 表示去除右端空白填充(定宽文件靠空格补齐列宽,所以这个规则几乎必不可少)。
  • INTO postgresql:///pgloader?places (usps, fips, fips_code, "LocationName"):目标表只取源数据 12 列中的 4 列。

运行结果

$ pgloader census-places.load
...
     table name       read   imported     errors            time
-----------------  ---------  ---------  ---------  --------------
       download          0          0          0          1.494s
        extract          0          0          0          1.013s
    before load          2          2          0          0.013s
-----------------  ---------  ---------  ---------  --------------
         places      25375      25375          0          0.499s
-----------------  ---------  ---------  ---------  --------------
Total import time      25375      25375          0          3.019s

可以看到 pgloader 把下载(download)、解压(extract)、加载前 SQL(before load)和数据导入都作为独立阶段统计。仓库中的 test/census-places.load 保留了几乎完全相同的命令,本地数据文件则位于 test/data/2013_Gaz_113CDs_national.txt(同一数据集的另一个年份版本)。定宽列定义的底层解析与字段规则实现可参考 command-fixed.lisp 与 fixed.lisp。

加载 MaxMind GeoLite 数据:一次命令完成下载、转换与建索引

MaxMind 提供的 GeoLite 免费地理位置数据集非常适合演示 pgloader 的组合能力:下载 ZIP → 解压出两个 CSV → 边加载边做类型转换 → 加载完成后建 GiST 索引。

完整命令

LOAD ARCHIVE
   FROM http://geolite.maxmind.com/download/geoip/database/GeoLiteCity_CSV/GeoLiteCity-latest.zip
   INTO postgresql:///ip4r

   BEFORE LOAD DO
     $$ create extension if not exists ip4r; $$,
     $$ create schema if not exists geolite; $$,
     $$ create table if not exists geolite.location
       (
          locid      integer primary key,
          country    text,
          region     text,
          city       text,
          postalcode text,
          location   point,
          metrocode  text,
          areacode   text
       );
     $$,
     $$ create table if not exists geolite.blocks
       (
          iprange    ip4r,
          locid      integer
       );
     $$,
     $$ drop index if exists geolite.blocks_ip4r_idx; $$,
     $$ truncate table geolite.blocks, geolite.location cascade; $$

   LOAD CSV
        FROM FILENAME MATCHING ~/GeoLiteCity-Location.csv/
             WITH ENCODING iso-8859-1
             (
                locId,
                country,
                region     null if blanks,
                city       null if blanks,
                postalCode null if blanks,
                latitude,
                longitude,
                metroCode  null if blanks,
                areaCode   null if blanks
             )
        INTO postgresql:///ip4r?geolite.location
             (
                locid,country,region,city,postalCode,
                location point using (format nil "(~a,~a)" longitude latitude),
                metroCode,areaCode
             )
        WITH skip header = 2,
             fields optionally enclosed by '"',
             fields escaped by double-quote,
             fields terminated by ','

 AND LOAD CSV
        FROM FILENAME MATCHING ~/GeoLiteCity-Blocks.csv/
             WITH ENCODING iso-8859-1
             (
                startIpNum, endIpNum, locId
             )
        INTO postgresql:///ip4r?geolite.blocks
             (
                iprange ip4r using (ip-range startIpNum endIpNum),
                locId
             )
        WITH skip header = 2,
             fields optionally enclosed by '"',
             fields escaped by double-quote,
             fields terminated by ','

  FINALLY DO
    $$ create index blocks_ip4r_idx on geolite.blocks using gist(iprange); $$;

值得注意的进阶用法

  • 多段加载:LOAD CSV ... AND LOAD CSV ... 让一个命令文件同时处理归档内的两个 CSV(Location 与 Blocks)。
  • null if blanks:把空白字符串转换为 NULL,避免地理数据中常见的空城市/邮编字段污染数据库。
  • using (...) 投影表达式:这是本教程最有价值的特性——pgloader 允许在加载时对字段做任意转换:
    • location point using (format nil "(~a,~a)" longitude latitude):把两列浮点经纬度拼成 PostgreSQL point 类型的输入表示 (经度,纬度)。注意这里的 format 是 pgloader 命令语言内嵌的 Lisp format 调用,括号表达式会被投影引擎求值(相关实现见 project-fields.lisp)。
    • iprange ip4r using (ip-range startIpNum endIpNum):把 startIpNum/endIpNum 两个整数转换成 ip4r 扩展的 IP 范围类型。ip-range 转换函数定义在 transforms.lisp。
  • FINALLY DO:所有数据加载完成后执行的阶段,这里创建 GiST 索引。

教程特别解释:GeoLite 用一对整数表示 IPv4 范围,而 pgloader 借助 PostgreSQL 的 ip4r 扩展 将其动态转换成 IP 范围类型;经纬度两列则在加载过程中即时转换成 point 类型。

运行效果

$ pgloader archive.load
...
     table name       read   imported     errors            time
-----------------  ---------  ---------  ---------  --------------
       download          0          0          0         11.592s
        extract          0          0          0          1.012s
    before load          6          6          0          0.019s
-----------------  ---------  ---------  ---------  --------------
 geolite.location     470387     470387          0          7.743s
   geolite.blocks    1903155    1903155          0         16.332s
-----------------  ---------  ---------  ---------  --------------
        finally          1          1          0         31.692s
-----------------  ---------  ---------  ---------  --------------
Total import time    2373542    2373542          0        1m8.390s

约 190 万对整数在加载时被实时转换成 IP 范围,finally 阶段的 31.7s 主要花在 GiST 索引构建上。加载完成后即可用 ip4r 的范围包含查询定位某个 IP 的地理信息:

ip4r> select *
        from      geolite.location l
             join geolite.blocks b using(locid)
       where iprange >>= '8.8.8.8';
-[ RECORD 1 ]------------------
locid      | 223
country    | US
...
iprange    | 8.8.8.8-8.8.37.255

加载 dBase(DBF)文件:自动建表

dBase 格式至今仍在 Filemaker、Excel 等工具中保留支持。pgloader 同样支持它,并且能从 DBF 文件的元数据中自动推导建表。

极简命令

LOAD DBF
    FROM http://www.insee.fr/fr/methodes/nomenclatures/cog/telechargement/2013/dbf/historiq2013.zip
    INTO postgresql:///pgloader
    WITH truncate, create table
     SET client_encoding TO 'latin1';

教程强调:这里 pgloader 会利用 dBase 文件内嵌的元数据信息,自动创建一张能够承载数据的 PostgreSQL 表,然后再加载数据——WITH create table 正是触发自动建表的关键选项。这也解释了为什么这条命令比 CSV/FIXED 场景短得多:列的类型信息来自 DBF 头文件,无需手写 create table。

仓库中对应的真实测试 test/dbf-zip.load 还展示了 TARGET TABLE dbf.france2016(指定目标表名与 schema)与 with encoding cp850 等变体;本地样例数据见 test/data/reg2013.dbf,dBase 的解析与类型映射实现在 src/sources/db3/。

运行结果

$ pgloader dbf-zip.load
...
     table name       read   imported     errors            time
-----------------  ---------  ---------  ---------  --------------
       download          0          0          0          0.167s
        extract          0          0          0          1.010s
 create, truncate          0          0          0          0.071s
-----------------  ---------  ---------  ---------  --------------
   historiq2013       9181       9181          0          0.658s
-----------------  ---------  ---------  ---------  --------------
Total import time       9181       9181          0          1.906s

create, truncate 行对应 WITH truncate, create table 自动完成的两步 DDL 操作。

加载 SQLite 数据库:一条命令完成整体迁移

SQLite 的嵌入式特性使得项目在需要处理更高并发时往往要迁移到 PostgreSQL——pgloader 正是为此设计的。

最简用法:单行命令

$ createdb chinook
$ pgloader https://github.com/lerocha/chinook-database/raw/master/ChinookDatabase/DataSources/Chinook_Sqlite_AutoIncrementPKs.sqlite pgsql:///chinook

一条命令行完成全部迁移:schema、数据、约束、主键、外键等全部自动处理。甚至可以直接把 URL 作为源——pgloader 会先下载再加载(与 LOAD ARCHIVE 的下载逻辑一致)。

教程也如实展示了迁移过程中可能遇到的问题:Chinook 示例 schema 里 playlisttrack 表被声明了多个主键,PostgreSQL 不接受,日志中会出现:

ERROR Database error 42P16: multiple primary keys for table "playlisttrack" are not allowed
QUERY: ALTER TABLE playlisttrack ADD PRIMARY KEY USING INDEX idx_66873_sqlite_autoindex_playlisttrack_1;

这类错误会被计入汇总表中的 errors 列(该次运行 Primary Keys 阶段为 12 导入、1 错误),其余数据照常导入——这正是 pgloader 容错与统计报表机制的价值。

进阶:使用命令文件

load database
     from 'sqlite/Chinook_Sqlite_AutoIncrementPKs.sqlite'
     into postgresql:///pgloader

 with include drop, create tables, create indexes, reset sequences

  set work_mem to '16MB', maintenance_work_mem to '512 MB';
  • load database ... from ... into ...:数据库级迁移的顶层命令(对应 LOAD DATABASE 语法),区别于文件级加载的 LOAD CSV 等。
  • with include drop:加载前对每个目标表执行 DROP TABLE IF EXISTS。因此当目标库为空时会刷出一堆 WARNING Postgres warning: table "xxx" does not exist, skipping——教程特别说明这些警告是预期行为,不是错误。
  • create tables, create indexes, reset sequences:自动建表、建索引、重置序列(把 SQLite 的自增列换算成 PostgreSQL 序列当前值)。

运行结果

$ pgloader sqlite.load
...
             table name       read   imported     errors            time
----------------------  ---------  ---------  ---------  --------------
      create, truncate          0          0          0          0.052s
                 Album        347        347          0          0.070s
                Artist        275        275          0          0.014s
                ...
            Create Indexes         20         20          0          0.279s
           reset sequences          0          0          0          0.043s
----------------------  ---------  ---------  ---------  --------------
  Total streaming time      15607      15607          0          0.476s

仓库中的实际测试命令见 test/sqlite.load,它额外展示了 snake_case identifiers(标识符转蛇形命名)与 cast column ... to ...(按列定制类型转换)等进阶选项;SQLite 连接与 schema 读取实现位于 src/sources/sqlite/。完整迁移后还会自动处理序列、索引、外键,类似的全库迁移效果同样体现在单命令行场景的汇总输出中(Create tables 22/22、Create Indexes 22/22、Create Foreign Keys 11/11)。

从 MySQL 迁移到 PostgreSQL:类型转换与物化视图

MySQL 迁移到 PostgreSQL 最大的痛点不是"搬数据",而是类型转换与数据清洗——大多数工具会跳过这一步,而 pgloader 把这一块做成了核心能力。

最简用法:单行命令

教程以 f1db(一级方程式历史数据)为例:

$ mysql -u root
> create database f1db;
> source f1db.sql

$ createdb f1db
$ pgloader mysql://root@localhost/f1db pgsql:///f1db

迁移结果(节选):

$ pgloader mysql://root@localhost/f1db pgsql:///f1db
...
                   table name       read   imported     errors      total time
-------------------------  ---------  ---------  ---------  --------------
              fetch meta data         33         33          0          0.365s
               Create tables         26         26          0          0.068s
...
                 f1db.laptimes     413578     413578          0          2.892s
...
            Total import time     506641     506641          0          5.547s

一行命令完成了 schema、表定义、约束、索引、主键的迁移,并且:MySQL 的 auto_increment 列自动转换为 bigserial;外键、注释全部保留;如果源库存在 ON UPDATE CURRENT_TIMESTAMP 之类的默认值,pgloader 会自动翻译成 PostgreSQL 的 before update 触发器。MySQL 的类型映射规则集中在 mysql-cast-rules.lisp,模式读取 SQL 见 src/sources/mysql/sql/。

进阶:命令文件与 CAST 规则

教程以 MySQL Sakila 示例库为例:

load database
     from      mysql://root@localhost/sakila
     into postgresql:///sakila

 WITH include drop, create tables, no truncate,
      create indexes, reset sequences, foreign keys

  SET maintenance_work_mem to '128MB', work_mem to '12MB', search_path to 'sakila'

 CAST type datetime to timestamptz
                drop default drop not null using zero-dates-to-null,
      type date drop not null drop default using zero-dates-to-null

 MATERIALIZE VIEWS film_list, staff_list

 -- INCLUDING ONLY TABLE NAMES MATCHING ~/film/, 'actor'
 -- EXCLUDING TABLE NAMES MATCHING ~<ory>

 BEFORE LOAD DO
 $$ create schema if not exists sakila; $$;

关键点:

  • CAST 规则:MySQL 允许 0000-00-00 这类 PostgreSQL 日历中不存在的日期,所以教程用 zero-dates-to-null 转换函数把零日期变成 NULL。该函数定义在 transforms.lisp,底层被 mysql-cast-rules.lisp 注册为可用转换。规则既可按类型(type datetime to ...)也可按列(column tab.col to ...)定制——教程提示:当某些 tinyint 实际是 smallint、另一些实际是 boolean 时,逐列规则非常有用。
  • MATERIALIZE VIEWS:把选中的 MySQL 视图连同数据内容一起迁到 PostgreSQL。pgloader 还支持在子句里同时给出视图名和视图定义 SQL(MySQL 方言),它会先建视图、加载数据、最后再删掉视图。仓库测试 test/sakila.load 展示了更完整的变体,如 MATERIALIZE ALL VIEWS、ALTER TABLE NAMES MATCHING ~/_list$/ ... SET SCHEMA 'mv'、ALTER SCHEMA 'sakila' RENAME TO 'pagila' 等重命名与改 schema 的高级操作。
  • 表过滤:注释掉的 INCLUDING ONLY TABLE NAMES MATCHING ~/film/, 'actor' 与 EXCLUDING TABLE NAMES MATCHING ~<ory> 演示了如何按正则或精确名字挑选/排除要迁移的表。

运行结果

$ pgloader sakila.load
...
             table name       read   imported     errors            time
----------------------  ---------  ---------  ---------  --------------
              before load          1          1          0          0.007s
          fetch meta data         45         45          0          0.402s
             create, drop          0         36          0          0.208s
----------------------  ---------  ---------  ---------  --------------
                  actor        200        200          0          0.071s
                  ...
              film_list        997        997          0          0.247s
             staff_list          2          2          0          0.135s
    Index Build Completion          0          0          0          0.000s
----------------------  ---------  ---------  ---------  --------------
        Create Indexes         41         41          0          0.964s
       Reset Sequences          0          1          0          0.035s
          Foreign Keys         22         22          0          0.254s
----------------------  ---------  ---------  ---------  --------------
     Total import time      48272      48272          0          3.502s

注意 film_list 与 staff_list 出现在数据表区域——这正是 MATERIALIZE VIEWS 把视图数据实体化导入的证明。同样地,include drop 在空目标库上会触发大量 WARNING: table "xxx" does not exist,属于预期输出。

从教程到实战:命令文件写作要点小结

综合六个官方示例,可以提炼出 pgloader 命令文件的通用骨架与易错点:

  1. 数据源与目标用连接串描述:本地文件用普通路径,归档用 URL,数据库用 URI(如 mysql://root@localhost/f1db、pgsql:///f1db、postgresql:///pgloader?table),?表名 用于指定目标表。
  2. 文件类加载用 LOAD CSV / LOAD FIXED / LOAD DBF / LOAD ARCHIVE,库级迁移用 load database:前者需要你描述格式与列,后者自动读取源库元数据。
  3. WITH 控制加载行为(truncate、skip header、create table、include drop 等),SET 控制会话参数,BEFORE LOAD DO / FINALLY DO 挂接加载前/后的 SQL 阶段。
  4. 列映射按名字而非位置:FROM (...) 与 INTO (...) 的列名对应即可,天然支持列过滤与重排。
  5. using (...) 投影与 CAST 规则是清洗数据的利器:零日期、空白转 NULL、IP 范围、坐标拼接等都能在加载管线中完成;相关内置转换可查阅 transforms.lisp 与 project-fields.lisp。
  6. 读懂汇总报表:read / imported / errors / time 四列分别表示读取、成功导入、出错行数与耗时,download、extract、before load、finally 等阶段行让你一眼定位瓶颈;遇到空库 + include drop 的 table does not exist 警告不必惊慌。

教程中每个场景都能在仓库中找到对应的可运行测试命令文件(test/csv.load、test/census-places.load、test/dbf-zip.load、test/sqlite.load、test/sakila.load),以及配套的测试数据(test/data/ 目录下的 .csv、.dbf、.txt 文件)。把它们当作练手素材,配合 pgloader <file>.load 逐条运行,即可在真实环境中复现本教程的全部加载场景。

登录后查看全文
pgloader