必须用read_sql配合params防注入,to_sql不支持参数化;con必须为SQLAlchemy Engine实例,占位符仅适用WHERE值替换,表名列名等须白名单校验。

必须用 read_sql 配合 params,不能拼接字符串;to_sql 本身不支持参数化,别在这儿找“参数化选项”
read_sql 的 con 参数必须是 SQLAlchemy Engine,不是原始 connection
很多人卡在 TypeError: read_sql() missing 1 required positional argument: 'con',其实是传了 sqlite3.Connection 或 psycopg2.connection 这类原生连接对象。Pandas 的 read_sql 明确要求 con 是 sqlalchemy.Engine 实例。
- 错误写法:
conn = sqlite3.connect("db.db"); pd.read_sql("SELECT * FROM t", conn) - 正确写法:
from sqlalchemy import create_engine; engine = create_engine("sqlite:///db.db"); pd.read_sql("SELECT * FROM t", engine) - MySQL/PostgreSQL 需提前装驱动:
pip install pymysql或pip install psycopg2-binary,否则create_engine直接报错 - SQLite 路径注意三个斜杠:
sqlite:///./data.db(相对路径从当前工作目录算起,不是脚本所在目录)
WHERE 条件必须用 params,别用 f-string 或 % 拼接
拼接 SQL 字符串等于主动放弃防护。哪怕变量看着“很安全”,只要来源不可控(比如来自 HTTP 请求、CSV 文件、用户输入框),就存在注入风险。
- 高危示例:
f"SELECT * FROM users WHERE name = '{name}'"—— 若name = "admin' --",整条语句逻辑就被破坏 - 安全写法:
pd.read_sql("SELECT * FROM users WHERE name = %(name)s", engine, params={"name": name}) - 占位符要匹配数据库:SQLite 用
?,MySQL 用%s,PostgreSQL 推荐用命名式%(name)s,最稳妥 -
params只能替换 WHERE 中的值,不能用于表名、列名、ORDER BY 字段——这些必须走白名单校验
to_sql 不接受 SQL 占位符,所谓“参数化插入”是误解
to_sql 的作用是把 DataFrame 转成批量 INSERT,它不解析 SQL 字符串,也不支持 params。你给它传一个带 %s 或 ? 的表名或字段名,它会直接报错或静默失败。
- 错误尝试:
df.to_sql("users WHERE id = %s", con=engine, ...)→ 报DatabaseError,因为表名里不能含WHERE - 真正可控的只有
if_exists、index这类开关型参数,和防注入无关 - 如果需动态写入不同表,必须白名单校验:
if table_name not in ["logs", "events"]: raise ValueError("Invalid table") - 对 DataFrame 列名也要清洗:
df.columns = [re.sub(r'[^a-zA-Z0-9_]', '_', c) for c in df.columns],防止反引号或注释符被误解析
大数据量必须加 chunksize,否则内存爆掉不是 bug 是设计
read_sql 默认一次性加载全部结果进内存。几百万行查出来,DataFrame 没建完,Python 进程就可能被系统 kill,Jupyter kernel 自动重启,现象就是“卡住”或“无响应”。
- 加
chunksize=10000,返回的是Iterator[pd.DataFrame],每次只读 1 万行 - 适合做分批统计、清洗、写入其他库:
for chunk in pd.read_sql(sql, engine, chunksize=5000): process(chunk) - 注意:
chunksize和params可共存,但不能和read_sql_table混用(后者不支持该参数) - datetime 字段异常常见于驱动版本旧,可强制转换:
parse_dates=["created_at"]或升级psycopg2/pymysql
最容易被忽略的是:表名、列名、ORDER BY 字段这三类动态内容,params 完全不覆盖。它们没有参数化路径,只能靠白名单或正则清洗兜底——漏掉这一环,前面所有 params 都白做了。











