Python SQLAlchemy 2.0 深度解析:从 Core 到 ORM 的数据访问双引擎

工业数采与数据平台系列 · 2026-09-29 适用读者:已有 Python 基础、想系统掌握 Python 生态数据访问层的开发者;正在用 FastAPI/Flask 构建数据服务的工程团队。


1. 背景:为什么需要 SQLAlchemy

1.1 Python 原生数据库访问的三层痛点

Python 访问数据库,标准路径是 PEP 249(DB-API 2.0):sqlite3、psycopg2、pymysql、cx_Oracle 等驱动都遵循它。但直接用 DB-API 有三个长期痛点:

痛点 具体表现
SQL 字符串拼接与注入 动态条件、分页、排序全靠 f-string 拼接,引号转义地狱,SQL 注入风险高
结果集手工映射 cursor.fetchall() 返回元组/字典,每张表都要手写行转对象、对象转行的胶水代码
驱动方言割裂 占位符风格不同(? vs %s vs :name)、分页语法不同(LIMIT ? OFFSET ? vs ROWNUM vs TOP)、类型系统不同,换库要重写大量代码

1.2 SQLAlchemy 的定位与设计哲学

SQLAlchemy 是 Python 生态最成熟、最完整的数据库访问抽象层 + ORM,其核心哲学可以概括为一句话:

数据库是关系代数的载体,Python 是面向对象的语言------SQLAlchemy 负责把两者之间的鸿沟管理起来,而不是消灭 SQL。

关键设计决策:

  • 不隐藏 SQL:ORM 生成的 SQL 是透明的、可审查的,你可以随时拿到 str(stmt) 看最终 SQL;
  • Core 与 ORM 分层:Core(Schema/Table/select)面向 SQL 本身,ORM(Mapped/mapped_column/relationship/Session)面向对象模型,两者共享同一套表达式语言,可自由混用;
  • 一次编写,多库运行:方言层(Dialect)屏蔽 MySQL/PostgreSQL/SQLite/Oracle/SQL Server 差异,LIMIT、RETURNING、upsert 等由方言自动生成。

1.3 版本现状:为什么是 2.0

SQLAlchemy 2.0 于 2023 年 1 月发布,是十年来的大版本重构,核心变化:

变化 1.x(旧) 2.0(新)
查询方式 session.query(User).filter(...)(Legacy Query) select(User).where(...)(统一 select 表达式)
声明式映射 declarative_base() + Column DeclarativeBase + MappedT + mapped_column(),全类型注解
结果处理 session.query(...).all() 返回 ORM 对象列表 session.execute(stmt) 返回 Result,.scalars() 取标量
自动提交 session.autocommit=True(易踩坑) 全部显式 session.commit(),消灭隐式提交
同步/异步 同步为主,异步为半成品 async_sessionmaker + AsyncSession 一等公民
关系加载 lazy='select' 默认 默认 lazy='selectin'(2.0 风格显式声明),推荐显式 selectinload/joinedload

核心结论 :2.0 把"查询 API 分裂"(Query 与 select 并存)收敛为一套 select() 表达式体系,类型注解成为一等公民,异步成为官方支持路径。新项目一律从 2.0 语法起步,不要写 1.x 的 session.query()。

1.4 何时用 SQLAlchemy,何时不用

场景 推荐 理由
复杂业务系统 + 关系模型 SQLAlchemy ORM 对象关系映射、事务、迁移全链路
数据平台/数仓 ETL SQLAlchemy Core(或 pandas + read_sql) 批量、动态 SQL、少对象建模
简单脚本/一次性查询 直接用驱动或 pandas 引入依赖成本不划算
极致性能热路径 手写 SQL + 驱动层 ORM 有映射开销(可通过 Core/批量优化缓解)

2. 核心架构:Core 与 ORM 双引擎

2.1 分层总览

