第30期 SELECT * 对SQL性能不利的原因

今天讨论一下SELECT * 对SQL性能不利的原因有哪些,有时候我们应用程序并不需要查询所有的列,

当我编写生产代码时,我在查询选择列表(投影)中都会明确指定感兴趣的列,

这不仅是出于性能原因,也是出于应用程序可靠性原因。

例如,当我们为表添加了新列或表中的列顺序发生变化时,应用程序的数据处理代码是否能正常运行,会不会突然中断?

在本文中,我将只关注SQL性能方面。我们使用的是基于Oracle的示例,但这也适用于大多数的关系数据库。


我们从以下几点来进行讨论:

1、网络流量增加

2、客户端CPU使用率增加

3、某些查询计划优化是不可能的

4、服务器端内存使用情况

5、服务器端CPU使用率增加

6、硬解析/优化需要更多时间

7、缓存游标占用共享池中的更多内存

8、LOB获取

9、总结


1、网络流量增加

首先我们从网络流量方面讨论一下,网络流量增加是非常显著的一个方面,

因为我们查询的数据量多,当然网络流量也就会增加,这是最明显的效果。

比如我们查询一个每行800列和每行8列的一个表,查询是不同的。

那么每次执行查询时,你最终可能会在网络上发送100倍以上的字节(当然,可能会因单个列的长度不同有所差异)。

更多的网络字节意味着发送更多的网络数据包,根据您的RDBMS实现,也意味着更多的应用程序与数据库之间需要更多数据包网络往返。

Oracle可以将单个fetch调用的结果数据以多个连续的SQL*Net数据包的形式以流式的形式传输回客户端,而不需要客户端应用程序首先确认前面的每个数据包。

这种突发的吞吐量取决于TCP发送缓冲区的大小,当然还有网络链路带宽和延迟。我们可以参考关于SQL*Net更多数据到客户端等待事件的信息。

SQL> SET AUTOT TRACE STAT

SQL> SELECT * FROM scott.customers;


1699260 rows selected.


Elapsed: 00:01:35.82


Statistics

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

          0  recursive calls

          0  db block gets

      45201  consistent gets

          0  physical reads

          0  redo size

  169926130  bytes sent via SQL*Net to client

     187267  bytes received via SQL*Net from client

      16994  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

    1699260  rows processed


大约需要1分35秒的时间,169 MB的数据从数据库发送回客户端(平均每行大约100字节)。

有趣的是,数据字典统计的粗略行长估计显示,平均行大小应该是119个字节(116加上3个字节的行头、锁字节和列计数):


SQL> SELECT COUNT(*),SUM(avg_col_len) FROM dba_tab_columns 

     WHERE owner = 'SCOTT' AND table_name = 'CUSTOMERS';


  COUNT(*) SUM(AVG_COL_LEN)

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

        16              116

CUSTOMERS上表只有16列,现在让我们只选择我的应用程序需要的3列:


SQL> SELECT customer_id, credit_limit, customer_since FROM scott.customers;


1699260 rows selected.


Elapsed: 00:00:43.20


Statistics

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

          0  recursive calls

          0  db block gets

      45201  consistent gets

          0  physical reads

          0  redo size

   31883155  bytes sent via SQL*Net to client

     187307  bytes received via SQL*Net from client

      16994  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

    1699260  rows processed


因此,在16列中仅选择3列,查询响应时间就提高了2倍多(1分35秒对比43秒)。

sqlplus Elapsed指标包括在DB服务器上执行查询并将其所有记录从客户端获取所需的时间,因此网络延迟、吞吐量和TCP发送缓冲区配置将对其产生影响。

Oracle可以在每次提取调用的结果集中消除重复的字段值,因此,如果您需要提取大量行和列并节省网络带宽(例如,通过数据库链接将数据从纽约复制到新加坡),

您可以通过按最重复(最不明显的值)的列对查询结果集进行排序来最大限度地实现这种“压缩”,这些列也很宽。


SQL> SELECT * FROM scott.customers 

     ORDER BY customer_class,nls_territory,nls_language,cust_first_name;


1699260 rows selected.


Elapsed: 00:01:09.23


Statistics

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

          0  recursive calls

          0  db block gets

      28478  consistent gets

          0  physical reads

          0  redo size

   65960489  bytes sent via SQL*Net to client

     187334  bytes received via SQL*Net from client

      16994  SQL*Net roundtrips to/from client

          1  sorts (memory)

          0  sorts (disk)

    1699260  rows processed


