perf: enable fast_executemany and eliminate duplicate Excel reads
- Set cursor.fast_executemany = True (pyodbc 5.x moved it to cursor) - merge_sources() now returns source row counts, avoiding re-reading files - Single commit at end of batch_insert instead of per-batch commits - Pre-build all rows before SQL execution - Result: PG_2022 migration improved from ~5min to ~36s (7.6x faster) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
48
migrate.py
48
migrate.py
@@ -129,12 +129,13 @@ def merge_sources(sources, excel_dir, primary_key, cfg):
|
||||
"""合并多个工作表的数据。
|
||||
- 列名取并集(按出现顺序)
|
||||
- 数据按主键去重,后读覆盖先读
|
||||
返回 (unified_headers, actual_pk_col, merged_data_ordered_dict)
|
||||
返回 (unified_headers, actual_pk_col, merged_data_ordered_dict, source_row_counts)
|
||||
"""
|
||||
all_headers = [] # 保持顺序的列名并集
|
||||
header_set = set()
|
||||
merged = OrderedDict() # key=主键值, value=dict
|
||||
resolved_pk = primary_key # 实际使用的主键列名
|
||||
source_row_counts = [] # 各源的行数 [(file, sheet, count), ...]
|
||||
|
||||
for src in sources:
|
||||
file_path = os.path.join(excel_dir, src['file'])
|
||||
@@ -142,6 +143,7 @@ def merge_sources(sources, excel_dir, primary_key, cfg):
|
||||
|
||||
headers, actual_pk_col, data = read_sheet(file_path, sheet_name, primary_key, cfg)
|
||||
resolved_pk = actual_pk_col
|
||||
source_row_counts.append((src['file'], src['sheet'], len(data)))
|
||||
|
||||
# 扩展统一列名集
|
||||
for h in headers:
|
||||
@@ -159,7 +161,7 @@ def merge_sources(sources, excel_dir, primary_key, cfg):
|
||||
else:
|
||||
merged[pk_val] = record
|
||||
|
||||
return all_headers, resolved_pk, merged
|
||||
return all_headers, resolved_pk, merged, source_row_counts
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -169,7 +171,8 @@ def merge_sources(sources, excel_dir, primary_key, cfg):
|
||||
def get_connection(cfg):
|
||||
"""根据配置建立 SQL Server 连接"""
|
||||
conn_str = get_connection_string(cfg)
|
||||
return pyodbc.connect(conn_str, autocommit=False)
|
||||
conn = pyodbc.connect(conn_str, autocommit=False)
|
||||
return conn
|
||||
|
||||
|
||||
def create_schema(cursor, schema_name):
|
||||
@@ -383,7 +386,7 @@ TYPE_CONVERTERS = {
|
||||
def batch_insert(cursor, table_name, headers, data_dict, cfg, actual_pk):
|
||||
"""批量插入数据到 SQL Server。"""
|
||||
schema = cfg['schema']
|
||||
batch_size = cfg.get('batch_size', 500)
|
||||
batch_size = cfg.get('batch_size', 5000)
|
||||
type_rules = cfg['type_rules']
|
||||
full_name = f'[{schema}].[{table_name}]'
|
||||
|
||||
@@ -393,31 +396,30 @@ def batch_insert(cursor, table_name, headers, data_dict, cfg, actual_pk):
|
||||
placeholders = ', '.join(['?'] * len(headers))
|
||||
insert_sql = f"INSERT INTO {full_name} ({col_list}) VALUES ({placeholders})"
|
||||
|
||||
batch = []
|
||||
total = 0
|
||||
# 预构建所有行
|
||||
all_rows = []
|
||||
for pk, record in data_dict.items():
|
||||
row = []
|
||||
for h in headers:
|
||||
val = record.get(h)
|
||||
if isinstance(val, str) and val.strip() == '':
|
||||
val = None
|
||||
# 强类型列清洗
|
||||
if h in typed_cols and val is not None:
|
||||
elif h in typed_cols and val is not None:
|
||||
converter = TYPE_CONVERTERS[typed_cols[h]]
|
||||
val = converter(val)
|
||||
row.append(val)
|
||||
batch.append(row)
|
||||
total += 1
|
||||
all_rows.append(row)
|
||||
|
||||
total = len(all_rows)
|
||||
|
||||
# 启用 fast_executemany(pyodbc 5.x 在 cursor 上设置)
|
||||
cursor.fast_executemany = True
|
||||
|
||||
# 分批 executemany,最后统一 commit
|
||||
for i in range(0, total, batch_size):
|
||||
cursor.executemany(insert_sql, all_rows[i:i + batch_size])
|
||||
|
||||
if len(batch) >= batch_size:
|
||||
cursor.executemany(insert_sql, batch)
|
||||
cursor.commit()
|
||||
batch = []
|
||||
|
||||
if batch:
|
||||
cursor.executemany(insert_sql, batch)
|
||||
cursor.commit()
|
||||
|
||||
return total
|
||||
|
||||
|
||||
@@ -435,13 +437,13 @@ def migrate_table(cursor, table_config, cfg, dry_run=False, use_truncate=False):
|
||||
logger.info(f"迁移 {table_name} ...")
|
||||
|
||||
# 1. 读取并合并数据
|
||||
headers, actual_pk, merged = merge_sources(sources, excel_dir, primary_key, cfg)
|
||||
headers, actual_pk, merged, source_row_counts = merge_sources(
|
||||
sources, excel_dir, primary_key, cfg
|
||||
)
|
||||
|
||||
# 打印各源行数
|
||||
for src in sources:
|
||||
fpath = os.path.join(excel_dir, src['file'])
|
||||
_, _, data = read_sheet(fpath, src['sheet'], primary_key, cfg)
|
||||
logger.info(f" 读取: {src['file']} > {src['sheet']} → {len(data):,} 行")
|
||||
for file_name, sheet_name, count in source_row_counts:
|
||||
logger.info(f" 读取: {file_name} > {sheet_name} → {count:,} 行")
|
||||
|
||||
logger.info(f" 合并去重后: {len(merged):,} 行, {len(headers)} 列 (主键列: {actual_pk})")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user