复制代码
┌─────────────────────────────────────────────┐
│  ORM 层                                     │
│  DeclarativeBase / Mapped / relationship    │
│  Session / Sessionmaker / ORM 事件          │
├─────────────────────────────────────────────┤
│  Core 层                                    │
│  Engine / Connection / MetaData / Table     │
│  select / insert / update / delete 表达式    │
├─────────────────────────────────────────────┤
│  Dialect 方言层(MySQL/PostgreSQL/SQLite…)  │
├─────────────────────────────────────────────┤
│  DBAPI(PEP 249 驱动:pymysql/psycopg2…)    │
└─────────────────────────────────────────────┘
  • Engine:连接池 + 方言 + 执行入口的封装,一个数据库一个 Engine;
  • Connection:底层数据库连接的工作单元,Core 层的"会话";
  • Session:ORM 层的"工作单元",内部持有(或借用)一个 Connection,管理对象状态(Persistent/Detached/Transient)与事务边界;
  • MetaData:Schema 的 Python 描述,可 create_all() 建表、可反向 reflect() 读表;
  • 表达式语言:select()/insert()/update()/delete() 构造 SQL 表达式树,编译时由方言渲染成具体 SQL。

2.2 ORM 对象状态机(理解 Session 的关键)

复制代码
Transient(新建未关联)
    │  session.add(obj)
    ▼
Pending(已加入,未 flush)
    │  session.flush() / commit()
    ▼
Persistent(已持久化,与数据库行对应)
    │  session.expunge() / commit 后关闭
    ▼
Detached(对象仍可读,但脱离 Session)

理解状态机是排查一切 Session 问题的前提:Detached 对象访问未加载的懒加载属性会抛 DetachedInstanceError;Pending 对象拿不到主键(obj.id 为 None)直到 flush。

2.3 2.0 查询心智模型

2.0 的核心转变:一切查询都是 select() 表达式,执行统一走 session.execute(),结果统一是 Result 对象。

复制代码
stmt = select(User).where(User.name == "alice")
result = session.execute(stmt)     # Result
user = result.scalar_one()         # 取唯一一行 ORM 对象
users = result.scalars().all()     # 取多行 ORM 对象列表
row = result.one()                 # 取一行 Row(元组式,含多列)
rows = result.all()                # 取多行 Row

3. 核心 API 说明

3.1 Engine 与连接池

python 复制代码
from sqlalchemy import create_engine, text

# SQLite 内存库(单连接)
engine = create_engine("sqlite:///:memory:", echo=False)

# SQLite 文件库
engine = create_engine("sqlite:///./data/app.db")

# PostgreSQL
engine = create_engine("postgresql+psycopg2://user:pass@host:5432/dbname")

# MySQL
engine = create_engine("mysql+pymysql://user:pass@host:3306/dbname?charset=utf8mb4")

# 连接池参数(默认 QueuePool,SQLite 除外)
engine = create_engine(
    "postgresql+psycopg2://...",
    pool_size=10,          # 池中常驻连接数
    max_overflow=20,       # 溢出上限(pool_size + max_overflow 为最大连接数)
    pool_timeout=30,       # 获取连接超时秒数
    pool_recycle=1800,     # 连接回收周期(防止数据库侧空闲断开)
    pool_pre_ping=True,    # 取连接前先 ping,避免拿到已断开的连接
    echo=False,            # 打印 SQL 日志
)

关键点:

  • pool_pre_ping=True 是生产环境标配,尤其容器化、NAT 环境下数据库空闲回收会静默断连;
  • SQLite 默认 StaticPool(单连接串行),多线程并发写会报 database is locked,需要 connect_args={"check_same_thread": False} + 业务层串行化;
  • pool_recycle 必须小于数据库 wait_timeout/idle_in_transaction_session_timeout。

3.2 Core:MetaData / Table / 表达式

python 复制代码
from sqlalchemy import MetaData, Table, Column, Integer, String, DateTime, select, insert, update, delete, func, text

metadata = MetaData()

devices = Table(
    "devices", metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("device_id", String(64), nullable=False, unique=True),
    Column("name", String(128), nullable=False),
    Column("created_at", DateTime, server_default=func.now()),
)

# 建表
metadata.create_all(engine)

# 插入(Core 风格)
with engine.begin() as conn:          # 事务自动 begin/commit
    conn.execute(
        insert(devices).values(device_id="CNC-001", name="加工中心1号")
    )

# 查询
with engine.connect() as conn:
    rows = conn.execute(
        select(devices).where(devices.c.device_id == "CNC-001")
    ).all()
    # rows 是 Row 序列,rows[0].name 或 rows[0][2] 访问

Core vs ORM 核心差异:Core 的 Table 是"描述性 Schema 对象",select(devices) 返回行(Row);ORM 的 Mapped 类是"Python 类",select(Device) 返回 Device 对象实例。ORM 建立在 Core 之上。