上面的测试再次是SELECT *,但是这次查询是按几个VARCHAR2列排序,这些列的最大大小为10-40字节,有很多重复值。在SQL*Net协议级重复数据删除后,服务器仅发送了约65MB数据包。

请注意,对于上述所有测试运行,SQL*Net往返客户端的值都是相同的,这是因为我的应用程序中的fetch arraysize已设置为100。

arraysize控制您最终通过网络发送多少个用于数据检索的fetch调用,第一个fetch之后的每个fetch都会请求返回arraysize的行,无论它们有多宽:

1699260行除以arraysize 100将需要16993次取数+1次初始单行取数=16994次的SQL*Net roundtrips to/from client

因此,SQL*net往返度量取决于通过网络发送的DB调用的数量(取数),而每个往返派生度量发送的字节数取决于取数在单个DB调用中请求的行数以及这些行的宽度。

实际情况稍微复杂一些,取决于应用程序客户端库的行为,但为了简洁起见,我将跳过这一部分。

请注意,您可以进一步增加arraysize(例如从100增加到1000),这样不仅可以减少SQL*Net的往返次数(1700而不是16994),而且传输的字节数也会略有减少,这可能是由于更好的压缩和SQL*Net数据包开销的略微降低。

通过Oracle links传输数据时,不需要在客户端会话中增加arraysize,因为Oracle会自动为dblinks使用最大可能的arraysize(~32767)。

2、客户端CPU使用率增加

在客户端处理的行越多,列越多(列越宽),处理它们所需的CPU时间就越多。

在我的例子中,应用程序认为时间就是提取、格式化记录并将其写入输出文件。

我直接登录到Linux数据库服务器,并通过本地管道运行sqlplus,以排除任何网络/TCP开销。我正在运行的两个脚本是:

使用select *  选择全部16列的情况:

selectstar.sql:选择全部16列:


SET ARRAYSIZE 100 TERMOUT OFF

SPOOL customers.txt

SELECT * FROM scott.customers;

SPOOL OFF

EXIT


选择3列的情况:

selectsome.sql: Select 3 columns:


SET ARRAYSIZE 100 TERMOUT OFF

SPOOL customers.txt

SELECT customer_id, credit_limit, customer_since FROM scott.customers;

SPOOL OFF

EXIT


所以,在本地运行脚本:


$ time sqlplus -s system/oracle @selectstar


real   1m21.056s

user   1m3.053s

sys    0m15.736s


当将user+sysCPU加在一起时,我们得到了大约1分19秒的CPU时间,而real的总运行时间为1米21秒,这意味着sqlplus只花了很少的时间,

等待更多的结果从管道中到达。因此,我的“应用程序”在客户端的应用程序思考时间中花费了99%的运行时间,在处理检索到的数据时消耗了CPU。

由于几乎所有的时间都花在客户端应用程序上,我在数据库上做的“调优”不多,添加索引或增加各种数据库缓冲区无济于事,因为数据库时间仅占我总运行时间的1%。

但是,通过更改应用程序代码,只获取我需要的列,我可以大大减少客户端处理/应用程序的时间:

$ time sqlplus -s system/oracle @selectsome


real   0m4.047s

user   0m2.752s

sys    0m0.349s


总运行时间仅为4秒,其中约3.1秒用于CPU。更好的性能,更低的CPU使用率!


当然,您的查询可能会有所不同,具体取决于您运行的应用程序类型以及您使用的DB客户端库。

然而,当你的表有500多列时(就像许多数据仓库表一样),SELECT *和SELECT 10列之间的区别。。。可以是巨大的。

顺便说一句,从Oracle 12.2开始,您可以使用sqlplus -fast选项使sqlplus启用一些性能选项(arraysize、大输出页面大小等):


$ time sqlplus -fast -s system/oracle @selectstar


real 0m16.046s

user 0m11.851s

sys     0m1.718s


而这次选择星脚本现在只需16秒即可运行,而不是1分21秒。

通过将输出直接打印到CSV,sqlplus可以避免一些(列对齐的)格式化代码路径,可以使用更少的CPU:

$ time sqlplus -m "csv on" -fast -s system/oracle @selectstar


real 0m12.048s

user 0m10.144s

sys 0m0.447s


Oracle编写的快速CSV卸载器终于来了!


3、某些查询计划优化是不可能的


