Oracle里面的COUNT(1)与COUNT(*)函数的执行效率比较。

Oracle数据库:COUNT(1)与COUNT(*)的效率对比分析
本文探讨了Oracle数据库中COUNT(1)与COUNT(*)的执行效率,通常认为COUNT(*)会读取所有字段,而COUNT(1)仅统计1的个数,效率更高。但实验结果显示,两者在效率上几乎没有差别,且在有符合条件的索引情况下,都能利用索引。总结得出,COUNT(1)与COUNT(*)在实际应用中效果相当,但在大量数据查询时,推荐使用更简洁的COUNT(*)。

目录


前言

 

Oracle中的统计COUNT(*)和COUNT(1)的效率比较,一直以来都是觉得COUNT(1)会比COUNT(*)快一些。至于为什么,我个人以前总觉得*这个符号会将Oracle中所有的字段记录会返回回来,而COUNT(1)只是统计了1的个数,相对比下就自认为COUNT(1)要快一些。然而有一次去参加客户方的一项数据开发时,他们一篇文档中写着COUNT(*)反而要比COUNT(1)快!这严重地打破了之前的认知。基于这个疑问,就尝试写了一下这篇文章,来探讨一下COUNT(1)和COUNT(*)的效率哪个快?

以下是本篇文章正文内容,下面案例可供参考

一、Oracle的存储机制

众所周知,Oracle是关系型数据库。跟Hbase等非关系型数据库不同的一点在于它的存储形式是采用行式存储的。行式存储也就是将表中的数据一行行的写入数据块。而数据块是Oracle的最小单位。每次我们在查询数据的时候,Oracle时常会从磁盘中读取一些数据块到内存中。也就是说你在查询某些字段时,Oracle是把表中一整行的数据读取到内存当中,然后再从头到尾扫描你需要的字段。并非只读取你查询的那几个字段。

二、COUNT(*)与COUNT(字段)的比较

1.COUNT 表中各个字段的比较

如上面所说,Oracle是一行行读取数据,并且在访问字段的时候是根据表中字段的顺序从左往右访问。所以字段在表中的顺序越靠左访问速度越快。为了验证这个设想,进行了如下实验:

/* 
 * 步骤1
 * 目的:创建一张21个字段的的表
 * 大小:1W条记录
*/
CREATE TABLE TMP_TEST_COUNT AS            --创建表
SELECT LEVEL AS ID                        --序号
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT1  --字母,用ASCII计算而来
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT2
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT3
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT4
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT5
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT6
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT7
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT8
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT9
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT10
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT11
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT12
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT13
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT14
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT15
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT16
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT17
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT18
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT19
      ,CHR(65 + MOD(LEVEL, 26)) AS TEXT20
  FROM DUAL
CONNECT BY LEVEL <= 10000;

/*
 *步骤2
 *目的:手动获取表的信息,为了后续SQL执行能有一个正确的执行计划
 */
CALL DBMS_STATS.GATHER_TABLE_STATS(OWNNAME=>RPT, TABNAME=>'TMP_TEST_COUNT');


/*
 *步骤3
 *目的:统计各个字段统计的时长
 */
DECLARE
  L_DUMMY PLS_INTEGER;
  L_START PLS_INTEGER;
  L_STOP  PLS_INTEGER;
  L_SQL   VARCHAR2(100);
BEGIN
  FOR I IN 1 .. 20 LOOP
    L_SQL   := 'SELECT COUNT(TEXT' || I || ') FROM TMP_TEST_COUNT';
    
    --因为Oracle有缓存机制,第一次执行的数据会被缓存在内存中,当下次被访问时就不会去磁盘中加载。为了精准测试先加载数据
    EXECUTE IMMEDIATE L_SQL;
    
    L_START := DBMS_UTILITY.GET_TIME;
    
    --增加CPU的计算,得到更精确的结果
    FOR J IN 1 .. 1000 LOOP
      EXECUTE IMMEDIATE L_SQL
        INTO L_DUMMY;
    END LOOP;
    
    L_STOP := DBMS_UTILITY.GET_TIME;
    DBMS_OUTPUT.PUT_LINE((L_STOP - L_START) / 100);
  END LOOP;

END;

测试结果:

如上图发现,字段的顺序是有影响SQL的执行效率。也就是字段越靠右速度就越慢。

 

