工业数采与数据平台系列 · 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 数据库访问收敛为一套统一心智:
- 双引擎分层:Core 面向 SQL 表达式,ORM 面向对象模型,共享表达式语言,可自由混用;
- 2.0 统一查询:一切 select() 表达式 + session.execute() + Result,类型注解驱动映射成为标准;
- Session 状态机:理解 Transient/Pending/Persistent/Detached 是排查一切诡异问题的钥匙;
- 连接池是命脉:pool_pre_ping/pool_recycle/连接归还,生产配置三件套;
- 异步一等公民:AsyncEngine/AsyncSession 支撑 FastAPI 全异步栈,代价是禁用懒加载;
- 性能三板斧:Core executemany 批量、selectinload 消除 N+1、keyset 分页。
工业数采链路建议:关系元数据(设备台账、配置、用户)用 SQLAlchemy ORM 管理;点位时序数据走 TDengine/专用时序库;批量采集落库用 Core executemany;FastAPI 服务层用 AsyncSession + 依赖注入;分析侧用 read_sql 接 pandas 无缝衔接。SQLAlchemy 是这条链路上"关系数据管理"的枢纽,与 Kafka(采集传输)、TDengine(时序存储)、FastAPI(服务网关)共同构成完整数据平台底座。