🦏 第5章 高级查询
📄 一句话概括

like 是「长得像」而不是「等于」,靠两个通配符干活: % 顶任意长度(包括零个),_ 顶恰好一个字符。 Demo 里 LIKE '%球%' 中 4 件、LIKE '%拍' 中 2 件; LIKE '_球拍' 给 0 行,LIKE '__球拍' 给 2 行—— 四个字的「羽毛球拍」,一个下划线顶不住。 要搜百分号、下划线本身,得用 ESCAPE 声明转义符: LIKE '%\%%' ESCAPE '\' 才找得到 50%。 LIKE '%张%' 用不上索引,前缀不确定就只能全表扫。

📝 本章和前面几章的关系

前一章的子句都是「精确条件」:=、>、in。 这一篇开始「像不像」,也是搜索框背后最常见的那一句。 like 本身不难,难在两处:_ 到底顶几个字,以及搜「百分号」的时候为什么搜不到。

一、百分号:任意长度,包括零个字符

LIKE '%球%' 是「中间有球」,% 能顶零个、一个、很多个字符。 Demo 里四件全中:篮球、足球、 羽毛球拍、乒乓球拍。 % 放后面就是「以某字开头」:LIKE '张%' 找姓张的; 放前面是「以某字结尾」:LIKE '%拍' 找两个拍的。 not like 是取反,但要留意 null:like 碰上 null 给的是「未知」,不是「假」, 所以 not like 也筛不出 null 的那些行。

二、下划线:只顶一个字符

_ 顶恰好一个字符,多一个少一个都不匹配。 LIKE '_球' 是「两个字,第二个是球」,篮球、足球中,羽毛球拍不中。 这个差别用一句话验最快:LIKE '_球拍' 给 0 行,LIKE '__球拍' 给 2 行。 因为「羽毛球拍」是四个字,得两个下划线。 实用场景:_ 常用来卡「长度固定」的编码,比如订单号 DD___________(13 位)。 但更清楚的写法是把长度写进约束,别靠 _ 数。

三、转义:要搜通配符本身

问题:想找「名字里含百分号」的记录, LIKE '%%%' 不行——% 全被当通配符了,等于「什么都匹配」。 办法是声明一个转义符:LIKE '%\%%' ESCAPE '\'。 \% 就表示「字面的百分号」,\_ 同理。 Demo 里前者找出 50%,后者找出 a_b。 这里有一处两边不一样:MySQL 的默认转义符就是反斜杠,不写 ESCAPE 也能过; SQLite 没有默认转义符,不写就匹配不到。 跨库的 SQL 一律显式写 ESCAPE。

四、大小写:答案取决于排序规则

SELECT 'abc' LIKE 'ABC' 在 Demo 里给 1, 不区分大小写。 MySQL 默认的 utf8mb4_0900_ai_ci 也不区分—— 名字末尾的 ci 就是 case insensitive。 换成 _bin 或 _cs 结尾的排序规则就区分了。 所以「like 区分大小写吗」这个问题问得不完整,该问「这个列用的是哪个排序规则」。 真要强制不区分(或强制区分),可以在比较时临时转:WHERE LOWER(name) LIKE 'abc%'。 代价是用不上索引,慎用。

五、性能:百分号放前面,索引就废了

LIKE '张%' 能用索引——前缀是确定的,B+ 树能直接定位到「张」那一段。 LIKE '%张%' 用不上——开头不确定,只能一行行扫。 数据量一大,这就是全表扫描,几百万行要跑几秒到几十秒。 真要搜中间的内容,MySQL 走全文索引(FULLTEXT),或者干脆上专门的搜索引擎。 别用 %...% 硬扛。

六、动手 Demo:两个通配符,一次转义,一处两边不一样

Demo 五段:先用 % 找三种位置,再用 _ 卡长度,然后建一张小表专门试转义。 第四段是这一篇的重点:同一句 SQL,写不写 ESCAPE 结果不同, 而且这里和 MySQL 的默认行为还不一样。

import _q46 as Q
c = Q.con()

# 百分号:任意长度
c.execute("SELECT name FROM goods WHERE name LIKE '%球%'")
c.execute("SELECT name FROM goods WHERE name LIKE '%拍'")
c.execute("SELECT name FROM goods WHERE name NOT LIKE '%球%'")
# 下划线:只顶一个字符
c.execute("SELECT name FROM goods WHERE name LIKE '_球'")
c.execute("SELECT name FROM goods WHERE name LIKE '_球拍'")      # 0 行
c.execute("SELECT name FROM goods WHERE name LIKE '__球拍'")     # 2 行
# 转义:要搜百分号、下划线本身
c2 = Q.blank()
c2.execute("CREATE TABLE codes (v TEXT)")
c2.executemany("INSERT INTO codes VALUES (?)",
               [("50%",), ("a_b",), ("abc",)])
c2.execute("SELECT v FROM codes WHERE v LIKE '%\\%%' ESCAPE '\\'")
c2.execute("SELECT v FROM codes WHERE v LIKE '%\\_%' ESCAPE '\\'")
c2.execute("SELECT v FROM codes WHERE v LIKE '%\\%%'")        # 这里 0 行
# 大小写
c.execute("SELECT 'abc' LIKE 'ABC' AS ci")

实跑输出:

