7

我有一个非常大的表,其中包含 IP 地址的整数表示形式和第二个表,其中包含 IP 地址的整数表示形式的起始和结束范围。第二个表用于根据几个 stackoverflow 文章返回国家/地区。尽管这会返回所需的结果,但性能相当差。除了加入某个范围之外,还有其他性能更高的替代方案吗?下面是一组示例代码,显示了连接当前的工作方式:

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

4 回答 4

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 的查询执行计划:

使用 JOIN 与非规范化表执行查询

于 2013-10-03T18:31:44.723 回答
0

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

我不太熟悉该BETWEEN子句的性能质量,但是您可以通过编写不等式检查的比较双方来保证这不是问题。


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

于 2013-03-22T21:31:37.007 回答
0

我测试了 15 种我认为可行的独立方法,这个解决方案是迄今为止最好的:

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

这会在查找表上创建聚集索引搜索,并且能够在几秒钟内翻阅数百万条记录。请注意,我根据此博客中的代码改编了此解决方案。

于 2014-03-21T23:24:52.010 回答
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。

于 2013-03-22T21:46:59.090 回答