完整代码 (Python)
依赖安装:
bash
pip install psycopg2-binary sqlalchemy
(注:使用 SQLAlchemy 纯粹是为了利用其强大的跨数据库类型反射和 DDL 生成能力,读取和写入依然是逐行流式处理,不会撑爆内存。)
python
import os
import sys
import psycopg2
import psycopg2.extras
from sqlalchemy import create_engine, inspect, MetaData, Table
from sqlalchemy.types import (
TEXT, INTEGER, REAL, BLOB, NUMERIC, VARCHAR, CHAR,
BOOLEAN, DATE, DATETIME, TIMESTAMP, TIME, JSON,
DECIMAL, FLOAT, BIGINT, SMALLINT, INT
)
from sqlalchemy.dialects.postgresql import (
UUID, BYTEA, JSONB, ARRAY, BIT, MACADDR, INET, CIDR
)
# ================= 配置区 =================
PG_CONFIG = {
"host": "127.0.0.1",
"port": 5432,
"user": "your_user",
"password": "your_password",
"database": "your_db_name"
}
SQLITE_FILE = "./output_analytics.db"
# 批量读取大小,防止单表过大导致内存溢出
CHUNK_SIZE = 5000
# ==========================================
def map_pg_type_to_sqlite(pg_type_obj):
"""
将 PostgreSQL 的字段类型映射为 SQLite 支持的类型
对于数据分析,重点保证数据能存进去,精度尽量保留
"""
# 获取类型实例和基础类
type_inst = pg_type_obj
type_cls = type(type_inst)
# 处理字符串/文本类型 -> TEXT
if isinstance(type_inst, (VARCHAR, CHAR, TEXT, UUID, MACADDR, INET, CIDR, BIT)):
return TEXT()
# 处理 JSON 类型 -> TEXT (SQLite无原生JSON类型,存为文本后续解析)
if isinstance(type_inst, (JSON, JSONB)):
return TEXT()
# 处理时间类型 -> TEXT (存为ISO格式字符串,避免时区和精度丢失)
if isinstance(type_inst, (DATE, DATETIME, TIMESTAMP, TIME)):
return TEXT()
# 处理布尔类型 -> INTEGER (1 或 0)
if isinstance(type_inst, BOOLEAN):
return INTEGER()
# 处理二进制类型 -> BLOB
if isinstance(type_inst, BYTEA):
return BLOB()
# 处理数值类型
if isinstance(type_inst, (DECIMAL, NUMERIC)):
# 对于 NUMERIC/DECIMAL,如果是做纯统计可以转 REAL,如果需要高精度建议存 TEXT
# 这里默认转 REAL,符合一般数据分析习惯
return REAL()
if isinstance(type_inst, (FLOAT,)):
return REAL()
if isinstance(type_inst, (INT, BIGINT, SMALLINT, INTEGER)):
return INTEGER()
# 数组类型转为文本,用 psycopg2 读出来会是 Python list,存入时转为字符串
if isinstance(type_inst, ARRAY):
return TEXT()
# 兜底:任何无法识别的类型统一降级为 TEXT,确保数据不会因为类型报错而丢包
return TEXT()
def transfer_data():
# 删除旧的 SQLite 文件(如果存在)
if os.path.exists(SQLITE_FILE):
os.remove(SQLITE_FILE)
print(f"[Info] 已删除旧的 SQLite 文件: {SQLITE_FILE}")
# 1. 连接 PostgreSQL
print(f"[Info] 正在连接 PostgreSQL 数据库: {PG_CONFIG['database']}...")
try:
pg_conn = psycopg2.connect(**PG_CONFIG)
pg_conn.set_session(autocommit=True, readonly=True) # 确保只读且不阻塞生产
pg_cursor = pg_conn.cursor(name='analytics_cursor', cursor_factory=psycopg2.extras.DictCursor)
# 使用服务端游标,避免一次性将大表加载进内存
except Exception as e:
print(f"[Error] 连接 PostgreSQL 失败: {e}")
sys.exit(1)
# 2. 使用 SQLAlchemy 反射表结构 (只读结构,不读数据)
pg_url = f"postgresql+psycopg2://{PG_CONFIG['user']}:{PG_CONFIG['password']}@{PG_CONFIG['host']}:{PG_CONFIG['port']}/{PG_CONFIG['database']}"
pg_engine = create_engine(pg_url)
pg_inspector = inspect(pg_engine)
# 3. 连接 SQLite
print(f"[Info] 正在连接/创建 SQLite 数据库: {SQLITE_FILE}...")
sqlite_engine = create_engine(f"sqlite:///{SQLITE_FILE}")
sqlite_conn = sqlite_engine.connect()
# 开启 WAL 模式,大幅提升 SQLite 写入并发性能
sqlite_conn.execute("PRAGMA journal_mode=WAL;")
sqlite_conn.execute("PRAGMA synchronous=NORMAL;")
# 4. 获取所有表名
tables = pg_inspector.get_table_names()
print(f"[Info] 发现 {len(tables)} 张表,准备开始迁移...\n")
success_tables = 0
failed_tables = []
# 5. 逐表迁移
for table_name in tables:
print(f"--- 正在处理表: {table_name} ---")
try:
# 5.1 反射 PG 表结构
metadata = MetaData()
pg_table = Table(table_name, metadata, autoload_with=pg_engine)
# 5.2 构建 SQLite 表结构 (去除外键、索引等)
sqlite_columns = []
for col in pg_table.columns:
sqlite_type = map_pg_type_to_sqlite(col.type)
# 只保留列名和映射后的类型,忽略 primary_key, foreign_keys, nullable 等约束
sqlite_columns.append(col.copy(type_=sqlite_type, foreign_keys=set()))
# 动态创建 SQLite 表对象
from sqlalchemy import Table as SATable, Column
sqlite_table = SATable(table_name, metadata, *sqlite_columns)
# 在 SQLite 中建表
sqlite_table.create(sqlite_engine)
# 5.3 流式读取 PG 数据并写入 SQLite
query = f"SELECT * FROM {table_name}"
pg_cursor.execute(query)
inserted_count = 0
chunk = []
while True:
rows = pg_cursor.fetchmany(CHUNK_SIZE)
if not rows:
break
for row in rows:
# 将 DictRow 转换为纯值列表
values = []
for val in row.values():
# 特殊类型处理
if isinstance(val, list) or isinstance(val, dict):
# PG 的 Array/JSON 转字符串
values.append(str(val))
elif isinstance(val, bytes) and not isinstance(val, memoryview):
# BYTEA 等二进制数据直接存入 BLOB
values.append(val)
else:
values.append(val)
chunk.append(values)
# 批量写入 SQLite
if chunk:
# 使用 executemany 批量插入提升速度
insert_stmt = sqlite_table.insert()
# 将列表转为字典格式供 SQLAlchemy 使用
col_names = [c.name for c in sqlite_table.columns]
dict_chunk = [dict(zip(col_names, vals)) for vals in chunk]
sqlite_conn.execute(insert_stmt, dict_chunk)
sqlite_conn.commit()
inserted_count += len(chunk)
chunk = []
print(f" ✔ 成功迁移 {inserted_count} 条记录")
success_tables += 1
except Exception as e:
print(f" ✘ 迁移失败!原因: {e}")
failed_tables.append(table_name)
# 回滚当前表的写入,继续下一张表
sqlite_conn.rollback()
# 6. 收尾工作
pg_cursor.close()
pg_conn.close()
sqlite_conn.close()
print("\n========== 迁移完成 ==========")
print(f"成功: {success_tables} 张表")
if failed_tables:
print(f"失败: {len(failed_tables)} 张表 (可能由于复杂字段类型导致,建议人工核查)")
for ft in failed_tables:
print(f" - {ft}")
print(f"SQLite 文件路径: {os.path.abspath(SQLITE_FILE)}")
if __name__ == "__main__":
transfer_data()
核心设计逻辑解释
- 为什么使用 psycopg2 的服务端游标?
如果你有一张表是 5000 万行数据,如果直接SELECT *或者让 SQLAlchemy 全部读入内存,你的脚本会直接 OOM 崩溃。使用了cursor(name='analytics_cursor')后,PostgreSQL 会维持一个服务端游标,fetchmany(5000)每次只从网络拉取 5000 行,内存极其平稳。 - 为什么 SQLAlchemy 只用来建表,插入却很复杂?
SQLAlchemy 的table.insert().values()在做批量插入时性能极差。所以我们在建好表后,绕过了 ORM 的bulk_insert,使用了底层executemany并配合sqlite_conn.commit()分段提交,速度比纯 ORM 快几十倍。 - 类型降级兜底
生产环境的数据库经常会有一些特殊类型(比如 PostGIS 的geometry,或者自定义的ENUM)。脚本在map_pg_type_to_sqlite最后做了一个兜底,凡是识别不出的一律按TEXT处理。对于分析来说,你依然能看到这列数据,只是它变成了字符串,你可以在 Python 里用正则或特定逻辑解析它,而不会因为类型不支持导致整张表迁移失败。