PostgreSQL迁移实战:从SQLite到生产级数据库的平滑演进

PostgreSQL迁移实战:从SQLite到生产级数据库的平滑演进

摘要:当AI应用从原型走向生产,SQLite往往成为第一个瓶颈。本文基于一个真实的跑步教练AI项目,详细解析如何在不中断服务的前提下,将数据库从SQLite迁移到PostgreSQL。我们将深入源码,结合流程图和调用链,展示SQLAlchemy 2.0异步ORM的适配技巧、同步转异步的代码改造模式,以及如何处理PostgreSQL严格的类型检查陷阱。这套方案确保了数据零丢失,并将并发处理能力提升了10倍以上。


一、背景:为什么必须告别SQLite?

在项目初期,为了快速验证想法,我选择了SQLite。它只有一个文件,无需配置,非常适合开发。但随着用户量增长,三个致命问题逐渐暴露:

问题1:并发写入锁死

场景:多个用户同时保存训练记录或更新Profile。

现象

  • SQLite采用文件级锁,同一时间只能有一个写操作。
  • 日志频繁出现 database is locked 错误。
  • 用户看到"保存失败",体验极差。

问题2:缺乏异步支持

场景 :FastAPI是异步框架,但SQLite的驱动(sqlite3)是同步的。

现象

  • 每次数据库查询都会阻塞整个事件循环。
  • 一个慢查询会让所有其他用户的请求排队等待。

问题3:数据类型过于宽松

场景:用户ID在代码里是字符串,但在SQLite里存成了整数。

现象

  • 迁移到严格类型的数据库时,大量逻辑报错。
  • 缺乏生产级数据库的完整性约束检查。

二、解决方案:引入PostgreSQL + SQLAlchemy 2.0

2.1 为什么选择PostgreSQL?

特性 SQLite PostgreSQL 优势
并发能力 低(文件锁) 高(MVCC) 支持高并发读写
异步支持 强(asyncpg) 完美契合FastAPI
数据类型 动态/宽松 静态/严格 保证数据一致性
扩展性 单机 集群/分片 应对海量数据

2.2 整体架构变化

新架构 (PostgreSQL)
异步非阻塞
asyncpg驱动
FastAPI
SQLAlchemy 2.0 Async
PostgreSQL Server

Docker容器
持久化存储
旧架构 (SQLite)
同步阻塞
FastAPI
sqlite3
running_coach.db

本地文件


三、核心实现:异步ORM适配

3.1 引擎与会话配置

文件位置:app/db/session.py

python 复制代码
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
import os

# 1. 定义Base类
Base = DeclarativeBase()

# 2. 获取数据库URL(支持SQLite和PostgreSQL切换)
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./running_coach.db")

# 3. 创建异步引擎
engine = create_async_engine(
    DATABASE_URL,
    echo=False,          # 生产环境关闭SQL日志
    pool_size=10,        # 连接池大小
    max_overflow=20      # 最大溢出连接数
)

# 4. 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False
)

# 5. 依赖注入:获取数据库会话
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

关键点

  • create_async_engine :替代原来的create_engine,支持异步IO。
  • expire_on_commit=False:防止访问已关闭会话中的属性时报错。
  • yield:FastAPI推荐的依赖注入方式,确保会话自动关闭。

3.2 模型定义统一化

文件位置:app/db/models.py

python 复制代码
from sqlalchemy import Column, Integer, String, Float, DateTime, Text
from app.db.session import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(String, unique=True, index=True)  # 业务ID
    username = Column(String, unique=True)
    vo2max = Column(Float, nullable=True)
    
class RunRecord(Base):
    __tablename__ = "run_records"
    
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(String, ForeignKey("users.user_id"))
    distance = Column(Float)
    duration = Column(Integer)
    pace = Column(Float)

注意:PostgreSQL对表名和字段名的大小写敏感,建议统一使用小写加下划线。


四、代码改造:从同步到异步

这是迁移过程中工作量最大的部分。我们需要将所有数据库操作改为await模式。

4.1 查询操作改造

旧代码(同步)

python 复制代码
def get_user(db: Session, user_id: str):
    return db.query(User).filter(User.user_id == user_id).first()

新代码(异步)

python 复制代码
async def get_user(db: AsyncSession, user_id: str):
    result = await db.execute(select(User).where(User.user_id == user_id))
    return result.scalars().first()

关键变化

  1. select():SQLAlchemy 2.0推荐使用这种声明式写法。
  2. await db.execute():异步执行SQL。
  3. scalars().first():提取标量结果。

4.2 插入操作改造

旧代码(同步)

python 复制代码
def create_user(db: Session, user: UserCreate):
    db_user = User(**user.dict())
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

新代码(异步)

python 复制代码
async def create_user(db: AsyncSession, user: UserCreate):
    db_user = User(**user.model_dump())  # Pydantic V2语法
    db.add(db_user)
    await db.commit()
    await db.refresh(db_user)
    return db_user

五、踩坑记录与解决方案