2.COUNT(*)的效率

代码如下(示例):

/*
 *目的:统计COUNT(*)统计的时长
 */
DECLARE
  L_DUMMY PLS_INTEGER;
  L_START PLS_INTEGER;
  L_STOP  PLS_INTEGER;
  L_SQL   VARCHAR2(100);
BEGIN

  L_SQL := 'SELECT COUNT(*) FROM TMP_TEST_COUNT';

  --因为Oracle有缓存机制,第一次执行的数据会被缓存在内存中,当下次被访问时就不会去磁盘中加载。为了精准测试先加载数据
  EXECUTE IMMEDIATE L_SQL;

  L_START := DBMS_UTILITY.GET_TIME;

  --增加CPU的计算,得到更精确的结果
  FOR J IN 1 .. 1000 LOOP
    EXECUTE IMMEDIATE L_SQL
      INTO L_DUMMY;
  END LOOP;

  L_STOP := DBMS_UTILITY.GET_TIME;
  DBMS_OUTPUT.PUT_LINE((L_STOP - L_START) / 100);

END;

结果:0.14s

经过上面的实验发现,COUNT(*)的效率是要比COUNT(字段)要高。也就是说Oracle在统计COUNT(*)时,并不会去读取字段。

 

三、COUNT(1)的效率,以及与COUNT(*)的比较

1. COUNT(1)与COUNT(*)的效率比较

先测试一下COUNT(1)的效率,代码如下(示例):

/*
 *目的:统计COUNT(1)统计的时长
 */
DECLARE
  L_DUMMY PLS_INTEGER;
  L_START PLS_INTEGER;
  L_STOP  PLS_INTEGER;
  L_SQL   VARCHAR2(100);
BEGIN

  L_SQL := 'SELECT COUNT(1) FROM TMP_TEST_COUNT';

  --因为Oracle有缓存机制,第一次执行的数据会被缓存在内存中,当下次被访问时就不会去磁盘中加载。为了精准测试先加载数据
  EXECUTE IMMEDIATE L_SQL;

  L_START := DBMS_UTILITY.GET_TIME;

  --增加CPU的计算,得到更精确的结果
  FOR J IN 1 .. 1000 LOOP
    EXECUTE IMMEDIATE L_SQL
      INTO L_DUMMY;
  END LOOP;

  L_STOP := DBMS_UTILITY.GET_TIME;
  DBMS_OUTPUT.PUT_LINE((L_STOP - L_START) / 100);

END;

结果:0.15s,与COUNT(*)相差很少。由于每次执行的结果时间是不一样的,所以会有误差。但其实效率上是一致的。

由于上面的实验并不能很明显的看出COUNT(1)与COUNT(*)的效率,接下去从真实的执行计划去查看。看看哪个比较耗费资源。

 

2. COUNT(1)与COUNT(*)的执行计划比较

在Oracle中选择执行计划,是根据优化器来决定的。而优化器是基于两种方式进行计算的。一种是规则,另一种是成本。在Oracle 11g当中,Oracle默认采用的优化策略是成本(Cost)。

代码如下(示例):

SET TIMING ON;   --设置显示最终执行完所花费的时间

SET AUTOTRACE ON;  --设置显示执行过程

SET AUTOTRACE TRACEONLY; --设置只显示执行过程,不显示SQL结果集

分别执行如下代码,获取执行计划:

SELECT COUNT(1) FROM TMP_TEST_COUNT;

SELECT COUNT(*) FROM TMP_TEST_COUNT;

分别的执行计划如下:

SQL> SELECT COUNT(1) FROM TMP_TEST_COUNT;

已用时间:  00: 00: 00.04

执行计划
----------------------------------------------------------
Plan hash value: 728617846

-----------------------------------------------------------------------------
| Id  | Operation          | Name           | Rows  | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                |     1 |    22   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |                |     1 |            |          |
|   2 |   TABLE ACCESS FULL| TMP_TEST_COUNT |  9756 |    22   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Note
-----
   - dynamic sampling used for this statement (level=2)


统计信息
----------------------------------------------------------
          4  recursive calls
          0  db block gets
        131  consistent gets
          0  physical reads
          0  redo size
        527  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed
SQL> SELECT COUNT(*) FROM TMP_TEST_COUNT;

已用时间:  00: 00: 00.00

执行计划
----------------------------------------------------------
Plan hash value: 728617846

-----------------------------------------------------------------------------
| Id  | Operation          | Name           | Rows  | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                |     1 |    22   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |                |     1 |            |          |
|   2 |   TABLE ACCESS FULL| TMP_TEST_COUNT |  9756 |    22   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Note
-----
   - dynamic sampling used for this statement (level=2)


统计信息
----------------------------------------------------------
          4  recursive calls
          0  db block gets
        131  consistent gets
          0  physical reads
          0  redo size
        527  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

从上面的执行计划得出,COUNT(1)和COUNT(*)的花费其实是一致的,成本(Cost)都是22。

 

3.有索引的情况下,COUNT(1)与COUNT(*)的比较

在有非空索引或者主键的情况下,Oracle执行COUNT运算时会大大地提高性能。这里需要注意的点是索引列不能为空(NULL),因为在Oracle当中,聚合函数是不对NULL值进行运算。也就是说当索引有NULL值,那么它统计出来的就不是表中的记录总条数。所以当索引中有NULL值,那么Oracle就不会走索引扫描。那么问题来了,在执行COUNT(1)或者COUNT(*)时,Oracle会不会走索引?

首先在表中创建主键,代码如下(示例):

ALTER TABLE TMP_TEST_COUNT ADD CONSTRAINT PK_TMP_TEST_COUNT PRIMARY KEY (ID);

根据上面的方式,获取执行计划:

SQL> SELECT COUNT(1) FROM TMP_TEST_COUNT;

已用时间:  00: 00: 00.09

执行计划
----------------------------------------------------------
Plan hash value: 251659311

--------------------------------------------------------------------------------

---

| Id  | Operation             | Name              | Rows  | Cost (%CPU)| Time
  |

--------------------------------------------------------------------------------

---

|   0 | SELECT STATEMENT      |                   |     1 |     9   (0)| 00:00:0

1 |

|   1 |  SORT AGGREGATE       |                   |     1 |            |
  |

|   2 |   INDEX FAST FULL SCAN| PK_TMP_TEST_COUNT |  9756 |     9   (0)| 00:00:0

1 |

--------------------------------------------------------------------------------

---


Note
-----
   - dynamic sampling used for this statement (level=2)


统计信息
----------------------------------------------------------
        218  recursive calls
          6  db block gets
        117  consistent gets
         20  physical reads
          0  redo size
        527  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          6  sorts (memory)
          0  sorts (disk)
          1  rows processed

 

SQL> SELECT COUNT(*) FROM TMP_TEST_COUNT;

已用时间:  00: 00: 00.01

执行计划
----------------------------------------------------------
Plan hash value: 251659311

--------------------------------------------------------------------------------

---

| Id  | Operation             | Name              | Rows  | Cost (%CPU)| Time
  |

--------------------------------------------------------------------------------

---

|   0 | SELECT STATEMENT      |                   |     1 |     9   (0)| 00:00:0

1 |

|   1 |  SORT AGGREGATE       |                   |     1 |            |
  |

|   2 |   INDEX FAST FULL SCAN| PK_TMP_TEST_COUNT |  9756 |     9   (0)| 00:00:0

1 |

--------------------------------------------------------------------------------

---


Note
-----
   - dynamic sampling used for this statement (level=2)


统计信息
----------------------------------------------------------
          4  recursive calls
          0  db block gets
         86  consistent gets
          0  physical reads
          0  redo size
        527  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

从执行计划来看,COUNT(1)与COUNT(*)的成本是一样的,而且都是走了索引!(统计信息不同,是因为Oracle缓存的原因。执行两遍就能发现其实是一样的)。

 


总结

经过上面的实验发现,其实COUNT(1)与COUNT(*)是一致的。而且在有符合条件的索引下,它们都会走索引。但是这两个语句在真实的环境中是很少被运用。因为实际业务中,很少会去判断表中有多少条记录。常常是用COUNT来判断表中是否有记录。在这种业务需求下,单单上面的COUNT(1)或者COUNT(*)都不太好,特别是当表中数据量非常大的时候,执行效率非常慢。需要通过以下的代码进行改写:

SELECT COUNT(*) FROM TMP_TEST_COUNT WHERE ROWNUM = 1;

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值