按分组列值的变化顺序分组数据
Posted
技术标签:
【中文标题】按分组列值的变化顺序分组数据【英文标题】:Group data by the change of grouping column value in order 【发布时间】:2012-04-11 16:26:57 【问题描述】:有以下数据
create table #ph (product int, [date] date, price int)
insert into #ph select 1, '20120101', 1
insert into #ph select 1, '20120102', 1
insert into #ph select 1, '20120103', 1
insert into #ph select 1, '20120104', 1
insert into #ph select 1, '20120105', 2
insert into #ph select 1, '20120106', 2
insert into #ph select 1, '20120107', 2
insert into #ph select 1, '20120108', 2
insert into #ph select 1, '20120109', 1
insert into #ph select 1, '20120110', 1
insert into #ph select 1, '20120111', 1
insert into #ph select 1, '20120112', 1
我想产生以下输出:
product | date_from | date_to | price
1 | 20120101 | 20120105 | 1
1 | 20120105 | 20120109 | 2
1 | 20120109 | 20120112 | 1
如果我按价格分组并显示最大和最小日期,那么我将得到以下不是我想要的(请参阅日期重叠)。
product | date_from | date_to | price
1 | 20120101 | 20120112 | 1
1 | 20120105 | 20120108 | 2
所以基本上我要做的是根据组列产品和价格按步骤更改数据进行分组。
实现此目的最简洁的方法是什么?
【问题讨论】:
这是所谓的“差距和岛屿”问题的一个实例,仅供参考。 @AakashM 会看一下,我尝试搜索,但对问题没有如此明确的定义。谢谢 np。拥有一个“数字表”(在这种情况下是一个“日期表”)将大有帮助。 【参考方案1】:有一种(或多或少)已知的解决此类问题的技术,涉及两个ROW_NUMBER()
调用,如下所示:
WITH marked AS (
SELECT
*,
grp = ROW_NUMBER() OVER (PARTITION BY product ORDER BY date)
- ROW_NUMBER() OVER (PARTITION BY product, price ORDER BY date)
FROM #ph
)
SELECT
product,
date_from = MIN(date),
date_to = MAX(date),
price
FROM marked
GROUP BY
product,
price,
grp
ORDER BY
product,
MIN(date)
输出:
product date_from date_to price
------- ---------- ------------- -----
1 2012-01-01 2012-01-04 1
1 2012-01-05 2012-01-08 2
1 2012-01-09 2012-01-12 1
【讨论】:
谢谢,我刚刚查看了我最终实现的内容,它是相同的,但我在两个不同的 CTE 中做了,我没有想到在一个中使用减法。谢谢。 @andriy ,还有其他更优化的方法吗? @eshirvana:还有其他选择,是的,但很难知道它们是否更适合您。在我的脑海中,有一种方法涉及使用 CASE + LAG() 用 1 和 0 标记行,然后将 SUM() OVER 应用于该列。浏览gaps-and-islands
标签以获取现成的解决方案here on SO 或over at DBA.SE 可能会有所帮助。当然,您也可以尝试提交自己的问题。【参考方案2】:
我是这个论坛的新手,希望我的贡献能有所帮助。
如果您真的不想使用 CTE(尽管我认为这可能是最好的方法),您可以使用基于集合的代码获得解决方案。您需要测试这段代码的性能!
我已经添加了一个额外的临时表,以便我可以为每条记录使用一个唯一标识符,但我怀疑你的源表中已经有了这个列。这是临时表。
If Exists (SELECT Name FROM tempdb.sys.tables WHERE name LIKE '#phwithId%')
DROP TABLE #phwithId
CREATE TABLE #phwithId
(
SaleId INT
, ProductID INT
, Price Money
, SaleDate Date
)
INSERT INTO #phwithId SELECT row_number() over(partition by product order by [date] asc) as SalesId, Product, Price, Date FROM ph
现在是 Select 语句的主体
SELECT
productId
, date_from
, date_to
, Price
FROM
(
SELECT
dfr.ProductId
, ROW_NUMBER() OVER (PARTITION BY ProductId ORDER BY ChangeDate) AS rowno1
, ChangeDate AS date_from
, dfr.Price
FROM
(
SELECT
sl1.ProductId AS ProductId
, sl1.SaleDate AS ChangeDate
, sl1.price
FROM
#phwithId sl1
LEFT JOIN
#phwithId sl2
ON sl1.SaleId = sl2.SaleId + 1
WHERE
sl1.Price <> sl2.Price OR sl2.Price IS NULL
) dfr
) da1
LEFT JOIN
(
SELECT
ROW_NUMBER() OVER (PARTITION BY ProductId ORDER BY ChangeDate) AS rowno2
, ChangeDate AS date_to
FROM
(
SELECT
sl1.ProductId
, sl1.SaleDate AS ChangeDate
FROM
#phwithId sl1
LEFT JOIN
#phwithId sl3
ON sl1.SaleId = sl3.SaleId - 1
WHERE
sl1.Price <> sl3.Price OR sl3.Price IS NULL
) dto
) da2
ON da1.rowno1 = da2.rowno2
通过将数据源偏移量绑定 1 条记录(+或-),我们可以识别价格桶何时发生变化,然后只需将桶的开始日期和结束日期返回到单个记录中即可。
有点繁琐,我不确定它是否会提供更好的性能,但我很享受挑战。
【讨论】:
【参考方案3】:WITH marked AS (
SELECT
*,
case
when (lag(price,1,'') over (partition by product order by date_from)) = price
then 0 else 1
end is_price_change
FROM #ph
),
marked_as_group AS
( SELECT m.*,
SUM(is_price_change) over (PARTITION BY product order by date_from ROWS
BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS price_change_group
FROM marked m
),
SELECT
product,
date_from = MIN(date_from),
date_to = MAX(date_to),
price = MIN(price)
FROM marked_as_group
GROUP BY
product,
price_change_group
ORDER BY
product,
date_to
【讨论】:
我发布这个解决方案是因为我有类似的问题,但是在应用 Andriy M 的解决方案时我遇到了一些错误。【参考方案4】:我想出的一个相对“干净”的解决方案是:
;with cte_sort (product, [date], price, [row])
as
(select product, [date], price, row_number() over(partition by product order by [date] asc) as row
from #ph)
select a.product, a.[date] as date_from, c.[date] as date_to, a.price
from cte_sort a
left outer join cte_sort b on a.product = b.product and (a.row+1) = b.row and a.price = b.price
outer apply (select top 1 [date] from cte_sort z where z.product = a.product and z.row > a.row order by z.row) c
where b.row is null
order by a.[date]
我使用带有row_number
的CTE,因为如果您使用dateadd
之类的函数,您就不必担心是否缺少任何日期。如果你想拥有 date_to 列(我这样做),你显然只需要外部应用。
这个解决方案确实解决了我的问题,但是我有一个小问题让它在我的 500 万行表上以我想要的速度执行。
【讨论】:
【参考方案5】:Create function [dbo].[AF_TableColumns](@table_name nvarchar(55))
returns nvarchar(4000) as
begin
declare @str nvarchar(4000)
select @str = cast(rtrim(ltrim(column_name)) as nvarchar(500)) + coalesce(' ' + @str , ' ')
from information_schema.columns
where table_name = @table_name
group by table_name, column_name, ordinal_position
order by ordinal_position DESC
return @str
end
--select dbo.AF_TableColumns('YourTable') Select * from YourTable
【讨论】:
以上是关于按分组列值的变化顺序分组数据的主要内容,如果未能解决你的问题,请参考以下文章