收获不止数据库

国产数据库创新集锦⓵——小count也有大智慧

引言

问,select count(*) from t 能否优化?有人纳闷了,如此简单,还能有优化空间?是的,简单的背后不简单。

本文将通过Oracle数据库的各种既有手段,展示count(*)从单车变火箭,踏上性能腾飞的梦幻之旅。然而,火箭并非尽头,因为创新永无止境,小小count也能蕴藏大智慧!接下来,国产原创数据库的创新大表演正式拉开序幕。

Image

注:手机端脚本展现不全,可左右滑动观看。

1

顺其自然 ,性能很拉垮


Image
大饼

彬老师,select count(*) from t  这种简单的SQL,还有优化空间吗?

彬老师
Image

先看看什么都不做的情况,测试如下:

DROP TABLE t PURGE;CREATE TABLE t AS SELECT * FROM dba_objects;ALTER TABLE T MODIFY OBJECT_NAME NOT NULL;SELECT COUNT(*) FROM t;SET AUTOTRACE TRACEONLY;SET LINESIZE 1000;SET TIMING ON;SELECT COUNT(*) FROM T;-- 执行计划Plan hash value: 2966233522-------------------------------------------------------------------| Id | Operation          | Name | Rows   | Cost (%CPU) | Time     ||  0 | SELECT STATEMENT   |      |      1 |   292    (1)| 00:00:04 ||  1 |   SORT AGGREGATE   |      |      1 |             |          ||  2 |   TABLE ACCESS FULL| T    |  92256 |   292    (1)| 00:00:04 |---------------------------------------------------------------------- 统计信息          0  recursive calls          0  db block gets       1048  consistent gets
Image
小渣

执行计划走的是全表扫描,逻辑读高达1048,看起来性能的确很拉垮。

2

普通索引,单车变摩托

彬老师
Image
我们加个B树索引再来看效果:
drop table t purge;create table t as select * from dba_objects;alter table T modify OBJECT_NAME not null;create index idx_object_name on t(object_name);set autotrace traceonly;set timing on ;select count(*) from t;执行计划---------------------------------Plan hash value: 1178070731--------------------------------------------------------------------------------------| Id | Operation           | Name              | Rows  | Cost (%CPU)| Time     |--------------------------------------------------------------------------------------|  0 | SELECT STATEMENT    |                   |     1 |   105   (1)| 00:00:02 ||  1 |   SORT AGGREGATE    |                   |     1 |            |          ||  2 |   INDEX FAST FULL SCAN| IDX_OBJECT_NAME | 92256 |   105   (1)| 00:00:02 |--------------------------------------------------------------------------------------统计信息---------------------------------          0  recursive calls          0  db block gets        372  consistent gets
Image
大饼

执行计划从全表扫描变成了全索引扫描,逻辑读从1048降到372,单车变摩托了!

Image
小渣

效果很炸裂啊!不过,count(*) 这种遍历所有记录的查询,不是一般不建议使用索引吗?

弘棋.jpg
弘老师

这个问题问得好。索引的本质是索引列值加上对应的rowid。当我们只需要计数时,访问索引比访问完整的表数据要小得多,IO开销自然大幅降低。

Image
小渣

原来如此!通过访问更小的数据结构来完成相同任务,确实是个聪明的方法。

彬老师
Image
不过要特别注意,这里的索引列必须是非空约束或者主键,否则索引中不会包含NULL值的行,统计结果就会不准确,这时数据库会自动拒绝使用索引。
Image
大饼
学到了!还有更快的方法吗?

3

位图索引,摩托变赛车
彬老师
Image
当然!我们来尝试位图索引的威力:
drop table t purge;create table t as select * from dba_objects;Update t Set object_name='abc';Update t Set object_name='evf' Where rownum<=20000;create bitmap index idx_object_name on t(object_name);set autotrace traceonly;set timing on;select count(*) from t;执行计划----------------------------------Plan hash value: 1696023018-------------------------------------------------------------------------------------------| Id | Operation                | Name              | Rows  | Cost (%CPU)| Time      |-------------------------------------------------------------------------------------------|  0 | SELECT STATEMENT         |                   |     1 |     5    (0)| 00:00:01 ||  1 |   SORT AGGREGATE         |                   |     1 |             |          ||  2 |   BITMAP CONVERSION COUNT|                   | 92256 |     5    (0)| 00:00:01 ||  3 |     BITMAP INDEX FAST FULL SCAN| IDX_OBJECT_NAME |   |             |          |-------------------------------------------------------------------------------------------统计信息----------------------------------          0  recursive calls          0  db block gets          6  consistent gets

