在SQL Server中优化数值范围(间隔)搜索


18

此问题类似于优化IP范围搜索?但是那只限于SQL Server 2000。

假设我将1000万个范围临时存储在一个表中,该表的结构和填充如下。

CREATE TABLE MyTable
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX IX1 (RangeFrom,RangeTo),
INDEX IX2 (RangeTo,RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO MyTable
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(1)
FROM   RandomNumbers 

我需要知道所有包含该值的范围50,000,000。我尝试以下查询

SELECT *
FROM MyTable
WHERE 50000000 BETWEEN RangeFrom AND RangeTo

SQL Server显示,总共进行了10,951次逻辑读取,读取了近500万行以返回12个匹配的行。

在此处输入图片说明

我可以改善这个表现吗?该表的任何重组或其他索引都可以。


如果我正确地理解了表格的设置,那么您将统一选择随机数以形成范围,而对每个范围的“大小”没有任何限制。而且您的探头位于1..100M总范围的中间。在那种情况下-由于均匀的随机性而没有明显的聚类-我不知道为什么下限或上限的索引会有所帮助。你能解释一下吗?
davidbak

@davidbak在最坏的情况下,该表上的常规索引确实不是很有用,因为它必须扫描一半的范围,因此需要对其进行潜在的改进。通过引入“颗粒”,SQL Server 2000的链接问题有了很大的改进,我希望空间索引可以在这里有所帮助,因为它们支持contains查询,并且在减少读取的数据量方面很有效,它们似乎可以添加其他内容。开销抵消了这一点。
马丁·史密斯

我没有尝试的工具-但是我想知道两个索引-一个在下限,一个在上限-然后是内部联接-是否会让查询优化器发挥作用。
davidbak

Answers:


11

与扫描表一半的非聚集索引相比,Columnstore在这里非常有用。非聚集的列存储索引提供了大多数好处,但是将有序数据插入群集的列存储索引甚至更好。

DROP TABLE IF EXISTS dbo.MyTableCCI;

CREATE TABLE dbo.MyTableCCI
(
Id        INT PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX CCI CLUSTERED COLUMNSTORE
);

INSERT INTO dbo.MyTableCCI
SELECT TOP (987654321) *
FROM dbo.MyTable
ORDER BY RangeFrom ASC
OPTION (MAXDOP 1);

通过设计,我可以在RangeFrom列上消除行组,从而消除一半的行组。但是由于数据的性质,我也可以在RangeTo列上消除行组:

Table 'MyTableCCI'. Segment reads 1, segment skipped 9.

对于具有更多可变数据的较大表,有不同的加载方法,以确保在两列上尽可能消除行组。特别是对于您的数据,查询需要1毫秒。


是的,肯定会寻找其他不受2000年限制的方法。听起来不会被打败。
马丁·史密斯

9

保罗·怀特(Paul White)指出了一个类似问题的答案,其中包含与Itzik Ben Gan的一篇有趣文章的链接。这描述了“静态关系间隔树”模型,该模型可以有效地完成此任务。

总而言之,该方法涉及基于行中的间隔值存储计算的(“ forknode”)值。当搜索与另一个范围相交的范围时,可以预先计算匹配行必须具有的可能的forknode值,并使用它来查找最多31个搜寻操作的结果(以下内容支持范围为0到最大有符号32的整数)位int)

基于此,我将表重组如下。

CREATE TABLE dbo.MyTable3
(
  Id        INT IDENTITY PRIMARY KEY,
  RangeFrom INT NOT NULL,
  RangeTo   INT NOT NULL,   
  node  AS RangeTo - RangeTo % POWER(2, FLOOR(LOG((RangeFrom - 1) ^ RangeTo, 2))) PERSISTED NOT NULL,
  CHECK (RangeTo > RangeFrom)
);

CREATE INDEX ix1 ON dbo.MyTable3 (node, RangeFrom) INCLUDE (RangeTo);
CREATE INDEX ix2 ON dbo.MyTable3 (node, RangeTo) INCLUDE (RangeFrom);

SET IDENTITY_INSERT MyTable3 ON

INSERT INTO MyTable3
            (Id,
             RangeFrom,
             RangeTo)
SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable

SET IDENTITY_INSERT MyTable3 OFF 

