如何配置 Oracle ODP.Net 以将所有行带入单次往返?
How to configure Oracle ODP.Net to bring all rows in a single round-trip?
我在不同的机器上使用客户端和 Oracle 在不同的场景下表现不佳,即使延迟很低,我检查了配置 OracleDataReader.FetchSize¹ = OracleDataReader.RowSize * {number-of-rows}
(在第一个 Read()
之前),我得到更好的结果取决于 {number-of-rows}
值,以内存为代价。
只有在不同机器上测试时才会出现这种差异。在 localhost 中测试时,性能仍然几乎相同。我猜是因为即使多次往返数据库,也不涉及网络,而且 Oracle 使用的是除 TCP 之外的另一种协议(我猜是 IPC)。
为了澄清事情,让我们检查这个例子:
using (OracleConnection conn = new OracleConnection(connString))
{
conn.Open();
using (OracleCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM table"; // any query
using (OracleDataReader dr = cmd.ExecuteReader())
{
// Configure FetchSize and vary {number} to get performance results
dr.FetchSize = dr.RowSize * {number}; // Must be configured before first Read()
Stopwatch watch = Stopwatch.StartNew(); // To check the elapsed time
while (dr.Read())
{
object[] values = new object[dr.FieldCount];
dr.GetValues(values);
}
watch.Stop();
Console.WriteLine(watch.Elapsed); // Print the time elapsed to read all data
}
}
}
如果查询 returns 1000 行,每行 10 个字节 (RowSize),那么 FetchSize 为 10 * 1000 (FetcSize = RowSize * {number-of-rows}) 将是理想值,因此客户端只需往返数据库一次即可获取(获取)所有数据。如果FetchSize = 10,客户端需要访问数据库1000次才能获取所有数据,每次只获取1行。
如果查询 return 只有 10 行,每行 1 个字节,那么 FetchSize = 10 会获得更好的性能 (10 * 1),因为客户端只需要往返数据库一次。
在我的测试中,我遇到了一些场景(例如:约 90k 行的查询),使用默认的 FetchSize (= 128kb = 131.072) 需要大约 12.6 秒,而使用 FetchSize = 10000 * RowSize 需要 1.6 秒!
因此,FetchSize 取决于查询估计的行数和行的中等大小(又名 OracleDataReader.RowSize
)。
所以我想到了两种解决方案:
- 让数据库决定一次获取多少行(因此类似于
FetchSize = -1
类似于 InitialLOBFetchSize = -1
' 等其他配置)
- 以一般方式,我需要制定一个好的策略来估计查询将 return 的行数并使用它配置
{number}
。
- 在这种情况下,我正在考虑使用一些数据库统计信息来找出它,但我确信数据库应该知道并使用它(或者让我们选择使用它)真的比一个更简单应用程序开发人员可以做到(也必须有一种方法让数据库选择最佳选项)。
重要的是,对于我的场景,我总是读取所有行,客户端和数据库之间的网络延迟非常低 (<1ms)。
所以,我的问题是:如何实现案例 1?我需要配置什么才能使 ODP.Net Oracle 在一次往返中获取所有行以获得更好的性能结果?
备注:
¹ OracleDataReader.FetchSize
定义客户端在单次往返中从数据库中获取的字节数(也称为通过客户端 -> 数据库 -> 客户端的行程)。
参考文献:
- https://docs.oracle.com/cd/E11882_01/win.112/e23174/featData.htm#ODPNT302
- https://books.google.com.br/books?id=AsSBza2KxogC&pg=PA85&lpg=PA85&dq=oracle+fetchsizer+retrieve+all+rows+at+once&source=bl&ots=Y7U87tE8g7&sig=RZB4wPXQOgeUgqzCmFISdA9QBCc&hl=pt-BR&sa=X&ved=0ahUKEwist_Cyj_DVAhVKi5AKHY9hAHYQ6AEIZTAI#v=onepage&q=oracle%20fetchsizer%20retrieve%20all%20rows%20at%20once&f=false
- https://docs.oracle.com/middleware/1221/bip/BIPDM/best_practices.htm#BIPDM530
如果可能,您最好根据您的应用程序逻辑估计行数。您不需要确切的数字,只是大小应该匹配。
您可以根据 Oracle 执行计划估计数量或行数。在执行查询 运行 语句之前:
DELETE FROM PLAN_TABLE;
EXPLAIN PLAN FOR SELECT * FROM table;
SELECT CARDINALITY
FROM PLAN_TABLE
WHERE ID = 0;
ID = 0
指的是执行计划的第一行,即整个语句的基数。
为了更好的性能,您可以 运行 在应用程序启动时只执行一次这样的过程(对于每个查询),并将数字保存在内存中。
基于 Oracle 的体系结构,您将永远无法实施您的 #1 解决方案(即告诉 Oracle 找出如何最大限度地减少或消除浪费的网络往返)。
但是您可能能够部分实施#2。您的成功将取决于 (a) 您确定适合单个 Send Data Unit (SDU)
的最大 平均 行的能力,这可能是 2048 字节,以及 (b) 数据获取符合您在 a.
中定义的模型
了解存在 SDU 约束并且 Oracle 获取完整行是优化网络的关键 activity。
我建议您在通过执行 FetchSize = RowSize * {number-of-rows}
.
有效设置数组提取大小时(即每次提取操作提取的完整行数)时考虑 SDU
如果生成的 FetchSize 始终导致过多的额外网络往返(请参阅跟踪数据),那么您需要增加或减少 FetchSize,直到您满意没有其他单个 FetchSize 可以进一步减少浪费的网络循环给定数据 reader 对象的行程。
如果您预计有太多分析可能会随着数据的变化而随着时间的推移而失效,那么我建议您对所有 SQL 语句的每次提取操作提取 100 到 1,000 行仅取决于 OracleCommand 的估计行大小。如果行大小很小,则每次提取 1,000 行。如果rowsize真的很大,那么每次取100行。
这应该可以将您的应用与我经常看到的遇到此类问题的客户的恶劣示例隔离开来。它不会是完美的,但至少您不必过度分析问题并且生成的代码也不必更改。
下面是我尝试详细说明的,如果有需要的话。
Oracle 的架构使得客户端被迫一次获取多个完整的行。因此,客户端驱动程序永远不会被允许将控制权交给应用程序,从而给它一个不完整的行。
最糟糕的数据包浪费示例是应用一次获取 "select 'x' from millionRowTable" 一行(通过将获取大小设置为行大小)。这将导致 1,000,000 次网络往返。令人惊奇的是,我经常去拜访甚至不知道自己有问题或可能存在问题的客户。在此示例中,您会看到类似这样的内容在跟踪数据中重复了 1,000,000 次:
WAIT #1: nam='SQL*Net message from client' ela= ...
WAIT #1: nam='SQL*Net message to client' ela= ...
FETCH #1:c=1999,e=1243,p=0,cr=0,cu=0,mis=0,r=1,...
其中,r=1,表示为每次网络往返获取一行。两个跟踪文件 WAIT 行表示读取完成和写入开始。换句话说,这对表示网络往返。
一个不那么痛苦的例子是,当您将数组大小设置为一个值时,该值会导致负载略大于 Oracle 可以装入单个 SQL*Net 数据包的负载。假设您一次从 "select uniformlyTenByteRow from millionRowTable" 中获取 200 行,并且如果 SDU 为 2056,那么您可以获得一次往返以包含所有 200 行的唯一方法。由于这种不幸的组合,您会看到类似这样的内容在跟踪数据中重复 5,000 次:
WAIT #1: nam='SQL*Net message from client' ela= ...
WAIT #1: nam='SQL*Net message to client' ela= ...
WAIT #1: nam='SQL*Net more data to client' ela= ...
FETCH #1:c=19999,e=12433,p=0,cr=0,cu=0,mis=0,r=200,..
more data to client
WAIT 行表示一个额外的网络往返。您可能期望有 5,000 次网络往返,但实际上得到了 10,000 次网络往返。一万次网络往返来获取 1,000,000 个小行并不可怕,但它可能会更好。
我在不同的机器上使用客户端和 Oracle 在不同的场景下表现不佳,即使延迟很低,我检查了配置 OracleDataReader.FetchSize¹ = OracleDataReader.RowSize * {number-of-rows}
(在第一个 Read()
之前),我得到更好的结果取决于 {number-of-rows}
值,以内存为代价。
只有在不同机器上测试时才会出现这种差异。在 localhost 中测试时,性能仍然几乎相同。我猜是因为即使多次往返数据库,也不涉及网络,而且 Oracle 使用的是除 TCP 之外的另一种协议(我猜是 IPC)。
为了澄清事情,让我们检查这个例子:
using (OracleConnection conn = new OracleConnection(connString))
{
conn.Open();
using (OracleCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM table"; // any query
using (OracleDataReader dr = cmd.ExecuteReader())
{
// Configure FetchSize and vary {number} to get performance results
dr.FetchSize = dr.RowSize * {number}; // Must be configured before first Read()
Stopwatch watch = Stopwatch.StartNew(); // To check the elapsed time
while (dr.Read())
{
object[] values = new object[dr.FieldCount];
dr.GetValues(values);
}
watch.Stop();
Console.WriteLine(watch.Elapsed); // Print the time elapsed to read all data
}
}
}
如果查询 returns 1000 行,每行 10 个字节 (RowSize),那么 FetchSize 为 10 * 1000 (FetcSize = RowSize * {number-of-rows}) 将是理想值,因此客户端只需往返数据库一次即可获取(获取)所有数据。如果FetchSize = 10,客户端需要访问数据库1000次才能获取所有数据,每次只获取1行。
如果查询 return 只有 10 行,每行 1 个字节,那么 FetchSize = 10 会获得更好的性能 (10 * 1),因为客户端只需要往返数据库一次。
在我的测试中,我遇到了一些场景(例如:约 90k 行的查询),使用默认的 FetchSize (= 128kb = 131.072) 需要大约 12.6 秒,而使用 FetchSize = 10000 * RowSize 需要 1.6 秒!
因此,FetchSize 取决于查询估计的行数和行的中等大小(又名 OracleDataReader.RowSize
)。
所以我想到了两种解决方案:
- 让数据库决定一次获取多少行(因此类似于
FetchSize = -1
类似于InitialLOBFetchSize = -1
' 等其他配置) - 以一般方式,我需要制定一个好的策略来估计查询将 return 的行数并使用它配置
{number}
。- 在这种情况下,我正在考虑使用一些数据库统计信息来找出它,但我确信数据库应该知道并使用它(或者让我们选择使用它)真的比一个更简单应用程序开发人员可以做到(也必须有一种方法让数据库选择最佳选项)。
重要的是,对于我的场景,我总是读取所有行,客户端和数据库之间的网络延迟非常低 (<1ms)。
所以,我的问题是:如何实现案例 1?我需要配置什么才能使 ODP.Net Oracle 在一次往返中获取所有行以获得更好的性能结果?
备注:
¹ OracleDataReader.FetchSize
定义客户端在单次往返中从数据库中获取的字节数(也称为通过客户端 -> 数据库 -> 客户端的行程)。
参考文献:
- https://docs.oracle.com/cd/E11882_01/win.112/e23174/featData.htm#ODPNT302
- https://books.google.com.br/books?id=AsSBza2KxogC&pg=PA85&lpg=PA85&dq=oracle+fetchsizer+retrieve+all+rows+at+once&source=bl&ots=Y7U87tE8g7&sig=RZB4wPXQOgeUgqzCmFISdA9QBCc&hl=pt-BR&sa=X&ved=0ahUKEwist_Cyj_DVAhVKi5AKHY9hAHYQ6AEIZTAI#v=onepage&q=oracle%20fetchsizer%20retrieve%20all%20rows%20at%20once&f=false
- https://docs.oracle.com/middleware/1221/bip/BIPDM/best_practices.htm#BIPDM530
如果可能,您最好根据您的应用程序逻辑估计行数。您不需要确切的数字,只是大小应该匹配。
您可以根据 Oracle 执行计划估计数量或行数。在执行查询 运行 语句之前:
DELETE FROM PLAN_TABLE;
EXPLAIN PLAN FOR SELECT * FROM table;
SELECT CARDINALITY
FROM PLAN_TABLE
WHERE ID = 0;
ID = 0
指的是执行计划的第一行,即整个语句的基数。
为了更好的性能,您可以 运行 在应用程序启动时只执行一次这样的过程(对于每个查询),并将数字保存在内存中。
基于 Oracle 的体系结构,您将永远无法实施您的 #1 解决方案(即告诉 Oracle 找出如何最大限度地减少或消除浪费的网络往返)。
但是您可能能够部分实施#2。您的成功将取决于 (a) 您确定适合单个 Send Data Unit (SDU)
的最大 平均 行的能力,这可能是 2048 字节,以及 (b) 数据获取符合您在 a.
了解存在 SDU 约束并且 Oracle 获取完整行是优化网络的关键 activity。
我建议您在通过执行 FetchSize = RowSize * {number-of-rows}
.
如果生成的 FetchSize 始终导致过多的额外网络往返(请参阅跟踪数据),那么您需要增加或减少 FetchSize,直到您满意没有其他单个 FetchSize 可以进一步减少浪费的网络循环给定数据 reader 对象的行程。
如果您预计有太多分析可能会随着数据的变化而随着时间的推移而失效,那么我建议您对所有 SQL 语句的每次提取操作提取 100 到 1,000 行仅取决于 OracleCommand 的估计行大小。如果行大小很小,则每次提取 1,000 行。如果rowsize真的很大,那么每次取100行。
这应该可以将您的应用与我经常看到的遇到此类问题的客户的恶劣示例隔离开来。它不会是完美的,但至少您不必过度分析问题并且生成的代码也不必更改。
下面是我尝试详细说明的,如果有需要的话。
Oracle 的架构使得客户端被迫一次获取多个完整的行。因此,客户端驱动程序永远不会被允许将控制权交给应用程序,从而给它一个不完整的行。
最糟糕的数据包浪费示例是应用一次获取 "select 'x' from millionRowTable" 一行(通过将获取大小设置为行大小)。这将导致 1,000,000 次网络往返。令人惊奇的是,我经常去拜访甚至不知道自己有问题或可能存在问题的客户。在此示例中,您会看到类似这样的内容在跟踪数据中重复了 1,000,000 次:
WAIT #1: nam='SQL*Net message from client' ela= ...
WAIT #1: nam='SQL*Net message to client' ela= ...
FETCH #1:c=1999,e=1243,p=0,cr=0,cu=0,mis=0,r=1,...
其中,r=1,表示为每次网络往返获取一行。两个跟踪文件 WAIT 行表示读取完成和写入开始。换句话说,这对表示网络往返。
一个不那么痛苦的例子是,当您将数组大小设置为一个值时,该值会导致负载略大于 Oracle 可以装入单个 SQL*Net 数据包的负载。假设您一次从 "select uniformlyTenByteRow from millionRowTable" 中获取 200 行,并且如果 SDU 为 2056,那么您可以获得一次往返以包含所有 200 行的唯一方法。由于这种不幸的组合,您会看到类似这样的内容在跟踪数据中重复 5,000 次:
WAIT #1: nam='SQL*Net message from client' ela= ...
WAIT #1: nam='SQL*Net message to client' ela= ...
WAIT #1: nam='SQL*Net more data to client' ela= ...
FETCH #1:c=19999,e=12433,p=0,cr=0,cu=0,mis=0,r=200,..
more data to client
WAIT 行表示一个额外的网络往返。您可能期望有 5,000 次网络往返,但实际上得到了 10,000 次网络往返。一万次网络往返来获取 1,000,000 个小行并不可怕,但它可能会更好。