🪟 第6章 SQL基础
📄 一句话概括

% 任意多个字符、_ 正好一个字符,这两个是标准的;[] 和 [^] 是 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 秒实验

  1. 给商品表的 gname 建索引,然后对比 LIKE '联想%' 和 LIKE '%联想%' 的执行计划。 一个走索引查找,一个是全表扫描——差别在计划里看得清清楚楚。
  2. 插入一行名字里真的带下划线的数据,然后用 LIKE '%_%' 和 LIKE '%!_%' ESCAPE '!' 分别查。 前一个会查出所有行(因为每行都至少有下划线这个通配符能匹配的字符),后一个才查得准。
  3. 想一想:为什么「包含」查询不能走索引? 因为索引是按「开头」排的 B 树。 你要找的东西可能在任何一个位置, 它没法定位到某个区间——只能从头扫到尾。

常见误区

  1. 「LIKE '%关键字%' 也能走索引」。不能。 前导通配会让索引失效,只能全表扫。要频繁做「包含」查询,得考虑全文索引。
  2. 「下划线就是下划线」。在 LIKE 里它是「正好一个字符」的通配符。 要查字面量下划线,必须 ESCAPE 声明转义符,否则查出来的结果比预期多。
  3. 「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 函数: 字符串函数、日期函数、数学函数,各家的名字差别最大的一块——同一个功能常常有三个名字。

相关笔记