然后使用以下查询(本文正在寻找相交的间隔,因此,找到包含点的间隔是这种情况的简写)

DECLARE @value INT = 50000000;

;WITH N AS
(
SELECT 30 AS Level, 
       CASE WHEN @value > POWER(2,30) THEN POWER(2,30) END AS selected_left_node, 
       CASE WHEN @value < POWER(2,30) THEN POWER(2,30) END AS selected_right_node, 
       (SIGN(@value - POWER(2,30)) * POWER(2,29)) + POWER(2,30)  AS node
UNION ALL
SELECT N.Level-1,   
       CASE WHEN @value > node THEN node END AS selected_left_node,  
       CASE WHEN @value < node THEN node END AS selected_right_node,
       (SIGN(@value - node) * POWER(2,N.Level-2)) + node  AS node
FROM N 
WHERE N.Level > 0
)
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS L
    ON I.node = L.selected_left_node
    AND I.RangeTo >= @value
    AND L.selected_left_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS R
    ON I.node = R.selected_right_node
    AND I.RangeFrom <= @value
    AND R.selected_right_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
WHERE node = @value;

1ms当所有页面都在缓存中时,这通常在我的计算机上执行-带有IO统计信息。

Table 'MyTable3'. Scan count 24, logical reads 72, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 4, logical reads 374, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

和计划

在此处输入图片说明

注意:源使用多语句TVF而不是递归CTE来使节点加入,但是出于使我的答案自成一体的目的,我选择了后者。对于生产用途,我可能会使用TVF。


9

我能够找到与N / CCI方法竞争的行模式方法,但是您需要了解一些有关数据的知识。假设你有包含的差异列RangeFromRangeTo你索引它连同RangeFrom

ALTER TABLE dbo.MyTableWithDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableWithDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

如果您知道的所有不同值,DiffOfColumns则可以DiffOfColumns使用范围过滤器对的每个值进行搜索,RangeTo以获取所有相关数据。例如,如果我们知道DiffOfColumns= 2,则其唯一允许的值RangeFrom是49999998、49999999和50000000。可以使用递归来获取的所有不同值,DiffOfColumns因为只有256个,所以它对您的数据集效果很好。下面的查询在我的计算机上大约需要6毫秒:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        DiffOfColumns
    FROM dbo.MyTableWithDiff AS T
    ORDER BY
        T.DiffOfColumns

    UNION ALL

    -- Recursive
    SELECT R.DiffOfColumns
    FROM
    (
        -- Number the rows
        SELECT 
            T.DiffOfColumns,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.DiffOfColumns)
        FROM dbo.MyTableWithDiff AS T
        JOIN RecursiveCTE AS R
            ON R.DiffOfColumns < T.DiffOfColumns
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT ca.*
FROM RecursiveCTE rcte
CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableWithDiff mt
    WHERE mt.DiffOfColumns = rcte.DiffOfColumns
    AND mt.RangeFrom >= 50000000 - rcte.DiffOfColumns AND mt.RangeFrom <= 50000000
) ca
OPTION (MAXRECURSION 0);

您可以看到通常的递归部分以及针对每个不同值的索引查找:

查询计划1

这种方法的缺陷在于,当的不同值太多时,它开始变慢DiffOfColumns。让我们进行相同的测试,但使用CRYPT_GEN_RANDOM(2)代替CRYPT_GEN_RANDOM(1)

DROP TABLE IF EXISTS dbo.MyTableBigDiff;

CREATE TABLE dbo.MyTableBigDiff
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO dbo.MyTableBigDiff
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(2) -- note the 2
FROM   RandomNumbers;


ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableBigDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

现在,同一查询从递归部分中找到65536行,并占用了我的计算机上823 ms的CPU。有PAGELATCH_SH等待和其他不良情况。我可以通过对diff值进行存储以保持唯一值的数量在控制之下并针对中的存储进行调整来提高性能CROSS APPLY。对于此数据集,我将尝试256个存储桶:

ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns_bucket256 AS CAST(CEILING((RangeTo-RangeFrom) / 256.) AS INT);

CREATE INDEX [IXDIFF😎] ON dbo.MyTableBigDiff (DiffOfColumns_bucket256, RangeFrom) INCLUDE (RangeTo);

避免获取多余的行(现在我正在将其与舍入值而不是真实值进行比较)的一种方法是过滤RangeTo

CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableBigDiff mt
    WHERE mt.DiffOfColumns_bucket256 = rcte.DiffOfColumns_bucket256
    AND mt.RangeFrom >= 50000000 - (256 * rcte.DiffOfColumns_bucket256)
    AND mt.RangeFrom <= 50000000
    AND mt.RangeTo >= 50000000
) ca

现在,完整查询在我的计算机上需要6毫秒。


8

表示范围的一种替代方法是将其表示为一条线上的点。

下面将所有数据迁移到一个新表中,该表的范围表示为geometry数据类型。

CREATE TABLE MyTable2
(
Id INT IDENTITY PRIMARY KEY,
Range GEOMETRY NOT NULL,
RangeFrom AS Range.STPointN(1).STX,
RangeTo   AS Range.STPointN(2).STX,
CHECK (Range.STNumPoints() = 2 AND Range.STPointN(1).STY = 0 AND Range.STPointN(2).STY = 0)
);

SET IDENTITY_INSERT MyTable2 ON

INSERT INTO MyTable2
            (Id,
             Range)
SELECT ID,
       geometry::STLineFromText(CONCAT('LINESTRING(', RangeFrom, ' 0, ', RangeTo, ' 0)'), 0)
FROM   MyTable

SET IDENTITY_INSERT MyTable2 OFF 


CREATE SPATIAL INDEX index_name   
ON MyTable2 ( Range )  
USING GEOMETRY_GRID  
WITH (  
BOUNDING_BOX = ( xmin=0, ymin=0, xmax=110000000, ymax=1 ),  
GRIDS = (HIGH, HIGH, HIGH, HIGH),  
CELLS_PER_OBJECT = 16); 

查找包含该值的范围的等效查询50,000,000如下。

SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable2
WHERE  Range.STContains(geometry::STPointFromText ('POINT (50000000 0)', 0)) = 1 

读取的内容显示了对10,951原始查询的改进。

Table 'MyTable2'. Scan count 0, logical reads 505, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'extended_index_1797581442_384000'. Scan count 4, logical reads 17, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

但是,在经过时间方面,没有比原始版本有明显的改进。典型的执行结果是250毫秒和252毫秒。

执行计划更加复杂,如下所示

在此处输入图片说明

唯一可靠地对我执行重写的情况是使用冷缓存。

因此,在这种情况下令人失望,很难推荐这种重写,但是发布负面结果也可能很有用。


5

为了向我们的新机器人霸主致敬,我决定看看是否有任何新的R和Python功能可以在这里帮助我们。答案是否定的,至少对于我可以开始工作并返回正确结果的脚本而言。如果有更好的知识的人出现,那么,请随意打我。我的价格是合理的。

为此,我设置了一个具有4个内核和16 GB RAM的VM,认为这足以处理约200MB的数据集。

让我们从波士顿不存在的语言开始!