Image
大饼
       执行计划变成位图索引扫描,逻辑读从372降至6。摩托变赛车!
彬老师
Image
位图索引可用与或来判断是否是某个取值,如性别是男则取1,否则取0,位图索引通过二进制压缩后体积极小,所以扫描速度惊人。
Image
大饼
真没想到,位图索引能让count(*)提速这么多!那还能更快吗?

4

物化视图,赛车变飞机

彬老师
Image
能!让我们再试试物化视图:
create table t as select * from dba_objects;Update t Set object_name='abc';Update t Set object_name='evf' Where rownum<=20000;create materialized view mv_count_t                build immediate                refresh on commit                enable query rewrite                as                select count(*) FROM T;select COUNT(*) FROM T;执行计划---------------------------------Plan hash value: 3655891017---------------------------------------------------------------------------------------|Id | Operation                   | Name       | Rows | Bytes | Cost (%CPU) | Time     |---------------------------------------------------------------------------------------| 0| SELECT STATEMENT             |            |    1 |    13 |     3    (0)| 00:00:01 || 1|  MAT_VIEW REWRITE ACCESS FULL| MV_COUNT_T |    1 |    13 |     3    (0)| 00:00:01 |----------------------------------------------------------------------------------------          0  recursive calls          0  db block gets          3  consistent gets
Image
大饼
不可思议!逻辑读从6又降到了3。赛车变飞机了!
弘棋.jpg
弘老师
物化视图本质上是一种预计算策略,是典型的空间换时间方式。注意看执行计划,查询直接访问的是MV_COUNT_T,而不是原表T。
Image
小渣

厉害,手段真多啊!

弘棋.jpg
弘老师
不过需要提醒的是,物化视图并非万能。对于频繁更新的表来说,维护物化视图的开销可能会抵消查询性能的提升,甚至可能导致整体性能下降。技术选型必须结合具体业务场景。
Image
大饼
合理使用才是王道。那么,这就是优化的极限了吗?

5

缓存结果,飞机变火箭

彬老师
Image
NO!NO!NO!请看下面的“大杀器”:
drop table t purge;create table t as select * from dba_objects;select count(*) from t;set linesize 1000;set autotrace traceonly;select /*+ result_cache */ count(*) from t;执行计划---------------------------------Plan hash value: 2966233522----------------------------------------------------------------------------------|Id | Operation       | Name                   | Rows  | Cost (%CPU)| Time     |----------------------------------------------------------------------------------| 0 | SELECT STATEMENT|                            |     1 |    23   (0)| 00:00:01 || 1 | RESULT CACHE    | 034w33ddtzk0mc7kvwt84zayre |       |            |          || 2 |   SORT AGGREGATE|                            |     1 |            |          || 3 |    TABLE ACCESS FULL| T                      | 10000 |    23   (0)| 00:00:01 |----------------------------------------------------------------------------------统计信息---------------------------------          0  recursive calls          0  db block gets          0  consistent gets
Image
大饼
天啊,逻辑读变成0了?这是什么魔法,飞机变火箭了!
弘棋.jpg
弘老师
这是结果集缓存技术。它将查询结果存储在共享内存中,当同一查询再次执行时,数据库直接返回缓存的结果,无需重新计算。这就是为什么逻辑读显示为0。
Image
小渣
我明白了,这比物化视图更进一步,连访问物化视图的开销都省了。不过这和物化视图一样,都是看场景使用吧?
彬老师
Image
嗯,说到场景,我接下来分享一个非常特别的count优化案例:不需要任何索引、物化视图或缓存,只需对SQL做一个微小的改写,性能就能提升数百倍!
Image
大饼
哇,太牛了,请赐教!

6

等价改写,平地起惊雷

彬老师
Image
秘诀就是在查询中添加'rownum=1'条件:select count(*) from t where rownum=1。就这么简单,性能立刻飙升。
Image
大饼
等等,加上rownum=1后,查询不是只返回第一行了吗?这和原来的count(*)完全不等价啊!
彬老师
Image
表面上看确实不同,但在特定场景下,它们是等价的。具体来说,很多开发人员使用count(*)只是为了判断表中是否有数据,比如这样的代码:
begin  select count(*) into v_cnt from t1;  if v_cnt > 0 then    ...A逻辑...  -- 表中有记录时执行  else    ...B逻辑...  -- 表中没有记录时执行  end if;end;
彬老师
Image
其实这和判断表的首条记录是否存在是一样的,如下: