MySQL 动态 SQL 中 `${}` 和 `#{}` 的实战避坑指南与性能优化

张开发
2026/4/10 21:16:42 15 分钟阅读

分享文章

MySQL 动态 SQL 中 `${}` 和 `#{}` 的实战避坑指南与性能优化
1. 初识MySQL动态SQL中的占位符刚接触MyBatis框架时我经常被${}和#{}这两个占位符搞得晕头转向。记得有一次在项目中我把表名写成了#{table}结果程序直接抛出了语法错误。后来才发现原来这两种占位符的使用场景完全不同。简单来说#{}是预编译占位符而${}是字符串替换占位符。它们最直观的区别体现在生成的SQL语句上。比如下面这个例子-- 使用#{} SELECT * FROM users WHERE name #{name}; -- 预编译后实际执行的是 SELECT * FROM users WHERE name ?; -- 使用${} SELECT * FROM ${table} WHERE id 1; -- 替换后实际执行的是 SELECT * FROM users_2023 WHERE id 1;在实际开发中#{}就像是给参数穿上了防护服它会先把SQL语句编译好再把参数值安全地传递进去。而${}则像是直接把参数裸奔着拼接进SQL语句这在某些场景下会带来安全隐患。2. 深入理解两种占位符的底层机制2.1 预编译机制详解#{}占位符之所以安全是因为它利用了JDBC的PreparedStatement预编译机制。当MyBatis遇到#{}时它会做以下处理先将SQL语句中的#{}替换成问号(?)发送SQL模板到数据库进行预编译最后再将实际参数值安全地绑定到对应的问号位置这个过程就像是先准备好一个填空题模板等模板确定无误后再把答案填进去。数据库会严格区分SQL结构和参数值从根本上杜绝了SQL注入的可能。2.2 字符串拼接的本质相比之下${}的处理就简单粗暴得多。MyBatis会直接把${}中的内容原样替换到SQL语句中相当于做了个字符串拼接。比如SELECT * FROM ${table} WHERE id ${id};如果tableusers且id1那么最终生成的SQL就是SELECT * FROM users WHERE id 1;这种替换发生在SQL编译之前数据库看到的就是一个完整的SQL语句。如果替换内容来自用户输入且未经处理就可能被注入恶意代码。3. 为什么表名必须使用${}3.1 数据库协议的限制很多开发者都会疑惑既然#{}更安全为什么表名不能用#{}呢这其实和数据库协议的设计有关。在JDBC规范中预编译占位符(?)只能用于替换值类型的参数不能用于替换表名、列名等SQL标识符。尝试这样写会直接报错-- 错误的写法 SELECT * FROM ? WHERE id 1;数据库引擎在解析SQL时需要先确定操作的是哪个表然后才能处理查询条件等部分。如果用问号代替表名数据库就不知道你要查询哪个表了自然无法执行。3.2 动态表名的实现方案在分表分库的场景中我们经常需要根据业务规则动态选择表名。比如按月分表的日志系统!-- MyBatis映射文件示例 -- select idselectLogs resultTypeLog SELECT * FROM logs_${month} /select这里的${month}会被替换成具体的月份后缀比如202308。这种需求下我们别无选择只能使用${}占位符。4. 安全使用${}的最佳实践4.1 常见风险场景最典型的安全问题就是SQL注入。假设有这样一段代码String userInput users; DROP TABLE users; --; String sql SELECT * FROM userInput;执行后会变成SELECT * FROM users; DROP TABLE users; --这会导致users表被直接删除后果不堪设想。4.2 安全防护措施在实际项目中如果要使用${}我总结了几个防护要点严格控制参数来源表名应该在代码层面生成而不是直接使用用户输入。比如根据时间自动生成表名后缀。白名单校验如果必须接收外部参数要严格校验参数格式。比如只允许字母数字和下划线if (!tableName.matches(^[a-zA-Z0-9_]$)) { throw new IllegalArgumentException(Invalid table name); }最小权限原则执行动态SQL的数据库账号应该只有必要的最小权限避免使用高权限账号。日志监控记录所有动态生成的SQL语句便于事后审计和问题排查。5. 性能优化建议5.1 预编译的性能优势使用#{}不仅更安全还能提升性能。因为预编译语句可以被数据库缓存和重用。当同一条SQL需要多次执行时比如批量插入数据库只需要编译一次后续只需绑定不同的参数值即可。5.2 动态SQL的优化技巧对于必须使用${}的场景我有几个优化建议避免频繁变更表名动态表名的变化频率越低越好这样数据库的查询计划缓存才能发挥作用。使用索引提示对于动态生成的查询可以通过索引提示来确保查询效率SELECT * FROM ${table} USE INDEX(index_name) WHERE ...批量操作优化当需要操作多个动态表时尽量合并操作或使用事务减少连接开销。6. 面试常见问题解析6.1 基础原理类问题问题MyBatis中#{}和${}的主要区别是什么回答要点#{}是参数化查询使用预编译机制安全但只能用于值类型${}是字符串替换直接拼接到SQL中可用于标识符但需要防注入举例说明两者的使用场景和注意事项6.2 安全实践类问题问题如何安全地实现动态表名查询回答建议优先在代码层面控制表名生成如果必须接收外部输入实施严格的白名单校验使用低权限数据库账号记录完整的SQL日志用于审计考虑使用中间映射层如数字编码对应表名7. 实际开发中的经验分享在电商项目中我们遇到过商品表按月分库的需求。最初直接使用${month}来拼接表名后来发现有两个问题一是表名校验不完善导致潜在注入风险二是频繁变更表名影响查询性能。优化后的方案是在服务层根据日期自动计算表名后缀对所有动态表名添加前缀校验为常用查询添加缓存层使用数据库中间件来管理分表逻辑这个方案实施后既保证了灵活性又提升了安全性和性能。特别是在大促期间系统稳定支撑了每秒上万的查询请求。

更多文章