四个条件「选或不选」一共十六种组合,写十六条 SQL 是死路——要「拼」。
动态 SQL 唯一的难点是「条件和参数必须同步增减」,一个 append 漏了结果就错。
必选条件写在 SQL 里,可选条件才进 if——混在一起写,等于给自己挖坑。
前面几节的查询条件都是固定的(这个班、这一科), 这一节的查询条件是「用户填的」——填几个、填哪几个,运行时才知道。 教材 20.11 分三小节:概述、技术分析、实现过程。 「技术分析」里讲的是「动态拼 SQL」,这是全章唯一会写错且不容易发现的地方。 沙箱没有 JSP,条件用函数参数模拟表单, 拼出来的 SQL 原样打出来——这样能一眼看出拼得对不对,比在浏览器里点半天强。
一、四个条件,十六种组合
| 条件 | 可能的值 |
|---|---|
| 班级 | 四个班之一,或不选 |
| 性别 | 男/女/不选 |
| 姓名关键字 | 任意字符串,或空 |
| 分数区间 | 下限、上限,或都不填 |
四个条件「选或不选」,一共 2⁴ = 16 种组合。
如果每种组合写一条 SQL,就是 16 条——而且加一个条件变 32 条。
正确做法是「拼」:条件选了才 AND 上去,参数跟着一起加。
关键在「条件和参数必须同步增减」——这是动态 SQL 唯一的难点。
二、WHERE 1=1 这个技巧
| 写法 | 每个分支长什么样 |
|---|---|
不加 1=1 |
第一个条件不能带 AND,得判断「是不是第一个」 |
加 1=1 |
每个分支都是 AND xxx,长得一模一样 |
WHERE 1=1 这个写法被很多人嫌弃「丑」,
但它换来的是「五个 if 长得一模一样」。 代码整齐比好看重要,因为整齐的代码不容易漏。
另一种做法是用 List<String> 攒条件、最后 join(" AND "),
也能避免「第一个条件」的问题,而且更干净。
两种都行,别用的是「每个分支各写一套判断」。
三、参数个数是自检的抓手
| 现象 | 说明什么 |
|---|---|
| 参数个数比预期少 | 某个 if 里 append 漏了 |
| 参数个数对但查不到 | 顺序错了,或者条件写反了 |
| 报「参数个数不匹配」 | SQL 里 ? 和参数数量不一致 |
「参数个数」这一列是自检用的。
如果某次运行参数个数跟预期对不上,一定是某个 if 里 append 漏了或者多写了。
第三种错反而是好事——它会报错,能被发现。
真正麻烦的是第二种:个数对、顺序错、结果错,一声不响。
四、拼接错法:看着能跑,其实会炸
| 输入 | 拼接的结果 | 参数化的结果 |
|---|---|---|
张 |
查到 3 行 | 查到 3 行 |
O'Brien |
SQL 语法错误 | 正常执行,0 行 |
这里一个坏人都没有,只是一个带撇的输入。
'O'Brien' 里那个单引号把 SQL 提前闭合了。
参数化之后「带撇」和「不带撇」走的是同一条路径——
这才是「正常」的样子。 拼接之所以危险,是因为它把「数据」当成了「代码」。
还有一个 LIKE 特有的坑: % 和 _ 在 LIKE 里是通配符。
用户搜「_」会匹配任意一个字符,搜「%」会匹配一切。
要按字面搜,得用 ESCAPE 转义——教材没讲,但线上一定有人会踩。
五、动手 Demo:动态 WHERE:两个列表一起长
这一段把「条件列表」和「参数列表」一起构建,然后跑五种组合,每种都打印「行数 + 参数个数」。 最后对比拼接与参数化在「带单引号」时的表现。
def q(cname=None, sex=None, kw=None, lo=None, hi=None):
sql = 'SELECT sno, sname, cname, score FROM v_score'
sql += ' WHERE subject = ?' # 必选条件直接写
args = ['数学']
if cname:
sql += ' AND cname = ?'; args.append(cname)
if sex:
sql += ' AND sno IN (SELECT sno FROM tb_student WHERE sex = ?)'
args.append(sex)
if kw:
sql += ' AND sname LIKE ?'; args.append('%' + kw + '%')
if lo is not None:
sql += ' AND score >= ?'; args.append(lo)
if hi is not None:
sql += ' AND score <= ?'; args.append(hi)
sql += ' ORDER BY score DESC'
return sql, args
for label, kw in CASES:
sql, args = q(**kw)
rows = c.execute(sql, args).fetchall()
print('%-18s %2d 行 参数 %d 个' % (label, len(rows), len(args)))
实跑输出:
--------------------------------------------------------------------
多条件查询:动态 WHERE 的安全拼法
--------------------------------------------------------------------
① 四个条件,十六种组合
--------------------------------------------------------------------
| 条件 | 可能的值 |
|---|---|
| 班级 | 四个班之一,或不选 |
| 性别 | 男/女/不选 |
| 姓名关键字 | 任意字符串,或空 |
| 分数区间 | 下限、上限,或都不填 |
四个条件「选或不选」,一共 2⁴ = 16 种组合。
如果每种组合写一条 SQL,就是 16 条——而且加一个条件变 32 条。
正确做法是「拼」:条件选了才 AND 上去,参数跟着一起加。
关键在「条件和参数必须同步增减」——这是动态 SQL 唯一的难点。
② 正确的拼法:两个列表一起长
--------------------------------------------------------------------
sql = "SELECT ... FROM v_score WHERE 1=1"
args = []
if 班级: sql += " AND cname = ?"; args.append(班级)
if 性别: sql += " AND sno IN (子查询)"; args.append(性别)
if 关键字: sql += " AND sname LIKE ?"; args.append("%" + 关键字 + "%")
if 下限: sql += " AND score >= ?"; args.append(下限)
if 上限: sql += " AND score <= ?"; args.append(上限)
WHERE 1=1 是个技巧:后面每条都能以 AND 开头,不用判断「是不是第一条」。
WHERE 1=1 这个写法被很多人嫌弃「丑」,但它换来的是「五个 if 长得一模一样」——代码整齐比好看重要,
因为整齐的代码不容易漏。
③ 真跑四种组合
--------------------------------------------------------------------
只选科目 → 12 行 参数 1 个
20260201 | 陈子轩 | 2026级计算机2班 | 数学 | 96.0
20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
…… 共 12 行
科目 + 班级 → 4 行 参数 2 个
20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
20260102 | 李思彤 | 2026级计算机1班 | 数学 | 88.0
20260104 | 赵雨欣 | 2026级计算机1班 | 数学 | 78.0
…… 共 4 行
科目 + 姓名含「张」 → 1 行 参数 2 个
20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
科目 + 80~90 分 → 5 行 参数 3 个
20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
20260102 | 李思彤 | 2026级计算机1班 | 数学 | 88.0
20250101 | 孙志强 | 2025级计算机1班 | 数学 | 88.0
…… 共 5 行
全不选(等于全查) → 12 行 参数 1 个
20260201 | 陈子轩 | 2026级计算机2班 | 数学 | 96.0
20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
…… 共 12 行
第 1 条和第 5 条一模一样(都是 12 行),因为「科目」是必选条件。
这也说明:必选条件和可选条件要分开写,必选的直接写在 SQL 里,可选的才进 if。
「参数个数」那一列是自检用的。
如果某次运行参数个数跟预期对不上,一定是某个 if 里 append 漏了或者多写了。
④ 拼接错法:看着能跑,其实会炸
--------------------------------------------------------------------
拼接·正常输入 → 3 行
拼接·带单引号 → 直接报错:near "Brien": syntax error
参数化·带单引号 → 正常执行,0 行
LIKE 里的 % 也要注意。
参数化的时候,两个百分号要拼在参数值上(Python 里一个百分号、加号、关键字、加号、一个百分号),
不能拼进 SQL 文本——拼进 SQL 就等于又回到字符串拼接了。
还有个小坑:% 和 _ 在 LIKE 里是通配符。用户搜「_」会匹配任意一个字符,搜「%」会匹配一切。
要按字面搜,得用 ESCAPE 转义——教材没讲,但线上一定有人会踩。
「参数个数」那一列是这段代码的自检表。
第一个用例参数 1 个(只有必选的科目),第四个用例参数 3 个(科目 + 上下限)。
数一数就知道有没有漏 append。
必选条件「科目」直接写在初始 SQL 里,没有进 if。
这一条很重要:必选和可选要分开写。
如果必选条件也进 if,某次忘了传参数,就会变成「查全表」。
最后一个用例是「全不选」,结果跟「只选科目」一样。
这不是 bug,是「没有可选条件」的自然结果——
但如果业务上「全不选」应该报错(不能查全表),就要单独判断。
30 秒实验
- 给
q()加上一个「班级下拉框默认选第一个」的行为,看第一个用例变成几行。 这就是「默认值」对结果的影响。 - 把
kw分支改成拼接(不用参数),传O'Brien,看报什么错。 - 想一想:如果用户搜「%」,会查出什么?
全部。 要按字面搜得用
ESCAPE。
常见误区
- 「十六种组合写十六条 SQL」。 加一个条件变三十二条。 要拼。
- 「
WHERE 1=1太丑,不用」。 不用它就得处理「第一个条件」,那个判断更容易错。 - 「
LIKE的%拼在 SQL 里和拼在参数里一样」。 拼在 SQL 里就等于回到字符串拼接。 必须拼在参数值上。
自测三问(点开对答案)
1. 动态 SQL 唯一的难点是什么? —— **条件和参数必须同步增减。** **一个 `append` 漏了,SQL 和参数就对不上。** 2. `WHERE 1=1` 解决了什么问题? —— **让每个分支都能以 `AND` 开头**,**不用判断「是不是第一个条件」。** 3. 为什么必选条件不要进 `if`? —— **忘传参数时会变成「查全表」**,**而全表查询往往就是数据泄露。**下一篇 20.12 收尾: 按班级汇总和明细怎么在一个页面里共存、窗口函数算排名——最后把三个项目共用的那套骨架收一次口。
- ☐ (本篇完,下一篇 20.12 考试成绩班级明细数据查询模块设计)