yield_per()不能用于TB级增量抽取,因其仅模拟客户端分页而不改变SQL,仍导致全表扫描和OOM;应采用主键/时间戳游标分页+显式范围查询+每批独立事务。

SQLAlchemy的yield_per()为什么不能直接用于TB级增量抽取
它只是客户端侧的内存分页模拟,并不改变SQL本身——底层仍是SELECT * FROM table全表扫描,数据库仍需构建完整结果集并传输,网络和内存压力一点没少。对TB级表,yield_per(1000)可能让Python进程OOM,而数据库连接还卡在传输途中。
真正可行的路径是:**用主键/时间戳做游标分页 + 显式控制查询范围 + 每批独立事务**。
- 必须有单调递增、无空缺、高基数的字段(如自增
id或created_at) - 避免
OFFSET——它随偏移量增大越来越慢,TB级下完全不可用 -
yield_per()可配合游标使用,但仅作为单批内对象生成器,不是分页主力
基于主键范围的流式分批查询(推荐方案)
假设表有id主键,且已知最小值min_id=1、最大值max_id=2856749201,目标每批50万行:
batch_size = 500_000
current_id = min_id
while current_id <= max_id:
next_id = current_id + batch_size
stmt = select(MyTable).where(
MyTable.id >= current_id,
MyTable.id < next_id
)
# 注意:用 execution_options + yield_per 控制单批内存
result = session.execute(stmt.execution_options(yield_per=10_000))
for row in result.scalars():
process(row) # 处理单行
current_id = next_id
- 每次查询只拉取确定ID范围的数据,数据库可走主键索引,响应快、压力稳
-
execution_options(yield_per=...)作用于当前result,防止单批内对象堆积 - 务必在循环内显式调用
session.expunge_all()或新建session,否则ORM缓存持续膨胀 - 若主键不连续(如删过数据),可用
SELECT MIN(id), MAX(id)动态估算边界,或改用id > last_seen_id方式
基于时间戳的增量抽取(适用于日志/事件类表)
当表有updated_at字段且业务保证“更新只追加或严格单调”,可按时间窗口切片:
立即学习“Python免费学习笔记(深入)”;
start_time = datetime(2024, 1, 1)
end_time = start_time + timedelta(hours=1)
while start_time < datetime.now():
stmt = select(MyLog).where(
MyLog.updated_at >= start_time,
MyLog.updated_at < end_time
)
result = session.execute(stmt.execution_options(yield_per=5000))
for row in result.scalars():
emit_to_kafka(row) # 或写入目标存储
start_time, end_time = end_time, end_time + timedelta(hours=1)
- 时间窗口不宜过大(如单次查7天),否则可能跨分区、锁表或超时;1小时以内较稳妥
- 注意数据库时区与Python时区一致性,
updated_at字段类型应为TIMESTAMP WITH TIME ZONE或统一转UTC存储 - 若存在“迟到更新”(旧记录被修改),需额外维护
last_updated_window或结合双时间字段(created_at+updated_at)
连接与事务管理的关键细节
TB级抽取持续数小时,连接中断、事务过长、锁等待都极易发生。绕不开的实操约束:
- 禁用
autocommit=False默认行为——每批处理完立即session.commit(),释放锁和连接资源 - 使用
session.close()或上下文管理器重建session,避免长连接老化断开(尤其MySQL默认wait_timeout=28800) - 对PostgreSQL,加
stream_results=True参数(create_engine(..., connect_args={"options": "-c statement_timeout=30000"}))强制服务端流式返回 - 不要依赖
session.query().yield_per()老写法——SQLAlchemy 2.0+已弃用,统一用select().execution_options(yield_per=...)
最易被忽略的是:游标字段的索引必须存在且高效。没索引的WHERE updated_at > ?在TB表上等于全表扫描——再好的Python逻辑也救不回来。


















