Cri*_*ilă 6 performance sql-server-2008 sql-server index-tuning query-performance
我有一个包含连接几个表的查询的过程,但我遇到了一些性能问题。
主表(这是一个巨大的表)有一个 PK 和一些 NC 索引。
CREATE TABLE [dbo].[TableA]
(
[TableAID] [bigint] NOT NULL,
[UserID] [int] NOT NULL,
[IP1] [tinyint] NOT NULL,
[IP2] [tinyint] NOT NULL,
[IP3] [tinyint] NOT NULL,
[IP4] [tinyint] NOT NULL
CONSTRAINT [PK_TableA]
PRIMARY KEY CLUSTERED ([TableAID] ASC)
) ON [PRIMARY]
CREATE NONCLUSTERED INDEX [idx_1] ON [dbo].[TableA]
(
[UserID] ASC
)
CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA]
(
[IP1] ASC,
[IP2] ASC,
[IP3] ASC,
[IP4] ASC
)
Run Code Online (Sandbox Code Playgroud)
这是性能不佳的查询:
SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
FROM [dbo].[TableA] a WITH (NOLOCK)
JOIN [dbo].[TableB] b WITH (NOLOCK) ON b.UserID = a.UserID
JOIN [dbo].[Tablec] c WITH (NOLOCK) ON b.CountryID = c.CountryID
JOIN (
SELECT
IP1, IP2, IP3, IP4
from
@IPs
) as ip ON
((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND
((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
((ip.IP4 is NULL) OR (ip.IP4=a.IP4))
Run Code Online (Sandbox Code Playgroud)
@IPs
表的定义:
DECLARE @IPs TABLE (
IP1 int,
IP2 int,
IP3 int,
IP4 int
)
INSERT INTO @IPs(IP1,IP2,IP3,IP4)
SELECT T.v.value('(IP1/node())[1]', 'int'),
T.v.value('(IP2/node())[1]', 'int'),
T.v.value('(IP3/node())[1]', 'int'),
T.v.value('(IP4/node())[1]', 'int')
FROM @IPAddresses.nodes('//IPAddresses/IPAddress') T(v)
Run Code Online (Sandbox Code Playgroud)
@IPAddresses
是 xml。刚刚发现xml可以发送更多的IP,所以这意味着IP表中不止一行。
问题是对 TableA 的读取次数。即使我有 IP 列的 NC 索引,该连接条件也会强制进行表扫描...
如何提高性能?如何重构此表/查询?
我还在考虑是否有更简单更好的方法来重写这段代码:
SELECT
IP1, IP2, IP3, IP4
from
@IPs
) as ip ON
((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND
((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
((ip.IP4 is NULL) OR (ip.IP4=a.IP4))
Run Code Online (Sandbox Code Playgroud)
...如果我们有更多的 IP。
有几件事使这具有挑战性。如果您不小心,NULL
检查会阻止索引查找。此外,当列是NULL
你显然不能搜索它们。所以如果IP1
是,NULL
那么四列索引idx_2
将不会很有用。似乎不可能定义一个对任何NULL
变量组合都有选择性的索引。此外,SQL Server在查找不等式谓词后不能继续索引查找:
同样,如果我们在两列上有一个索引,那么如果我们在第一列上有一个等式谓词,我们只能使用索引来满足第二列上的谓词。
这意味着使用TINYINT
数据类型边界的技巧不太可能有效,例如:
a.IP1 >= NULLIF(ip.IP1, 0) AND a.IP1 <= NULLIF(ip.IP1, 255)
Run Code Online (Sandbox Code Playgroud)
除此之外,我使用的策略似乎与 SQL Server 2014 中引入的新基数估计器配合得更好,并且您的问题被标记为 SQL Server 2008。
我强烈建议将表变量拆分成行并一次处理一行。这主要是为了处理NULL
值,因为您的评论暗示大部分时间您只会得到一行。只要一行的性能足够好并且您没有太多行,它应该没问题。如果这是不可接受的,也许您可以检查临时表中的任何行(不使用表变量)是否存在NULL
并在这种情况下分支您的代码。
综上所述,只要最多两个 IP 地址段不是NULL
. 当它们中的三个是时,NULL
您将取回大部分表,此时进行聚簇索引扫描可能是有意义的。
以下是我为测试各种解决方案而模拟的数据。我通过为每个部分随机选择一个 0 到 255 之间的整数生成了 1 亿个 IP 地址。我知道现实生活中的 IP 地址分布并不是那么随机,但没有办法生成更好的数据。
CREATE TABLE [dbo].[TableA](
[TableAID] [bigint] NOT NULL,
[UserID] [int] NOT NULL,
[IP1] [tinyint] NOT NULL,
[IP2] [tinyint] NOT NULL,
[IP3] [tinyint] NOT NULL,
[IP4] [tinyint] NOT NULL
CONSTRAINT [PK_TableA] PRIMARY KEY CLUSTERED
( [TableAID] ASC )
);
-- insert 100 million random IP addresses
INSERT INTO [dbo].[TableA] WITH (TABLOCK)
SELECT TOP (100000000)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 10000
, ABS(BINARY_CHECKSUM(NEWID()) % 256)
, ABS(BINARY_CHECKSUM(NEWID()) % 256)
, ABS(BINARY_CHECKSUM(NEWID()) % 256)
, ABS(BINARY_CHECKSUM(NEWID()) % 256)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3;
CREATE TABLE [dbo].[TableB] (
[UserID] [int] NOT NULL,
FILLER VARCHAR(100),
PRIMARY KEY (UserId)
);
-- insert 10k users
INSERT INTO [dbo].[TableB] WITH (TABLOCK)
SELECT TOP (10000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Run Code Online (Sandbox Code Playgroud)
请注意我没有创建您已经拥有的任何一个非聚集索引。相反,我将为每个 IP 片段创建一个索引:
CREATE NONCLUSTERED INDEX [idx_IP1] ON [dbo].[TableA] ([IP1] ASC);
CREATE NONCLUSTERED INDEX [idx_IP2] ON [dbo].[TableA] ([IP2] ASC);
CREATE NONCLUSTERED INDEX [idx_IP3] ON [dbo].[TableA] ([IP3] ASC);
CREATE NONCLUSTERED INDEX [idx_IP4] ON [dbo].[TableA] ([IP4] ASC);
Run Code Online (Sandbox Code Playgroud)
索引所需的空间并非微不足道。这是比较:
???????????????????????????
? IndexName ? IndexSizeKB ?
???????????????????????????
? idx_1 ? 1786488 ?
? idx_2 ? 1786480 ?
? idx_IP1 ? 1487616 ?
? idx_IP2 ? 1487616 ?
? idx_IP3 ? 1487632 ?
? idx_IP4 ? 1487608 ?
? PK_TableA ? 2482056 ?
???????????????????????????
Run Code Online (Sandbox Code Playgroud)
如有必要,您可以权衡使用行或页压缩来减小索引大小的利弊。但是,如果您不知道将是哪些 IP 片段NULL
并且您需要避免聚集索引扫描,那么我看不到四个索引的更好替代方案。我将采用的策略称为索引连接。非聚集索引包括聚集键TableAID
,这使得将索引连接在一起成为可能。每个索引应该具有大约 0.4% 的选择性,并且使用非聚集索引查找找到那些行应该相对便宜。将所有索引连接在一起应该会大大减少结果集,此时您可以对表进行聚集索引查找以获得您需要的其他列值,例如UserID
.
这是查询:
DECLARE @ip1 TINYINT = ?;
DECLARE @ip2 TINYINT = ?;
DECLARE @ip3 TINYINT = ?;
DECLARE @ip4 TINYINT = ?;
SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
FROM [dbo].[TableA] a
JOIN [dbo].[TableB] b ON b.UserID = a.UserID
WHERE
((@ip1 is NULL) OR (@ip1=a.IP1)) AND
((@ip2 is NULL) OR (@ip2=a.IP2)) AND
((@ip3 is NULL) OR (@ip3=a.IP3)) AND
((@ip4 is NULL) OR (@ip4=a.IP4))
OPTION (RECOMPILE, QUERYTRACEON 9481);
Run Code Online (Sandbox Code Playgroud)
根据RECOMPILE
提示,我正在利用参数嵌入优化。此优化仅适用于特定服务包 (SP4?),因此请确保您已打好补丁。TableA
如果它认为合适,查询优化器能够将单个表访问拆分为一个索引连接。请注意,这里的估计计划很可能会产生误导。你要注意实际的计划。
该QUERYTRACEON 9481
提示不应包含在您的查询版本中。我正在使用它来强制 SQL Server 使用旧版 CE,这仅是必要的,因为我正在针对 SQL Server 2016 进行测试。
让我们运行一些测试。具有以下参数:
DECLARE @ip1 TINYINT = 1;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;
Run Code Online (Sandbox Code Playgroud)
我得到合并连接和循环连接。
具有以下参数:
DECLARE @ip1 TINYINT = NULL;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;
Run Code Online (Sandbox Code Playgroud)
我得到了一个非常相似的计划,只是IP1
查询中没有使用索引。查询仍会在大约 125 毫秒内完成。
具有以下参数:
DECLARE @ip1 TINYINT = 88;
DECLARE @ip2 TINYINT = NULL;
DECLARE @ip3 TINYINT = NULL;
DECLARE @ip4 TINYINT = NULL;
Run Code Online (Sandbox Code Playgroud)
查询在大约 5 秒内完成。我得到一个散列连接和一个聚集索引扫描。这不一定是坏事,但可以通过一些努力来避免(请参阅以下段落):
如果需要有效地强制索引联接(也许 SQL Server 2008 缺少我正在利用的优化),可以这样做,但要复杂得多。
小智 3
关于您的性能问题 - 根据表变量的大小,使用 #tempTable 应该是更好的选择。查询优化器期望表变量始终返回 1 行(在 SQL Server 2016 中为 100 行),因此它可能会创建嵌套循环连接,这会增加额外的开销。使用临时表,您可以获得有效的统计数据和基数估计=更好的计划。
另外,您不会从表变量中返回任何行,我认为这仅用于验证目的,在这种情况下,我会将查询更改为:
CREATE table #IPs (IP1 tinyint,IP2 tinyint, IP3 tinyint, IP4 tinyint)
insert into #IPs values (1,2,3,4),(null,2,3,null)
SELECT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
FROM [dbo].[TableA] a WITH (NOLOCK)
WHERE EXISTS (Select 1 from #IPs as b where a.IP1 = b.IP1 or a.IP2 = b.IP2 OR a.IP3 = b.IP3 OR a.IP4 = b.IP4)
Run Code Online (Sandbox Code Playgroud)
EXISTS
如果出于验证目的,我会使用连接而不是连接。它还将使您免于选择不同的值,这可能会增加 tempdb(统计数据中的工作表)的开销,对它们进行排序并选择每个值。
您创建的包含 4 列的列将不起作用,因为您的查询使用这些列作为可选列(IP1、IP2 或 IP3),之后它必须进行查找以获取您请求的其他剩余列,因此查询优化器估计搜索整个表会更容易。
您可以做的是更改非聚集索引并将 UserID 列包含为:
CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA]
(
[IP1] ASC,
[IP2] ASC,
[IP3] ASC,
[IP4] ASC
) INCLUDE ([UserID])
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
3668 次 |
最近记录: |