to_sql()默认覆盖写入(if_exists='replace'),但多数场景应选追加(if_exists='append');追加不校验重复,易主键冲突;replace会丢索引约束;chunksize和dtype未调优是性能崩盘主因;需显式设dtype、合理设chunksize=1000、生产开pool_pre_ping。

直接说结论:to_sql() 默认是覆盖写入(if_exists='replace'),但绝大多数场景该用追加(if_exists='append');不设 chunksize 或乱设 dtype 是性能崩盘最常见原因。
追加写入时为什么数据重复或报主键冲突?
因为 to_sql() 的 if_exists='append' 完全不校验目标表里是否已有相同记录——它只管把 DataFrame 逐行/逐块插进去,像倒沙子一样。
- 主键或唯一索引冲突?说明你没在入库前去重,或者数据库已有同主键旧数据
- 想“存在则更新”?
to_sql()原生不支持 upsert,得换pd.DataFrame.to_sql(..., method='multi')配合自定义 SQL,或改用sqlalchemy的on_conflict_do_update(PostgreSQL)或REPLACE INTO(MySQL) - 临时解法:入库前用
df.drop_duplicates(subset=['id']),或先DELETE FROM table WHERE id IN (...)再 append
覆盖写入(replace)的坑比想象中多
if_exists='replace' 看似省事,实际会先 DROP TABLE 再重建——这意味着:索引、约束、注释、权限全部丢失,下游依赖可能当场报错。
- PostgreSQL 下还会顺手删掉序列(
serial列的自增计数器),新表从 1 开始计数 - MySQL 5.7+ 虽保留 AUTO_INCREMENT 值,但表重建期间锁表时间长,大表操作容易超时
- 真正安全的“覆盖”做法是:先
truncate table(清空+重置自增),再append;需手动执行con.execute("TRUNCATE TABLE mytable")
性能差八成是因为 chunksize 和 dtype 没调对
默认不设 chunksize 会让 pandas 把整个 DataFrame 拼成一条巨长 INSERT,数据库解析压力大;不传 dtype 则 pandas 自推类型常偏宽泛(比如全用 TEXT),导致索引失效、查询变慢。
-
chunksize=1000是较稳的起点;超过 5000 行/块易触发 MySQL 的max_allowed_packet错误 -
dtype必须显式传:比如{'user_id': sqlalchemy.Integer(), 'created_at': sqlalchemy.DateTime()},避免 pandas 把整数列存成TEXT - SQLite 下
chunksize影响不大,但dtype仍要设,否则所有列都是TEXT,排序/范围查询全失效
SQLAlchemy 连接要不要加 echo=True 或 pool_pre_ping?
调试时开 echo=True 能看到每条生成的 INSERT,但上线必须关——日志量爆炸且拖慢 20%+;而 pool_pre_ping=True 对长连接池很关键,否则数据库连接超时断开后,to_sql() 会直接抛 OperationalError。
- 开发阶段:加
echo=True查 SQL 是否符合预期(比如有没有意外触发CAST) - 生产环境:必开
pool_pre_ping=True,并设pool_recycle=3600(避免连接过期) - 别信 “连接池自动重连”,
to_sql()不走常规 execute 流程,异常处理更脆弱
最常被忽略的一点:to_sql() 的 index 参数默认为 False,但如果你的 DataFrame 有有意义的索引(比如时间戳),设成 index=True 会自动作为一列写入——这列默认叫 index,名字丑还容易和业务字段冲突,务必用 index_label='ts' 显式命名。


