Oracle的优化器,如果它认为这对性能有好处的话,可以将sql的查询转换为不同的、但在逻辑上等效的sql查询。

sql的查询转换开辟了额外的优化机会(更高效的数据访问路径),有些甚至允许您跳过执行查询的一部分。

例如,如果恰好有一个索引覆盖了SQL所需的所有列,Oracle可以只扫描“索引”,而不是整个“胖”宽的表。

这种索引快速全扫描不是使用索引树遍历,而是更像是按照存储顺序对所有索引块进行全表扫描(忽略根和分支块)。

下面是一个select * 与select col1、col2的示例,其中col1、coll2恰好位于索引中:




SQL> select * from table(dbms_xplan.display_cursor('f2czqvfz3pj5w',0));


SELECT * FROM scott.customers


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

| Id | Operation         | Name      | Starts | A-Rows | A-Time   | Reads |

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

|  0 | SELECT STATEMENT  |           |      1 |   1699K| 00:00.57 | 28475 |

|  1 |  TABLE ACCESS FULL| CUSTOMERS |      1 |   1699K| 00:00.57 | 28475 |

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

上面的select * 必须扫描表以获取其所有列。总运行时间0.57秒,读取28475个块。


现在,让我们只选择恰好被单个多列索引覆盖的几列:

SQL> select * from table(dbms_xplan.display_cursor('9gwxhcvwngh96 ',0));


SELECT customer_id, dob FROM scott.customers


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

| Id  | Operation            | Name              | Starts | A-Rows | A-Time   | Reads |

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

|   0 | SELECT STATEMENT     |                   |      1 |   1699K| 00:00.21 |  5915 |

|   1 |  INDEX FAST FULL SCAN| IDX_CUSTOMER_DOB2 |      1 |   1699K| 00:00.21 |  5915 |

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

上述查询从全表扫描访问切换到索引快速全扫描,因此只读取5915个索引块,并在0.21秒内完成,而不是0.57秒。


另一个更复杂的例子是Oracle的联接消除转换。

它可以帮助处理访问幕后使用多个连接的视图的大型复杂查询,但我将在这里展示一个微观测试用例。

U(用户)和O(对象)测试表之间定义了一个外键约束-O.owner指向U.username,FK约束强制O表中的每个对象记录在U表中都必须有一个相应的用户记录。

那么,让我们在SQL中运行一个双表连接:


SELECT o.owner FROM u, o WHERE u.username = o.owner


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

| Id  | Operation         | Name | Starts | A-Rows | Buffers |

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

|   0 | SELECT STATEMENT  |      |      1 |  61477 |    1346 |

|   1 |  TABLE ACCESS FULL| O    |      1 |  61477 |    1346 |

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


根据上述执行计划,实际上只访问了一个表?这就是Oracle正在实施的加入消除转换。

通过仅访问父子关系中的子表可以满足此查询,因为我们希望O中的记录在U中有相应的记录,外键约束保证这是真的!

在上面的查询中,我们只从子表O中选择了列,

下面让我们也将U.username添加到选择列表中:


SELECT o.owner,u.username FROM u, o WHERE u.username = o.owner


Plan hash value: 3411128970


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

| Id  | Operation         | Name | Starts | A-Rows | Buffers |

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

|   0 | SELECT STATEMENT  |      |      1 |  61477 |    1346 |

|   1 |  TABLE ACCESS FULL| O    |      1 |  61477 |    1346 |

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

尽管从表U中选择了一列,但我们仍然不必去表U——这是因为由于WHERE U.username=o.owner连接条件,该列保证与o.owner完全相同。Oracle足够聪明,可以避免进行连接,

因为它知道这是一个逻辑上有效的快捷方式。

但是现在让我们从表U中选择一个额外的非连接列,我甚至没有使用具有相同效果的select *:


SELECT o.owner,u.username,u.created FROM u, o WHERE u.username = o.owner


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

| Id  | Operation          | Name | Starts | A-Rows | Buffers | Used-Mem |

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

|   0 | SELECT STATEMENT   |      |      1 |  61477 |    1350 |          |

|*  1 |  HASH JOIN         |      |      1 |  61477 |    1350 | 1557K (0)|

|   2 |   TABLE ACCESS FULL| U    |      1 |     51 |       3 |          |

|   3 |   TABLE ACCESS FULL| O    |      1 |  61477 |    1346 |          |

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


   1 - access("U"."USERNAME"="O"."OWNER")


