前言
先说一个标题里的不准确之处:pandas 本身并不是一个 MySQL 驱动,也没有自己的 数据库连接能力 。它自己不会连 MySQL,所谓"用 pandas 操作 MySQL", 实际是 pandas 把这件事委托给别的库:要么交给一个 DBAPI 驱动(比如 pymysql), 要么交给 SQLAlchemy 建立连接再交给驱动执行。
所以"两种方式"其实准确的说法是:
- 驱动直连方式 :用
pymysql自己建连接、自己执行 SQL、自己取结果,
想变 DataFrame 再手动构造;
- pandas 方式 :用
pandas.read_sql/DataFrame.to_sql,
背后由 SQLAlchemy + 驱动(MySQL 场景下通常还是 pymysql)干活。
两者不是对立的,而是"底层"和"上层封装"的关系。本文分别演示,并说明各自适用场景。 需要强调:pymysql 与 pandas 都是第三方库,本机没有安装、也没有 Python 解释器, 示例无法运行验证,只能逐行人工推演。参数细节请以 pymysql 与 Pandas 官方文档为准。
一、方式一:pymysql 直连
pymysql.connect 的签名里,连接参数都是仅关键字 的(关键字参数), 常用的是 host、user、password、database、port、charset。 中文场景建议 charset="utf8mb4",否则 emoji 和部分生僻字会丢。
python
# 需要 pip install pymysql;Python 3.8+
import pymysql
conn = pymysql.connect(
host="127.0.0.1",
user="app",
password="secret",
database="shop",
port=3306,
charset="utf8mb4",
)
try:
with conn.cursor() as cur:
cur.execute("SELECT id, name FROM users WHERE age > %s", (18,))
rows = cur.fetchall() # 返回元组的元组
columns = [d[0] for d in cur.description]
conn.commit()
finally:
conn.close()
print(columns)
print(rows[:3])
要点:
cursor.execute(sql, params)里的%s是参数占位符(不是字符串拼接),
参数用元组或列表传入,由驱动负责转义------这是防 SQL 注入的正确姿势。
cur.description里每项的第一个元素是列名,可用来拿列头。fetchall()取全部、fetchone()取一行、fetchmany(n)取 n 行。- 增删改之后要
conn.commit(),否则事务不落库;默认autocommit=False。 conn.cursor()也支持with,退出时自动关闭游标。
把结果转成 DataFrame 只是把 columns 和 rows 组装起来:
python
# 需要 pip install pymysql pandas
import pandas as pd
df = pd.DataFrame(rows, columns=columns)
因为 fetchall() 返回的是元组的元组,DataFrame(rows, columns=columns) 正好能吃下这个形状。数据量很大时,一次性 fetchall() 会把全部结果读进内存, 此时更该用 pandas 的 chunksize 分批读。
二、方式二:pandas + SQLAlchemy
pandas 官方文档给出的连接方式是 SQLAlchemy。MySQL 场景下, 连接串形如 mysql+pymysql://用户:密码@主机/库名,+ 后面就是驱动名。
python
# 需要 pip install pandas sqlalchemy pymysql
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine(
"mysql+pymysql://app:secret@127.0.0.1:3306/shop?charset=utf8mb4"
)
# 参数化查询:用 params,别自己拼字符串
df = pd.read_sql(
"SELECT id, name FROM users WHERE age > %(age)s",
engine,
params={"age": 18},
)
print(df.head())
read_sql(sql, con, ...) 的第一个参数是 SQL、第二个才是连接,别写反 。 它的 params 支持序列或字典两种占位风格,具体用哪种取决于驱动的 paramstyle。
写回数据库用 to_sql:
python
# pandas 1.0+
df.to_sql("users_snapshot", engine, if_exists="append", index=False)
if_exists 可取 "fail"(默认,表已存在就报错)、"replace"(覆盖)、 "append"(追加)。index=False 防止把行号当成一列写进去。
| 对比项 | pymysql 直连 | pandas + SQLAlchemy |
|---|
|----|--------------|------------------------------|
| 依赖 | 只要 pymysql | pandas + sqlalchemy + 驱动 |
|------|----------|-----------------|
| 结果形态 | 元组,需自己组装 | 直接是 DataFrame |
|----|-----------|-----------|
| 适用 | 精细控制事务、游标 | 快速取数、直接分析 |
|----|--------------|-------------|
| 写入 | 自己拼 INSERT | to_sql 一步 |
|-----|-----------|-----------|
| 参数化 | %s + 元组 | params= |
三、该选哪一种
- 只要"把表取出来分析",用
read_sql,代码最短。 - 需要精细事务控制、逐行处理、调用存储过程,用
pymysql直连。 - 两者也可以混用:pandas 取数做分析,pymysql 负责小而频繁的写入。
这里有个细节值得特别说明:pandas 官方文档在"没有 SQLAlchemy 时怎么办"这一节里, 给出的回退方案是使用 sqlite3.Connection(标准库自带的 SQLite 驱动), 并没有承诺接受任意 DBAPI 驱动对象 。因此在 MySQL 场景下, 把 pymysql 的原始连接直接丢给 read_sql 并不是文档推荐的路径; 稳妥做法仍然是通过 SQLAlchemy 建 engine,再把 engine 交给 pandas。 如果你在旧代码里见到 pd.read_sql(sql, pymysql_conn),迁移到 engine 写法更安全。 engine 还有一个附带好处:连接池由 SQLAlchemy 统一管理,不必每次手动开关连接。
四、把结果做成一张汇总
python
# 需要 pip install pandas sqlalchemy pymysql
import pandas as pd
from sqlalchemy import create_engine
def load(engine, days: int) -> pd.DataFrame:
sql = (
"SELECT region, amount, created_at FROM orders "
"WHERE created_at >= DATE_SUB(NOW(), INTERVAL %(days)s DAY)"
)
return pd.read_sql(sql, engine, params={"days": days})
if __name__ == "__main__":
eng = create_engine("mysql+pymysql://app:secret@127.0.0.1:3306/shop")
data = load(eng, 30)
print(data.groupby("region", as_index=False)["amount"].sum())
常见坑点
- 把用户输入拼进 SQL 字符串
❌ cur.execute("SELECT * FROM users WHERE name='%s'" % name), 遇到引号就注入,是严重安全问题 ✅ cur.execute("SELECT * FROM users WHERE name=%s", (name,)),交给驱动转义
read_sql的参数顺序写反
❌ pd.read_sql(engine, "SELECT ..."),把连接当成 SQL ✅ pd.read_sql("SELECT ...", engine)
- 忘记
commit
❌ 插入后直接 close(),数据库里查不到新数据 ✅ 增删改后 conn.commit()(或理解 autocommit 的含义)
- 字符集没设
utf8mb4
❌ 存 emoji 或部分中文时报错/变成问号 ✅ charset="utf8mb4",连接串里也写成 ?charset=utf8mb4
fetchall()拉大表
❌ 千万行结果一次性读进内存,进程被撑爆 ✅ 用 read_sql(..., chunksize=...) 分批,或 LIMIT 先取样本
to_sql默认if_exists="fail"
❌ 第二次运行就报"表已存在",还以为代码坏了 ✅ 明确写 if_exists="append" 或 "replace"
- 连接串里写明文密码并提交进仓库
❌ 把 mysql+pymysql://root:真实密码@... 直接写进代码 ✅ 密码从环境变量或配置文件读取,敏感信息不进版本库
- 用 pandas 时以为它自带驱动
❌ 只装了 pandas 就报"缺少模块 pymysql"/"需要 SQLAlchemy" ✅ 记住 pandas 只是上层,必须另装 SQLAlchemy 和驱动器
总结
| 主题 | 结论 |
|---|
|--------|-------------------------------------------------------|
| 标题中的说法 | pandas 不是数据库驱动,"pandas 方式"实为 pandas + SQLAlchemy + 驱动 |
|------|---------------------------------------------------|
| 直连方式 | pymysql.connect + cursor.execute + fetchall |
|-----------|---------------------------------------------|
| pandas 方式 | read_sql(sql, con, params=...) / to_sql |
|------|--------------|
| 安全要点 | 一律参数化,绝不拼字符串 |
|------|-----------------------------|
| 内存要点 | 大结果集用 chunksize 或 LIMIT |
|------|---------------------|
| 事务要点 | 增删改要 commit,注意字符集 |
掌握这两种方式的关键,是理解"分层":pymysql 管连接和事务,pandas 管取数和分析。 把密码、字符集、参数化这三件小事做对,剩下的就是写 SQL 和拼报表了。