【问题标题】:SQL join with a range of values (int ranges, date ranges, whatever)SQL join 与一系列值(整数范围、日期范围等)
【发布时间】:2010-11-06 19:51:20
【问题描述】:

我有两个表,第一个是一个大表(数百万行),最有趣的列是一个整数,我将称之为“键”。我相信这个解决方案对于日期或日期时间范围也是相同的。

第二个表要小得多(数千行),其中包含一堆我感兴趣的属性,这些属性是在一系列键上定义的。它的结构如下:

key_lower_bound : int key_upper_bound : 整数 有趣的值1:浮动 有趣的值2:整数 有趣的值3:varchar(50) ...

我想查找第一个表中的所有值,并根据第一个表中的键是否落在区间 [key_lower_bound, key_upper_bound) 内,将它们与第二个表“连接”。

这在数学上有点像稀疏内积或稀疏点积,但有点奇怪,因为第二个表中涉及这些范围。不过,如果我用代码编写它,那将是一个 O(|first table| + |second table|) 算法。我会保留一个指向两个(排序的)列表的指针并遍历它们,以确定第一个表中的每个键是否属于第二个表的范围。诀窍是,每次检查第一个表中的键时,我都不会遍历第二个列表,因为两个列表都已排序。

当我构造最明显的 SQL 查询时(包括检查键是否为 > key_lower_bound 和

这个天真的查询会出现某种二次行为,因为我认为查询引擎正在对第二个表中的每一行进行每次比较,而实际上,如果第二个表按 key_lower_bounds 排序,这不应该是必要的。所以我得到了 O(|first table| x |second table|) 类型的行为,而不是所需的 O(|first table| + |second table|) 行为。

是否可以使用线性 SQL 查询来执行此操作?

【问题讨论】:

  • 第二个表对于给定键的范围将有多少条记录?即假设 key = 71。第二个表将有多少条记录,其中值 71 将落在开始和结束之间?如果它只有 1 条记录,事情可能会更容易。
  • 最简单的,你试过带EXISTS子句的SQL吗?
  • 我们可以假设两个表都在键列上建立了索引吗?
  • 第二个表充满了不相交的范围。所以第一行可能是 (0,1000) 下一个是 (1001,487294) 等等。所以第一个表中的每一行将匹配第二个表中的最多一行(又名范围)。此外,您可以随意索引表,但要使其快速运行!
  • shahkalpesh:我真的不明白 EXISTS 子句如何让我得到一个线性解决方案,感觉它必然会导致一个二次解决方案。如果我遗漏了什么,请详细说明!

标签: sql performance optimization query-optimization


【解决方案1】:

好吧,我已经解决了这个问题并提出了一些建议。 但首先让我们填充辅助表

CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY)
GO
DECLARE @i INT;
SET @i = 1;
INSERT INTO dbo.Numbers(n) SELECT 1;
WHILE @i<1024000 BEGIN
  INSERT INTO dbo.Numbers(n)
    SELECT n + @i FROM dbo.Numbers;
  SET @i = @i * 2;
END;
GO

和测试数据,一年每分钟一分钟的广告,同年每分钟一个客户来电:

CREATE TABLE dbo.Commercials(
  StartedAt DATETIME NOT NULL 
    CONSTRAINT PK_Commercials PRIMARY KEY,
  EndedAt DATETIME NOT NULL,
  CommercialName VARCHAR(30) NOT NULL);
GO
INSERT INTO dbo.Commercials(StartedAt, EndedAt, CommercialName)
SELECT DATEADD(minute, n - 1, '20080101')
    ,DATEADD(minute, n, '20080101')
    ,'Show #'+CAST(n AS VARCHAR(6))
  FROM dbo.Numbers
  WHERE n<=24*365*60;
GO
CREATE TABLE dbo.Calls(CallID INT 
  CONSTRAINT PK_Calls NOT NULL PRIMARY KEY,
  AirTime DATETIME NOT NULL,
  SomeInfo CHAR(300));
GO
INSERT INTO dbo.Calls(CallID,
  AirTime,
  SomeInfo)
SELECT n 
    ,DATEADD(minute, n - 1, '20080101')
    ,'Call during Commercial #'+CAST(n AS VARCHAR(6))
  FROM dbo.Numbers
  WHERE n<=24*365*60;
GO
CREATE UNIQUE INDEX Calls_AirTime
  ON dbo.Calls(AirTime) INCLUDE(SomeInfo);
GO

最初尝试选择年中三个小时的广告期间拨打的所有电话非常慢:

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Commercials s JOIN dbo.Calls c 
  ON c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 15 ms, elapsed time = 30 ms.

(1 row(s) affected)
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 2, logical reads 3338264, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Commercials'. Scan count 2, logical reads 7166, 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.

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

原因很简单:我们知道广告不重叠,所以一个电话 最多适合一个广告,但优化器不知道。 我们知道广告很短,但优化器也不知道。 这两个假设都可以作为约束强制执行,但优化器不会仍然这样做。

假设广告不超过 15 分钟,我们可以看出 到优化器,查询非常快:

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Commercials s JOIN dbo.Calls c 
  ON c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
AND s.StartedAt BETWEEN '20080630 23:45' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 15 ms, elapsed time = 15 ms.

(1 row(s) affected)
Table 'Worktable'. Scan count 1, logical reads 753, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Commercials'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

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