现在我们看到这两个表都被访问和连接,因为没有有效的快捷方式(优化)可供选择。

您可能会说,这似乎是一个非常奇特的优化,在现实生活中几乎没有价值(您不需要父表中的列,并且父表无论如何都是由其主键索引的)。

在实践中,对于复杂的sql查询(连接了数十个表,有多个子查询、视图等),sql的执行计划,这可能是非常有益的。

此外,如果转换阶段可以从连接中删除一些表,那么“物理优化器”将更容易为剩余的表找出一个好的连接顺序。


4、服务器端内存使用情况

在查看sql执行计划时,有一个名为Used-Mem的列。

如用于排序的排序缓冲区或用于哈希连接的哈希表、区分和分组,都需要一个内存暂存区(SQL游标工作区)来操作。一次处理的行越多,通常需要的内存就越多。

而且,每行缓冲的列越多,所需的内存就越多!

最简单的例子就是ORDER BY:

SELECT * FROM scott.customers ORDER BY customer_since


Plan hash value: 2792773903


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

| Id  | Operation          | Name      | Starts | A-Rows |   A-Time   | Used-Mem |

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

|   0 | SELECT STATEMENT   |           |      1 |   1699K|00:00:02.31 |          |

|   1 |  SORT ORDER BY     |           |      1 |   1699K|00:00:02.31 |  232M (0)|

|   2 |   TABLE ACCESS FULL| CUSTOMERS |      1 |   1699K|00:00:00.24 |          |

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

232 MB内存用于上述排序。(0)表示零通过操作,我们不必将任何临时结果溢出到磁盘,整个排序都适合内存。


现在只选择2列(按第3列排序):


SELECT customer_id,dob FROM scott.customers ORDER BY customer_since


Plan hash value: 2792773903


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

| Id  | Operation          | Name      | Starts | A-Rows |   A-Time   | Used-Mem |

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

|   0 | SELECT STATEMENT   |           |      1 |   1699K|00:00:00.59 |          |

|   1 |  SORT ORDER BY     |           |      1 |   1699K|00:00:00.59 |   67M (0)|

|   2 |   TABLE ACCESS FULL| CUSTOMERS |      1 |   1699K|00:00:00.13 |          |

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



内存使用量从232 MB下降到67 MB。查询仍然必须扫描整个Customers表,

并像以前一样处理1699k行,但它的运行速度快了4倍,因为它在排序阶段没有花费太多的CPU时间。

更窄的记录不仅在缓冲区中使用更少的内存,而且对CPU缓存友好,需要移动更少的字节(RAM访问速度较慢)。


由于通过数据库网络和客户端库发送/接收记录数组(这里甚至不谈论TCP发送/接收缓冲区),宽结果集也会增加内存使用量(包括服务器和客户端)。

当你每次提取检索1000条记录,每条1000列记录的平均大小为5kB时,我们谈论的是数据库端的每个连接至少有5MB的内存,

应用程序端的每个打开的游标至少有5Mb的内存。在实践中,由于处理和打包(和保存)结果的数据结构有一些开销,因此使用量会更大。

如今,内存相对便宜,这不是什么大问题,但我记得15年前的一个大型Oracle系统,客户不得不减小数组大小,否则他们的80000个数据库连接会耗尽服务器内存:-)此外,

如果你的应用程序有某种游标泄漏(语句句柄泄漏),与未使用的游标结果集数组相关的内存可能会积聚起来。


5、服务器端CPU使用率增加


当然,如果上述更高级别的查询方式和优化方式没有发挥作用,最终会做更多的工作。更多的工作意味着更多的CPU使用率(可能还有磁盘I/O等)。

当从数据块中的记录中提取所有500个字段时,而不是仅仅20个字段时,并将其传递给执行计划树时,您将使用更多的CPU周期来完成这项工作。如果使用列式存储布局,您也可能最终也会执行更多的I/O。

例如,使用Oracle的传统的数据块,数据库表有500列,我们要查询第500列,需要跳过所有前499列的信息(行程编码),

以找到最后一列的起始位置,开始读取数据。但是,现在,如果你真的需要从所有行中检索所有500个字段,SELECT* 对于这个任务来说是有效的,

但如果你的应用程序只使用结果集中的少数列,你将在(昂贵的)数据库服务器上不必要地消耗大量额外的CPU时间。

