由于ORACLE的缓存和共享池的机制,SQL语句逻辑读一般从第二次执行才稳定下来,出于篇幅的考虑,下面所有的SELECT COUNT(*) FROM
查询表中的记录总数的语法就是SELECT COUNT(*) FROM TABLE_NAME。这可能是最经常使用的一类SQL语句。
本文讨论怎样才能最快的得到这个记录数。本文纯粹主要是理论上的讨论,文章中很多内容(如常数索引)对实际的指导意义不大。
在具体描述之前,强调几个前提:
首先表中的记录数不能太少,否则讨论的意义就不大了,在我下面的例子中记录数是3万左右,其实这个数量级还是比较小,不过已经能够看出一些效果了。
根据执行时间的长短进行判断偶然性比较大,本文以没种方法逻辑读的多少来进行判断。由于包括查询重写(需要的相对较多的执行计划的分析)和索引压缩(属于CPU密集型,消耗CPU资源较多),仅仅用逻辑读来衡量各种方法的优劣肯定不会很准确,但是考虑到表中的数据量比较大,而且我们以SQL的第二次执行结果为准,所以,其他方面的影响还是可以忽略的。
另外一个前提就是结果的准确性,查询USER_TABLES的NUM_ROWS列等类似的方法不在本文讨论范畴之内。
最后,由于Oracle的缓存和共享池的机制,SQL语句逻辑读一般从第二次执行才稳定下来,出于篇幅的考虑,下面所有的SELECT COUNT(*) FROM T的结果都是该SQL语句第二次执行的结果。
如果存在一个查询语句为SELECT COUNT(*)的物化视图,则最快的方式一定是扫描这张物化视图。
SQL> CREATE TABLE T (ID NUMBER NOT NULL, NAME VARCHAR2(30), TYPE VARCHAR2(18));
表已创建。
SQL> INSERT INTO T SELECT ROWNUM, OBJECT_NAME, OBJECT_TYPE FROM DBA_OBJECTS;
已创建30931行。
SQL> COMMIT;
提交完成。
SQL> CREATE MATERIALIZED VIEW LOG ON T WITH ROWID INCLUDING NEW VALUES;
实体化视图日志已创建。
SQL> CREATE MATERIALIZED VIEW MV_T REFRESH FAST ON COMMIT ENABLE QUERY REWRITE AS
2 SELECT COUNT(*) FROM T;
实体化视图已创建。
SQL> ALTER SESSION SET QUERY_REWRITE_ENABLED = TRUE;
会话已更改。
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T')
PL/SQL 过程已成功完成。
SQL> SET AUTOT ON
SQL> SELECT COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=82 Bytes=1066)
1 0 TABLE ACCESS (FULL) OF 'MV_T' (Cost=2 Card=82 Bytes=1066)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
根据上面的查询可以看出,扫描物化视图,只需3个逻辑读就可以了。但是,物化视图对系统的限制比较多。首先要创建物化视图日志,还要在SYSTEM或SESSION级设置参数,必须使用CBO等很多的条件,限制了物化视图的使用,而且最重要的是,一般情况下不会存在一个单纯查询全表记录数的物化视图,而一般建立的物化视图是为了加快一些更加复杂的表连接或聚集的查询的。因此,,即使存在物化视图,也不会直接得到结果,一般是对物化视图上的结果进行再次计算。
如果不考虑物化视图,那么得到记录总数的最快的方法一定是BITMAP索引扫描。BITMAP索引的机制使得BITMAP索引回答COUNT(*)之类的查询具有最快的响应速度和最小的逻辑读。至于BITMAP索引的机制,这里就不重复描述了,还是看看BITMAP索引的表现吧:
SQL> DROP MATERIALIZED VIEW MV_T;
实体化视图已删除。
SQL> DROP MATERIALIZED VIEW LOG ON T;
实体化视图日志已删除。
SQL> CREATE BITMAP INDEX IND_B_T_TYPE ON T (TYPE);
索引已创建。
SQL> EXEC DBMS_STATS.GATHER_INDEX_STATS(USER, 'IND_B_T_TYPE')
PL/SQL 过程已成功完成。
SQL> SELECT COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1)
1 0 SORT (AGGREGATE)
2 1 BITMAP CONVERSION (COUNT)
3 2 BITMAP INDEX (FAST FULL SCAN) OF 'IND_B_T_TYPE'
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
5 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
可以看到,BITMAP索引的表现十分出色,只需5个逻辑读就可以得到结果。可惜的是,BITMAP索引比较适合在数据仓库中使用,而对于OLTP环境,BITMAP索引的锁粒度将给整个系统带来严重的灾难。因此,对于OLTP系统,BITMAP索引也是不合适的。
不考虑BITMAP索引,那么速度最快的应该是普通索引的快速全扫了,比如主键列。
SQL> DROP INDEX IND_B_T_TYPE;
索引已丢弃。
SQL> ALTER TABLE T ADD CONSTRAINT PK_T PRIMARY KEY (ID);
表已更改。
SQL> SELECT COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=1)
1 0 SORT (AGGREGATE)
2 1 INDEX (FAST FULL SCAN) OF 'PK_T' (UNIQUE) (Cost=4 Card=30931)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
69 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
主键的快速全扫只需69个逻辑读。但是由于主键这里用的是ROWNUM,也就是说是主键的值是从1到30931,Oracle存储这些NUMBER类型则需要2到4位不等。如果建立一个常数索引,则在存储空间上要节省一些。而在执行索引快速全扫时,就能减少一些逻辑读。
SQL> CREATE INDEX IND_T_CON ON T(1);
索引已创建。
SQL> SELECT COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=1)
1 0 SORT (AGGREGATE)
2 1 INDEX (FAST FULL SCAN) OF 'IND_T_CON' (NON-UNIQUE) (Cost=4 Card=30931)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
66 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
果然,扫描常数索引比扫描主键的逻辑读更小一些。考虑到NUMBER类型中,1的存储需要两位,而0的存储只需一位,那么用0代替1创建常数索引,应该效果更好。
SQL> CREATE INDEX IND_T_CON_0 ON T(0);
索引已创建。
SQL> SELECT /*+ INDEX(T IND_T_CON_0) */ COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=26 Card=1)
1 0 SORT (AGGREGATE)
2 1 INDEX (FULL SCAN) OF 'IND_T_CON_0' (NON-UNIQUE) (Cost=26 Card=30931)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
58 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
由于常数索引中所有节点值都相同,如果压缩一下的话,应该还能减少逻辑读。
SQL> DROP INDEX IND_T_CON_0;
索引已丢弃。
SQL> CREATE INDEX IND_T_CON_COMPRESS ON T(0) COMPRESS;
索引已创建。
SQL> SELECT /*+ INDEX(T IND_T_CON_COMPRESS) */ COUNT(*) FROM T;
COUNT(*)
----------
30931
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=26 Card=1)
1 0 SORT (AGGREGATE)
2 1 INDEX (FULL SCAN) OF 'IND_T_CON_COMPRESS' (NON-UNIQUE) (Cost=26 Card=30931)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
49 consistent gets
0 physical reads
0 redo size
378 bytes sent via SQL*Net to client
503 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
和预计的一样,经过压缩,索引扫描的逻辑读进一步减少,现在和最初的主键扫描相比,逻辑读已经减少了30%。
如果只为了得到COUNT(*),那么压缩过的常数索引是最佳选择,不过这个索引对其他查询是没有任何帮助的,因此,实际中的用处不大。