3.3 ORM:声明式映射(2.0 风格)

python 复制代码
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase):
    """所有 ORM 模型的基类"""

class Device(Base):
    __tablename__ = "devices"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    device_id: Mapped[str] = mapped_column(String(64), unique=True)
    name: Mapped[str] = mapped_column(String(128))
    active: Mapped[bool] = mapped_column(default=True)
    # Optional[int] → nullable=True;纯 Mapped[int] → nullable=False
    firmware_version: Mapped[Optional[str]] = mapped_column(String(32))
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))

    # 一对多关系
    readings: Mapped[list["Reading"]] = relationship(back_populates="device")

class Reading(Base):
    __tablename__ = "readings"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    device_id: Mapped[int] = mapped_column(ForeignKey("devices.id"))
    value: Mapped[float]
    ts: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))

    device: Mapped["Device"] = relationship(back_populates="readings")

类型注解驱动的映射规则(2.0 新特性):

注解写法 生成列属性
Mappedint 非空 INTEGER,NOT NULL
MappedOptional\[int] 可空 INTEGER
Mappedstr = mapped_column(String(64)) 显式类型覆盖
Mappedlist\["Child"] 一对多关系(集合)
MappedOptional\["Child"] 多对一/一对一关系(标量)
Mappeddict\[str, str] / Mappedlist\[str] 配合 JSON/ARRAY 类型使用

3.4 Session 与 sessionmaker

python 复制代码
from sqlalchemy.orm import sessionmaker

SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)

# 每次请求/工作单元一个 Session
with SessionLocal() as session:
    user = session.get(Device, 1)      # 按主键取
    ...

Session 铁律:

  • Session 不是线程安全的,每个线程/协程/请求应创建独立 Session(scoped_session 或依赖注入框架管理);
  • expire_on_commit=False 是生产推荐:commit 后对象属性不自动过期,避免 DetachedInstanceError 与重复查询;
  • Session 生命周期 = 业务事务生命周期,用 with 或 try/finally 保证关闭。

3.5 查询 API 全家桶(2.0)

python 复制代码
from sqlalchemy import select, func, or_, and_, desc, update, delete
from sqlalchemy.orm import selectinload, joinedload

# 基础查询
stmt = select(Device).where(Device.active == True)          # noqa: E712 注意用 is_(True) 更安全
stmt = select(Device).order_by(Device.created_at.desc())
stmt = select(Device).limit(100).offset(0)                   # 分页

# 组合条件
stmt = select(Device).where(
    or_(Device.name.like("%CNC%"), Device.device_id.in_(["CNC-001", "CNC-002"]))
)

# 聚合
stmt = select(Device.active, func.count(Device.id)).group_by(Device.active)

# 列选择(返回 Row 而非对象)
stmt = select(Device.id, Device.name)

# 关系预加载:解决 N+1
stmt = select(Device).options(selectinload(Device.readings))

# 联表
stmt = select(Device, Reading).join(Reading, Reading.device_id == Device.id)

# 执行
session.execute(stmt).scalars().all()    # ORM 对象列表
session.execute(stmt).scalar_one()       # 恰好一行,多/少都报错
session.execute(stmt).scalar_one_or_none()  # 零或一行
session.execute(stmt).all()              # Row 列表

3.6 写入 API:新增/更新/删除

python 复制代码
# 新增
device = Device(device_id="CNC-003", name="立加3号")
session.add(device)
session.flush()              # 立即生成 SQL(可拿到 device.id)
print(device.id)             # 现在有值了
session.commit()

# 更新(对象方式)
device.active = False
session.commit()

# 更新(批量表达式方式,不加载对象,高效)
session.execute(
    update(Device).where(Device.device_id == "CNC-003").values(active=False)
)

# 删除(对象方式)
session.delete(device)
session.commit()

# 删除(批量表达式方式)
session.execute(delete(Device).where(Device.active == False))  # noqa: E712

# 批量插入(ORM 2.0 推荐)
session.add_all([
    Device(device_id=f"CNC-{i:03d}", name=f"设备{i}") for i in range(1000)
])
session.commit()

3.7 事务控制

python 复制代码
# 方式一:Session 上下文即事务边界
with SessionLocal() as session:
    try:
        a = session.get(Device, 1)
        b = session.get(Device, 2)
        a.active = False
        b.active = True
        session.commit()       # 原子提交
    except Exception:
        session.rollback()     # 整体回滚
        raise

# 方式二:显式 begin / commit / rollback
session = SessionLocal()
try:
    session.begin()
    ...
    session.commit()
except:
    session.rollback()
finally:
    session.close()

注意:session.commit() 之后对象进入 Persistent 状态;若事务内出现异常但未 rollback,Session 处于"脏状态",继续使用会抛 PendingRollbackError------正确姿势是 rollback() 后新建 Session。

3.8 异步:AsyncEngine / AsyncSession(2.0 一等公民)

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

async_engine = create_async_engine("sqlite+aiosqlite:///./data/app.db")
AsyncSessionLocal = async_sessionmaker(async_engine, expire_on_commit=False)

async def get_device(device_id: str):
    async with AsyncSessionLocal() as session:
        stmt = select(Device).where(Device.device_id == device_id)
        result = await session.execute(stmt)
        return result.scalar_one_or_none()

异步铁律 :AsyncSession 及其查询到的对象不能跨协程共享;所有数据库操作必须 await;ORM 关系懒加载在异步下不可用(会触发 MissingGreenlet 错误),必须显式 selectinload/joinedload。

3.9 类型扩展:JSON、时间、自定义类型

python 复制代码
from sqlalchemy import JSON, DateTime, TypeDecorator
import json

class Point:
    """自定义类型:存储为 JSON 字符串"""
    def __init__(self, x, y):
        self.x, self.y = x, y

class PointType(TypeDecorator):
    impl = String(64)                      # 底层列类型
    cache_ok = True

    def process_bind_param(self, value, dialect):   # Python → 数据库
        return json.dumps({"x": value.x, "y": value.y}) if value else None

    def process_result_value(self, value, dialect): # 数据库 → Python
        d = json.loads(value)
        return Point(d["x"], d["y"]) if value else None

class Config(Base):
    __tablename__ = "configs"
    id: Mapped[int] = mapped_column(primary_key=True)
    extra: Mapped[dict] = mapped_column(JSON)        # 原生 JSON 列(PG/MySQL)
    origin: Mapped[Optional[Point]] = mapped_column(PointType)  # 自定义类型

4. 详细使用:从最小示例到生产工程

4.1 最小可运行示例(SQLite + ORM CRUD)

python 复制代码
# 依赖:pip install sqlalchemy
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import create_engine, select, ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker

engine = create_engine("sqlite:///./demo.db", echo=False)
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)

class Base(DeclarativeBase):
    pass

class Device(Base):
    __tablename__ = "devices"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    device_id: Mapped[str] = mapped_column(String(64), unique=True)
    name: Mapped[str] = mapped_column(String(128))
    active: Mapped[bool] = mapped_column(default=True)
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))

Base.metadata.create_all(engine)

with SessionLocal() as session:
    # 增
    session.add_all([
        Device(device_id="CNC-001", name="加工中心1号"),
        Device(device_id="CNC-002", name="加工中心2号"),
    ])
    session.commit()

    # 查
    d = session.scalars(select(Device).where(Device.device_id == "CNC-001")).one()
    print(d.name, d.active)

    # 改
    d.active = False
    session.commit()

    # 删
    session.delete(d)
    session.commit()

4.2 关系模型:一对多 + 预加载(解决 N+1)

python 复制代码
# 设备 → 采集读数(一对多)
stmt = select(Device).where(Device.device_id == "CNC-001").options(
    selectinload(Device.readings)
)
device = session.scalars(stmt).one()
for r in device.readings:          # 不会触发额外查询
    print(r.ts, r.value)

N+1 问题:不带 selectinload 时,访问 device.readings 会对每个设备额外发一条查询。列表页 100 台设备就是 1 + 100 条 SQL。selectinload 用一条 IN (...) 查询解决。

4.3 与 pandas 互转(数据平台高频场景)

python 复制代码
import pandas as pd
from sqlalchemy import text

# 方式一:pandas 直接读 SQL(内部走 SQLAlchemy)
df = pd.read_sql("SELECT * FROM readings WHERE ts >= :ts", engine, params={"ts": "2026-09-29 00:00:00"})

# 方式二:先取 Core 结果再转 DataFrame
with engine.connect() as conn:
    rows = conn.execute(text("SELECT device_id, value, ts FROM readings LIMIT 10000")).all()
df = pd.DataFrame(rows, columns=["device_id", "value", "ts"])

# DataFrame → 批量入库(Core insert,比 ORM 快数倍)
with engine.begin() as conn:
    conn.execute(
        text("INSERT INTO readings(device_id, value, ts) VALUES (:device_id, :value, :ts)"),
        [{"device_id": r.device_id, "value": r.value, "ts": r.ts} for r in df.itertuples()],
    )

4.4 工业数采场景:采集点批量落库 + 最近值查询

python 复制代码
from sqlalchemy import select, func

# 批量写入(万级点位,Core executemany 最优)
points = [
    {"device_id": 1, "value": 22.5, "ts": "2026-09-29 10:00:00"},
    {"device_id": 1, "value": 22.6, "ts": "2026-09-29 10:00:05"},
    # ... 批量构造
]
with engine.begin() as conn:
    conn.execute(
        text("INSERT INTO readings(device_id, value, ts) VALUES (:device_id, :value, :ts)"),
        points,
    )

# 最近一条(按时间倒序取 1)
stmt = (
    select(Reading)
    .where(Reading.device_id == 1)
    .order_by(Reading.ts.desc())
    .limit(1)
)
latest = session.scalars(stmt).one()

# 按分钟聚合均值(配合时间序列)
stmt = (
    select(func.date_trunc("minute", Reading.ts).label("bucket"), func.avg(Reading.value))
    .where(Reading.device_id == 1)
    .group_by("bucket")
    .order_by("bucket")
)

4.5 与 FastAPI 集成(依赖注入 + 异步)

python 复制代码
# app/db.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from fastapi import Depends

engine = create_async_engine("sqlite+aiosqlite:///./app.db", echo=False)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_session():
    async with AsyncSessionLocal() as session:
        yield session          # FastAPI 依赖,每请求一个 Session

# app/main.py
from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

@app.get("/devices/{device_id}")
async def get_device(device_id: str, session: AsyncSession = Depends(get_session)):
    stmt = select(Device).where(Device.device_id == device_id)
    device = (await session.execute(stmt)).scalar_one_or_none()
    if not device:
        raise HTTPException(404, "device not found")
    return {"id": device.id, "device_id": device.device_id, "name": device.name}

4.6 数据库迁移:Alembic 简介

python 复制代码
# 安装:pip install alembic
# alembic init alembic
# 配置 alembic.ini 的 sqlalchemy.url
# alembic/env.py 中 target_metadata = Base.metadata

# 生成迁移脚本(对比模型与库差异)
# alembic revision --autogenerate -m "add readings table"
# 应用迁移
# alembic upgrade head

为什么需要 Alembic :Base.metadata.create_all() 只建表、不修改已存在的表(加列、改类型、加索引都无效)。生产环境必须用迁移脚本管理 schema 演进。


5. 底层实现剖析:一次查询的生命周期

5.1 SQL 表达式树 → 方言渲染 → DBAPI 执行

复制代码
select(Device).where(Device.active == True)
        │
        ▼
表达式树(ColumnElement / BinaryExpression / Select 节点)
        │  .compile(dialect=...)
        ▼
SQL 字符串:SELECT devices.id, ... FROM devices WHERE devices.active = ?
        │  参数: [True](参数化,杜绝注入)
        ▼
DBAPI 驱动执行 → Result(行游标)
        │  ORM 层 Row 映射(按映射列名装配成 Device 实例,身份映射去重)
        ▼
Device 对象(Persistent,注册进 Session 身份映射)

5.2 身份映射(Identity Map)

同一个 Session 内,同主键的行只存在一个 Python 对象实例。session.get(Device, 1) 两次调用返回同一个对象------这是 ORM 的核心能力,保证对象状态与数据库一致、避免脏写。

5.3 工作单元(Unit of Work)与 flush 时机

  • session.add(obj) 只把对象标记为 Pending,不立即发 SQL;
  • 触发 flush 的时机:session.flush()、session.commit()、查询前自动 flush(autoflush 默认开启)、访问 Pending 对象主键;
  • flush 时 SQLAlchemy 按依赖顺序生成 INSERT/UPDATE/DELETE(先父后子、先删子后删父),保证外键约束。

