SQLite和PostgreSQL分别代表了嵌入式数据库与服务端数据库的两个典型方向。前者以单文件、零配置、轻量级著称,后者则凭借强大的并发控制、丰富的数据类型和成熟的生态成为生产环境的主流选择。随着业务增长,不少团队需要将原本跑在SQLite上的应用迁移到PostgreSQL。这个过程看似只是换一个数据库驱动,实际涉及的细节远比想象中复杂。本文将通过三个主流方案的实际操作对比,帮助你找到最适合自己项目的迁移路径。

在动手之前,先花时间理解两者的差异,可以避免大量返工。下面我们从差异点入手,再逐一拆解迁移方案。
一、迁移前必须理清的差异点
SQLite和PostgreSQL在数据定义语言(DDL)层面就有显著不同。最典型的是自增主键:SQLite中只要声明INTEGER PRIMARY KEY,该列就会自动成为rowid的别名,插入时自动递增;而PostgreSQL早期使用SERIAL类型(内部创建序列),现在更推荐GENERATED BY DEFAULT AS IDENTITY。如果把SQLite的表结构原样导入,PostgreSQL会报错或产生非预期行为。另一个常见差异是布尔类型:SQLite没有原生布尔,习惯用INTEGER存0或1;PostgreSQL有独立的boolean类型,不接受整数直接写入(需要显式转换)。日期时间也存在类似问题:SQLite常用TEXT或INTEGER存储时间,PostgreSQL则有timestamp、date、interval等专业类型,解析规则也不同。
除了类型映射,事务隔离和并发控制模型也有很大区别。SQLite默认串行化写入,同一时间只允许一个写事务,读写混合场景容易遇到database is locked错误;PostgreSQL使用MVCC(多版本并发控制),读写互不阻塞,但要求应用处理事务可见性和锁等待。此外,SQLite的引号规则宽松,双引号可以表示字符串或标识符,而PostgreSQL严格遵循SQL标准,双引号只用于标识符,字符串必须用单引号。触发器、视图、索引的语法也有细微差异,例如SQLite的AUTOINCREMENT关键字在PostgreSQL中并不存在。迁移前最好先梳理一遍所有表结构、约束、触发器和索引,逐项对照差异列表,避免遗漏。
还有一个容易被忽视的问题是数据文件本身的大小和编码。SQLite数据库通常是单个文件,可以轻松复制;PostgreSQL需要完整的服务端实例,迁移时要规划好目标库的字符集(建议UTF8)、排序规则(collation)和表空间。对于包含大量BLOB或TEXT字段的表,还要考虑大对象存储方式和网络传输效率。如果源库中有视图依赖SQLite特有的函数(如datetime('now')、strftime()),迁移后这些函数在PostgreSQL中并不存在,需要用对应函数替换。下面我们结合具体方案来看如何处理这些问题。
二、方案一:pgloader自动化迁移
pgloader是一个用Common Lisp编写的开源数据迁移工具,原生支持从SQLite加载数据到PostgreSQL。它的最大优势是自动化程度高:只需要一条命令,工具会自动读取SQLite的表结构,转换类型,创建目标表并导入数据。安装方式根据操作系统不同有所区别,例如Ubuntu下通过apt install pgloader,macOS下用brew install pgloader,也可以在官方仓库下载二进制包。基本用法如下:
pgloader sqlite:///data.db postgresql://user:pass@localhost/dbname
这条命令会读取当前目录下的data.db,连接本机PostgreSQL的dbname数据库,自动完成迁移。pgloader内置了一套类型映射规则,比如SQLite的INTEGER PRIMARY KEY会映射为serial,TEXT映射为text,BLOB映射为bytea。对于布尔类型,pgloader会将SQLite中的0和1转换成PostgreSQL的false和true。这个默认行为在大多数简单场景下够用,但一旦源表结构复杂,比如存在触发器、自定义视图或者使用了SQLite特有的WITHOUT ROWID表,pgloader可能会忽略这些对象,只迁移表和数据。
pgloader的另一个优点是支持并行加载和批量提交,迁移速度非常快。对于数百万行的数据,通常几分钟就能完成。它还可以通过配置文件进行精细控制,例如指定只迁移某些表、重命名表、自定义类型转换规则等。下面是一个简单的配置文件示例:
LOAD DATABASE
FROM sqlite:///data.db
INTO postgresql://user:pass@localhost/dbname
WITH batch rows = 10000,
prefetch rows = 5000,
workers = 4
CAST type text to varchar using text-to-varchar,
type integer to boolean using integer-to-boolean;
不过,pgloader的缺点也很明显:它对SQLite特有的触发器、视图、外键约束处理不完整,迁移后需要人工检查。如果源数据库中大量使用datetime函数或者自定义聚合函数,pgloader无法自动转换这些SQL逻辑。另外,pgloader要求目标PostgreSQL数据库已经存在,并且连接用户有创建表权限。对于需要高度定制化清洗逻辑的场景(例如合并字段、拆分表、处理NULL语义),pgloader的配置表达能力有限,不如自己写脚本灵活。
总的来说,pgloader适合数据量较大、表结构简单、没有复杂业务逻辑依赖的迁移项目。如果团队希望快速完成上线,且能接受后续手工修补触发器或视图,这是一个值得优先尝试的方案。
三、方案二:SQL导出加脚本预处理的转换方式
第二种方案是使用SQLite自带的导出工具将整个数据库结构导出为SQL脚本,然后通过脚本修改SQL语法,使其兼容PostgreSQL。具体做法是先执行sqlite3 data.db .dump > dump.sql,得到包含建表语句和数据插入语句的纯文本文件。这个文件通常以BEGIN TRANSACTION;开头,后面是CREATE TABLE、INSERT INTO等语句。直接把这个文件导入PostgreSQL会失败,因为两者的DDL语法差异很大。例如SQLite的建表语句:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
is_active INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
在PostgreSQL中需要改成:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
is_active BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这里的修改点包括:去掉AUTOINCREMENT关键字,将INTEGER改为SERIAL;is_active字段从INTEGER改为BOOLEAN,默认值从0改为FALSE;created_at类型从TEXT改为TIMESTAMP,默认值从datetime('now')改为CURRENT_TIMESTAMP。对于大量表,手动修改效率太低,通常需要编写一个Python(或Perl、Ruby)脚本来批量替换。下面是一个Python脚本的简化示例,它读取导出的SQL文件,执行一系列字符串替换,然后输出新的SQL文件:
import re
with open('dump.sql', 'r', encoding='utf-8') as f:
content = f.read()
# 替换 AUTOINCREMENT 并简化主键
content = re.sub(r'INTEGER PRIMARY KEY AUTOINCREMENT', 'SERIAL PRIMARY KEY', content)
content = re.sub(r'INTEGER PRIMARY KEY', 'SERIAL PRIMARY KEY', content)
# 处理常见的布尔列:此处假设约定 is_ 开头的 INTEGER 字段转为 BOOLEAN
content = re.sub(r'("is_\w+")\s+INTEGER\s+DEFAULT\s+0', r'\1 BOOLEAN DEFAULT FALSE', content)
content = re.sub(r'("is_\w+")\s+BOOLEAN\s+DEFAULT\s+FALSE', r'\1 BOOLEAN DEFAULT FALSE', content)
# 替换 datetime('now') 为 CURRENT_TIMESTAMP
content = content.replace("datetime('now')", 'CURRENT_TIMESTAMP')
# 处理双引号字符串问题:SQLite允许双引号字符串,PostgreSQL不允许,这里简单将双引号字符串改为单引号
# 注意:此正则可能误伤标识符,需要根据实际情况调整
content = re.sub(r'"(?P<value>[^"]*)"', r"'\g<value>'", content)
with open('postgres_dump.sql', 'w', encoding='utf-8') as f:
f.write(content)
这个脚本只演示了最基础的替换规则,实际项目中还需要处理更多细节,比如:SQLite的INSERT语句可能包含双引号字符串,PostgreSQL不接受;布尔默认值0/1需要根据字段名或值上下文判断;日期字符串格式可能不一致,需要统一成PostgreSQL可解析的ISO格式。此外,SQLite导出的INSERT语句通常使用双引号包围标识符,而PostgreSQL标识符用双引号没问题,但字符串必须用单引号,因此替换时要注意区分。一个更稳妥的方式是分段处理:先处理建表语句,再处理插入语句,避免全局替换误伤。
这种方案的优点是过程完全透明,可以保留完整的迁移历史,方便审计和回滚。对于数据量不大的项目(例如几十万行以内),脚本预处理后通过psql -f postgres_dump.sql导入通常几分钟就能完成。缺点是需要编写和维护转换脚本,对开发者的正则表达式和SQL功底有一定要求;如果源库结构频繁变动,脚本也需要同步更新。另外,复杂视图、触发器、外键约束仍然需要在导入后手动重建,因为导出的SQL中这些对象可能使用了SQLite特有语法。
四、方案三:程序化数据搬运与ORM适配
第三种方案是编写程序代码逐个表地读取SQLite数据并写入PostgreSQL。这种方法的核心思路是利用编程语言连接两个数据库,通过应用程序来控制数据转换过程。以Python为例,可以同时使用sqlite3标准库和psycopg2(或SQLAlchemy)驱动。下面是一个基本框架:
import sqlite3
import psycopg2
sqlite_conn = sqlite3.connect('data.db')
pg_conn = psycopg2.connect(host='localhost', dbname='target', user='postgres', password='pass')
pg_cursor = pg_conn.cursor()
# 获取SQLite中所有表名
sqlite_cursor = sqlite_conn.cursor()
sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
tables = [row[0] for row in sqlite_cursor.fetchall()]
for table in tables:
# 读取表结构
sqlite_cursor.execute(f"PRAGMA table_info({table})")
columns = sqlite_cursor.fetchall()
# 根据列类型生成PostgreSQL建表语句(此处简化,实际需要类型映射)
col_defs = []
for col in columns:
cname, ctype, notnull, dflt, pk = col[1], col[2], col[3], col[4], col[5]
pg_type = 'TEXT' if ctype.upper() in ('TEXT','CLOB') else ctype
col_def = f"{cname} {pg_type}"
if pk:
col_def += " PRIMARY KEY"
col_defs.append(col_def)
create_sql = f"CREATE TABLE IF NOT EXISTS {table} ({', '.join(col_defs)});"
pg_cursor.execute(create_sql)
# 逐条读取数据并插入,或使用批量插入
sqlite_cursor.execute(f"SELECT * FROM {table}")
rows = sqlite_cursor.fetchall()
placeholders = ','.join(['%s'] * len(columns))
insert_sql = f"INSERT INTO {table} VALUES ({placeholders})"
for row in rows:
# 在此处进行数据类型转换,例如将0/1转为False/True
converted = []
for val, col_info in zip(row, columns):
ctype = col_info[2].upper()
if ctype in ('BOOLEAN','BOOL') and isinstance(val, int):
val = bool(val)
converted.append(val)
pg_cursor.execute(insert_sql, converted)
pg_conn.commit()
print("迁移完成")
这个框架展示了基本思路:读取SQLite的系统表获取所有表,根据PRAGMA table_info得到列信息,动态生成PostgreSQL的建表语句,然后逐表搬运数据。在实际项目中,类型映射远比示例复杂,需要处理DATETIME、BLOB、NUMERIC等类型;还需要处理NULL值、特殊字符转义、外键依赖顺序(先迁移主表,再迁移子表)。为了提高速度,可以使用executemany批量插入,或者使用COPY命令直接从管道导入。例如,先用Python将转换后的数据写入CSV,再通过psql的\copy导入,效率会高很多。
使用ORM框架(如Django、SQLAlchemy、Entity Framework)的项目还可以通过代码迁移数据。思路是:用旧模型连接SQLite读取数据,再用新模型连接PostgreSQL写入,在ORM层完成类型适配和关联重建。这种方式的优势是可以复用业务模型中的关系和验证逻辑,尤其适合那些原本就用ORM管理SQLite的项目。缺点是性能往往不如前两种方案,因为ORM会引入额外的对象映射开销。对于百万行级别的数据,逐条ORM插入可能需要数小时。此时可以结合原生SQL批量操作,或者直接用数据库驱动编写底层迁移脚本。
程序化迁移的最大价值在于灵活性:你可以在搬运过程中做任意复杂的数据清洗、合并、拆分、加密、脱敏等操作。如果业务要求迁移后对某些字段进行隐私处理,或者源数据存在大量脏数据需要清理,这是唯一可行的方案。另外,这种方案天然支持断点续传和增量同步:可以记录已迁移的主键范围,迁移失败后从断点继续,非常适合需要在线迁移、尽量减少停机时间的场景。
五、方案对比与落地建议
为了更直观地比较三种方案,可以从迁移速度、开发工作量、定制能力、停机时间四个维度进行评估。pgloader的迁移速度最快,开发工作量最小,但定制能力较弱,通常需要停机导出源库;SQL脚本预处理方案速度中等,需要编写正则或转换脚本,定制能力较强,但迁移过程仍然是离线批量导入;程序化迁移速度最慢,开发工作量最大,但定制能力最强,而且可以实现增量迁移和在线切换。下面的表格简要总结了这些差异:
| 方案 | 迁移速度 | 开发工作量 | 定制能力 | 停机时间 |
|---|---|---|---|---|
| pgloader | 快 | 小 | 弱 | 需要停机 |
| SQL脚本转换 | 中 | 中 | 中 | 需要停机 |
| 程序化迁移 | 慢 | 大 | 强 | 可在线 |
选择方案时,优先评估数据量和表结构复杂度。如果数据量小于100万行,表结构简单(没有复杂触发器、视图、自定义函数),pgloader是最省事的选择;如果数据量在几十万到几百万行之间,且需要调整类型映射或清洗少量脏数据,SQL脚本转换方案性价比最高;如果数据量巨大、迁移过程不能长时间停机、或者需要执行复杂的数据转换,那么程序化迁移是唯一稳妥的路径。在实践中,不少团队会组合使用这些方案:先用pgloader或脚本导入核心表,再用程序处理剩余的复杂关联和特殊逻辑。
无论采用哪种方案,迁移完成后的验证工作都不可省略。建议至少做以下几项检查:行数对比(每个表的记录数是否一致);抽样数据校验(随机抽取若干行,对比关键字段的值);约束检查(主键、唯一索引、外键是否都正确创建);应用冒烟测试(用迁移后的数据库启动应用,跑核心业务流程)。对于布尔类型转换,要特别注意TRUE/FALSE与1/0的映射是否准确,避免出现查询结果与实际预期不符。对于时间戳字段,要统一时区设置,PostgreSQL默认使用timestamp without time zone,如果源系统存储的是本地时间,迁移后可能需要调整。
最后,迁移前务必备份源SQLite文件,并在测试环境完整走通迁移流程后再操作生产库。迁移过程中,建议将应用切换到只读模式,防止数据在导出后发生变化。如果必须保持写入,可以考虑双写方案:迁移开始时应用同时向SQLite和PostgreSQL写入,待数据追平后切换读流量,最后下线SQLite。这种策略实施成本较高,但能最大程度降低停机影响。
SQLite迁移PostgreSQL数据库迁移修改时间:2026-09-21 06:31:58