Python如何批量导出数据库数据到Excel
作者:菩提风
很多人在做数据交付或者报表需求时,第一步想到的是:“写个SQL查出来,然后复制粘贴到Excel里”。一开始数据量小无所谓,等到几百张表、几十万行数据摆在面前,手动复制分分钟让人崩溃。后来我开始用Python写批量导出脚本,一套流程跑下来,几十张表的Excel文件自动生成,表头、格式、列宽、多Sheet一次搞定,省下来的时间足够喝三杯咖啡。这篇就说说我自己的实操思路和踩过的坑,从技术选型到完整代码,再到常见问题排查,尽量给你一条能直接用的路径。
整个方案的适用范围很广:给业务部门定期出数据、做线下分析、给客户提供数据交付、或者内部数据接口的兜底方案,都能用上。适合有Python基础但没怎么写过“数据库到Excel”这种批量同学们,也适合正在找更高效替代手工报表的开发者。
1. 整体思路:为什么选择Python + Excel作为数据交付方案
1.1 需求的真实场景
“批量导出数据库数据至Excel文件”听起来简单,真正落地的场景往往是一串连环需求。比如某个业务方每个月要一份订单明细表,数据量大概是50万行,字段50多个;另外还要按地区拆分成多个省份子表,每个子表还要单独附一个汇总Sheet;再比如一次导出12个月的数据,每月一个Excel文件,每个文件里又有多个Sheet。这些需求如果靠手动操作,不是“累”的问题,是根本完不成。
我遇到比较典型的还有一个:给一个下游系统做数据核对,对方要求把数据库里某一段时间的全量流水导出成Excel,按日期分文件,每个文件里按平台类型分Sheet,还要保证表头统一、字段顺序一致、数值不丢精度。这种任务本质上就是一条流水线:连接数据库 → 按规则取数 → 整理成结构化DataFrame → 写入Excel → 检查产物。
Python适合这个场景,是因为它正好覆盖了整个链路。pandas操作表格数据非常顺手,sqlalchemy或原生驱动做数据库连接很成熟,openpyxl又能对Excel做精细控制。三者配合,无论数据量是几千行还是几十万行,都能写出一套可复用脚本。
1.2 技术选型对比:Python、SQL直接导出、商业工具
很多人疑惑:数据库客户端不是能直接“导出结果集”成Excel吗?为什么还要用Python?
确实,像MySQL Workbench、Navicat这些工具都自带导出Excel功能。但它们有天然短板。第一,如果你要按多个维度拆分文件,比如按省份拆、按日期拆、按品类拆,这些工具只能导出一个大的结果集,拆分逻辑还得自己写;第二,Excel格式控制弱,列宽、表头颜色、冻结窗格、多级表头、公式,基本做不了;第三,难以自动化调度,你不可能每月手动用工具点几十次。
商业ETL工具(比如一些数据同步平台)倒是能做自动化,但太重,而且配置复杂,为了导几十个Excel去引一套平台,有点杀鸡用牛刀。Python脚本的好处是轻量、透明、可改、可复用,同时直接跟你的业务代码、定时任务、监控告警处在一个技术栈里。
1.3 方案架构与核心流程
我常用的设计模式很简单,把整件事拆成四层:
- 数据源层:负责数据库连接、查询执行。支持从MySQL、PostgreSQL、Oracle里取数,必要时走多条查询语句。
- 数据加工层:用pandas做字段筛选、类型转换、脱敏、拆分、汇总。这一步决定了导出来的是干净数据还是脏数据。
- 文件输出层:用openpyxl/pandas写出Excel,控制Sheet结构、样式、列宽、冻结窗格、筛选按钮等。
- 调度与校验层:给脚本加日志、输出文件清单、检查行数、校验非空和唯一值,方便排查问题。
这几层没必要做成复杂的类结构,但函数职责要分清楚。我曾经见过有人把查询、处理、保存全写在一个几百行的大函数里,改一个字段格式要翻半天,后来我坚持拆成独立函数,后面每次新需求只改配置或加新函数,舒服得多。
2. 环境准备与核心库选择
2.1 必需的Python库及其分工
先过一遍最核心的库,省得你到时候临时缺一个。
- pandas :数据处理核心,读数据库、操作DataFrame、调to_excel写文件,都靠它。基本是必须的。
- sqlalchemy :数据库连接抽象层,用create_engine生成连接引擎,配合pandas的read_sql_query特别方便。它也隔离了不同数据库的方言差异,比如你从MySQL切到PostgreSQL,改个连接串就行。
- openpyxl :Excel文件底层读写引擎,pandas写Excel时会用到它。你要做格式控制,比如调列宽、合并单元格、加颜色,就得直接操作openpyxl。
- PyMySQL / psycopg2 / cx_Oracle :具体数据库驱动,按你实际库选。MySQL用PyMySQL,PostgreSQL用psycopg2,Oracle用python-oracledb。
额外建议装一个 tqdm ,批量跑几十个Sheet时,进度条会让你知道是死循环还是正在跑,这很重要。
2.2 安装与版本注意事项
安装命令本身不复杂,但版本坑不少。
pip install pandas openpyxl sqlalchemy pymysql tqdm
我用得比较多的是Python 3.10到3.12,pandas版本在2.x以上,openpyxl在3.1.x。这几个版本搭配起来基本稳定。要提醒的是, pandas 2.x对openpyxl的依赖行为有变化 ,如果你之前用的pandas 1.x,升级后记得重新验证写Excel的代码,特别是datetime格式和列宽设置。
另外建议在虚拟环境里装,不要在系统Python里直接装一堆包。我吃过亏:一次给客户演示时,因为系统Python里有个旧版本的openpyxl,导致DataFrame写入时出现“AttributeError: 'Workbook' object has no attribute 'add_chart'”之类的怪错,后来查了半天,就是版本冲突。
2.3 数据库连接串的常见坑
用sqlalchemy时,连接串要写对。MySQL的经典格式是:
from sqlalchemy import create_engine
engine = create_engine(
"mysql+pymysql://用户名:密码@主机:3306/数据库名?charset=utf8mb4"
)
这里有几个地方容易翻车。
- 密码里有特殊字符(比如@、#、%),需要做URL转义,不然解析会错。最省事的方法是先用
urllib.parse.quote_plus把密码编码一下。 - 端口不是3306时,一定要在连接串里写明确。
- MySQL要显式加
charset=utf8mb4,否则中文可能会乱码,尤其是字段里有emoji的时候。 - 连接池和超时参数有时候也要调。对于长时间跑的大查询,建议加上
connect_args={"connect_timeout": 30, "read_timeout": 3600}之类,避免中途掉线。
3. 从数据库读数据:高效查询讲解
3.1 分批读取还是全量读取
很多新手一上来就是 pd.read_sql_query("select * from table", engine) ,然后发现内存爆炸或者跑半天没反应。这个问题的核心在于: pandas的read_sql_query会一次性把所有结果加载到内存 。如果你只是导出一张几万行的表,没事;如果是几十万行、字段又多,内存占用会非常难看。
处理方式有两种。
方式一:利用sqlalchemy的 server_side_cursor 或者pandas的 chunksize 参数。比如:
chunks = pd.read_sql_query(sql, engine, chunksize=20000)
for chunk in chunks:
process(chunk)
这种方式适合“读一批写一批”的场景,但要注意: chunksize的实现是分批从数据库游标取数,不是在内存里一次性加载 ,所以配合写入Excel能做到边读边写,不占太多内存。
方式二:如果只是导出,干脆让数据库先汇总好,尽量把复杂的聚合、排序、类型转换放在SQL里做,Python只负责接收结果。比如你SQL里 CAST(create_time AS DATE) ,就省得在Python里再转一次。
我自己的习惯是:先确认数据量级。量级在10万以下全量读没问题;10万到100万,视字段数量和内存情况分批读;超过100万,优先考虑导出CSV而非Excel(Excel单Sheet上限1048576行,超了也会出问题),或者按条件拆成多个文件导出。
3.2 查询语句优化
别小看这一步,SQL写得烂,脚本跑得再快也没用。几条实用建议:
- 只select你需要的字段,不要无脑
select *。50个字段里真正导出10个,剩下的全是内存垃圾。 - 分页导出时,不要用
limit offset,因为大偏移量会越来越慢。用where id > 上次最大id order by id limit 50000这种“滚动键”方式,效率天差地别。 - 如果不需要查询过程中数据变化,就开一个只读事务或对表加一致性的快照,避免导出到一半数据被更新。
- 对过滤字段建索引很重要,这属于数据库端的事,但在你写脚本的时候要记得提醒DBA。
比如我要按日期分段导出,SQL写成:
SELECT
order_id, user_id, product_id, amount, status, create_time
FROM
order_table
WHERE
create_time >= '2024-01-01'
AND create_time < '2024-02-01'
ORDER BY
create_time, order_id然后通过Python循环去拼每个月的查询,代码很容易维护。
3.3 数据类型转换与处理
数据库字段类型跟Excel类型不是一一对应的。最容易出问题的几个:
日期时间 :pandas读进来通常是 datetime64[ns] ,直接写到Excel是标准日期格式。但如果SQL里用了 varchar 存时间,或者数据库返回了字符串,一定要在Python里转一次,否则Excel显示的是文本,无法做日期筛选。
小数 :金额字段如果是 decimal 或者 numeric ,pandas会转成 float64 ,导致精度丢失。特别明显的例子是0.1 + 0.2这种二进制浮点误差。解决办法是读的时候把这类字段转成字符串,或者在SQL里用 CAST(amount AS CHAR) ,保证Excel里看到的是精确值。如果后续要运算,再用Decimal处理。
注意:金额字段直接转float写入Excel是大忌,尤其是财务对账场景,浮点误差可能让数字对不上。
大整数 :某些ID字段是BIGINT,超过15位,写入Excel会被自动转成科学计数法,显示成 1.23457E+14 。解决办法是先把ID转成字符串,或者用openpyxl设置单元格格式为文本。
这些转换逻辑建议写成一个函数,统一处理DataFrame,不要散落在各处。
4. 写入Excel:性能与格式控制
4.1 pandas.to_excel 与 openpyxl 的取舍
pandas自带 to_excel 很方便,但玩过的人都知道,它对格式的控制太弱。比如你想设置列宽、冻结表头、给表头加底色,pandas做不到。所以我的组合拳是: 先用pandas的to_excel或者通过pandas直接生成数据,再用openpyxl打开文件调整格式 。
示例套路:
df.to_excel(writer, sheet_name="订单明细", index=False) # 然后用 writer.book 或直接加载文件调整格式
如果你只写一个Sheet、数据量不大,直接用pandas很舒服。但如果你要批量生成多Sheet、带样式,建议用openpyxl自己操作单元格,虽然代码略多,但可控性最强。
4.2 多Sheet批量写入
多Sheet的处理,核心是全局写一个ExcelWorkbook,再逐表填充。
with pd.ExcelWriter("订单数据.xlsx", engine="openpyxl") as writer:
for name, df in sheet_items:
df.to_excel(writer, sheet_name=name, index=False)
这里要注意Sheet命名不要超过31个字符,不能用 \/?*[] 这些特殊字符,否则openpyxl直接报错。还有如果Sheet名重复,需要先判断 writer.sheets 里有没有同名,有就要加后缀。
另一个常见需求是“每个Sheet里先放汇总统计,再放明细”。这样我们可以把汇总DataFrame和明细DataFrame写到同一个Sheet的不同起始行,用 startrow 参数控制:
df_summary.to_excel(writer, sheet_name="数据汇总", startrow=0, index=False) df_detail.to_excel(writer, sheet_name="数据汇总", startrow=len(df_summary)+3, index=False)
中间空两行,看起来更清爽。
4.3 格式化:列宽、冻结窗格、表头样式
这步往往是让交付成果“看起来专业”的关键。用openpyxl实现:
from openpyxl.utils import get_column_letter
wb = writer.book
ws = writer.sheets["订单明细"]
# 设置列宽
for i, col in enumerate(df.columns, start=1):
max_len = max(df[col].astype(str).map(len).max(), len(col)) + 2
ws.column_dimensions[get_column_letter(i)].width = min(max_len, 40)
# 冻结首行
ws.freeze_panes = "A2"
# 表头样式
from openpyxl.styles import Font, PatternFill, Alignment
for cell in ws[1]:
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="DDEBF7", end_color="DDEBF7", fill_type="solid")
cell.alignment = Alignment(horizontal="center", vertical="center")
这段几乎是标配。一个经验: 列宽不要直接按数据最大字符串长度设 ,因为中文字符占两个字节,如果用正则或astype是统计字符数,中文长的列会偏窄。更好的办法是把中文字符按长度×2去估算,然后clamp到合理的宽度范围。
再有就是给全表加自动筛选:
ws.auto_filter.ref = ws.dimensions
这样业务方拿到的文件每一列都能直接下拉筛选,非常实用。
5. 批量导出的完整代码实现
5.1 函数设计
不整复杂框架,我习惯把核心逻辑封装成三个函数:
get_engine():创建数据库引擎。fetch_data(sql, engine):执行SQL,返回DataFrame,并做类型修正。write_excel_with_style(filepath, sheet_specs):按Sheet配置写Excel并加样式。run_export(config):编排整个流程,带日志和进度提示。
其中 sheet_specs 是这样一个列表结构:
sheet_specs = [
{"name": "订单明细", "df": df_orders},
{"name": "商品汇总", "df": df_products},
]
这样后续新增/修改Sheet都特别方便。
5.2 完整代码示例
下面给一个可直接改改就能跑的版本。以MySQL为例,导出最近三个月订单数据,按月份拆成3个文件,每个文件里包含订单明细和每日汇总两个Sheet。
import pandas as pd
from sqlalchemy import create_engine
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, PatternFill, Alignment
from pathlib import Path
from tqdm import tqdm
def get_engine():
# 注意密码特殊字符需要转义,这里用 quote_plus
from urllib.parse import quote_plus
user = "your_user"
password = quote_plus("your_pass_with_@special")
host = "10.0.0.10"
port = "3306"
dbname = "your_db"
engine = create_engine(
f"mysql+pymysql://{user}:{password}@{host}:{port}/{dbname}?charset=utf8mb4",
connect_args={"connect_timeout": 30, "read_timeout": 3600},
pool_recycle=3600,
)
return engine
def fetch_data(sql, engine):
df = pd.read_sql_query(sql, engine)
# 统一类型修正:整数ID转字符串,避免科学计数法
for col in df.columns:
if "id" in col.lower() or "code" in col.lower():
df[col] = df[col].astype(str)
return df
def write_excel_with_style(filepath, sheet_specs):
with pd.ExcelWriter(filepath, engine="openpyxl") as writer:
for spec in sheet_specs:
df = spec["df"]
sheet_name = spec["name"][:31] # 截断太长名字
df.to_excel(writer, sheet_name=sheet_name, index=False)
ws = writer.sheets[sheet_name]
# 设置列宽
for i, col in enumerate(df.columns, start=1):
# 中文按2个字符宽度估算
col_len = len(col) * 2
max_len = max(
df[col].astype(str).map(lambda x: sum(2 if ord(c) > 127 else 1 for c in x)).max(),
col_len
) + 2
ws.column_dimensions[get_column_letter(i)].width = min(max_len, 40)
# 冻结首行
ws.freeze_panes = "A2"
# 表头样式
for cell in ws[1]:
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="DDEBF7", end_color="DDEBF7", fill_type="solid")
cell.alignment = Alignment(horizontal="center", vertical="center")
# 开启筛选
ws.auto_filter.ref = ws.dimensions
def run_export():
engine = get_engine()
export_dir = Path("./export")
export_dir.mkdir(exist_ok=True)
months = ["2024-01-01", "2024-02-01", "2024-03-01"]
for i in range(len(months) - 1):
start = months[i]
end = months[i + 1]
year_month = start[:7] # 2024-01
print(f"正在处理 {year_month} ...")
# 取明细数据
sql_detail = f"""
SELECT order_id, user_id, product_id, amount, status, create_time
FROM order_table
WHERE create_time >= '{start}' AND create_time < '{end}'
ORDER BY create_time
"""
df_detail = fetch_data(sql_detail, engine)
if df_detail.empty:
print(f"{year_month} 无数据,跳过")
continue
# 取每日汇总
sql_summary = f"""
SELECT DATE(create_time) AS day, COUNT(*) AS order_cnt, SUM(amount) AS total_amount
FROM order_table
WHERE create_time >= '{start}' AND create_time < '{end}'
GROUP BY DATE(create_time)
"""
df_summary = fetch_data(sql_summary, engine)
filepath = export_dir / f"订单数据_{year_month}.xlsx"
write_excel_with_style(filepath, [
{"name": "每日汇总", "df": df_summary},
{"name": "订单明细", "df": df_detail},
])
print(f"已生成 {filepath}")
if __name__ == "__main__":
run_export()
这段代码的核心是 SQL即配置 :你要调整导出的日期范围、字段、汇总逻辑,只需要改SQL和循环列表。以后再遇到类似需求,直接复制改一改就能用。
5.3 运行效果说明
跑完之后,export目录下会生成类似这样的文件:
订单数据_2024-01.xlsx
订单数据_2024-02.xlsx
订单数据_2024-03.xlsx
每个文件打开后,“每日汇总”Sheet在最前面,方便快速看整体;后面跟着“订单明细”,冻结首行,表头带底色,每列都有筛选按钮。间隔两行,也没挤在一起,视觉上很清爽。
我实际跑过几十万行、20个字段的导出,整体时间主要耗在数据库查询和Excel写入上。如果遇到超大的Sheet,可以考虑用 xlsxwriter 引擎,写入速度比openpyxl快一些,但格式控制API略有不同。这篇我用openpyxl,因为它后来调整样式更灵活。
6. 常见问题与排查技巧
6.1 内存溢出与超时
症状:脚本跑到一半进程被杀,或者数据库报 Lock wait timeout exceeded 。
排查方向:
- 先用
EXPLAIN看SQL有没有走索引,没索引的大查询很可能把数据库拖垮。 - 如果单表数据量大,缩小查询范围,比如按小时或按ID段分批导出。
- 在代码中打印日志,看卡在哪一步。我一般会在读取前、读取后、写Excel前分别打一条带有行数、内存占用的日志。
- 如果你的服务器内存只有4G,还非要一次读100万行,那是配置问题。要么分批,要么换机器。
内存管理还有一个细节: 用完了的DataFrame要及时del,必要时gc.collect() 。在循环里跑几十张表,上一轮残留的大DataFrame会占用大量内存,导致后续越来越慢。
6.2 数据含特殊字符或时间格式乱掉
这种情况很常见。比如SQL查询出来某个字符串字段里包含了换行符 \n ,写进Excel后一个单元格会显示成多行。如果你不希望它换行,就要清洗掉:
df["备注"] = df["备注"].str.replace("\n", " ").replace("\r", " ")
还有时间格式问题:pandas读出来是 2024-01-01 00:00:00 ,但Excel打开后显示成 2024/1/1 0:00 ,或者变成一串数字。解决办法是写Excel之前把日期列转成字符串,比如 df["date"] = df["date"].dt.strftime("%Y-%m-%d") 。虽然Excel里变成文本,但至少显示正常,不会乱变。如果你希望它是真日期格式,可以用openpyxl的 number_format 设置单元格格式为 yyyy-mm-dd hh:mm:ss ,这样两种需求都能满足。
Excel的单元格本身还有一个限制:单个单元格最多显示32767个字符,超过会截断。如果字段里存了大段文本(比如长描述),要做好预处理或明确提示。
6.3 连接中断和权限问题
批量导出时间长,数据库连接很容易中途断开。解决思路:
- 脚本里做重试机制,比如查询失败后等待几秒重试三次。
- 连接串加
pool_pre_ping=True,sqlalchemy会在取连接前检查连接是否有效。 - 确认数据库账号有足够权限。有些只读账号可能只给了SELECT权限,没问题;但如果你用了
read_sql_table或者某些特殊功能,可能需要额外权限。
权限问题最常见的报错是 pymysql.err.OperationalError: (1044, "Access denied for user ...") ,这种要去找DBA开权限。另外注意生产环境数据库导出尽量走只读副本或者从库,别去压主库,不然业务投诉迟早找上门。
排查这类问题,我的经验是先看报错栈里是SQLAlchemy层还是PyMySQL层,一般连接问题在 pymysql.err ,权限问题在 sqlalchemy.exc.OperationalError ,定位思路完全不同。
最后分享一个我自己用得很顺的小技巧: 每次导出前,先写一个SQL统计各表行数 ,然后跟导出后的Excel行数做比对。这个校验逻辑虽然只有几行代码,但能避免“文件生成了但数据少了一半”这类隐患。数据交付这种事,信任建立起来很慢,崩塌却只要一次,校验永远不嫌多。
如果你也需要处理类似的批量导出任务,这套思路基本可以直接落地。先从最简单的单表导出开始,跑通后再叠加格式、拆分、多Sheet,一步步来。真遇到问题,多打日志,多拆函数,比纠结一行搞定的“炫技代码”要可靠得多。
到此这篇关于Python如何批量导出数据库数据到Excel的文章就介绍到这了,更多相关Python导出数据库数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
