今天讨论一下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 *。
第30期 SELECT * 对SQL性能不利的原因
来源:这里教程网
时间:2026-03-03 21:30:25
作者:
编辑推荐:
- 第30期 SELECT * 对SQL性能不利的原因03-03
- [20250208]library cache mutex X与library cache bucket mutex X的区别(21c).txt03-03
- OGG-downstream模式测试之安装部署03-03
- 虚拟机 hyper-v,虚拟机 hyper-v的实操步骤,hyper-v批量管理工具的使用指南03-03
- OGG在DG端挖掘的相关模拟测试——生产节点异常03-03
- gc buffer busy acquire导致的重大数据库性能故障03-03
- 19C单机以及RAC手工清理步骤03-03
- hyper -v管理,hyper -v管理的实操步骤,hyper-v批量管理工具的使用指南03-03
下一篇:
相关推荐
-
雷神推出 MIX PRO II 迷你主机:基于 Ultra 200H,玻璃上盖 + ARGB 灯效
2 月 9 日消息,雷神 (THUNDEROBOT) 现已宣布推出基于英
-
制造商 Musnap 推出彩色墨水屏电纸书 Ocean C:支持手写笔、第三方安卓应用
2 月 10 日消息,制造商 Musnap 现已在海外推出一款 Oce
热文推荐
- 干了15年Oracle,今天重新认识了这个问题
干了15年Oracle,今天重新认识了这个问题
26-03-03 - 虚拟机 hyper-v,虚拟机 hyper-v的实操步骤,hyper-v批量管理工具的使用指南
- gc buffer busy acquire导致的重大数据库性能故障
gc buffer busy acquire导致的重大数据库性能故障
26-03-03 - hyper -v管理,hyper -v管理的实操步骤,hyper-v批量管理工具的使用指南
- 第27期 Oracle在线重定义介绍
第27期 Oracle在线重定义介绍
26-03-03 - hyper-v虚拟化,hyper-v虚拟化的实操步骤,hyper-v批量管理工具的使用指南
- win10 hyper v,win10 hyper v的实操指南,hyper-v批量管理工具的使用指南
- 数据库管理-第293期 奇怪的sys.user$授权+(20250210)
- 数据库管理-第291期 Exadata换磁盘(20250207)
数据库管理-第291期 Exadata换磁盘(20250207)
26-03-03 - vmware与hyper v,vmware与hyper v的实操指南,hyper-v批量管理工具的使用指南