还有就是我们查询的结果如果需要类型转换,例如在服务器端执行数据类型转换(数字、日期格式到客户端期望的格式)以及字符串的字符集转换(如果有的话),您也会在数据库服务器上消耗更多的CPU。

例如,如果你在某个分析应用程序中从数据库中“仅”选择1M行,但选择了表的所有500列,那么你最终只会为这一个查询进行5亿次数据类型/字符集转换操作,你很快就会意识到这些操作非常消耗CPU。



6、硬解析/优化需要更多时间


我使用create_wide_table.sql脚本创建了一个宽表(1000列)。它有100行,每列都有直方图。

DECLARE

    cmd CLOB := 'CREATE TABLE widetable ( id NUMBER PRIMARY KEY ';

    ins CLOB := 'INSERT INTO widetable SELECT rownum';

BEGIN

    FOR x IN 1..999 LOOP

        cmd := cmd || ', col'||TRIM(TO_CHAR(x))||' VARCHAR2(10)';

        ins := ins || ', TRIM(TO_CHAR(rownum))';

    END LOOP;

    cmd := cmd || ')';

    ins := ins || ' FROM dual CONNECT BY level <= 100';

    EXECUTE IMMEDIATE cmd;

    EXECUTE IMMEDIATE ins;

END;

/


COMMIT;


-- stats with histograms

EXEC DBMS_STATS.GATHER_TABLE_STATS(user,'WIDETABLE',method_opt=>'FOR TABLE, FOR ALL COLUMNS SIZE 254');


-- no histograms

-- EXEC DBMS_STATS.GATHER_TABLE_STATS(user,'WIDETABLE',method_opt=>'FOR TABLE, FOR ALL COLUMNS SIZE 1');


-- EXEC sys.dbms_shared_pool.purge('SYSTEM', 'WIDETABLE', 1, 1);



我正在对它运行一个非常简单的单表选择查询(注释中的 /* test nnn */用于在每次运行查询时强制执行新的硬解析)。

在前两个测试中,我在重新创建表并收集统计数据后立即运行select语句(在此表上没有执行其他查询):



SQL> SET AUTOTRACE TRACE STAT


SQL> SELECT * FROM widetable /* test100 */;


100 rows selected.


Statistics

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

       2004  recursive calls

       5267  db block gets

       2458  consistent gets

          9  physical reads

    1110236  redo size

     361858  bytes sent via SQL*Net to client

        363  bytes received via SQL*Net from client

          2  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

        100  rows processed


2004 recursive calls 的SELECT*的递归调用(用于数据字典访问,可以使用SQL*Trace进行验证)。

我再次创建了表,然后只运行了两列select next:


SQL> SELECT id,col1 FROM widetable /* test101 */;


100 rows selected.


Statistics

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

          5  recursive calls

         10  db block gets

         51  consistent gets

          0  physical reads

       2056  redo size

       1510  bytes sent via SQL*Net to client

        369  bytes received via SQL*Net from client

          2  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

        100  rows processed


硬解析只需要5次递归调用。请看,要求Oracle做更多的工作(“请检查、评估和提取1000列而不是2列”)会对性能产生影响。

好吧,这可能不是什么大问题——假设你的共享池足够大,可以将所有列(及其统计数据/直方图)信息保存在字典缓存中,

那么你就不会有所有这些具有良好热缓存的递归SQL。让我们看看当所有内容都很好地缓存在字典缓存中时,硬解析阶段需要多少时间。

我在一个单独的Oracle会话中使用会话跟踪工具 orasnapper来报告另一个会话(1136)中硬解析测试的指标:

SQL> SELECT * FROM widetable /* test1 */;


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

    SID, USERNAME  , TYPE, STATISTIC                          ,         DELTA

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

   1136, SYSTEM    , TIME, hard parse elapsed time            ,         78158

   1136, SYSTEM    , TIME, parse time elapsed                 ,         80912

   1136, SYSTEM    , TIME, PL/SQL execution elapsed time      ,           127

   1136, SYSTEM    , TIME, DB CPU                             ,         89580

   1136, SYSTEM    , TIME, sql execute elapsed time           ,          5659

   1136, SYSTEM    , TIME, DB time                            ,         89616


--  End of Stats snap 1, end=2020-11-24 19:31:49, seconds=5

硬解析/优化/编译阶段花费了78毫秒(所有CPU时间)来执行这个非常简单的查询,该查询选择了所有1000列,即使所有表元数据和列统计数据和直方图都已缓存。Oracle必须对所有1000列进行分析和类型检查。现在让我们在同一个表上运行另一个查询,只选择2列:

SQL> SELECT id,col1 FROM widetable /* test2 */;


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

    SID, USERNAME  , TYPE, STATISTIC                          ,         DELTA

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

   1136, SYSTEM    , TIME, hard parse elapsed time            ,          1162

   1136, SYSTEM    , TIME, parse time elapsed                 ,          1513

   1136, SYSTEM    , TIME, PL/SQL execution elapsed time      ,           110

   1136, SYSTEM    , TIME, DB CPU                             ,          2281

   1136, SYSTEM    , TIME, sql execute elapsed time           ,           376

   1136, SYSTEM    , TIME, DB time                            ,          2128

   

硬解析只花了大约1毫秒!SQL在结构上是相同的,在同一个表上,只选择了更少的列。

出于好奇,当我们删除所有列上的直方图并再次执行SELECT*时会发生什么:

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(user,'WIDETABLE',method_opt=>'FOR ALL COLUMNS SIZE 1');


PL/SQL procedure successfully completed.



SQL> SELECT * FROM widetable /* test3 */;


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

    SID, USERNAME  , TYPE, STATISTIC                          ,         DELTA

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

   1136, SYSTEM    , TIME, hard parse elapsed time            ,         30018

   1136, SYSTEM    , TIME, parse time elapsed                 ,         30547

   1136, SYSTEM    , TIME, PL/SQL execution elapsed time      ,           202

   1136, SYSTEM    , TIME, DB CPU                             ,         37899

   1136, SYSTEM    , TIME, sql execute elapsed time           ,          5770

   1136, SYSTEM    , TIME, DB time                            ,         37807

现在,对于1000列查询,硬解析需要30毫秒,显然它枚举/映射了查询中涉及的所有列的直方图,

包括刚刚选择的列(在任何过滤器或连接中都没有使用,直方图实际上用于计划优化)。


7、缓存游标占用共享池中的更多内存

Oracle将编译的游标缓存在共享池内存中。Oracle很聪明,只将所需的元数据(各种操作码、数据类型、规则)包含在编译的游标中。

因此,使用1000列的缓存游标将比仅使用2列的游标大得多:

SQL> SELECT sharable_mem, sql_id, child_number, sql_text FROM v$sql 

     WHERE sql_text LIKE 'SELECT % FROM widetable';


SHARABLE_MEM SQL_ID        CHILD_NUMBER SQL_TEXT

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

       19470 b98yvssnnk13p            0 SELECT id,col1 FROM widetable

      886600 c4d3jr3fjfa3t            0 SELECT * FROM widetable



在共享池中,2列游标占用19kB内存,1000列游标占用886kB内存!

从大约10g开始,Oracle将大多数大型库缓存对象分配拆分为标准化的数据块大小(4kB),以减少共享池碎片的影响。

让我们用我的orasqlmem.sql脚本(v$sql_shared_memory)来看看这些游标:

Show shared pool memory usage of SQL statement with SQL_ID c4d3jr3fjfa3t


CHILD_NUMBER SHARABLE_MEM PERSISTENT_MEM RUNTIME_MEM

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

           0       886600         324792      219488



TOTAL_SIZE   AVG_SIZE     CHUNKS ALLOC_CL CHUNK_TYPE STRUCTURE            FUNCTION             CHUNK_COM            HEAP_ADDR

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

    272000        272       1000 freeabl           0 kccdef               qkxrMem              kccdef: qkxrMem      000000019FF49290

    128000        128       1000 freeabl           0 opn                  qkexrInitO           opn: qkexrInitO      000000019FF49290

    112568         56       2002 freeabl           0                      qosdInitExprCtx      qosdInitExprCtx      000000019FF49290

     96456         96       1000 freeabl           0                      qosdUpdateExprM      qosdUpdateExprM      000000019FF49290

     57320         57       1000 freeabl           0 idndef*[]            qkex                 idndef*[]: qkex      000000019FF49290

     48304         48       1000 freeabl           0 qeSel                qkxrXfor             qeSel: qkxrXfor      000000019FF49290

     40808         40       1005 freeabl           0 idndef               qcuAll               idndef : qcuAll      000000019FF49290

     40024      40024          1 freeabl           0 kafco                qkacol               kafco : qkacol       000000019FF49290

     37272        591         63 freeabl           0                      237.kggec            237.kggec            000000019FF49290

     16080       8040          2 freeabl           0 qeeRwo               qeeCrea              qeeRwo: qeeCrea      000000019FF49290

      8032       8032          1 freeabl           0 kggac                kggacCre             kggac: kggacCre      000000019FF49290

      8024       8024          1 freeabl           0 kksoff               opitca               kksoff : opitca      000000019FF49290

      3392         64         53 freeabl           0 kksol                kksnsg               kksol : kksnsg       000000019FF49290

      2880       2880          1 free              0                      free memory          free memory          000000019FF49290

      1152        576          2 freeabl           0                      16751.kgght          16751.kgght          000000019FF49290

      1040       1040          1 freeabl           0 ctxdef               kksLoadC             ctxdef:kksLoadC      000000019FF49290

       640        320          2 freeabl           0                      615.kggec            615.kggec            000000019FF49290

       624        624          1 recr           4095                      237.kggec            237.kggec            000000019FF49290

       472        472          1 freeabl           0 qertbs               qertbIAl             qertbs:qertbIAl      000000019FF49290