--------------------------------------------------------------------
第二十课:like 的两个通配符,和转义那个坑
--------------------------------------------------------------------

  百分号:代表任意长度,包括零个字符
  SQL> SELECT name FROM goods WHERE name LIKE '%球%'
  name
  ---------
  篮球
  足球
  羽毛球拍
  乒乓球拍
  (4 行)

  **`%球%` 是「中间有球」**,四件全中:篮球、足球、羽毛球拍、乒乓球拍。
  **`%` 能顶零个、一个、很多个字符。**

  SQL> SELECT name FROM goods WHERE name LIKE '%拍'
  name
  ---------
  羽毛球拍
  乒乓球拍
  (2 行)

  **`%拍` 是「以拍结尾」**,只中两件。
  **把 `%` 放前面就是「以某字开头」**,`LIKE '张%'` 找姓张的。

  SQL> SELECT name FROM goods WHERE name NOT LIKE '%球%'
  name
  ---------
  跳绳
  护腕
  运动水壶
  运动鞋
  运动袜
  瑜伽垫
  (6 行)

  **`not like` 就是取反**,剩下六件。
  **注意 null 的行两边都不中**——`like` 碰上 null 给的是未知,不是假。

  下划线:只顶一个字符,多一个少一个都不行
  SQL> SELECT name FROM goods WHERE name LIKE '_球'
  name
  -----
  篮球
  足球
  (2 行)

  **`_球` 是「两个字,第二个是球」**,只中篮球和足球。

  SQL> SELECT name FROM goods WHERE name LIKE '_球拍'
  name
  -----
  (0 行)

  SQL> SELECT name FROM goods WHERE name LIKE '__球拍'
  name
  ---------
  羽毛球拍
  乒乓球拍
  (2 行)

  **`_球拍` 给 0 行,`__球拍` 给 2 行。** **因为「羽毛球拍」是四个字,一个下划线顶不住,得两个。**
  **这就是 `_` 和 `%` 的区别**:`_` 是「恰好一个」,`%` 是「随便几个」。

  转义:要搜百分号本身怎么办
  SQL> SELECT v FROM codes
  v
  ----
  50%
  a_b
  abc
  (3 行)

  **表里三条:`50%`、`a_b`、`abc`。** 现在想精确找「含百分号的那条」。

  SQL> SELECT v FROM codes WHERE v LIKE '%\%%' ESCAPE '\'
  v
  ----
  50%
  (1 行)

  **`ESCAPE '\'` 声明「反斜杠是转义符」**,于是 `\%` 表示「字面的百分号」,不再当通配符。
  **`\_` 同理,能搜到下划线本身。**

  SQL> SELECT v FROM codes WHERE v LIKE '%\_%' ESCAPE '\'
  v
  ----
  a_b
  (1 行)

  **这一句给 `a_b`。** **不写 `ESCAPE` 的话,反斜杠就是个普通字符**,下面这一句在**这里**给 0 行。

  SQL> SELECT v FROM codes WHERE v LIKE '%\%%'
  v
  --
  (0 行)

  **MySQL 不写 `ESCAPE` 也能过,因为它的默认转义符就是反斜杠。**
  **这里(SQLite)没有默认转义符,所以必须显式写。**
  **这是两边行为不同的一处,跨库的 SQL 一律显式写 `ESCAPE`。**

  大小写:这里不敏感,MySQL 看排序规则
  SQL> SELECT 'abc' LIKE 'ABC' AS ci
  ci
  ---
  1
  (1 行)

  **给 1,说明这里不区分大小写。**
  **MySQL 默认的 `utf8mb4_0900_ai_ci` 结尾那个 `ci` 就是 case insensitive,也不区分;**
  **换成 `_bin` 或 `_cs` 的排序规则就区分了。**
  **所以「like 区分大小写吗」这个问题,答案取决于列上的排序规则,不是取决于 `like`。**

  性能:百分号放前面,索引就废了
  **`LIKE '张%'` 能用上索引**——前缀确定,B+ 树能定位到「张」开头的那一段。
  **`LIKE '%张%'` 用不上**——开头不定,只能一行行扫。 **数据量一大,这就是全表扫描。**
  **真要搜中间的内容,就别在 MySQL 里硬扛**,上全文索引或者专门的搜索引擎。

第一段 %。 4 件、2 件、6 件——% 换个位置,筛出来的完全是不同的东西。 第二段 _。 _球 给 2 行; _球拍 给 0 行、__球拍 给 2 行,这一个对比把 _ 的「恰好一个」讲透了。 第三段建表。 三条数据专门留着:50%、a_b、abc。 第四段转义。 带 ESCAPE 的两句各给 1 行;不带的给 0 行——这里没有默认转义符。 第五段大小写。 给 1,不区分。

30 秒实验

  1. LIKE '%球' 与 LIKE '%球%' 各给几行?差在哪?
  2. 把 ESCAPE '\' 里的反斜杠换成 !,LIKE '%!%%' ESCAPE '!' 还给 50% 吗?
  3. LIKE '张_' 能找出「张伟」吗?LIKE '张__' 呢?

常见误区

  1. 以为 _ 能顶多个字符。 它只顶一个,四个字的商品名要两个下划线,写一个就一行都筛不出来, 还不报错。
  2. 搜百分号时直接写 LIKE '%%%'。 那是「什么都匹配」,必须用 ESCAPE 声明转义符才有意义。
  3. 在大表上用 LIKE '%关键词%'。 索引完全用不上,几百万行就是全表扫描;该上全文索引就上。
自测三问(点开对答案) 1. `%` 和 `_` 的区别是什么? —— `%` 顶任意长度(包括零个),`_` 顶恰好一个字符。 2. 怎么搜「名字里含百分号」的记录? —— `LIKE '%\%%' ESCAPE '\'`,先声明转义符,再用转义符包住百分号。 3. 为什么 `LIKE '%张%'` 用不上索引? —— 前缀不确定,B+ 树没法定位起点,只能逐行扫。`LIKE '张%'` 才能用。

like 是按条件找。下一篇换一种玩法——不看条件,随机抽几条出来。

相关笔记