pgloader 官方教程全解:从 CSV、定宽文件到 SQLite/MySQL 的一站式 PostgreSQL 迁移实战
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):把两列浮点经纬度拼成 PostgreSQLpoint类型的输入表示(经度,纬度)。注意这里的format是 pgloader 命令语言内嵌的 Lispformat调用,括号表达式会被投影引擎求值(相关实现见 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 命令文件的通用骨架与易错点:
- 数据源与目标用连接串描述:本地文件用普通路径,归档用 URL,数据库用 URI(如
mysql://root@localhost/f1db、pgsql:///f1db、postgresql:///pgloader?table),?表名用于指定目标表。 - 文件类加载用
LOAD CSV / LOAD FIXED / LOAD DBF / LOAD ARCHIVE,库级迁移用load database:前者需要你描述格式与列,后者自动读取源库元数据。 WITH控制加载行为(truncate、skip header、create table、include drop等),SET控制会话参数,BEFORE LOAD DO/FINALLY DO挂接加载前/后的 SQL 阶段。- 列映射按名字而非位置:
FROM (...)与INTO (...)的列名对应即可,天然支持列过滤与重排。 using (...)投影与CAST规则是清洗数据的利器:零日期、空白转 NULL、IP 范围、坐标拼接等都能在加载管线中完成;相关内置转换可查阅 transforms.lisp 与 project-fields.lisp。- 读懂汇总报表:
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 逐条运行,即可在真实环境中复现本教程的全部加载场景。