青年数据库学习互助会

从 NULL 值看索引失效与 TABLE ACCESS FULL 现象

想学会更多实用技巧,欢迎加入青学会MOP技术社区(实名社区)。

加入方法:公众号后台回复关键字“加入”获取小助手微信,添加后登记入会。

Image

同时欢迎大家在评论区留言互动交流!社区会不定期举行相关的抽奖、公开分享活动。

如果你有想了解的知识点希望我们发文可以后台私信。

本期译文

翻译自:

https://www.howtosop.com/when-null-results-table-access-full/

正文开始

一、性能问题

在某些情况下,NULL 值会阻止语句使用列上的索引,这会导致全表扫描,在循环中严重影响性能。

让我们看看使用 index 和 full table scan 进行比较的情况。

(一)使用索引的情况(NOT NULL)

首先,我们想要统计 MANAGER_ID 不为 NULL 的数量。

SQL> select count(*) cnt from hr.employees where manager_id is not null;

CNT
----------
106

然后查看执行计划。

SQL> set linesize 85;
SQL> set pagesize 1000;
SQL> select * from table(dbms_xplan.display_cursor);

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------
SQL_ID  f67nzh2nk7pu0, child number 1
-------------------------------------
select count(*) cnt from hr.employees where manager_id is not null
Plan hash value: 3393209858
-----------------------------------------------------------------------------------
| Id  | Operation        | Name            | Rows  | Bytes | Cost (%CPU)| Time        |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |                |       |       |     1 (100)|            |
|   1 |  SORT AGGREGATE  |                |     1 |     4 |            |            |
|*  2 |   INDEX FULL SCAN| EMP_MANAGER_IX |   106 |   424 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
    2 - filter("MANAGER_ID" IS NOT NULL)
19 rows selected.

可以看到,语句使用了单列索引 EMP_MANAGER_IX,这还不错。

(二)全表扫描的情况(IS NULL)

另一方面,当我们想要统计 NULL 值时情况就不同了。

SQL> select count(*) cnt from hr.employees where manager_id is null;

        CNT
----------
         1
SQL> select * from table(dbms_xplan.display_cursor);

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------
SQL_ID  bp859wff0gpsp, child number 1
-------------------------------------
select count(*) cnt from hr.employees where manager_id is null
Plan hash value: 1756381138
--------------------------------------------------------------------------------
| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time        |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |           |       |       |     3 (100)|            |
|   1 |  SORT AGGREGATE    |           |     1 |     4 |            |            |
|*  2 |   TABLE ACCESS FULL| EMPLOYEES |     1 |     4 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
    2 - filter("MANAGER_ID" IS NULL)
19 rows selected.

这次是 TABLE ACCESS FULL。TABLE ACCESS FULL 意味着执行全表扫描而不是索引扫描,当表非常大时可能会耗费很多资源。

二、原因

这是因为单列索引 EMP_MANAGER_IX 不包含 MANAGER_ID 的 NULL 值,所以它无法知道哪个字段是 NULL。结果就是执行全表扫描。

这就是为什么索引的 NUM_ROWS 有时比其依赖的表的行数要小。

SQL> select num_rows from all_indexes where owner = 'HR' and index_name = 'EMP_MANAGER_IX';

  NUM_ROWS
----------
      106

SQL> select num_rows from all_tables where owner = 'HR' and table_name = 'EMPLOYEES';

  NUM_ROWS
----------
      107

差异是真实存在的。

三、解决方案

为了解决这个问题,我们需要一个复合索引,它能够包含 MANAGER_ID 的 NULL 值。

SQL> create index hr.emp_mgr_id_x on hr.employees (manager_id, 1);

Index created.

由于复合索引的第二个位置完全不重要,我们提供一个虚拟列作为占位。

让我们看看新的执行计划。

SQL> select count(*) cnt from hr.employees where manager_id is null;

        CNT
----------
         1
SQL> select * from table(dbms_xplan.display_cursor);

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------
SQL_ID  bp859wff0gpsp, child number 1
-------------------------------------
select count(*) cnt from hr.employees where manager_id is null
Plan hash value: 2770830432
----------------------------------------------------------------------------------
| Id  | Operation         | Name          | Rows  | Bytes | Cost (%CPU)| Time        |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |               |       |       |     1 (100)|            |
|   1 |  SORT AGGREGATE   |               |     1 |     4 |            |            |
|*  2 |   INDEX RANGE SCAN| EMP_MGR_ID_X |     1 |     4 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
    2 - access("MANAGER_ID" IS NULL)
19 rows selected.

虽然 SQL_ID 相同,但 PLAN_HASH_VALUE 已经改变,因为执行计划中使用了复合索引。

让我们总结一下索引的转变:单列索引 -> 添加虚拟列 -> 复合索引。完成!

END

往期文章回顾

MOP社区新闻

  青学会MOP技术社区成立了!

  青学会专家顾问团成员介绍

金仓专栏

  告别繁琐!KingbaseES v9数据库一键安装-青学会&金仓专栏(1)

  KingbaseES v9数据库Docker安装-青学会&金仓专栏(2)

KingbaseES数据脱敏-青学会&金仓专栏(3)

KingbaseES后台服务管理-青学会&金仓专栏(4)

  电科金仓KES日常运维命令集锦-青学会&金仓专栏(5)

DBA实战小技巧

推荐一款超实用的openGauss数据库安装工具!

  实战:记一次RAC故障排查
  DBA实战运维小技巧安装篇(一)Oracle 主流版本不同架构下的静默安装指南
  DBA实战运维小技巧存储篇(一)根目录满了如何处理
  DBA实战运维小技巧存储篇(二)打包迁移单机数据库至新存储

MOP社区投稿-内核开发

浅谈 PostgreSQL GUC 模块原理

简单解析 IvorySQL 增强 Oracle xml 兼容能力的原理

简单讨论 PostgreSQL C语言拓展函数返回数据表的方式

简单分析 pg_config 程序的作用与原理
  Redis 日志机制简介(一):SlowLog
Redis 日志机制简介(二):AOF 日志
  Redis 日志机制简介(三):RDB 日志
  pg_cron插件使用介绍
  Redis 的指令表实现机制简介
  pg几款源码工具介绍
  Redis 事务功能简介

MOP顾问说

MOP顾问说:MOP 三种主流数据库常用 SQL(一)

MOP顾问说:服务器内存

MOP 顾问说:Linux Nice 值与 CPU 优先级揭秘