SQL Server如何使用位图筛选器

本文的翻译是在“ MS SQL Server Developer”课程开始时准备的










并行运行的查询是否可以比顺序运行的查询使用更少的CPU并运行得更快?



是! 为了演示,我将使用具有一个列类型的两个表integer





注意-文本形式的TSQL脚本在本文结尾。



生成演示数据



我们#BuildInt表中插入5,000个随机整数(以便您拥有与我相同的值,我将RAND与种子和WHILE循环一起使用)。







表中#Probe插入5,000,000条记录。







顺序计划



现在让我们编写一个查询来计算这些表中值的匹配数。我们使用MAXDOP 1提示来确保查询不会并行执行。



其执行计划和统计信息如下:







该查询耗时891ms,使用890ms的CPU。



平行计划



现在,我们使用MAXDOP 2运行相同的查询。该







查询需要221毫秒436毫秒的CPU。执行时间减少了四倍,CPU使用率减少了一半!



魔术位图



并行查询执行效率更高的原因在于Bitmap运算符。



让我们仔细看一下并行查询的实际执行计划:







并将其与顺序计划进行比较:







Bitmap运算符的原理已被详细记录,因此在此我仅提供简短描述,并在文章末尾提供指向该文档的链接。



哈希联接



哈希联接分为两个步骤:



  1. “建筑物”的阶段(英语-build)。读取表之一的所有行,并为联接键创建一个哈希表。
  2. “验证”阶段(英语-探针)。读取第二个表的所有行,使用相同的哈希函数使用相同的连接键来计算哈希,并在哈希表中找到匹配的存储桶。




自然地,由于可能的哈希冲突,仍然有必要比较密钥的实际值。



译者注:有关散列连接如何工作的更多详细信息,请参见可视化和处理散列匹配 连接的文章




顺序计划中的位图



许多人不知道,即使在顺序请求中,哈希匹配始终使用位图。但是用这种方式,您将看不到它,因为它是Hash Match运算符内部实现的一部分。



在建立和创建哈希表的阶段,HASH JOIN将在位图中设置一个(或多个)位。然后您可以使用位图有效地匹配哈希值,而无需访问哈希表。



使用顺序计划,将为第二张表的每一行计算一个哈希,并对照位图进行检查。如果设置了位图中的相应位,则哈希表中可能存在匹配项,因此接下来将检查哈希表。相反,如果没有设置与哈希值相对应的位,则可以确保哈希表中没有匹配项,并且可以立即丢弃所检查的字符串。



不检查哈希表中不存在完全匹配的字符串所节省的时间,可以抵消构建位图所需的相对较低的成本。这种优化通常是有效的,因为检查位图比检查哈希表要快得多。



并行计划中的位图



在并行计划中,位图显示为单独的位图语句。



从构造阶段进入验证阶段时,位图将从第二个(探针)表的侧面传递给HASH MATCH运算符。至少,将位图传递给JOIN和交换运算符之前的探针侧(并行)。



在此,位图可以在传递给交换语句之前排除不满足连接条件的字符串。



当然,顺序计划中没有交换语句,因此,将位图移到HASH JOIN之外不会比HASH MATCH语句中的“嵌入”位图提供任何其他优势。



在某些情况下(尽管仅在并行计划中),优化器可能会将位图在连接探针侧的平面上进一步向下移动。



这里的想法是,越早对行进行过滤,则在语句之间移动数据所需的开销就越少,甚至可以消除某些操作。



此外,优化器通常会尝试将简单的过滤器放置在尽可能靠近叶子的位置:最有效的方法是尽早过滤行。但是,我必须提到,我们正在讨论的位图是在优化完成后添加的。



在优化之后,将根据过滤器的预期选择性做出将这种(静态)位图添加到计划的决定(因此准确的统计数据很重要)。



移动位图过滤器



让我们回到将位图过滤器移到连接的探针端的概念。



在许多情况下,位图过滤器可以移动到Scan或Seek语句。发生这种情况时,计划谓词将如下所示:







它适用于与查找谓词匹配的所有行(对于“索引查找”)或对“索引扫描”或“表扫描”的所有行。例如,上面的屏幕截图显示了应用于堆表的表扫描的位图过滤器。



更深入...



如果位图过滤器建立在单个整数或bigint列或表达式上,并应用于单个整数或bigint列,则位图运算符可以走得更远,甚至比Seek或Scan运算符更远。



该谓词仍将像上面的示例一样出现在Scan或Seek语句中,但现在将使用INROW属性进行标记,这意味着该过滤器将移动到存储引擎并在读取行时应用于行。



通过这种优化,行将在查询处理器看到行之前被过滤。从存储引擎仅发送与HASH MATCH JOIN匹配的那些字符串。



应用此优化的条件取决于SQL Server的版本。例如,在SQL Server 2005中,除了前面提到的条件外,还必须将probe列定义为NOT NULL。在SQL Server 2008中已经放宽了此限制。



您可能想知道INROW优化如何影响性能。将操作员移动到尽可能接近“搜索”或“扫描”的效率是否会像在存储引擎中进行过滤一样有效?我将在其他文章中回答这个有趣的问题。在这里,我们还将介绍MERGE JOIN和NESTED LOOP JOIN。



其他JOIN选项



使用没有索引的嵌套循环是一个坏主意。我们必须从另一张表的每一行中完全扫描一张表-总共进行50亿次比较。此请求可能需要很长时间。



合并加入



这种类型的物理联接需要排序的输入,因此,强制的MERGE JOIN会导致在排序之前出现排序。顺序计划如下所示:







查询现在使用3105ms CPU,总执行时间为5632ms



