构建可扩展的应用
设计您的应用以使用相同的资源,而不考虑用户数量和数据量,并且不会使系统资源过载。
关于可扩展应用程序
可伸缩应用程序可以处理较大的工作量,同时系统资源使用量也会相应增加。
可伸缩应用程序可以处理较大的工作量,同时系统资源使用量也会相应增加。例如,如果工作量增加一倍,则可伸缩应用程序使用的系统资源数量是其两倍。
不可扩展应用程序耗尽系统资源;因此,如果增加应用程序工作量,则无法再增加吞吐量。不可伸缩的应用程序会导致固定的吞吐量和较差的响应时间。
资源耗尽的示例包括:
-
硬件耗尽
-
导致不可避免的磁盘输入/输出 (I/O) 短缺的大量事务中的表扫描
-
网络请求过多导致网络和调度瓶颈
-
导致分页和交换的内存分配
-
进程和线程分配过多导致操作系统崩溃
设计您的应用以使用相同的资源,而不考虑用户数量和数据量,并且不会使系统资源过载。
使用绑定变量提高可扩展性
正确使用绑定变量,可以开发高效、可扩展的应用程序。
绑定变量是 SQL 语句中的占位符,必须将其替换为语句成功执行的有效值或值地址。通过使用绑定变量,可以编写 SQL 语句,在运行时接受输入或参数。
正如子程序可以有参数,其值由调用程序提供一样,SQL 语句可以有绑定变量占位符,其值(称为绑定变量)在运行时提供。正如子程序被编译一次,然后多次使用不同的参数运行一样,具有绑定变量占位符的 SQL 语句会硬性解析一次,然后使用不同的绑定变量进行软性语法分析。
硬性语法分析(包括优化和行源生成)是 CPU 密集型操作。soft parse 跳过优化和行源生成并直接执行,通常比对同一语句进行硬性语法分析快得多。(有关 SQL 处理的概述(包括硬语法分析与软语法分析之间的区别),请参阅 Oracle Database Concepts。)
硬性语法分析不仅是 CPU 密集型操作,而且是无法扩展的操作,因为它无法与许多其他操作同时执行。有关并发性和可扩展性的更多信息,请参阅关于并发性和可扩展性。
Example 8-1 显示了没有绑定变量的查询与具有绑定变量的语义等效查询之间的性能差异。前者速度较慢,使用更多闩锁(有关闩锁如何影响可扩展性的信息,请参见)。为了收集和显示性能统计信息,该示例使用 Runstats 工具,如“ Comparing Programming Techniques with Runstats ”中所述。
注:
-
示例 8-1 显示了单个用户的性能成本。随着越来越多的用户加入,成本急剧上升。
-
使用以下设置生成了 Example 8-1 的结果:
SET SERVEROUTPUT ON FORMAT TRUNCATED
注:
-
使用绑定变量而不是字符串文字是使代码不受 SQL 注入攻击的最有效方法。有关详细信息,请参阅 Oracle Database PL/SQL Language Reference。
-
绑定变量有时会降低数据仓库系统的效率。由于大多数查询需要很长时间,因此优化程序会尝试为每个查询生成最佳计划,而不是生成最佳的一般查询。使用绑定变量有时会强制优化程序生成最佳的一般查询。有关提高数据仓库系统性能的信息,请参阅 Oracle Database Data Warehousing Guide。
虽然软性语法分析比硬性语法分析更有效,但多次软性语法分析语句的成本仍然很高。要最大限度地提高应用的效率和可扩展性,请尽量减少语法分析。最小化语法分析的最简单方法是使用 PL/SQL。
示例 8-1 绑定变量可提高性能
CREATE TABLE t ( x VARCHAR2(5) );
DECLARE
TYPE rc IS REF CURSOR;
l_cursor rc;
BEGIN
runstats_pkg.rs_start; -- Collect statistics for query without bind variable
FOR i IN 1 .. 5000 LOOP
OPEN l_cursor FOR 'SELECT x FROM t WHERE x = ' || TO_CHAR(i);
CLOSE l_cursor;
END LOOP;
runstats_pkg.rs_middle; -- Collect statistics for query with bind variable
FOR i IN 1 .. 5000 LOOP
OPEN l_cursor FOR 'SELECT x FROM t WHERE x = :x' USING i;
CLOSE l_cursor;
END LOOP;
runstats_pkg.rs_stop(500); -- Stop collecting statistics
end;
/
结果与以下文本类似:
Run 1 ran in 740 hsec
Run 2 ran in 30 hsec
Run 1 ran in
2466.67% of the time of run 2
Name Run 1 Run 2 Difference
STAT...recursive cpu usage 729 19 -710
STAT...CPU used by this sessio 742 30 -712
STAT...parse time elapsed 1,051 4 -1,047
STAT...parse time cpu 1,066 2 -1,064
STAT...session cursor cache hi 1 4,998 4,997
STAT...table scans (short tabl 5,000 1 -4,999
STAT...parse count (total) 10,003 5,004 -4,999
LATCH.session idle bit 5,003 3 -5,000
LATCH.session allocation 5,003 3 -5,000
STAT...execute count 10,003 5,003 -5,000
STAT...opened cursors cumulati 10,003 5,003 -5,000
STAT...parse count (hard) 10,001 5 -9,996
STAT...CCursor + sql area evic 10,000 1 -9,999
STAT...enqueue releases 10,008 7 -10,001
STAT...enqueue requests 10,009 7 -10,002
STAT...calls to get snapshot s 20,005 5,006 -14,999
STAT...calls to kcmgcs 20,028 35 -19,993
STAT...consistent gets pin (fa 20,013 17 -19,996
LATCH.call allocation 20,002 6 -19,996
STAT...consistent gets from ca 20,014 18 -19,996
STAT...consistent gets 20,014 18 -19,996
STAT...consistent gets pin 20,013 17 -19,996
LATCH.simulator hash latch 20,014 11 -20,003
STAT...session logical reads 20,080 75 -20,005
LATCH.shared pool simulator 20,046 5 -20,041
LATCH.enqueue hash chains 20,343 15 -20,328
STAT...recursive calls 40,015 15,018 -24,997
LATCH.cache buffers chains 40,480 294 -40,186
STAT...session pga memory max 131,072 65,536 -65,536
STAT...session pga memory 131,072 65,536 -65,536
LATCH.row cache objects 165,209 139 -165,070
STAT...session uga memory max 219,000 0 -219,000
LATCH.shared pool 265,108 152 -264,956
STAT...logical read bytes from 164,495,360 614,400 -163,880,960
Run 1 latches total compared to run 2 -- difference and percentage
Run 1 Run 2 Diff Pct
562,092 864 -561,228 2,466.67%
PL/SQL procedure successfully completed.
使用 PL/SQL 提高可扩展性
某些 PL/SQL 功能可以帮助您提高应用的可扩展性。
PL/SQL 如何尽可能减少语法分析
针对数据库访问进行了优化的 PL/SQL 可无提示地缓存语句。在 PL/SQL 中,当关闭游标时,游标会从您的角度关闭,也就是说,在需要打开游标的位置无法使用游标,但是 PL/SQL 实际上会使游标保持打开状态并缓存其语句。
如果再次使用高速缓存的语句,PL/SQL 将使用相同的游标,从而避免进行语法分析。(如有必要,PL/SQL 会关闭缓存的语句,例如,如果您的程序必须打开另一个游标,但这样做将超过 OPEN_CURSORS 的 init.ora 设置。)
PL/SQL 只能无提示地高速缓存运行时无法更改的 SQL 语句。
关于 EXECUTE IMMEDIATE 语句
EXECUTE IMMEDIATE 语句可以在单个操作中构建并运行动态 SQL 语句。
EXECUTE IMMEDIATE 语句的基本语法如下:
EXECUTE IMMEDIATE sql_statement
sql_statement 是表示 SQL 语句的字符串。如果每次运行 EXECUTE IMMEDIATE 语句时 sql_statement 具有相同的值,则 PL/SQL 可以高速缓存 EXECUTE IMMEDIATE 语句。如果每次运行 EXECUTE IMMEDIATE 语句时 sql_statement 可以不同,则 PL/SQL 无法高速缓存 EXECUTE IMMEDIATE 语句。
另请参见:
-
Oracle Database PL/SQL Language Reference(了解有关 EXECUTE IMMEDIATE 的信息)
关于 OPEN FOR 语句
OPEN FOR 语句具有以下基本语法。
OPEN FOR 语句的基本语法为:
OPEN cursor_variable FOR query
您的应用程序可以在关闭 cursor_variable 之前为其打开多个不同的查询。由于 PL/SQL 在运行时才能确定不同查询的数量,因此 PL/SQL 无法高速缓存 OPEN FOR 语句。
如果不需要使用游标变量,则使用声明的游标,以提高编程的性能和易用性。有关详细信息,请参阅 Oracle Database Development Guide。
另请参见:
-
Oracle Database PL/SQL Language Reference(了解有关 OPEN FOR 的信息)
-
“关于游标变量”
-
“关于游标”
关于 DBMS_SQL 程序包
DBMS_SQL 程序包是用于构建、运行和描述动态 SQL 语句的 API。如果 PL/SQL 编译器无法在编译时确定输出主机变量的数量或类型(选择列表项)或输入绑定变量,则必须使用 DBMS_SQL 程序包而不是 EXECUTE IMMEDIATE 语句。
DBMS_SQL 程序包是用于构建、运行和描述动态 SQL 语句的 API。与使用 EXECUTE IMMEDIATE 语句相比,使用 DBMS_SQL 程序包需要更多努力,但是,如果 PL/SQL 编译器在编译时无法确定输出主机变量(选择列表项)或输入绑定变量的数量或类型,则必须使用 DBMS_SQL 程序包。
另请参见:
-
Oracle Database PL/SQL Language Reference(了解有关何时使用 DBMS_SQL 程序包的详细信息)
-
Oracle Database PL/SQL Packages and Types Reference(了解有关 DBMS_SQL 程序包的完整信息)
关于批量 SQL
批量 SQL 可减少 PL/SQL 与 SQL 之间的“往返”次数,从而减少资源使用。
如果不使用批量 SQL,则一次从数据库 (SQL) 检索一行,然后对其进行处理 (PL/SQL),然后将其返回到数据库 (SQL)。使用批量 SQL,可以从数据库中检索一组行,处理一组行,然后将整个集合返回到数据库中。
Oracle 建议在从数据库检索多行时使用成批 SQL,将它们返回到数据库,如示例 8-2 中所示。如果检索多个行但未返回这些行,则不需要批量 SQL;例如:
FOR x IN (SELECT * FROM t WHERE ... ) -- Retrieve row set (implicit array fetch)
LOOP
DBMS_OUTPUT.PUT_LINE(t.x); -- Process rows but do not return them
END LOOP;
Example 8-2 通过具有列 object_name 的表 t 循环,检索 100 行集,处理这些行并将其返回到数据库中。(将批量 FETCH 语句限制为 100 行需要显式游标。)
Example 8-3 执行与 Example 8-2 相同的作业,而不执行批量 SQL。
如 Example 8-2 和 Example 8-3 的 TKPROF 报告所示,使用此作业的批量 SQL 将 CPU 时间缩短近 50%:
SELECT ROWID RID, OBJECT_NAME FROM T T_BULK
call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 721 0.17 0.17 0 22582 0 71825
********************************************************************************
UPDATE T SET OBJECT_NAME = :B1 WHERE ROWID = :B2
call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 719 12.83 13.77 0 71853 74185 71825
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 720 12.83 13.77 0 71853 74185 71825
SELECT ROWID RID, OBJECT_NAME FROM T T_SLOW_BY_SLOW
call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 721 0.17 0.17 0 22582 0 71825
********************************************************************************
UPDATE T SET OBJECT_NAME = :B2 WHERE ROWID = :B1
call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 71824 21.25 22.25 0 71836 73950 71824
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 71825 21.25 22.25 0 71836 73950 71824
但是,对于此作业使用批量 SQL 使用的 CPU 时间和代码比使用单个 SQL 语句更多,因为此 TKPROF 报告显示:
UPDATE T SET OBJECT_NAME = SUBSTR(OBJECT_NAME,2) || SUBSTR(OBJECT_NAME,1,1)
call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 1.30 1.44 0 2166 75736 71825
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 2 1.30 1.44 0 2166 75736 71825
示例 8-2 批量 SQL
CREATE OR REPLACE PROCEDURE bulk AS
TYPE ridArray IS TABLE OF ROWID;
TYPE onameArray IS TABLE OF t.object_name%TYPE;
CURSOR c is SELECT ROWID rid, object_name -- explicit cursor
FROM t t_bulk;
l_rids ridArray;
l_onames onameArray;
N NUMBER := 100;
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT
INTO l_rids, l_onames LIMIT N; -- retrieve N rows from t
FOR i in 1 .. l_rids.COUNT
LOOP -- process N rows
l_onames(i) := substr(l_onames(i),2) || substr(l_onames(i),1,1);
END LOOP;
FORALL i in 1 .. l_rids.count -- return processed rows to t
UPDATE t
SET object_name = l_onames(i)
WHERE ROWID = l_rids(i);
EXIT WHEN c%NOTFOUND;
END LOOP;
CLOSE c;
END;
/
示例 8-3 - 无批量 SQL
CREATE OR REPLACE PROCEDURE slow_by_slow AS
BEGIN
FOR x IN (SELECT rowid rid, object_name FROM t t_slow_by_slow)
LOOP
x.object_name := substr(x.object_name,2) || substr(x.object_name,1,1);
UPDATE t
SET object_name = x.object_name
WHERE rowid = x.rid;
END LOOP;
END;
另请参见:
-
Oracle Database Development Guide,了解批量 SQL 的概述
-
Oracle Database Development Guide,了解有关何时使用批量 SQL 的更具体信息
-
Oracle Database PL/SQL Language Reference(了解有关批量 SQL 的详细信息)
关于并发性和可扩展性
并发是同时执行多个事务处理。您的应用处理并发性越好,可扩展性就越高。可伸缩应用程序可以处理较大的工作量,同时系统资源使用量也会相应增加。
并发是同时执行多个事务处理。并发事务处理中的对账单可以更新相同的数据。您的应用处理并发性越好,可扩展性就越高。可伸缩应用程序可以处理较大的工作量,同时系统资源使用量也会相应增加。例如,如果工作量增加一倍,则可伸缩应用程序使用的系统资源数量是其两倍。
并发事务处理必须产生有意义且一致的结果。因此,多用户数据库必须提供以下内容:
-
数据并发,确保用户可以同时访问数据。
-
数据一致性,确保每个用户都能看到一致的数据视图,包括他/她自己的事务处理的可见更改以及其他用户的已提交事务处理
Oracle Database 使用多版本一致性模型以及各种类型的锁和事务隔离级别来保持数据一致性。有关 Oracle Database 锁定机制的概述,请参阅 Oracle Database Concepts。有关 Oracle Database 事务处理隔离级别的概览,请参阅 Oracle Database Concepts。
为了描述同时运行事务时的一致事务行为,数据库研究人员定义了一个名为 serializable 的事务隔离类别。串行化事务处理在显示为单用户数据库的环境中运行。在特定情况下,可串行化事务是可取的,但对于 99% 的工作负载,读取提交的隔离是完美的。
Oracle Database 具有可提高并发性和可扩展性的功能,例如序列、闩锁、非阻塞读取和写入以及共享 SQL。
另请参阅:Oracle Database Concepts,了解有关数据并发性和一致性的更多信息
关于序列和并发
序列可消除序列化,从而提高应用的并发性和可扩展性。
序列是多个用户可以从中生成唯一整数的方案对象,当您需要唯一主键时,该方案非常有用。
如果没有序列,必须以编程方式生成唯一主键值。用户通过选择最近生成的值并将其递增来获取新的主键值。此技术要求在事务处理期间锁定,并导致多个用户等待下一个主键值,即事务处理序列化。序列可消除序列化,从而提高应用的并发性和可扩展性。
另请参见:
-
Oracle Database Concepts,了解有关对序列的并发访问的信息
-
“创建和管理序列”
关于闩锁和并发
闩锁的增加意味着更多基于并发的等待,因此可伸缩性降低。
闩锁是一种简单的低级串行化机制,可协调多用户对共享数据结构的访问。闩锁可保护共享内存资源免受多个进程访问时的损坏。
闩锁的增加意味着更多基于并发的等待,因此可伸缩性降低。如果可以使用在开发期间运行速度稍快的方法或使用较少闩锁的方法,请使用后者。
另请参见:
-
Oracle Database Concepts,了解有关闩锁的信息
-
Oracle Database Concepts,了解有关互斥锁的信息,例如单个对象的闩锁
关于非阻塞读写和并发
在 Oracle Database 中,非阻塞读取和写入允许查询与正在读取的数据的更改同时执行,而不会阻塞或停止。非阻塞读取和写入允许一个会话读取数据,而另一个会话正在更改该数据。
关于共享 SQL 和并发
Oracle Database 将 SQL 语句编译为可执行对象一次,然后其他会话可以重用该对象(只要该对象存在)。这种名为 shared SQL 的 Oracle Database 功能允许数据库仅执行一次资源密集型操作编译和优化 SQL 语句,而不是每次会话使用同一 SQL 语句时。
另请参阅:Oracle Database Concepts,了解有关共享 SQL 的更多信息
限制并发会话数
您拥有的并发会话越多,基于并发的等待就越多,响应时间越慢。
如果您的计算机具有 n 个 CPU 核心,则至多 n 个会话可以同时处于活动状态。每个额外的“并发”会话必须等待 CPU 核心可用,然后才能变为活动状态。如果某些等待会话仅等待 I/O,则将并发会话数增加到稍微大于 n 可能会稍微提高运行时性能。但是,过多增加并发会话数会显著降低运行时性能。
SESSIONS 初始化参数确定系统中并发用户的最大数量。有关详细信息,请参阅 Oracle Database Reference。
另请参见:http://www.youtube.com/watch?v=xNDnVOCdvQ0 ,了解如何将具有 12 个 CPU 核心的计算机上的并发会话数从数千个减少到 96 个
比较编程技术与 Runstats
使用 Runstats 工具,您可以比较两种编程技术的性能,以查看哪一种更好。
关于 Runstats
使用 Runstats 工具,您可以比较两种编程技术的性能,以查看哪一种更好。
Runstats 度量以下值:
-
每种技术用时(以秒为单位)(以秒为单位)
-
第一个技术所用时间占第二个技术所用时间的百分比
-
这两种技术的系统统计信息(例如,语法分析调用)
-
锁定两种技术
在前面的测量中,最重要的是锁定(请参见 About Latches and Concurrency )。
另请参阅:使用 Runstats 的 Example 8-1
设置运行状态
Runstats 工具作为使用视图和临时表的程序包实现。
注:对于以下过程的步骤 1 ,您需要对动态性能视图 V$STATNAME、V$MYSTAT 和 V$LATCH 具有 SELECT 权限。如果您无法获得此权限,请让具有该权限的人在步骤 1 中创建该视图,并向您授予对该视图的 SELECT 权限。
设置 Runstats 工具的步骤:
-
创建运行状态使用的视图:
CREATE OR REPLACE VIEW stats AS SELECT 'STAT...' || a.name name, b.value FROM V$STATNAME a, V$MYSTAT b WHERE a.statistic# = b.statistic# UNION ALL SELECT 'LATCH.' || name, gets FROM V$LATCH; -
创建 Runstats 使用的临时表:
DROP TABLE run_stats; CREATE GLOBAL TEMPORARY TABLE run_stats ( runid VARCHAR2(15), name VARCHAR2(80), value INT ) ON COMMIT PRESERVE ROWS; -
创建此程序包说明:
CREATE OR REPLACE PACKAGE runstats_pkg AS PROCEDURE rs_start; PROCEDURE rs_middle; PROCEDURE rs_stop( p_difference_threshold IN NUMBER DEFAULT 0 ); end; /参数
p_difference_threshold控制 Runstats 显示的统计信息量和锁定数据。只有当两种技术的差异大于p_difference_threshold时,运行状态才会显示数据。默认情况下,Runstats 显示所有数据。 -
创建以下程序包主体:
CREATE OR REPLACE PACKAGE BODY runstats_pkg AS g_start NUMBER; g_run1 NUMBER; g_run2 NUMBER; PROCEDURE rs_start IS BEGIN DELETE FROM run_stats; INSERT INTO run_stats SELECT 'before', stats.* FROM stats; g_start := DBMS_UTILITY.GET_TIME; END rs_start; PROCEDURE rs_middle IS BEGIN g_run1 := (DBMS_UTILITY.GET_TIME - g_start); INSERT INTO run_stats SELECT 'after 1', stats.* FROM stats; g_start := DBMS_UTILITY.GET_TIME; END rs_middle; PROCEDURE rs_stop( p_difference_threshold IN NUMBER DEFAULT 0 ) IS BEGIN g_run2 := (DBMS_UTILITY.GET_TIME - g_start); DBMS_OUTPUT.PUT_LINE ('Run 1 ran in ' || g_run1 || ' hsec'); DBMS_OUTPUT.PUT_LINE ('Run 2 ran in ' || g_run2 || ' hsec'); DBMS_OUTPUT.PUT_LINE ('Run 1 ran in ' || round(g_run1/g_run2*100, 2) || '% of the time of run 2'); DBMS_OUTPUT.PUT_LINE( CHR(9) ); INSERT INTO run_stats SELECT 'after 2', stats.* FROM stats; DBMS_OUTPUT.PUT_LINE ( RPAD( 'Name', 30 ) || LPAD( 'Run 1', 14) || LPAD( 'Run 2', 14) || LPAD( 'Difference', 14) ); FOR x IN ( SELECT RPAD( a.name, 30 ) || TO_CHAR( b.value - a.value, '9,999,999,999' ) || TO_CHAR( c.value - b.value, '9,999,999,999' ) || TO_CHAR( ( (c.value - b.value) - (b.value - a.value)), '9,999,999,999' ) data FROM run_stats a, run_stats b, run_stats c WHERE a.name = b.name AND b.name = c.name AND a.runid = 'before' AND b.runid = 'after 1' AND c.runid = 'after 2' AND (c.value - a.value) > 0 AND abs((c.value - b.value) - (b.value - a.value)) > p_difference_threshold ORDER BY ABS((c.value - b.value) - (b.value - a.value)) ) LOOP DBMS_OUTPUT.PUT_LINE( x.data ); END LOOP; DBMS_OUTPUT.PUT_LINE( CHR(9) ); DBMS_OUTPUT.PUT_LINE( 'Run 1 latches total compared to run 2 -- difference and percentage' ); DBMS_OUTPUT.PUT_LINE ( LPAD( 'Run 1', 14) || LPAD( 'Run 2', 14) || LPAD( 'Diff', 14) || LPAD( 'Pct', 10) ); FOR x IN ( SELECT TO_CHAR( run1, '9,999,999,999' ) || TO_CHAR( run2, '9,999,999,999' ) || TO_CHAR( diff, '9,999,999,999' ) || TO_CHAR( ROUND( g_run1/g_run2*100, 2), '99,999.99' ) || '%' data FROM ( SELECT SUM (b.value - a.value) run1, SUM (c.value - b.value) run2, SUM ( (c.value - b.value) - (b.value - a.value)) diff FROM run_stats a, run_stats b, run_stats c WHERE a.name = b.name AND b.name = c.name AND a.runid = 'before' AND b.runid = 'after 1' AND c.runid = 'after 2' AND a.name like 'LATCH%' ) ) LOOP DBMS_OUTPUT.PUT_LINE( x.data ); END LOOP; END rs_stop; END; /
另请参见:
-
“创建表”
-
Oracle Database Reference,了解有关动态性能视图的信息
使用 Runstats
本主题提供了使用 Runstats 工具的语法。
要使用 Runstats 来比较两种编程技术,请使用以下语法从匿名块调用 runstats_pkg 过程:
[ DECLARE local_declarations ]
BEGIN
runstats_pkg.rs_start;
code_for_first_technique
runstats_pkg.rs_middle;
code_for_second_technique
runstats_pkg.rs_stop(n);
END;
/
另请参阅:使用 Runstats 的 Example 8-1
现实世界中的性能和数据处理技术
在数据仓库环境中,数据库应用程序中的一项常见任务是查询或修改庞大的数据集。应用程序开发人员面临的问题是如何在处理大型数据集时实现高性能。
处理技术分为两类:迭代和基于集合。经过多年的测试,RealWorld Performance 团队发现,对于处理大型数据集的数据库应用,基于集合的处理技术可以更好地执行多个数量级。
本主题包括以下主要子主题:
关于迭代数据处理
在迭代处理中,应用程序使用条件逻辑循环处理一组行。
通常,尽管不一定,迭代处理使用客户机/服务器模型,如下所示:
-
将一组行从数据库服务器传输到客户机应用程序。
-
在客户机应用程序中处理组。
-
将已处理的组传输回数据库服务器。
您可以使用三种主要技术实现迭代算法:逐行处理、数组处理和手动并行。
迭代处理:逐行处理
在逐行处理中,单个进程会循环处理数据集,并一次对单个行进行操作。在典型实施中,应用程序从数据库中检索每一行,在中间层对其进行处理,然后将该行发送回数据库,该数据库执行 DML 并提交。
假设您的功能要求是查询一个名为 ext_scan_events 的外部表,然后将其行插入到名为 stage1_scan_events 的按堆组织的中转表中。以下 PL/SQL 块使用逐行技术来满足此要求:
declare
cursor c is select s.* from ext_scan_events s;
r c%rowtype;
begin
open c;
loop
fetch c into r;
exit when c%notfound;
insert into stage1_scan_events d values r;
commit;
end loop;
close c;
end;
逐行方法具有以下优点:
-
它在小型数据集上表现良好。
-
循环算法对所有专业开发人员都很熟悉,编写起来方便快捷,易于理解。
逐行方法具有以下缺点:
-
对于大型数据集,处理时间可能会长得令人无法接受。
-
应用程序串行执行,因此无法利用在现代硬件上运行的 Oracle Database 的本机并行处理功能。
另请参见:RWP #7 Set-Based Processing
迭代处理:数组
数组处理与逐行处理相同,只是处理每个迭代中的一组行,而不是单个行。
假设您的功能要求与示例 X-X 中相同:查询一个名为 ext_scan_events 的外部表,然后将其行插入到名为 stage1_scan_events 的按堆组织的分段表中。以下 PL/SQL 块使用数组技术来满足此要求:
declare
cursor c is select s.* from ext_scan_events s;
type t is table of c%rowtype index by binary_integer;
a t;
rows binary_integer := 0;
begin
open c;
loop
fetch c bulk collect into a limit array_size;
exit when a.count = 0;
forall i in 1..a.count
insert into stage1_scan_events d values a(i);
commit;
end loop;
close c;
end;
在 FETCH STATEMENT 中使用 BULK COLLECT 运算符时,前面的代码与等效的逐行代码不同,后者受 PLS_INTEGER 类型的 array_size 值的限制。例如,如果 array_size 设置为 100,则应用程序将按 100 组提取行。
与逐行技术相比,数组技术具有以下优点:
-
该数组使应用程序能够同时处理一组行,这意味着它减少了网络往返、COMMIT 时间以及客户机和服务器中的代码路径。
-
数据库更高效,因为服务器进程对插入进行批处理,并在每组插入后提交,而不是在每次插入后提交。
此技术的缺点与逐行处理相同。对于大型数据集,处理时间可能是不可接受的。此外,应用程序必须在单个 CPU 核心上串行运行,因此无法利用 Oracle Database 的本机并行性。
迭代处理:手动并行处理
手动并行使用与逐行处理和数组处理相同的迭代算法,但使多个服务器进程可以划分工作并并行运行。
假设功能要求与逐行和数组示例中的要求相同。主要差异如下:
-
扫描事件记录存储在大量平面文件中。
-
32 个服务器进程必须并行运行,每个服务器进程查询不同的外部表。
-
您可以使用 PL/SQL 通过执行同一 PL/SQL 程序的 32 个线程来实现并行性,每个线程同时作为由 Oracle Scheduler 管理的单独作业运行。作业是计划和方案的组合。
以下 PL/SQL 代码使用手动并行处理:
declare
sqlstmt varchar2(1024) := q'[
-- BEGIN embedded anonymous block
cursor c is select s.* from ext_scan_events_${thr} s;
type t is table of c%rowtype index by binary_integer;
a t;
rows binary_integer := 0;
begin
for r in (select ext_file_name from ext_scan_events_dets where ora_hash(file_seq_nbr,${thrs}) = ${thr})
loop
execute immediate
'alter table ext_scan_events_${thr} location' || '(' || r.ext_file_name || ')';
open c;
loop
fetch c bulk collect into a limit ${array_size};
exit when a.count = 0;
forall i in 1..a.count
insert into stage1_scan_events d values a(i);
commit;
-- demo instrumentation
rows := rows + a.count; if rows > 1e3 then exit when not sd_control.p_progress('loading','userdefined',rows); rows := 0; end if;
end loop;
close c;
end loop;
end;
-- END embedded anonymous block
]';
begin
sqlstmt := replace(sqlstmt, '${array_size}', to_char(array_size));
sqlstmt := replace(sqlstmt, '${thr}', thr);
sqlstmt := replace(sqlstmt, '${thrs}', thrs);
execute immediate sqlstmt;
end;
ORA_HASH 函数将 ext_scan_events_dets 表分成 32 个均匀分布的存储桶,然后 SELECT 语句检索存储桶 0 的文件名。对于存储桶中的每个文件名,程序会将外部表的位置设置为此文件名。然后,程序使用批处理查询外部表,插入中转表,然后提交。
作业 1 正在执行时,其他 31 个 Oracle Scheduler 作业将并行执行。这样,每个作业将同时读取扫描事件文件的不同子集,并将记录从其子集插入到同一中转表中。
与替代迭代技术相比,手动并行技术具有以下优点:
-
它在大型数据集上的性能要好得多,因为服务器进程是并行工作的。
-
当应用程序使用 ORA_HASH 来分配工作量时,每个执行线程都可以访问相同数量的数据,这意味着并行进程可以同时完成。
手动并行技术具有以下缺点:
-
代码相对冗长、复杂且难以理解。
-
应用程序必须执行一定数量的准备工作,然后数据库才能开始主要工作,即并行处理行。
-
如果多个线程对一组公共数据库对象执行相同的操作,则可能存在锁定和闩锁争用。
-
与相互竞争的迭代技术相比,并行处理会占用大量 CPU 资源。
另请参阅:RWP #8:基于集合的并行处理
关于基于集合的处理
基于集合的处理是一种 SQL 技术,用于处理数据库内的数据集。
在基于集合的模型中,SQL 语句定义结果,并允许数据库确定最有效的方法来获取结果。相反,迭代算法使用条件逻辑将数据库中的每一行或每一组行从客户机应用程序中提取到客户机应用程序中,处理客户机上的数据,然后将数据发送回数据库。基于集的处理可消除网络往返和数据库 API 开销,因为数据永远不会离开数据库。
承担与前面的示例相同的功能要求。以下 SQL 语句使用基于集的算法满足此要求:
alter session enable parallel dml;
insert /*+ APPEND */ into stage1_scan_events d
select s.* from ext_scan_events s;
commit;
由于 INSERT 语句包含 ext_scan_events 表的子查询,因此 single SQL 语句会读取和写入所有行。此外,在数据库插入所有行之后,应用程序会执行单个 COMMIT。相反,迭代应用程序在插入每行或每组行后执行 COMMIT。
与迭代技术相比,基于集合的技术具有显著的优势:
-
如 Real-World Performance 演示和类所示,大型数据集的性能提升了几个数量级。程序运行时从数小时下降到数秒,这并不罕见。
-
处理速度提高的订单数量级的一个副作用是,DBA 可以消除长时间运行且容易出错的批处理作业,并实时处理不必要的业务流程。
-
由于 SQL 定义的是结果,而不是访问方法,因此代码的长度明显缩短(简称为两行或三行代码)。
-
与手动并行不同,并行 DML 针对性能进行了优化,因为数据库(而不是应用程序)管理进程。
-
在联接数据集时,数据库会自动使用高效的散列联接,而不是相对低效的应用程序级循环。
-
APPEND 提示强制执行直接路径负载,这意味着数据库不会创建重做和还原,从而避免浪费 I/O 和 CPU。
基于集合的处理确实有一些潜在的缺点:
-
对于许多数据库开发人员来说,这些技术并不熟悉,因此它们可能更加困难。
-
由于基于集合的模型与迭代模型完全不同,因此更改它需要完全重写源代码。
另请参见:RWP #7 Set-Based Processing、RWP #8:Set-Based Parallel Processing、RWP #9:Set-Based Processing – Data Deduplication、RWP #10:Set-Based Processing – Data Transformations 和 RWP #11:Set-Based Processing – Data Aggregation。