SQL 参数传递优化与性能提升

1次阅读

in列表过长会导致postgresqlmysql优化器选择率预估失准而放弃索引,应拆分为小批次union all或用values join替代;orm中字符串拼接易引发sql注入;pg函数内参数分布不均影响计划缓存;mysql预处理参数类型推断粗糙需显式转换。

SQL 参数传递优化与性能提升

SQL 查询中 IN 列表过长导致执行计划失效

PostgreSQL 和 MySQL 都会在 IN 子句元素超过一定数量(比如 PostgreSQL 默认 10000+)时放弃使用索引,退化为顺序扫描。这不是配置问题,是优化器对谓词选择率预估失准的典型表现。

  • EXPLAIN ANALYZE 看执行计划,如果 Seq Scan 出现在本该走 Index Scan 的表上,大概率是 IN 太长触发了统计偏差
  • 把大 IN 拆成多个小批次(如每 500 个值一组),用 UNION ALL 拼接查询,比单条巨长语句快 3–10 倍
  • MySQL 8.0.19+ 支持 VALUES ROW() 构造临时数据集,配合 JOIN 替代 IN,能稳定走索引
  • 别依赖 SET session work_mem 来“撑”大 IN,它不解决选择率误判,只影响排序/哈希内存

ORM 中参数化查询被拼接成字符串的隐性风险

djangoextra()、SQLAlchemy 的 text()mybatis${} 占位符,一旦混入用户输入,就等于绕过参数绑定,直接触发 SQL 注入——哪怕看起来用了“参数”。

  • Django 中 Filter(name__in=names) 是安全的;但 extra(where=["name IN %s"], params=[tuple(names)]) 在某些旧版本会降级为字符串格式化
  • SQLAlchemy 使用 text("select * FROM users WHERE id = :id") 是 OK 的,但写成 text(f"SELECT * FROM users WHERE id = {user_id}") 就彻底失效
  • MyBatis 里 #{} 走 PreparedStatement,${} 是字面量替换,连 ORDER BY ${sortField} 这种看似无害的用法也必须白名单校验

PostgreSQL 函数内传参与执行计划缓存冲突

PostgreSQL 对 PL/pgSQL 函数内 SQL 语句做“通用计划缓存”,但如果参数值分布极不均匀(比如一个值占 95% 查询量),缓存的执行计划很可能对多数调用次优。

  • 在函数开头加 PERFORM pg_sleep(0)raise DEBUG '' 可强制跳过通用计划,改用每次重规划(代价是解析开销上升)
  • 更稳妥的是用 EXECUTE 'SELECT ...' using param 动态拼接,让优化器每次看到真实参数值再决策
  • 注意:只有 STABLEIMMUTABLE 函数才可能被内联,volatile 函数不会,所以别指望函数体被展开优化

MySQL PREPARE + EXECUTE 的参数类型陷阱

MySQL 的预处理语句对参数类型推断非常粗糙,? 全部默认当成字符串处理,遇到数字比较、日期范围或 json 字段时容易隐式转换失败或索引失效。

  • WHERE created_at > ? 传入字符串 "2024-01-01" 可能走索引;但传入整数时间戳 1704067200 就会触发 created_at 列的函数化转换,索引失效
  • JSON 字段查询如 json_col->>'$.id' = ?,若传入数字而非字符串,MySQL 会先转成字符串再比,无法利用生成列上的索引
  • CONVERT(? USING utf8mb4) 显式声明编码,避免因连接字符集不同导致参数被截断或乱码

参数传递看着只是填空,实际牵扯执行计划生成、类型推导、缓存策略三层逻辑。最容易被忽略的是:数据库不会因为你写了 ? 就自动理解你的业务语义,它只认类型、统计信息和 SQL 结构。

text=ZqhQzanResources