4

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

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

key_lower_bound:int key_upper_bound:intinteresting_value1:浮动interesting_value2:intinteresting_value3:varchar(50) ...

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

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

当我构建最明显的 SQL 查询(涉及检查键是否为 > key_lower_bound 和 < key_upper_bound)时,它花费的时间太长了。

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

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

4

4 回答 4

6

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

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.
于 2009-06-30T05:58:40.070 回答
1

对于第一个表,我会在“键”上放置一个聚集索引。对于第二个表,我会在“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”上添加第二个非聚集索引,看看这是否提高了性能。

于 2009-06-29T21:24:28.793 回答
0

根据我的经验,没有简单而强大的解决方案。我已经在许多类似的情况下成功地使用了非规范化,将 key_lower_bound 和 key_upper_bound 复制到大表,并让外键从大表引用到具有间隔的表。您还创建了一个检查约束来确保 (key > key_lower_bound 和 key < key_upper_bound),但是这个检查只涉及一个表中的列,所以它工作正常。这绝对是非规范化,但数据永远不会不同步,因为 FK 约束确保大表中的 (key_lower_bound, key_upper_bound) 与父表中的间隔匹配。因为您不需要联接,所以您的选择执行得非常快。

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

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

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

于 2009-06-30T03:01:03.857 回答
0

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

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

我相信最接近您所描述的行为的是合并连接

select t1.key from largeTable t1 inner merge join smallTable t2 on t1.key >= t2.key_lower_bound 和 t1.key < t2.key_upper_bound

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

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


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

于 2009-06-30T07:06:51.930 回答