坑1:RuntimeWarning: coroutine was never awaited

现象:调用数据库函数后,返回的是一个协程对象而不是数据。

原因 :忘记在调用异步函数前加await

解决方案 :全局搜索所有数据库调用,确保全部加上await

坑2:PostgreSQL类型检查严格

现象 :报错 operator does not exist: text = integer

原因:SQLite允许字符串和整数比较,但PostgreSQL不允许。

案例

python 复制代码
# ❌ 错误:user_id是String类型,不能直接传int
stmt = select(User).where(User.user_id == 123)

# ✅ 正确:显式转换
stmt = select(User).where(User.user_id == str(123))

坑3:MetaData属性名冲突

现象 :启动时报错AttributeError: 'MetaData' object has no attribute 'clear'

原因 :自定义的Model中使用了metadata作为字段名,与SQLAlchemy内部属性冲突。

解决方案 :将字段名改为meta_datainfo


六、数据迁移:ETL流程

如何把旧数据从SQLite搬到PostgreSQL?

6.1 迁移脚本实现

文件位置:scripts/migrate_sqlite_to_pg.py

python 复制代码
import asyncio
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from app.db.models import Base, User, RunRecord

# 源数据库(SQLite)
sqlite_url = "sqlite:///./running_coach.db"
sqlite_engine = create_engine(sqlite_url)
SQLiteSession = sessionmaker(sqlite_engine)

# 目标数据库(PostgreSQL)
pg_url = os.getenv("PG_DATABASE_URL")
pg_engine = create_async_engine(pg_url)
AsyncPGSession = async_sessionmaker(pg_engine, class_=AsyncSession)

async def migrate_users():
    print("开始迁移用户数据...")
    async with AsyncPGSession() as pg_sess:
        with SQLiteSession() as sqlite_sess:
            users = sqlite_sess.query(User).all()
            for user in users:
                # 检查是否已存在
                result = await pg_sess.execute(
                    select(User).where(User.user_id == user.user_id)
                )
                if not result.scalars().first():
                    pg_sess.add(user)
            await pg_sess.commit()
    print(f"用户数据迁移完成,共{len(users)}条")

6.2 迁移步骤

  1. 备份 :复制running_coach.db文件。
  2. 建表 :运行init_db.py在PostgreSQL中创建空表。
  3. 执行脚本:运行迁移脚本,按表顺序导入数据。
  4. 验证:对比两个数据库的记录数。

七、Docker Compose部署

文件位置:docker-compose.yml

yaml 复制代码
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: coach
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: running_coach
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    build: .
    environment:
      DATABASE_URL: postgresql+asyncpg://coach:secret@db:5432/running_coach
    depends_on:
      - db

volumes:
  pgdata:

优势 :一键启动数据库和应用,且数据持久化在pgdata卷中。


八、总结与展望

核心价值

  1. 性能飞跃:异步IO让系统吞吐量提升10倍以上。
  2. 稳定性增强:MVCC机制彻底解决了写入锁死问题。
  3. 规范性提升:严格的类型检查提前发现了潜在Bug。

后续优化

  1. 连接池监控:集成Prometheus监控连接池使用情况。
  2. 读写分离:利用PostgreSQL的Standby节点分担读压力。
  3. 索引优化:根据慢查询日志添加复合索引。

九、完整源码

GitHub仓库AiRunCoachAgent

快速演示AiRunCoachAgent

核心文件清单

复制代码
app/
├── db/
│   ├── session.py                   # 异步引擎配置
│   ├── models.py                    # 统一模型定义
│   └── init_db.py                   # 初始化脚本
├── services/
│   └── db_service.py                # 异步CRUD封装
scripts/
└── migrate_sqlite_to_pg.py          # 数据迁移脚本

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言讨论。 🏃‍♂️💨

相关推荐
OpsEye7 小时前
数据库连接池爆了,这3个命令能救你一次
运维·数据库·后端
码云骑士8 小时前
Redis 入门实战:从 NoSQL 概念到安装与基础操作详解(一)
数据库·redis·缓存
YL200404268 小时前
MySQL-进阶篇-锁
数据库·mysql
爱喝水的鱼丶8 小时前
SAP-ABAP:数据类型与数据对象(8篇) 第七篇:进阶优化篇——基于类型与对象特征的性能优化技巧
运维·数据库·学习·性能优化·sap·abap·开发交流
SelectDB技术团队8 小时前
PB 级自动驾驶数据秒级检索:Apache Doris 统一多模态数据平台实践
数据库·人工智能·自动驾驶·apache doris·selectdb
爱编程的小新☆8 小时前
LangGraph4j工作流框架
前端·数据库·ai·langchain·langgraph4j
programhelp_9 小时前
Google 2026 New Grad SDE VO 三轮面试详解 | 含Behavioral、Coding、Design
java·服务器·数据库
czhc11400756639 小时前
数据库520 HALCONAN安装
数据库
阿坤带你走近大数据9 小时前
Oracle中的OGG介绍
数据库·oracle