asyncpg写入慢的根源在于误用execute()而非executemany();应改用executemany()批量插入、合理配置连接池(min_size=CPU核数×2)、分批控制事务粒度(每批5000–20000行),超大规模场景启用copy_records_to_table()。

asyncpg 写入慢?先确认是不是用了 execute() 而不是 executemany()
单条 execute() 插入在 asyncpg 中本质是逐条网络往返,哪怕加了 await 也扛不住千万级数据。真正能提速的是 executemany() —— 它把多行参数批量打包成一条 PostgreSQL 协议消息,服务端一次性解析执行。实测 10 万行插入,executemany() 比循环调用 execute() 快 8–12 倍。
注意两点:
-
executemany()不返回每条语句的返回值(比如RETURNING),只返回最终影响行数;需要返回值必须换用fetch()+UNION ALL或分批execute() - 参数必须是 list of tuples 或 list of dicts,不能是生成器——asyncpg 会一次性读取全部参数进内存,所以别传
iter(...),否则报TypeError: executemany() argument 2 must be a sequence - 推荐写法:
await conn.executemany("INSERT INTO t (a,b) VALUES ($1, $2)", batch_data),其中batch_data是[(1,'x'), (2,'y'), ...]
为什么开了连接池还是卡在 IO?检查 min_size 和 max_size 是否合理
asyncpg 的 create_pool() 默认 min_size=10、max_size=10,看似够用,但实际写入密集场景下,连接池容易成为瓶颈:所有写请求排队等连接,反而抵消异步优势。
调优建议:
立即学习“Python免费学习笔记(深入)”;
- 写入为主的服务,把
min_size设为cpu_count * 2(例如 8 核设 16),避免冷启动时频繁建连 -
max_size别盲目拉高,PostgreSQL 的max_connections有硬上限;建议按公式max_size ≤ (pg_max_connections − reserved_for_admin) / number_of_app_instances计算 - 务必设置
max_queries=50000(默认 50000)和max_inactive_time=300(秒),防连接长期空闲被 pg kill 导致InterfaceError: connection is closed
批量太大内存爆了?用 chunk_size 分片 + transaction 控制粒度
asyncpg 对单次 executemany() 的参数数量没硬限制,但 Python 进程内存和 PostgreSQL 的 work_mem 会双双告急。100 万行一次塞进去,很可能触发 MemoryError 或 pg 报 ERROR: out of memory。
安全做法是手动分片:
- 每批控制在 5000–20000 行(取决于单行数据大小),例如:
for i in range(0, len(data), 10000): batch = data[i:i+10000]; await conn.executemany(...) - 关键:每个 batch 包在显式事务里——
async with conn.transaction(): await conn.executemany(...)。不加事务的话,每批都是独立提交,WAL 日志刷盘太频繁,反而拖慢整体速度 - 避免在事务中混用 DDL(如
CREATE INDEX)或长耗时查询,事务持有时间越长,锁表风险越高
为什么 INSERT 还是比 COPY 慢?该上 copy_records_to_table() 了
当数据已加载到内存(比如从 CSV 解析完),executemany() 是通用解法;但若目标是极致吞吐(比如导入日志、ETL 场景),copy_records_to_table() 才是 asyncpg 的隐藏王者——它底层复用 PostgreSQL 的二进制 COPY 协议,绕过 SQL 解析,实测比 executemany() 快 3–5 倍。
使用前提和注意点:
- 数据必须是 list of tuples 或 iterator of tuples(支持流式,不全加载进内存)
- 字段顺序必须严格匹配表定义顺序,不支持列名映射;如有
serial或default字段,需显式传None或默认值 - 不走触发器(
BEFORE INSERT等),也不校验CHECK约束(除非开启force_row_order=False,但会降速) - 示例:
await conn.copy_records_to_table('my_table', records=my_data, columns=['id','name'])
千万级写入真正的难点不在语法,而在平衡内存、事务粒度、连接负载和 PostgreSQL 侧配置;漏掉任意一环,异步就退化成“看着像并发”的串行假象。


















