【SQL】PostgreSQL 转存 SQLite 用于数据分析

完整代码 (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()

核心设计逻辑解释

  1. 为什么使用 psycopg2 的服务端游标?
    如果你有一张表是 5000 万行数据,如果直接 SELECT * 或者让 SQLAlchemy 全部读入内存,你的脚本会直接 OOM 崩溃。使用了 cursor(name='analytics_cursor') 后,PostgreSQL 会维持一个服务端游标,fetchmany(5000) 每次只从网络拉取 5000 行,内存极其平稳。
  2. 为什么 SQLAlchemy 只用来建表,插入却很复杂?
    SQLAlchemy 的 table.insert().values() 在做批量插入时性能极差。所以我们在建好表后,绕过了 ORM 的 bulk_insert,使用了底层 executemany 并配合 sqlite_conn.commit() 分段提交,速度比纯 ORM 快几十倍。
  3. 类型降级兜底
    生产环境的数据库经常会有一些特殊类型(比如 PostGIS 的 geometry,或者自定义的 ENUM)。脚本在 map_pg_type_to_sqlite 最后做了一个兜底,凡是识别不出的一律按 TEXT 处理。对于分析来说,你依然能看到这列数据,只是它变成了字符串,你可以在 Python 里用正则或特定逻辑解析它,而不会因为类型不支持导致整张表迁移失败。
相关推荐
这个DBA有点耶2 小时前
死锁排查进阶:从日志到根因的完整分析链
java·开发语言·数据库·sql·运维开发·学习方法·dba
A-刘晨阳2 小时前
数据库挂了服务就瘫?我用PostgreSQL主从流复制搭了高可用架构,cpolar打通远程访问
数据库·postgresql·架构
这个DBA有点耶3 小时前
当时间数据不再只是“曲线”:聊聊时序数据库和融合分析
数据库·sql·程序人生·云原生·运维开发·时序数据库·业界资讯
IvorySQL3 小时前
PostgreSQL 技术日报 (6月4日)|SQL/PGQ 新特性,逻辑复制持续优化
数据库·sql·postgresql
前端与小赵3 小时前
数据库交互全链路实战:通用封装、批量优化与动态查询三大核心模块
数据库·python·sql
六月雨滴3 小时前
SQL 优化
sql·oracle·dba
J.Kuchiki4 小时前
【PostgreSQL内核学习:Unique 算子源码深度解读学习】
数据库·学习·postgresql
暴躁小师兄数据学院14 小时前
【AI大数据工程师特训笔记】第12讲:表分区与索引
大数据·笔记·sql·postgresql