作为一名DBA,我们经常会被灌输一条金科玉律:LIKE
查询的百分号 %
不能放在开头,否则索引会失效。
比如 WHERE name LIKE 'QYT%'
可以愉快地走索引扫描,而 WHERE name LIKE '%QYT'
就只能退化成全表扫描,在数据量大的时候简直是性能灾难。
这个原理很简单,B-Tree 索引是按从左到右的顺序排序的,你把 %
放前面,等于告诉 Oracle “开头是啥我不知道”,它自然就没法用索引快速定位了。
但规则是死的,人是活的。难道面对 LIKE '%QYT'
这种需求,我们真的就束手无策,只能任由它全表扫描吗?
今天,我们就来分享一个脑洞大开的“骚操作”,利用 Oracle 的函数索引,让这种“废柴”查询也能重新用上索引,起死回生!
实验场景
我们先来搭建一个简单的测试环境。
-- 创建测试表
CREATE TABLE t AS SELECT * FROM dba_objects WHERE object_id IS NOT NULL;
-- 插入两条特征明显的测试数据
UPDATE t SET object_name='ZZZQYT' WHERE object_id=8;
UPDATE t SET object_name='QYTZZZ' WHERE object_id=10;
COMMIT;
— 在 object_name 列上创建一个常规的 B-Tree 索引
CREATE INDEX idx_object_name ON t(object_name);
情况一:%
在后面(常规操作,走索引)
我们先看看标准的好学生是什么样的。
SELECT object_name, object_id FROM t WHERE object_name LIKE 'QYT%';
OBJECT_NAME OBJECT_ID
------------------------------ ----------
QYTZZZ 10
Execution Plan
----------------------------------------------------------
Plan hash value: 603483963
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 40 | 4 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T | 1 | 40 | 4 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_OBJECT_NAME | 1 | | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("OBJECT_NAME" LIKE 'QYT%')
filter("OBJECT_NAME" LIKE 'QYT%')
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
5 consistent gets
2 physical reads
0 redo size
639 bytes sent via SQL*Net to client
427 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
毫无悬念,走了INDEX RANGE SCAN,性能杠杠的。
情况二:%
在前面(索引失效,全表扫描)
接下来看看坏学生。
SELECT object_name, object_id FROM t WHERE object_name LIKE '%QYT';
OBJECT_NAME OBJECT_ID
------------------------------ ----------
ZZZQYT 8
Execution Plan
----------------------------------------------------------
Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3640 | 142K| 394 (1)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| T | 3640 | 142K| 394 (1)| 00:00:01 |
--------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("OBJECT_NAME" LIKE '%QYT' AND "OBJECT_NAME" IS NOT NULL)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
1419 consistent gets
0 physical reads
0 redo size
639 bytes sent via SQL*Net to client
648 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
同样不出所料,Oracle放弃了索引,选择了TABLE ACCESS FULL,逻辑读飙升。
奇思妙想:反转一下,世界大不同!
既然从左往右走不通,那我们能不能让它从右往左走呢?
答案是可以的!借助 Oracle 的REVERSE
函数。这个函数可以把一个字符串完全颠倒过来。
SELECT REVERSE('%QYT') FROM dual;
REVE
----
TYQ%
看到了吗? %QYT 被反转成了 TYQ% !那个讨厌的、挡在前面的%,一下子跑到了最后面,这不就又变成了索引最喜欢的格式了吗?
有了这个思路,我们的优化方案就来了:
1. 创建一个反向函数索引:
我们在object_name
列上,创建一个基于REVERSE
函数的索引。
CREATE INDEX idx_reverse_objname ON t(REVERSE(object_name));
这个索引里存储的不再是ZZZQYT,而是TYQZZZ。
2. 改造我们的 SQL 查询:
在写查询的时候,把WHERE
条件也用REVERSE
函数包起来。
SELECT object_name, object_id
FROM t
WHERE REVERSE(object_name) LIKE REVERSE('%QYT'); -- 等价于 WHERE REVERSE(object_name) LIKE 'TYQ%'
OBJECT_NAME OBJECT_ID
------------------------------ ----------
ZZZQYT 8
现在,让我们再来看看这条“改造”后 SQL 的执行计划:
执行计划:
Execution Plan
----------------------------------------------------------
Plan hash value: 944999405
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3640 | 376K| 392 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| T | 3640 | 376K| 392 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_REVERSE_OBJNAME | 655 | | 7 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access(REVERSE("OBJECT_NAME") LIKE 'TYQ%')
filter(REVERSE("OBJECT_NAME") LIKE 'TYQ%')
Statistics
----------------------------------------------------------
2 recursive calls
0 db block gets
7 consistent gets
7 physical reads
0 redo size
639 bytes sent via SQL*Net to client
445 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
奇迹发生了!Oracle聪明地使用了我们创建的反向函数索引IDX_REVERSE_OBJNAME,执行了INDEX RANGE SCAN路径。对比一下全表扫描的统计信息,逻辑读从1419骤降到了7!
通过 REVERSE
函数和函数索引的组合,我们成功地绕过了LIKE
查询对前导 %
的限制,把一个原本无法使用索引的查询,变成了一个高效的索引扫描操作。
这个方法虽然看起来有点“曲线救国”,但在某些特定场景下,不失为一种实用且有效的性能优化技巧。