% 任意多个字符、_ 正好一个字符,这两个是标准的;[] 和 [^] 是 SQL Server 的扩展。
前导通配(%xxx)会让索引失效,只能全表扫——所以「以什么开头」能走索引,「包含」不能。
想查字面量的下划线,得用 ESCAPE 声明转义符——直接写下划线会被当成通配符。
这是第 6 章最后一节。通配符只有一个地方用:LIKE。但它能查的东西比 = 多得多。
教材 6.8 有六个小节:LIKE 的用法、%、_、[]、[^],最后带「小结」和「实践与练习」。
六小节里前三个是重点,后两个是扩展。
演示里跑了 10 行商品数据,%、_、[]、[^] 各试了一遍,
还专门演了「查字面量下划线」这个坑,以及大小写在 LIKE 和 GLOB 下的差别。
一、LIKE 与百分号:三个位置三种含义
| 写法 | 含义 | 能走索引吗 |
|---|---|---|
LIKE '%联想' |
以它结尾 | 不能 |
LIKE '联想%' |
以它开头 | 能 |
LIKE '%联想%' |
包含它 | 不能 |
第三列是这一节最实用的东西。 % 放在前面,
索引就用不上了——因为索引是按「开头」排的,你从中间找,它没法定位。
所以同一个业务,两种写法性能差几十倍。 「查找以「联想」开头的商品」能走索引,
「查找名字里含「联想」的商品」只能全表扫。这不是实现问题,是索引的原理决定的。
实践建议:如果「包含」查询很频繁,考虑全文索引。它专门解决这类问题,代价是索引更大、写入更慢、
要额外配置。
二、下划线:正好一个字符
_ 匹配正好一个字符,不多不少。 联想__ 匹配「联想」后面正好两个字的,
联想___ 匹配三个字的。
这里有个经典坑:想查名字里带下划线的记录,直接写下划线是不行的。因为 _ 被当成了通配符,
宏基_4736 会匹配「宏基」+ 任意一个字符 +「4736」。
WHERE gname LIKE '宏基_4736' -- 下划线是通配符
WHERE gname LIKE '宏基!_4736' ESCAPE '!' -- 加了转义符才是字面量
ESCAPE 后面跟什么字符,那个字符就变成了转义符。 例子用 !,
也可以用 \ 或者 #——只要那一列的数据里不含这个字符就行。
这个坑的表现是「查出来的结果比预期多」。 比如你只想找「宏基_4736」,
结果「宏基A4736」也被查出来了——数据量大的时候很难发现。
三、方括号:一个字符的集合
[] 表示「这里的字符取自这个集合」。 [联戴]尔 匹配「联想」或「戴尔」——
方括号里的每一个字符都是一个候选。
| 写法 | 含义 |
|---|---|
[abc] |
a、b、c 任意一个 |
[a-z] |
a 到 z 之间任意一个 |
[0-9] |
任意一个数字 |
[^abc] |
不是 a、b、c 的任意一个 |
方括号不是标准 SQL,是 SQL Server 的扩展。 MySQL 和 Oracle 都没有——
它们要用正则表达式函数(REGEXP、REGEXP_LIKE)。
SQLite 也没有方括号的 LIKE,但它有 GLOB。 演示里就是用 GLOB 跑的——GLOB '[联戴]尔*'。
注意 GLOB 用 * 而不是 %。
方括号内部可以写区间,比如 [A-Za-z]。 这个写法在「首字母是英文」这类判断上很方便——
比写一长串 [ABCDEFG...] 清楚多了。
四、脱字符:排除
[^] 表示「不是这些字符」。 注意 ^ 必须放在方括号里的最前面,放后面就变成普通字符了。
教材用 ^ 这个写法,这是 SQL Server 的语法。 有些文档里写 !——
那是 MySQL 的 REGEXP 的写法,两家不一样,别混。
「排除」这个能力在业务上用得不多,但有一个场景很好用:「找出名字里不含数字的商品」——
LIKE '%[^0-9]%'。
这种条件用 NOT LIKE 写不出来,因为「不包含数字」不是「包含非数字」的简单否定。
演示里还看到一件事:排除的命中数可能是 0。这不一定是你写错了,
很可能是数据里真的没有符合条件的——先 SELECT * 看一眼再判断。
五、大小写:LIKE 认不认
这个问题的答案取决于「排序列的排序规则」,不是取决于 LIKE 本身。
| 环境 | 'Apple' = 'apple' |
原因 |
|---|---|---|
| CI 排序规则 | 真 | 不区分大小写 |
| CS 排序规则 | 假 | 区分大小写 |
SQLite 的 GLOB |
假 | 按字节比,一定区分 |
所以同一个 LIKE 'apple%',在 CI 的库上能查到「Apple Watch」,在 CS 的库上查不到。
这是「同一段代码在两台机器上结果不同」的常见原因。
GLOB 是按字节比的,一定区分大小写。 所以演示里 LIKE 'apple%' 命中 2 行、
GLOB 'apple*' 命中 1 行——同一个模式,两个运算符,命中数不同。
实践建议:需要精确控制大小写时,用 COLLATE 显式指定,
别依赖默认排序规则——默认值会随安装配置变。
六、动手 Demo:五种通配符各试一遍
这一段建 10 行商品数据,% 在三个位置各试一次,_ 试两次,[] 和 [^] 用 GLOB 跑,
最后对比 LIKE 和 GLOB 在大小写上的差别。
import sqlite3
c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_goods (gname TEXT)")
c.executemany(u"INSERT INTO tb_goods VALUES (?)",
[(n,) for n in (u"联想笔记本", u"联想键盘", u"联想",
u"经典联想", u"戴尔笔记本", u"Apple Watch")])
# % 的三个位置
for pat in (u"%联想", u"联想%", u"%联想%"):
n = c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE gname LIKE ?",
(pat,)).fetchone()[0]
print(u"LIKE %-12s %d 行" % (u"'" + pat + u"'", n))
# _ 正好一个字符
print(c.execute(u"SELECT gname FROM tb_goods WHERE gname LIKE '联想__'").fetchall())
# [] 和 [^]:SQLite 要用 GLOB
print(c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE gname GLOB '[联戴]尔*'").fetchone()[0])
print(c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE gname GLOB '[^联]尔*'").fetchone()[0])
# 大小写:LIKE 不区分,GLOB 区分
print(c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE gname LIKE 'apple%'").fetchone()[0])
print(c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE gname GLOB 'apple*'").fetchone()[0])
实跑输出:
--------------------------------------------------------------------
LIKE 的五种通配符
--------------------------------------------------------------------
样例数据 10 行:联想笔记本、联想键盘、联想、经典联想、戴尔笔记本、惠普显示器、Apple Watch、
apple pencil、宏基_4736、华硕X550
① 百分号:任意多个字符
--------------------------------------------------------------------
LIKE '%联想' 命中 2 行
LIKE '联想%' 命中 3 行
LIKE '%联想%' 命中 4 行
三个位置的含义完全不同:前面加 % 是「以它结尾」,后面加是「以它开头」,两边加是「包含」。
只有「以它开头」这种能走索引——前导通配(% xxx)会让索引失效,只能全表扫。
② 下划线:正好一个字符
--------------------------------------------------------------------
LIKE '联想__' 后面正好两个字符 → 联想键盘
LIKE '华硕X5__' 两个下划线两个字符 → 华硕X550
LIKE '宏基_4736' 下划线被当成通配符 → 宏基_4736
第三行是个经典坑:想找名字里带下划线的商品,直接写下划线是不行的——
它被当成了通配符。要查字面量的下划线,得用 ESCAPE 声明转义符。
加上 ESCAPE '!' 之后:命中 1 行
③ 方括号:指定一个字符集合
--------------------------------------------------------------------
GLOB '[联戴]尔*' 命中 1 行(SQLite 用 GLOB 才有方括号)
GLOB '[A-Za-z]*' 命中 2 行(首字母是英文字母)
SQL Server 里写 LIKE '[联戴]尔%',SQLite 要写 GLOB——这是方言差异。
方括号里可以列单个字符,也可以写区间 A-Z、0-9。
注意方括号不是标准的 LIKE 语法,是 SQL Server 的扩展;MySQL 和 Oracle 都没有。
④ 方括号加脱字符:排除
--------------------------------------------------------------------
GLOB '[^联]尔*' 命中 1 行(「尔」前不是「联」)
命中的是:戴尔笔记本
脱字符 ^ 放在方括号里最前面表示「不是这些」。
教材用 ^ 这个写法;有些文档里写 !,那是 MySQL 的 REGEXP 的用法,别混。
⑤ 大小写:LIKE 认不认
--------------------------------------------------------------------
LIKE 'apple%' 命中 2 行(默认不区分大小写)
GLOB 'apple*' 命中 1 行(GLOB 区分大小写)
LIKE 是否区分大小写,取决于列的排序规则——这一点 2.2b 讲过 CI 和 CS。
GLOB 是按字节比的,一定区分大小写。所以同一个模式换成 GLOB,命中数可能变。
SQL Server 里 LIKE 也走排序规则:CI 的列不区分,CS 的列区分。这点跟 SQLite 一致。
第一组三个数是 2、3、4——这就是「三个位置三种含义」的直接证据。同一个词,
% 放前面是「以它结尾」,放后面是「以它开头」,两边放是「包含」。
只有第二个(联想%)能走索引。 另外两个都是前导通配,
索引失效——这一点决定了同一个业务两种写法性能差几十倍。
GLOB '[联戴]尔*' 命中 1 行,「戴尔笔记本」。 为什么「联想」没命中?
因为它不以「尔」结尾——方括号指定的是「尔」前面那个字符。
[^联]尔* 也命中 1 行,还是「戴尔笔记本」。这两个模式在这批数据上结果一样,
因为「尔」前面只有「联」和「戴」两种——
数据集小的时候,排除和不排除看不出区别,别因此以为写错了。
最后那两行的对比是本节最容易忽略的一点: LIKE 命中 2 行(Apple 和 apple),
GLOB 命中 1 行(只有小写的 apple)——同一个模式,两个运算符,命中数不同。
30 秒实验
- 给商品表的
gname建索引,然后对比LIKE '联想%'和LIKE '%联想%'的执行计划。 一个走索引查找,一个是全表扫描——差别在计划里看得清清楚楚。 - 插入一行名字里真的带下划线的数据,然后用
LIKE '%_%'和LIKE '%!_%' ESCAPE '!'分别查。 前一个会查出所有行(因为每行都至少有下划线这个通配符能匹配的字符),后一个才查得准。 - 想一想:为什么「包含」查询不能走索引? 因为索引是按「开头」排的 B 树。 你要找的东西可能在任何一个位置, 它没法定位到某个区间——只能从头扫到尾。
常见误区
- 「
LIKE '%关键字%'也能走索引」。不能。 前导通配会让索引失效,只能全表扫。要频繁做「包含」查询,得考虑全文索引。 - 「下划线就是下划线」。在
LIKE里它是「正好一个字符」的通配符。 要查字面量下划线,必须ESCAPE声明转义符,否则查出来的结果比预期多。 - 「
LIKE肯定区分大小写」。看排序规则。 CI 的列不区分,CS 的列区分——同一段代码在两台机器上结果可能不同。 要精确控制就用COLLATE显式指定。
自测三问(点开对答案)
1. `LIKE '联想%'` 和 `LIKE '%联想%'` 性能差在哪? —— **前者能走索引(因为索引按开头排),后者不能(要从中间找,只能全表扫)。** 同一个业务两种写法,数据量大时性能差几十倍。 2. 怎么查名字里真的带下划线的记录? —— **用 `ESCAPE` 声明一个转义符**:`LIKE '宏基!_4736' ESCAPE '!'`。 直接写下划线会被当成「正好一个字符」的通配符,结果比预期多。 3. `GLOB 'apple*'` 和 `LIKE 'apple%'` 为什么命中数不同? —— **因为 `GLOB` 按字节比,一定区分大小写;`LIKE` 走排序规则,CI 的列不区分。** 所以 `LIKE` 能同时命中 Apple 和 apple,`GLOB` 只能命中完全匹配的那个。第 6 章到此结束。 下一篇进第 7 章 7.1,讲 SQL 函数: 字符串函数、日期函数、数学函数,各家的名字差别最大的一块——同一个功能常常有三个名字。
- ☐ (本篇完,下一篇 7.1 数学函数)