params参数仅用于替换SQL中的值(如WHERE条件、IN列表等),表名、列名、ORDER BY等语法结构不可参数化,必须通过白名单校验后拼接,否则将导致SQL注入。

read_sql 的 params 参数只管值,不管表名和字段名
你传给 read_sql 的 params 只会替换 SQL 字符串里的占位符(如 %s、?、:name),数据库驱动在底层把它们当「数据」处理,自动转义和类型适配。但表名、列名、ORDER BY、GROUP BY、LIMIT 这些属于「SQL 结构」,不能用 params 代入——强行拼进去就是注入高危点。
常见错误写法:f"SELECT * FROM {user_table} WHERE status = %s" 或 "SELECT * FROM users ORDER BY " + sort_col。哪怕加了 .strip() 或正则过滤单引号,也拦不住 "score; DROP TABLE users;" 这类绕过。
- 安全做法:表名/字段名必须走白名单校验,例如
if table_name not in ["users", "orders"]: raise ValueError - 更稳妥的做法是用映射字典,比如
table_map = {"user": "app_users", "order": "app_orders"},再取table_map.get(user_input) -
ORDER BY字段同理,只允许["created_at", "updated_at", "score"]几个固定值,别信“我只允许字母数字”这种弱校验
不同数据库的占位符写法不互通,命名式最省心
MySQL 用 %s,PostgreSQL 支持 %s 和 $1,SQLite 用 ?,SQL Server(pyodbc)常用 ? 或命名式 :name。混用会导致报错或静默失败,尤其跨环境部署时容易踩坑。
推荐统一用命名式占位符(%(col)s 或 :col),它不依赖顺序,可读性好,且 SQLAlchemy 引擎能自动适配多数后端:
sql = "SELECT * FROM logs WHERE level = %(level)s AND ts > %(since)s"
df = pd.read_sql(sql, engine, params={"level": "ERROR", "since": "2026-09-01"})- 别用位置式(
%s)配合字典params,会报TypeError: not all arguments converted - 用
text()包裹 SQL 时,必须匹配对应方言的命名规则;SQL Server 存储过程要用:param,不是@param - 如果用的是 raw
psycopg2.connect而非 SQLAlchemy Engine,read_sql可能不支持命名参数,得切回位置式
to_sql 不接受 params,别试图给它加参数化
to_sql 的设计目标是把 DataFrame 批量写入已知结构的表,它根本不解析 SQL 字符串,也不走参数化路径。你传一个带 %s 或 ? 的字符串给它,要么报错(DatabaseError: table name must be string),要么静默失败——这不是 bug,是机制使然。
- 错误示例:
df.to_sql("events WHERE ts > %s", engine, params=("2026-01-01",))→ 直接炸 - 真正需要动态条件写入?先用
read_sql+params查出数据,再用to_sql写固定表名 - 想按条件删旧数据再写入?用
engine.execute(text("DELETE FROM t WHERE ..."))配合params,而不是在to_sql上动脑筋
con 参数传错类型,params 就等于没用
read_sql 的 con 必须是 SQLAlchemy Engine 实例(create_engine(...) 返回的),不是 raw connection(如 sqlite3.Connection 或 psycopg2.connection)。传错类型会导致 params 被忽略,底层驱动直接走字符串拼接,防注入形同虚设。
典型症状:本地 SQLite 测试正常,一上 PostgreSQL 就报错或被注入;或者 params 明明传了,日志里却看到原始 SQL 被完整打印出来。
- 正确写法:
engine = create_engine("postgresql+psycopg2://..."),然后pd.read_sql(sql, engine, params=...) - 错误写法:
conn = psycopg2.connect(...); pd.read_sql(sql, conn, params=...)→params不生效 - 用
sqlite3时也建议走 SQLAlchemy("sqlite:///db.db"),避免因连接类型差异导致行为不一致
最易被忽略的一点:白名单校验必须在 SQL 拼接前完成,且校验逻辑要覆盖所有可能入口——API 请求、配置文件、CLI 参数、定时任务脚本,都不能漏。只要有一个地方绕过校验直接拼字符串,整个防护就崩了。

