...


53 rows selected.


1000列的SELECT*游标有很多内部分配(在游标堆内分配),其中内部块的计数为1000或接近1000的倍数,因此编译游标中的每一列都有一个(或两个)。

这些结构是执行计划所必需的(比如当字段#3需要向上传递到执行计划树时,需要调用Oracle内核的C函数)。

例如,如果列#77恰好是一个DATE,并且稍后在计划的一个单独步骤中将其与TIMESTAMP列#88进行比较,则需要在某处添加一个操作码,

指示Oracle在该计划步骤中为其中一个列执行额外的数据类型转换函数。执行计划是一个由动态分配的结构和其中的操作码组成的树。

显然,即使是从单个表中进行简单的选择而没有任何进一步的复杂性,也需要大量的内部分配。

让我们看看2列游标内存的内部:

Show shared pool memory usage of SQL statement with SQL_ID b98yvssnnk13p


CHILD_NUMBER SHARABLE_MEM PERSISTENT_MEM RUNTIME_MEM

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

           0        19470           7072        5560



TOTAL_SIZE   AVG_SIZE     CHUNKS ALLOC_CL CHUNK_TYPE STRUCTURE            FUNCTION             CHUNK_COM            HEAP_ADDR

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

      1640       1640          1 free              0                      free memory          free memory          00000001AF2B75D0

      1152        576          2 freeabl           0                      16751.kgght          16751.kgght          00000001AF2B75D0

      1040       1040          1 freeabl           0 ctxdef               kksLoadC             ctxdef:kksLoadC      00000001AF2B75D0

       640        320          2 freeabl           0                      615.kggec            615.kggec            00000001AF2B75D0

       624        624          1 recr           4095                      237.kggec            237.kggec            00000001AF2B75D0

       544        272          2 freeabl           0 kccdef               qkxrMem              kccdef: qkxrMem      00000001AF2B75D0

       472        472          1 freeabl           0 qertbs               qertbIAl             qertbs:qertbIAl      00000001AF2B75D0

       456        456          1 freeabl           0 opixpop              kctdef               opixpop:kctdef       00000001AF2B75D0

       456        456          1 freeabl           0 kctdef               qcdlgo               kctdef : qcdlgo      00000001AF2B75D0

       328         54          6 freeabl           0                      qosdInitExprCtx      qosdInitExprCtx      00000001AF2B75D0

       312        312          1 freeabl           0 pqctx                kkfdParal            pqctx:kkfdParal      00000001AF2B75D0

       296        296          1 freeabl           0                      unmdef in opipr      unmdef in opipr      00000001AF2B75D0

       256        128          2 freeabl           0 opn                  qkexrInitO           opn: qkexrInitO      00000001AF2B75D0

       256         42          6 freeabl           0 idndef               qcuAll               idndef : qcuAll      00000001AF2B75D0

       208         41          5 freeabl           0                      kggsmInitCompac      kggsmInitCompac      00000001AF2B75D0

       192         96          2 freeabl           0                      qosdUpdateExprM      qosdUpdateExprM      00000001AF2B75D0

       184        184          1 freeabl           0                      237.kggec            237.kggec            00000001AF2B75D0

...

事实上,我们不再看到数千个内部分配块(例如,与之前的1000个相比,只有2个kccdef)。


8、LOB获取

当您从表中选择LOB列时,由于为每个返回的行单独获取LOB项所做的额外网络往返,您的性能将大幅下降。

可以将arraysize设置为1000,但如果您从结果集中选择一个LOB列,那么对于每个(1000)行的数组,您将不得不进行1000次额外的网络往返以获取单个LOB值。

除了“普通”列之外,我还将创建一个包含2个LOB列的表:

SQL> CREATE TABLE tl (id INT, a VARCHAR2(100), b CLOB, c CLOB);


Table created.


SQL> INSERT INTO tl SELECT rownum, dummy, dummy, dummy FROM dual CONNECT BY LEVEL <= 1000;


1000 rows created.


SQL> COMMIT;


Commit complete.


让我们先只选择2个普通列:

SQL> SET AUTOT TRACE STAT

SQL> SET TIMING ON


SQL> SELECT id, a FROM tl;


1000 rows selected.


Elapsed: 00:00:00.04


Statistics

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

          0  recursive calls

          0  db block gets

         28  consistent gets

          0  physical reads

          0  redo size

      10149  bytes sent via SQL*Net to client

        441  bytes received via SQL*Net from client

         11  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

       1000  rows processed

   

获取2个正常列非常快(0.04秒),只需要11次SQL*Net往返(使用arraysize 100)。

现在,让我们添加一个LOB列:

SQL> SELECT id, a, b FROM tl;


1000 rows selected.


Elapsed: 00:00:05.50


Statistics

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

         10  recursive calls

          5  db block gets

       2027  consistent gets

          0  physical reads

       1052  redo size

     421070  bytes sent via SQL*Net to client

     252345  bytes received via SQL*Net from client

       2002  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

       1000  rows processed

由于LOB检索的“破坏性”,它需要5.5秒和2002 SQL*Net往返。默认情况下,

任何具有非空LOB列的行都会立即发送回(在获取的数组中只有一行),并且会发送回一个LOB定位器,而不是LOB列值,

这会导致客户端发出单独的LOBREAD数据库调用,只是为了获取单个LOB列值。当您选择多个LOB列时,情况会变得更糟:

SQL> SELECT id, a, b, c FROM tl;


1000 rows selected.


Elapsed: 00:00:09.28


Statistics

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

          6  recursive calls

          5  db block gets

       3026  consistent gets

          0  physical reads

        996  redo size

     740122  bytes sent via SQL*Net to client

     493348  bytes received via SQL*Net from client

       3002  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

       1000  rows processed

现在只需要一个LOB列就需要9秒多,而不是之前的5.5秒。我们有大约3000次往返,每行一次(因为LOB项检索会中断数组获取),每行有一次两个LOB项获取往返。

从Oracle 12.2(我认为)开始,sqlplus中有一个参数LOBPREFETCH,它允许将一定量的LOB数据“捆绑”到行提取往返中。

Oracle客户端库应允许更大的LOB预取值,但sqlplus中的限制为32kB:

   

SQL> SET LOBPREFETCH 32767

SQL> 

SQL> SELECT id, a, b, c FROM tl;


1000 rows selected.


Elapsed: 00:00:04.80


Statistics

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

          0  recursive calls

          0  db block gets

       1005  consistent gets

          0  physical reads

          0  redo size

     366157  bytes sent via SQL*Net to client

      11756  bytes received via SQL*Net from client

       1002  SQL*Net roundtrips to/from client

          0  sorts (memory)

          0  sorts (disk)

       1000  rows processed    

现在我们又减少到了大约1000次往返,因为我的LOB值很小,它们都捆绑在每行的获取结果中。但Oracle仍然一次只获取一行,尽管我的arraysize=100。

因此,由于偶然的SELECT*添加了LOB列,您的40毫秒查询最终可能需要超过9秒。而且,您根本看不到数据库的太多活动,

因为大部分响应时间都花在客户端和服务器之间的SQL*Net往返上。没有索引会让这更快,更多的CPU不会让这更快——修复你的应用程序代码会让这更快。

这就引出了一个问题,如果想将数百万个LOB值引入我的应用程序,该怎么办,但我会把这留给一个单独的博客条目!    

   

9、总结:

当我看到一个性能问题时,我会考虑如何少做。另一种选择是添加更多硬件。

“少做”的一种方法是确保你从数据库中准确地询问你想要什么,不多也不少。

仅选择您实际需要的列是该方法的最佳方案,而不是使用select *。




请使用浏览器的分享功能分享到微信等