整体执行时间的增加是由于以下事实之一:排序操作正在使用tempdb(即使SQL Server具有足够的内存用于排序)。



发生泄漏到tempdb的原因是默认的内存授予算法没有预先保留足够的内存。直到我们关注这一点,很明显,请求将在不到3105毫秒内完成。



让我们继续强制MERGE JOIN,但允许并行性(MAXDOP 2):







正如我们在前面的并行HASH JOIN中所见,位图过滤器位于MERGE JOIN的另一侧,更靠近表扫描,并使用INROW优化来应用。



使用468 ms CPU和240 ms经过时间,具有其他种类的MERGE JOIN几乎与并行HASH JOIN(436 ms / 221 ms一样快



但是并行的MERGE JOIN有一个缺点:它根据要排序的预期行数保留330 KB内存。由于这些类型的位图是在成本优化之后使用的,因此即使只有2488行经过最底层排序,也不会对估算值进行调整。



位图语句只能在带有MERGE JOIN的计划中显示,而后继才是阻塞语句(例如,排序)。阻塞运算符必须在生成第一行输出之前将所有必需值作为输入接收。这样可以确保在读取JOIN表中的行并对其进行检查之前,位图已完全填满。



阻塞语句不必位于MERGE JOIN的另一侧,但重要的是在位图的哪一侧使用。



带索引



如果有合适的索引可用,情况将有所不同。我们的“随机”数据的分布使得#BuildInt可以在表上创建唯一索引。并且该表#Probe包含重复项,因此您必须使用一个非唯一索引:







该更改不会影响HASH JOIN(串行和并行)。HASH JOIN无法使用索引,因此计划和性能保持不变。



合并加入



MERGE JOIN不再需要执行多对多联接操作,并且不再需要输入上的Sort运算符。

缺少阻塞排序运算符意味着无法使用位图。



结果,无论使用MAXDOP参数如何,我们都会看到一个顺序计划,并且在添加索引之前性能比并行计划差:702 ms CPU和704 ms经过时间:







但是,与原始顺序MERGE JOIN计划(3105 ms)相比,有明显的改进。/ 5632毫秒)。这是由于消除了排序和更好的一对多连接性能。



嵌套循环联接

如您所料,NESTED LOOP的性能明显更好。与MERGE JOIN相似,优化器决定不使用并发:







这是迄今为止最有效的计划-仅16ms CPU和16ms花时间。



当然,这假定完成请求所需的数据已在内存中。否则,探测表中的每个查找将生成随机I / O。



在我的笔记本电脑上,NESTED LOOP的冷缓存性能消耗78 ms CPU和2152 ms消耗的时间。在相同的情况下,MERGE JOIN使用686 ms的CPU和1471 ms的CPU 。哈希联接-391毫秒CPU和905毫秒



MERGE JOIN和HASH JOIN受益于使用预读的大型I / O(可能是顺序I / O)。



其他资源



并行哈希联接(Craig Freedman)

查询执行位图过滤器(SQL Server查询处理团队)

Microsoft SQL Server 2000中的位图(MSDN文章)

解释包含位图过滤器的执行计划(SQL Server文档)

了解哈希联接(SQL Server文档)



测试脚本



USE tempdb;
GO
CREATE TABLE #BuildInt
(
    col1    INTEGER NOT NULL
);
GO
CREATE TABLE #Probe
(
    col1    INTEGER NOT NULL
);
GO

-- Load 5,000 rows into the build table
SET NOCOUNT ON;
SET STATISTICS XML OFF;

DECLARE @I INTEGER = 1;

INSERT #BuildInt
    (col1) 
VALUES 
    (CONVERT(INTEGER, RAND(1) * 2147483647));

WHILE @I < 5000
BEGIN
    INSERT #BuildInt
        (col1)
    VALUES 
        (RAND() * 2147483647);
    SET @I += 1;
END;

-- Load 5,000,000 rows into the probe table
SET NOCOUNT ON;
SET STATISTICS XML OFF;

DECLARE @I INTEGER = 1;

INSERT #Probe
    (col1) 
VALUES 
    (CONVERT(INTEGER, RAND(2) * 2147483647));

BEGIN TRANSACTION;
WHILE @I < 5000000
BEGIN
    INSERT #Probe
        (col1) 
    VALUES 
        (CONVERT(INTEGER, RAND() * 2147483647));

    SET @I += 1;

    IF @I % 25 = 0
    BEGIN
        COMMIT TRANSACTION;
        BEGIN TRANSACTION;
    END;
END;

COMMIT TRANSACTION;
GO
-- Demos
SET STATISTICS XML OFF;
SET STATISTICS IO, TIME ON;

-- Serial
SELECT 
    COUNT_BIG(*) 
FROM #BuildInt AS bi 
JOIN #Probe AS p ON 
    p.col1 = bi.col1 
OPTION (MAXDOP 1);

-- Parallel
SELECT 
    COUNT_BIG(*) 
FROM #BuildInt AS bi 
JOIN #Probe AS p ON 
    p.col1 = bi.col1 
OPTION (MAXDOP 2);

SET STATISTICS IO, TIME OFF;

-- Indexes
CREATE UNIQUE CLUSTERED INDEX cuq ON #BuildInt (col1);
CREATE CLUSTERED INDEX cx ON #Probe (col1);

-- Vary the query hints to explore plan shapes

SELECT 
    COUNT_BIG(*) 
FROM #BuildInt AS bi 
JOIN #Probe AS p ON 
    p.col1 = bi.col1 
OPTION (MAXDOP 1, MERGE JOIN);
GO
DROP TABLE #BuildInt, #Probe;








阅读更多:






All Articles