我有两个表,第一个是一个大表(数百万行),最有趣的列是一个整数,我只称之为“键”。我相信这个解决方案对于日期或日期时间范围也是相同的。
第二个表要小得多(数千行),其中包含一些我感兴趣的属性,这些属性是通过一系列键定义的。它具有以下结构:
key_lower_bound:int key_upper_bound:int interesting_value1:浮动 interesting_value2:int interesting_value3:varchar(50) ...
我想查找第一个表中的所有值,并根据第一个表中的键是否属于[key_lower_bound,key_upper_bound]区间内的第二个表来“连接”它们。
这有点像稀疏内积或稀疏点积在数学上,但它有点奇怪,因为第二个表中涉及这些范围。不过,如果我在代码中写这个,那将是一个O(|第一个表| + |第二个表|)算法。我会指向两个(已排序)列表并逐个浏览它们,以确定第一个表中的每个键是否属于第二个表的范围。诀窍在于,每次检查第一个表中的键时,我都不会遍历第二个列表,因为两个列表都已排序。
当我构建最常见的SQL查询(涉及检查该键是> key_lower_bound和< key_upper_bound)时,它需要花费太长时间。
这种天真的查询会发生某种二次行为,因为我认为查询引擎正在对第二个表中的每一行进行比较,而实际上,如果第二个表是按key_lower_bounds排序的,那么这不应该是必要的。所以我得到一种O(|第一个表| x |第二个表|)行为而不是所需的O(|第一个表| + |第二个表|)行为。
是否可以获得线性SQL查询来执行此操作?
答案 0 :(得分: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.
答案 1 :(得分:1)
对于第一个表,我会在“key”上放置一个聚簇索引。对于第二个表,我会在“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”上添加第二个非聚集索引,以查看是否提高了性能。
答案 2 :(得分:0)
根据我的经验,没有简单而强大的解决方案。我已经成功地在许多类似的情况下使用了非规范化,将key_lower_bound和key_upper_bound复制到大表,并且有一个外键从大表引用到具有间隔的表。您还创建了一个检查约束以确保(key&gt; key_lower_bound和key&lt; key_upper_bound),但此检查仅涉及一个表中的列,因此它可以正常工作。这绝对是非规范化,但数据永远不会失去同步,因为FK约束确保大表中的(key_lower_bound,key_upper_bound)与父表中的间隔匹配。因为您不需要加入,所以您的选择执行速度非常快。
非规范化解决了类似的问题:
如果你需要完整的DDL,请告诉我,这很容易写出来。
答案 3 :(得分:0)
要执行您描述的线性算法,需要数据库没有的两件事:
我相信你最接近你所描述的行为的是merge join:
选择t1.key 从 largeTable t1 内部合并连接t1.key&gt; = t2.key_lower_bound和t1.key&lt;上的smallTable t2 t2.key_upper_bound
您应该了解一个表存储为B树或堆 - 因此它被优化以查找特定节点 - 而不是用于扫描。扫描意味着您必须跟上log_B(N)指针(例如在堆栈中)以记住您在树中的位置,而不必回溯。这甚至不是在讨论磁盘访问模式。
作为次要性能的想法,您应该尝试定义表示范围的单个值,并将其用作smallTable的主键,可以从largeTable作为外键引用。这比复合键更有效(这实际上是lower_bound和upper_bound列所代表的)。 也许是哈希值,例如PK = lower_bound&amp; upper_bound&lt;&lt;一定数量的位
只是another reference这应该说明为什么SQL很难将这个算法放在一起。如果你可以使用Matlab处理你的东西 - 这可能是一个更好的选择:)