问题现象与背景
在使用alembic执行数据库迁移时,op.bulk_insert()或单条op.execute()插入数据时经常遇到IntegrityError: (pymysql.err.IntegrityError) (1062, "Duplicate entry 'X' for key 'PRIMARY'")错误。这种主键冲突问题在以下场景尤为常见:
- 重复执行包含insert操作的迁移脚本
- 批量插入包含重复主键的数据
- 多服务器并行执行迁移时产生竞争条件
根本原因分析
通过分析MySQL错误代码1062和PostgreSQL的23505错误,发现主要成因包括:
- 幂等性问题:迁移脚本缺乏重复执行保护机制
- 数据源问题:CSV或JSON导入数据包含重复主键
- 并发控制缺失:分布式系统同时执行迁移
- 自增ID混乱:手动指定ID与自增序列冲突
7种解决方案对比
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| IGNORE语法 | INSERT IGNORE INTO table ... |
MySQL简单场景 |
| ON DUPLICATE UPDATE | INSERT ... ON DUPLICATE KEY UPDATE col=VALUES(col) |
需要更新已有记录 |
| 条件插入检查 | 先SELECT判断存在性再插入 | 所有数据库通用 |
| 事务回滚 | 捕获异常后执行ROLLBACK | 需要原子性操作 |
| UPSERT语法 | PostgreSQL的INSERT ... ON CONFLICT |
PostgreSQL 9.5+ |
| 临时表交换 | 先插入临时表再MERGE | 大数据量场景 |
| alembic版本控制 | 使用op.get_bind()精细控制 |
复杂迁移场景 |
最佳实践示例
def upgrade():
# 方案5:PostgreSQL的UPSERT实现
op.execute("""
INSERT INTO users (id, name)
VALUES (1, 'Alice'), (2, 'Bob')
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name
""")
# 方案3:通用条件插入
conn = op.get_bind()
if not conn.execute("SELECT 1 FROM products WHERE id=100").scalar():
op.bulk_insert('products', [{'id':100, 'name':'Laptop'}])
性能优化建议
处理大批量数据插入时应注意:
- 将批量操作放入单个事务减少提交次数
- 对百万级数据使用LOAD DATA INFILE替代INSERT
- 适当调整bulk_insert的batch_size参数(建议500-2000)
- 考虑禁用外键检查和唯一约束临时提升性能
监控与调试技巧
当问题发生时推荐采用以下诊断方法:
- 使用
alembic -x verbose=true upgrade查看详细SQL - 通过
op.get_context().config.print_stdout输出调试信息 - 检查alembic_version表确认迁移历史
- 在测试环境使用
--sql参数生成SQL而不执行