Sql 查找与最小/最大值关联的行,不带内部循环
我有一个关于T-SQL和SQL Server的问题 假设我有一个包含两列的订单表:Sql 查找与最小/最大值关联的行,不带内部循环,sql,sql-server,tsql,Sql,Sql Server,Tsql,我有一个关于T-SQL和SQL Server的问题 假设我有一个包含两列的订单表: ProductId int 客户ID int 日期时间 我想要每个产品的第一个订单日期,因此我执行以下类型的查询: SELECT ProductId, MIN(Date) AS FirstOrder FROM Orders GROUP BY ProductId 我在ProductId上有一个索引,包括CustomerId和Date列,以加快查询速度(IX_Orders)。查询计划看起来像是对IX_Orde
- ProductId int
- 客户ID int
- 日期时间
SELECT ProductId, MIN(Date) AS FirstOrder
FROM Orders
GROUP BY ProductId
我在ProductId
上有一个索引,包括CustomerId
和Date
列,以加快查询速度(IX_Orders
)。查询计划看起来像是对IX_Orders
进行非聚集索引扫描,然后是流聚合(由于索引,因此没有排序)
现在我的问题是,我还想检索与每个产品的第一个订单相关联的CustomerId
(产品26是客户12于25日星期二首次订购的)。棘手的是,我不希望在执行计划中有任何内部循环,因为这意味着表中的每个ProductId
都需要额外的读取,这是非常低效的
这应该可以使用相同的非聚集索引扫描,然后是流聚合,但是我似乎找不到一个可以这样做的查询。有什么想法吗
谢谢
这是我能想到的最好的方法,不过老实说,我不知道这个查询的性能特征是什么。如果这样做不好,我可能会建议运行两个查询来获取所需的信息
declare @Orders table (
ProductId int,
CustomerId int,
Date datetime
)
insert into @Orders values (1,1,'20090701')
insert into @Orders values (2,1,'20090703')
insert into @Orders values (3,1,'20090702')
insert into @Orders values (1,2,'20090704')
insert into @Orders values (4,2,'20090701')
insert into @Orders values (1,3,'20090706')
insert into @Orders values (2,3,'20090704')
insert into @Orders values (4,3,'20090702')
insert into @Orders values (5,5,'20090703')
select O.* from @Orders O inner join
(
select ProductId,
MIN(Date) MinDate
from @Orders
group by ProductId
) FO
on FO.ProductId = O.ProductId and FO.MinDate = O.Date
这方面的估计查询计划是无用的,因为我正在用表变量模拟它,但是匿名内部联接应该在子选择上进行优化。是按ProductId、CutomerId、Date排序的IX_订单,还是按ProductId、Date、CustomerId排序的订单?如果是前者,则改为后者 换句话说,不要使用此选项:
create index IX_Orders on Orders (ProductId, CustomerId, Date)
改用这个:
create index IX_Orders on Orders (ProductId, Date, CustomerId)
如果你这样做了:
SELECT o1.*
FROM [Order] o1
JOIN
(
SELECT ProductID, Min(Date) as Date
FROM [Order]
GROUP BY ProductID
) o2
ON o1.ProductID = o2.ProductID AND o1.Date = o2.Date
ORDER BY ProductID
最终只需对IX_订单进行一次索引扫描,但如果两个客户可以同时订购同一产品,则每个产品可能会有多行。您可以通过使用以下查询克服此问题,但其效率低于第一个查询:
WITH cte AS
(
SELECT ProductID, CustomerID, Date,
ROW_NUMBER() OVER(PARTITION BY ProductID ORDER BY Date ASC) AS row
FROM [Order]
)
SELECT ProductID, CustomerId, Date
FROM cte
WHERE row = 1
ORDER BY ProductID
这将处理具有重复日期的产品:
DECLARE @Orders table (ProductId int
,CustomerId int
,Date datetime
)
INSERT INTO @Orders VALUES (1,1,'20090701')
INSERT INTO @Orders VALUES (2,1,'20090703')
INSERT INTO @Orders VALUES (3,1,'20090702')
INSERT INTO @Orders VALUES (1,2,'20090704')
INSERT INTO @Orders VALUES (4,2,'20090701')
INSERT INTO @Orders VALUES (1,3,'20090706')
INSERT INTO @Orders VALUES (2,3,'20090704')
INSERT INTO @Orders VALUES (4,3,'20090702')
INSERT INTO @Orders VALUES (5,5,'20090703') --duplicate dates for product #5
INSERT INTO @Orders VALUES (5,1,'20090703') --duplicate dates for product #5
INSERT INTO @Orders VALUES (5,5,'20090703') --duplicate dates for product #5
;WITH MinOrders AS
(SELECT
o.ProductId, o.CustomerId, o.Date
,row_number() over(partition by o.ProductId order by o.ProductId,o.CustomerId) AS RankValue
FROM @Orders o
INNER JOIN (SELECT
ProductId
,MIN(Date) MinDate
FROM @Orders
GROUP BY ProductId
) dt ON o.ProductId=dt.ProductId AND o.Date=dt.MinDate
)
SELECT
m.ProductId, m.CustomerId, m.Date
FROM MinOrders m
WHERE m.RankValue=1
ORDER BY m.ProductId, m.CustomerId
这将返回相同的结果,只需使用与上述代码相同的声明和插入:
;WITH MinOrders AS
(SELECT
o.ProductId, o.CustomerId, o.Date
,row_number() over(partition by o.ProductId order by o.ProductId,o.CustomerId) AS RankValue
FROM @Orders o
)
SELECT
m.ProductId, m.CustomerId, m.Date
FROM MinOrders m
WHERE m.RankValue=1
ORDER BY m.ProductId, m.CustomerId
您可以尝试每个版本,看看哪个运行得更快…我看不到不执行子查询或窗口功能(如行数、秩)就可以很好地执行此操作的方法,因为最大值只显示在一列中 然而,你不能做得很好
SELECT
productid,
min(date),
cast(
substring(
min(convert(varchar(23),date,21) + cast(customerid as varchar(20)))
, 24, 44)
as int) customerid
from
orders
group by
productid
这仅在您的客户id少于20位时有效
编辑:
在SQL Server 2005+中添加了group by子句:
SELECT oo.*
FROM (
SELECT DISTINCT ProductId
FROM Orders
) od
CROSS APPLY
(
SELECT TOP 1 ProductID, Date, CustomerID
FROM Orders oi
WHERE oi.ProductID = od.ProductID
ORDER BY
Date DESC
) oo
名义上,查询的计划包含嵌套循环
但是,外部循环将使用索引扫描
和流聚合
,内部循环将包含索引搜索
,用于产品ID
和顶部
事实上,第二个操作几乎是免费的,因为内部循环中使用的索引页很可能位于缓存中,因为它刚刚用于外部循环
以下是对1000000
行的测试结果(其中100
不同
):
,而这仅仅是一个SELECT DISTINCT
查询的结果:
SELECT od.*
FROM (
SELECT DISTINCT ProductId
FROM Orders
) od
统计数字如下:
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
(строк обработано: 100)
Table 'Orders'. Scan count 3, logical reads 5648, 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 = 250 ms, elapsed time = 125 ms.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1 ms.
(строк обработано: 100)
Table 'Orders'. Scan count 1, logical reads 5123, 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 = 406 ms, elapsed time = 415 ms.
正如我们所看到的,性能是相同的,交叉应用
只需要400
额外的逻辑读取
(这很可能永远不会是物理
)
不知道如何改进此查询了
这个查询的另一个好处是它可以很好地并行化。您可能会注意到,CPU
时间是运行时间的两倍:这是由于我的旧核心Duo的并行化
一个4核CPU
将在一半的时间内完成此查询
使用窗口函数的解决方案不会并行化:
SELECT od.*
FROM (
SELECT ProductId, Date, CustomerID, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY Date DESC) AS rn
FROM Orders
) od
WHERE rn = 1
,以下是统计数字:
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
(строк обработано: 100)
Table 'Orders'. Scan count 3, logical reads 5648, 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 = 250 ms, elapsed time = 125 ms.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1 ms.
(строк обработано: 100)
Table 'Orders'. Scan count 1, logical reads 5123, 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 = 406 ms, elapsed time = 415 ms.
+1:我想你需要在匿名加入中为min(date)定义一个别名;否则,这正是我得到的。最好知道是否有更好的方法。此查询得到错误的答案,因为它在o1和FirstOrder之间的联接中不包含日期您不需要在联接中包含日期。您只需要子查询的产品id,因为它具有与之关联的最小日期。返回实际结果的select返回日期。您的select需要包含FO.MinDate。我从来没有听说过“匿名”联接,我一直使用术语“派生表”。如果同一产品有多行具有相同的最小日期,这将不起作用。试一试,将此代码添加到示例中:插入@Orders值(5,1,'20090703');在@Orders value(5,5,'20090703')中插入,您将在结果集中多次获得产品5。但这不是有效的吗?您需要任何产品的第一个订单的客户ID,但给定数据,如果多个客户在同一天订购同一产品,我认为您应该将它们全部取回。如果客户订购同一产品两次,那么可能不会发生这种情况-在这种情况下,您可以使O.*不同
。OP表示“我希望每个产品的第一次订购日期”,对我来说,这意味着只列出一次产品。OP还需要订购它的客户。如果OP使用带有实际时间的DATETIME列(而不仅仅是测试数据中的天数),您可能会得到第一个,因为同时两个订单的概率很低,但仍然可能发生。我认为,如果在同一时间订购相同的产品,OP仍然只需要结果集中的一行,但这是我的看法。Msg 8120,级别16,状态1,第51行列'@orders.ProductId'在选择列表中无效,因为它不包含在聚合函数或组中