假设广告不重叠,所以一个电话 最多适合一个广告,我们可以告诉 到优化器,查询又很快:

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Calls c CROSS APPLY(
  SELECT TOP 1 s.StartedAt, s.EndedAt FROM dbo.Commercials s 
  WHERE c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
  ORDER BY s.StartedAt DESC) AS s
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 7 ms.

(1 row(s) affected)
Table 'Commercials'. Scan count 181, logical reads 1327, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

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

【讨论】:

  • @Alex,这看起来很有希望!我很好奇,CROSS APPLY 是否有效(例如呼叫中每一行的 O(1))?换句话说,如果你将数据库的大小增加一倍,是否只需要两倍的时间?
  • 克里斯,它几乎是 O(N),但不完全是。因为您进行索引查找并且索引深度增加,所以它是 O(N + ln(N)) 因为索引深度以对数方式增加。
  • @Alex,你的意思是 O(N*lg(N)) 吗? O(N + log(N)) == O(N)。
【解决方案2】:

对于第一个表,我会在“键”上放置一个聚集索引。对于第二个表,我会在“key_lower_bound”上放置一个聚集索引。那我试试:

select *
from FirstTable f
inner join SecondTable s 
    on f.key between s.key_lower_bound and s.key_upper_bound

然后我会在“key_upper_bound”上添加第二个非聚集索引,看看这是否会提高性能。

【讨论】:

  • 我仍然认为这会产生二次行为 - 因为 SQL 服务器不会“知道”在 FirstTable 中的一定数量的行之后,它可以停止检查 SecondTable 中的第一行。因此,在查询的中途,它将重新评估 SecondTable 中每一行的 between 语句,直到“当前”secondtable 范围。你明白我的意思吗?
  • @Chris,你最好不要考虑 DBMS 迭代工作;有时可能是这样,但将其视为原子执行,对整个集合进行操作更有意义。我明白你的意思,但是 - 我认为它不适用于真正的数据库操作。
  • 它应该使用二分搜索或等效的方法来有效地定位它需要在 SecondTable 中搜索的 key_lower_bound 范围的开始。然后它将按顺序搜索到表的末尾,检查每一行的 key_upper_bound。对于非常低的键值(例如 1),这意味着检查 SecondTable 中的每一行。但是对于非常高的键值,它应该检查 SecondTable 中的很少行。我不确定是否足够聪明地使用 key_upper_bound 上的索引来帮助限制它检查 key_upper_bound 的行,这就是为什么我会尝试两种方式。
  • 我认为它实际上会使用两个索引,并创建一个与key_lower_bound或greakey_upper_boundter匹配的集合,以及另一个匹配或更少的集合,然后找到这两个集合的交集。
  • @Carl,在大多数情况下,我完全支持你。我只是想表达为什么我认为存在线性解决方案时会发生“二次”行为。我可能对 RDBMS 的问题组织得不好,如果是这样的话,我会喜欢关于如何改变它的建议,但目前我没有看到任何好的解决方案——我觉得这很奇怪,因为它是一个非常“面向集合的问题” ”而且我们使用的是“面向集合的技术”,我很难相信一个人不能构造一个线性的算法!
【解决方案3】:

根据我的经验,没有简单而可靠的解决方案。我已经在许多类似的情况下成功地使用了非规范化,将 key_lower_bound 和 key_upper_bound 复制到大表,并让外键从大表引用到具有间隔的表。您还创建了一个检查约束来确保 (key > key_lower_bound 和 key

通过非规范化解决的类似问题:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/08/storing-intervals-of-time-with-no-overlaps.aspx

如果您需要完整的 DDL,请告诉我,很容易编写。

【讨论】:

  • 有趣的解决方案 Alex - 谢谢!不幸的是,我认为非规范化是一个非常大的代价。这意味着额外的范围值将添加到更大表中的每一行。
【解决方案4】:

要执行您描述的线性算法,需要数据库没有的两件事:

  1. 能够理解小表中的每一行都包含多个 LargeTable.Key 到单个 SmallTable.Range 的不同(不相交)映射,并且
  2. 需要将表存储为链表或数组(它们不是)。

我相信最接近您所描述的行为的是merge join

选择 t1.key 从 大表 t1 t1.key >= t2.key_lower_bound 和 t1.key 上的内部合并连接 smallTable t2

您应该了解,表存储为 B-tree 或堆 - 因此它经过优化以查找特定节点 - 而不是用于扫描。扫描意味着您必须跟上 log_B(N) 指针(例如在堆栈中)以记住您在树中的位置,而不必重新遍历。这甚至不是在谈论磁盘访问模式。

作为次要的性能理念,您应该尝试定义一个表示范围的单个值并将其用作 smallTable 的主键,该主键可以从 largeTable 中作为外键引用。这比复合键(本质上是您的 lower_bound 和 upper_bound 列所代表的)更有效。 可能是一个散列值,例如 PK = lower_bound & upper_bound


another reference 应该说明了为什么 SQL 很难将这个算法放在一起。如果你可以使用 Matlab 来处理你的东西 - 这可能是一个更好的选择:)

【讨论】:

  • 以下内容与 Kalen Delaney 在她的 SQL Internals 书籍中所写的内容相矛盾:“您应该了解表存储为 B 树或堆 - 因此它经过优化以查找特定节点 -不用于扫描。扫描意味着您必须保持 log_B(N) 指针(例如在堆栈中)以记住您在树中的位置,而不必返回“。反之亦然 - 扫描索引非常高效。这就是索引覆盖如此有用的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 2015-03-15
  • 1970-01-01
相关资源
最近更新 更多