5.4 连接池状态机(QueuePool)

复制代码
┌─ idle 池(pool_size 个空闲连接)────────────────┐
│  checkout: 取一个 → 借出                       │
│  checkin: 归还 → 放回 idle 池                  │
│  借出超过 pool_timeout → TimeoutError          │
│  借用期间 max_overflow 允许临时新建溢出连接      │
└────────────────────────────────────────────────┘
  • pool_pre_ping=True 在 checkout 时发 SELECT 1,失败则丢弃重建;
  • pool_recycle 到期连接在归还时被丢弃重建;
  • 每个线程/协程借出的连接必须在 finally/上下文退出时归还,泄漏连接 = 池耗尽 = 应用假死。

6. 常错点与坑(20 条)

坑 现象 正确做法
1 忘记 commit() 数据写进去了但没提交,下个连接看不到 业务单元结束显式 commit,用 with SessionLocal() 包裹
2 异常后不 rollback() 继续用 Session PendingRollbackError except 分支先 rollback 再处理
3 Session 跨线程/跨协程共享 数据错乱、InterfaceError: connection is closed 每线程/每协程独立 Session
4 访问 Detached 对象的懒加载属性 DetachedInstanceError expire_on_commit=False + 预加载关系
5 N+1 查询 列表页 1+N 条 SQL,性能雪崩 selectinload/joinedload 显式预加载
6 异步 Session 里触发懒加载 MissingGreenlet: greenlet_spawn has not been called 异步下必须 selectinload,禁止懒加载
7 scalar() vs scalars() 混用 MultipleResultsFound 或拿不到对象 一行用 scalar_one,多行用 scalars().all()
8 忘了 session.flush() 就取主键 obj.id 为 None 先 flush 再读主键
9 批量插入用 ORM 逐条 add 万级数据极慢 add_all 或直接 Core execute(insert(...), params_list)
10 Optional 忘写 该可空的列被生成 NOT NULL MappedOptional\[str]
11 默认值只在 Python 侧生效 用原生 SQL 插入时默认值不落库 需要 DB 侧默认用 server_default=text("CURRENT_TIMESTAMP")
12 比较布尔值用 == True 某些方言/类型行为不一致 Device.active.is_(True)
13 连接池未设 pool_pre_ping 空闲断连导致随机 connection is closed 生产必须 pool_pre_ping=True
14 expire_on_commit 保持默认 True commit 后对象属性访问触发重查、Detached 报错 生产设为 False
15 每请求新建 Engine 连接池失效、资源泄漏 Engine 全局单例,Session 按请求创建
16 查询结果忘处理 None scalar_one() 抛 NoResultFound 用 scalar_one_or_none() + 判空
17 时间时区不统一 本地时间 vs UTC 混存 统一存 UTC(datetime.now(timezone.utc)),展示层再转
18 SQLite 多线程写 database is locked check_same_thread=False + 业务串行 + WAL 模式
19 忘配 pool_recycle 数据库侧超时回收后连接失效 pool_recycle 小于数据库 idle 超时
20 直接传 f-string 拼 SQL SQL 注入、引号错误 一律参数化:text() + params 或表达式 API

7. 性能优化实践

优化项 手段 效果
批量写入 Core executemany(conn.execute(insert, list_of_dicts)) 比 ORM 逐条快 5~20 倍
批量插入 ORM session.add_all() + 一次 commit 减少往返
消除 N+1 selectinload/joinedload 预加载 100 条变 2 条 SQL
只取需要的列 select(Model.id, Model.name) 减少传输与映射
分页而非全量 limit/offset 或 keyset(where id > last_id) 大表稳定
连接池调优 pool_size/max_overflow/pool_pre_ping 避免抖动
缓存热查询 结果缓存(Redis) 读多写少场景显著
索引 mapped_column(index=True) 或迁移脚本加复合索引 查询走索引

keyset 分页(工业数采大表推荐):

python 复制代码
# 避免深 offset 性能衰减
stmt = select(Reading).where(Reading.id > last_id).order_by(Reading.id).limit(1000)

8. FAQ 速查表

Q1:什么时候用 Core,什么时候用 ORM? 复杂业务关系模型用 ORM(对象建模、关系预加载、迁移);批量 ETL、动态 SQL、追求性能用 Core;两者可混用(session.execute(select(Table)) 也能返回 Row)。

