加入价值范围的最优绩效

时间:2013-03-22 21:22:02

标签: sql sql-server tsql query-optimization

我有一个非常大的表,包含IP地址的整数表示,第二个表包含IP地址的整数表示的起始和结束范围。第二个表用于按several stackoverflow articles返回国家/地区。虽然这会返回所需的结果,但性能相当差。加入一个范围是否有更高性能的替代方案?下面是一组示例代码,显示了当前连接的工作方式:

CREATE TABLE #BaseTable
    ( SomeIntegerValue INT PRIMARY KEY);

INSERT INTO #BaseTable (SomeIntegerValue)
SELECT SomeIntegerValue
FROM (VALUES
    (123), (456), (789)) Data (SomeIntegerValue);

CREATE TABLE #RangeLookupTable
    ( RangeStartValue INT PRIMARY KEY
    , RangeEndValue INT NOT NULL);

INSERT INTO #RangeLookupTable (RangeStartValue, RangeEndValue)
SELECT RangeStartValue, RangeEndValue
FROM (VALUES
      (0, 100), (101, 200), (201, 300)
    , (301, 400), (401, 500), (501, 600)
    , (701, 800), (901, 1000)) Data (RangeStartValue, RangeEndValue);

SELECT *
FROM #BaseTable bt
JOIN #RangeLookupTable rlt
    ON bt.SomeIntegerValue BETWEEN rlt.RangeStartValue AND rlt.RangeEndValue

4 个答案:

答案 0 :(得分:1)

如果特定情况允许在表中保存反规范化数据,然后从该表而不是规范化基表查询,则可以实现非常快速的检索时间。查询执行计划在SELECT中显示2倍增益,即使此样本数据为3行。

在具有相对较少的写入和更多读取操作的场景中,这种方法是可能的。只有在更新数据时才需要执行JOIN;使用实际数据进行测试将显示在整个(UPDATE + SELECT)系统图片中实际实现了多少(或者是否有任何改进!)。

示例代码以及SELECT语句的结果执行计划屏幕截图如下所示。

CREATE TABLE #BaseTable
    ( SomeIntegerValue INT PRIMARY KEY);

INSERT INTO #BaseTable (SomeIntegerValue)
SELECT SomeIntegerValue
FROM (VALUES
    (123), (456), (789)) Data (SomeIntegerValue);

CREATE TABLE #RangeLookupTable
    ( RangeStartValue INT PRIMARY KEY
    , RangeEndValue INT NOT NULL);

INSERT INTO #RangeLookupTable (RangeStartValue, RangeEndValue)
SELECT RangeStartValue, RangeEndValue
FROM (VALUES
      (0, 100), (101, 200), (201, 300)
    , (301, 400), (401, 500), (501, 600)
    , (701, 800), (901, 1000)) Data (RangeStartValue, RangeEndValue);

-- Alternative approach: Denormalized base table
CREATE TABLE #BaseTable2
    ( SomeIntegerValue INT PRIMARY KEY,
      RangeStartValue INT null,
      RangeEndValue INT NULL);

INSERT INTO #BaseTable2 (SomeIntegerValue)
SELECT SomeIntegerValue
FROM (VALUES
    (123), (456), (789)) Data (SomeIntegerValue);

UPDATE #BaseTable2
SET RangeStartValue = rlt.RangeStartValue,
    RangeEndValue = rlt.RangeEndValue
FROM #BaseTable2 bt2
JOIN #RangeLookupTable rlt
    ON bt2.SomeIntegerValue BETWEEN rlt.RangeStartValue AND rlt.RangeEndValue

-- The original: SELECT with a JOIN
SELECT *
FROM #BaseTable bt
JOIN #RangeLookupTable rlt
    ON bt.SomeIntegerValue BETWEEN rlt.RangeStartValue AND rlt.RangeEndValue

-- The alternative: SELECT from the denormalized base table
SELECT * from #BaseTable2;

GO

查询JOINed与非规范化SELECT的执行计划:

Query Execution with a JOIN vs. a denormalized table

答案 1 :(得分:0)

这几乎肯定是一个索引问题。您目前在RangeStartValue(主键)上有一个索引,但在RangeEndValue上没有,所以在缩小第一列后,它可能必须对第二列进行全面扫描。尝试为RangeEndValue建立索引,看看它是如何影响它的。

我并不精通BETWEEN条款的表现质量,但你可以通过将不同性质检查的比较两边都写成来保证不成问题。


同样在这个测试脚本中,您选择基表中的每一行,我想您在生产数据库中没有这样做?

答案 2 :(得分:0)

问题是您的查找表具有非重叠的地址(范围)。但是,SQL Server可能无法识别这一点。因此,当你有ipaddress between A and B时,它必须从头开始扫描整个索引并以A结尾。

我不知道是否有办法解释表格的实际用途,优化器将跳转到索引中的第一个相应记录。这样的事情可能有用:

select bt.*,
       (select top 1 RangeEndValue
        from #RangeLookupTable rlt
        where rlt.RangeStartValue <= bt.SomeIntegerValue
        order by RangeStartValue desc)
FROM #BaseTable bt 

这可能会“欺骗”优化器只查看索引中的一条记录。样本中的数据太小,无法判断这是否会对性能产生影响。

另一种方法是使用equi-join来停止搜索。在每个表中,添加地址的TypeA部分(第一个字节)。这可以是冗余的,第二个字段具有完整地址,或者您可以将其他三个字节放在下一个字段中。任何跨越多个TypeA地址的ip列表都需要拆分为单独的条目。

使此字段成为索引中的第一列,其中地址(或地址的其余部分)作为主键的第二部分。您可以使用约束来创建具有多个列的主键。

查询将如下所示:

SELECT *
FROM #BaseTable bt join
     #RangeLookupTable rlt
     ON bt.typeA = rlt.typeA and
        bt.SomeIntegerValue BETWEEN rlt.RangeStartValue AND rlt.RangeEndValue

索引扫描将仅限于具有相同第一个字节的值。当然,您也可以使用前两个字节将其扩展到TypeAB。

答案 3 :(得分:0)

我测试了15种不同的方法,我认为这些方法可以使用,这种解决方案是迄今为止最好的方法:

SELECT bt.*
    , RangeStartValue = 
        (SELECT TOP 1 RangeStartValue
        FROM #RangeLookupTable rlt
        WHERE bt.SomeIntegerValue >= rlt.RangeStartValue
        ORDER BY rlt.RangeStartValue)
FROM #BaseTable bt;

这会在查找表上创建聚簇索引查找,并且能够在几秒钟内完成数百万条记录的流失。请注意我从代码in this blog改编了此解决方案。