[R

EXEC sp_execute_external_script 
@language = N'R', 
@script = N'
tweener = 50000000
MO = data.frame(MartinIn)
MartinOut <- subset(MO, RangeFrom <= tweener & RangeTo >= tweener, select = c("Id","RangeFrom","RangeTo"))
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

那是一段糟糕的时光。

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3219 ms,  elapsed time = 5349 ms.

执行计划是相当无趣,虽然我不知道为什么中间操作员给我们打电话的名字。

坚果

接下来,用蜡笔编码!

蟒蛇

EXEC sp_execute_external_script 
@language = N'Python', 
@script = N'
import pandas as pd
MO = pd.DataFrame(MartinIn)
tweener = 50000000
MartinOut = MO[(MO.RangeFrom <= tweener) & (MO.RangeTo >= tweener)]
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

就在您认为它不会比R更糟的时候:

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3797 ms,  elapsed time = 10146 ms.

另一个臭嘴巴的执行计划

坚果

嗯和嗯

到目前为止,我没有留下深刻的印象。我等不及要删除此虚拟机。


1
您也可以传递参数,例如,DECLARE @input INT = 50000001; EXEC dbo.sp_execute_external_script @language = N'R', @script = N'OutputDataSet <- InputDataSet[which(x >= InputDataSet$RangeFrom & x <= InputDataSet$RangeTo) , ]', @parallel = 1, @input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable;', @params = N'@x INT', @x = 50000001 WITH RESULT SETS ( ( Id INT NOT NULL, RangeFrom INT NOT NULL, RangeTo INT NOT NULL ));但是性能不是很好。我将R用于您在SQL中无法完成的工作,例如,如果您想预测某些内容。
wBob

4

我发现了一个使用计算列的不错的解决方案,但是它仅适用于单个值。话虽如此,如果您有一个神奇的价值,也许就足够了。

从给定的样本开始,然后修改表:

ALTER TABLE dbo.MyTable
    ADD curtis_jackson 
        AS CONVERT(BIT, CASE 
                            WHEN RangeTo >= 50000000
                            AND RangeFrom < 50000000
                            THEN 1 
                            ELSE 0 
                        END);

CREATE INDEX IX1_redo 
    ON dbo.MyTable (curtis_jackson) 
        INCLUDE (RangeFrom, RangeTo);

查询变成:

SELECT *
FROM MyTable
WHERE curtis_jackson = 1;

返回与开始查询相同的结果。在关闭执行计划的情况下,以下是统计信息(为简洁起见,已被截断):

Table 'MyTable'. Scan count 1, logical reads 3...

SQL Server Execution Times:
  CPU time = 0 ms,  elapsed time = 0 ms.

这是查询计划

坚果


您不能克服索引为on的对计算列/过滤索引的模仿WHERE (50000000 BETWEEN RangeFrom AND RangeTo) INCLUDE (..)吗?
ypercubeᵀᴹ

3
@yper-crazyhat-c​​ubeᵀᴹ-是的。CREATE INDEX IX1_redo ON dbo.MyTable (curtis_jackson) INCLUDE (RangeFrom, RangeTo) WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000会工作。和查询SELECT * FROM MyTable WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000;使用它-这样的话没有太大必要为贫困柯蒂斯
马丁·史密斯

3

我的解决方案是基于所述间隔具有一个宽度的已知最大观察w ^。对于样本数据,这是一个字节或256个整数。因此,对于给定的搜索参数值P,我们知道结果集中可以存在的最小RangeFrom是P-W。将其添加到谓词中可得出

declare @P int = 50000000;
declare @W int = 256;

select
    *
from MyTable
where @P between RangeFrom and RangeTo
and RangeFrom >= (@P - @W);

给定原始设置并查询我的机器(64位Windows 10、4核超线程i7、2.8GHz,16GB RAM)返回13行。该查询使用(RangeFrom,RangeTo)索引的并行索引查找。修改后的查询还对同一索引执行并行索引查找。

原始查询和修订查询的度量为

                          Original  Revised
                          --------  -------
Stats IO Scan count              9        6
Stats IO logical reads       11547        6

Estimated number of rows   1643170  1216080
Number of rows read        5109666       29
QueryTimeStats CPU             344        2
QueryTimeStats Elapsed          53        0

对于原始查询,读取的行数等于或小于@P的行数。查询优化器(QO)只能读取所有内容,因为它无法预先确定这些行是否满足谓词。(RangeFrom,RangeTo)上的多列索引在消除与RangeTo不匹配的行时没有用,因为第一个索引键与第二个索引键之间没有关联。例如,第一行可能具有较小的间隔并被消除,而第二行则具有较大的间隔并被返回,反之亦然。

在一次失败的尝试中,我尝试通过检查约束来提供这种确定性:

alter table MyTable with check
add constraint CK_MyTable_Interval
check
(
    RangeTo <= RangeFrom + 256
);

没关系。

通过将我对数据分布的外部知识整合到谓词中,我可以使QO跳过永远不会成为结果集一部分的低值RangeFrom行,并将索引的前列遍历到允许的行。这显示了每个查询的不同搜索谓词。

在镜子参数RangeTo的上限是P + w ^。但是,这没有用,因为RangeFrom和RangeTo之间没有相关性,这将使多列索引的尾随列消除行。因此,将此子句添加到查询没有任何好处。

这种方法从较小的间隔大小中获得了大部分好处。随着可能的间隔大小增加,跳过的低值行的数量会减少,尽管有些行仍会被跳过。在有限的情况下,间隔与数据范围一样大,此方法并不比原始查询(我承认这很冷)更糟糕。

对于此答案中可能存在的一字不漏的错误,我深表歉意。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.