Q2:create_all 为什么不改已有表? create_all 只创建不存在的表,不执行 ALTER。Schema 演进必须用 Alembic 迁移。

Q3:异步和同步能混用吗? 不能。AsyncSession 与同步 Session 是两套对象,异步代码全程 await,同步代码全程同步,不要在一个协程里调同步 Session。

Q4:Session 什么时候关闭? 与业务事务同生命周期:请求级(FastAPI 依赖)、工作单元级(任务函数)。用 with 或 finally 保证关闭。

Q5:scalar() 和 scalars() 有什么区别? scalar() 取单值/单对象(多行报 MultipleResultsFound),scalars() 返回 ScalarResult,用 .all()/.first() 取列表。

Q6:为什么查询返回的是 Row 而不是对象? select(Model) 返回 ORM 对象;select(Model.id, Model.name) 列选择返回 Row(元组式)。想统一用对象就选完整实体。

Q7:怎么处理 DetachedInstanceError? 设置 expire_on_commit=False,并在查询时预加载所有要访问的关系;对象序列化(如 FastAPI 返回)前确保已加载或使用 DTO 显式字段。

Q8:SQLite 并发写报 database is locked 怎么办? connect_args={"check_same_thread": False} + PRAGMA journal_mode=WAL + 业务层串行写。生产多写场景应换 PostgreSQL/MySQL。

Q9:如何打印实际执行的 SQL? create_engine(..., echo=True),或 logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)。

Q10:SQLAlchemy 和 pymysql/psycopg2 是什么关系? SQLAlchemy 是上层抽象,DBAPI 驱动(pymysql 等)是底层实现。sqlalchemy+驱动 组成连接串,例如 mysql+pymysql://。


9. 总结

SQLAlchemy 2.0 用十年时间把 Python 数据库访问收敛为一套统一心智:

  1. 双引擎分层:Core 面向 SQL 表达式,ORM 面向对象模型,共享表达式语言,可自由混用;
  2. 2.0 统一查询:一切 select() 表达式 + session.execute() + Result,类型注解驱动映射成为标准;
  3. Session 状态机:理解 Transient/Pending/Persistent/Detached 是排查一切诡异问题的钥匙;
  4. 连接池是命脉:pool_pre_ping/pool_recycle/连接归还,生产配置三件套;
  5. 异步一等公民:AsyncEngine/AsyncSession 支撑 FastAPI 全异步栈,代价是禁用懒加载;
  6. 性能三板斧:Core executemany 批量、selectinload 消除 N+1、keyset 分页。

工业数采链路建议:关系元数据(设备台账、配置、用户)用 SQLAlchemy ORM 管理;点位时序数据走 TDengine/专用时序库;批量采集落库用 Core executemany;FastAPI 服务层用 AsyncSession + 依赖注入;分析侧用 read_sql 接 pandas 无缝衔接。SQLAlchemy 是这条链路上"关系数据管理"的枢纽,与 Kafka(采集传输)、TDengine(时序存储)、FastAPI(服务网关)共同构成完整数据平台底座。

相关推荐
2601_962885721 小时前
如何用 Python 计算 ROC 变动率指标做动量分析?
开发语言·python·算法
在世修行1 小时前
干货:文件上传端点解析
python·fastapi·接收上传
DongQiShanRen1 小时前
裁决台账双向互校(上):名册与实物的第一道对账
java·linux·运维·数据库·人工智能·自然语言处理·数据挖掘
ShineWinsu1 小时前
对于Redis:AOF持久化的解析
linux·数据库·redis·缓存·面试·持久化·aof
潼心1412o1 小时前
C++初阶(长期更新)第9讲:string(上)
开发语言·c++
仍然.2 小时前
Redis---集群
数据库·redis·缓存
Allstar_432 小时前
DV/PV 验证管理:为量产放行提供完整证据链——全星研发项目管理 APQP 软件系统汽车电子行业专业级研发项目管理平台
数据库·汽车
朝朝辞暮i2 小时前
C++ 第 39 章: 阶段性总复习——类、对象、构造、继承、this、指针与智能指针
开发语言·c++·算法·ros2
小范的技术工坊2 小时前
向量数据库解决了什么问题?
数据库