差距和孤岛:基于外部的分裂孤岛 Table

Gaps And Islands: Splitting Islands Based On External Table

我的场景一开始类似于孤岛和缺口问题,我需要找到连续几天的工作。我当前 SQL 查询答案 "ProductA was produced at LocationA from DateA through DateB, totaling X quantity".

但是,当我需要将价格纳入其中时,这还不够。价格在单独的 table 中,并在事后用 C# 处理。价格变化本质上是一个记录列表,上面写着 "ProductA from LocationA is now Y value per unit effective DateC".

最终结果是,只要该岛不与价格变化日期重叠,它就可以工作,但如果确实重叠,我会得到 "close" 答案,但并不准确。

C# 代码可以有效地处理价格应用,但我需要做的是根据价格变化拆分岛屿。我的目标是让 SQL 的分区考虑到另一个 table 的天数排名,但我无法应用我想做的事情。


当前生成我的岛SQL如下

SELECT MIN(ScheduledDate) as StartDate, MAX(ScheduledDate) as 
EndDate, ProductId, DestinationId, SUM(Quantity) as TotalQuantity
FROM (
    SELECT ScheduledDate, DestinationId, ProductId, PartitionGroup = DATEADD(DAY ,-1 * DENSE_RANK() OVER (ORDER BY ScheduledDate), ScheduledDate), Quantity
    FROM History
) tmp
GROUP BY PartitionGroup, DestinationId, ProductId;

当前SQL取自PriceChange table并对日期进行排名如下

DECLARE @PriceChangeDates TABLE(Rank int, SplitDate Date);
INSERT INTO @PriceChangeDates
SELECT DENSE_RANK() over (ORDER BY EffectiveDate) as Rank, EffectiveDate as SplitDate
FROM ProductPriceChange
GROUP BY EffectiveDate;

我的想法是以某种方式更新第一个查询内部 SELECT 语句,以某种方式利用第二个查询创建的 @PriceChangeDates table。我认为我们可以将 DATEADD 的增量参数乘以声明的 table 中的等级,但我正在努力编写它。

如果我要以某种方式使用循环来执行此操作,我的思考过程将是确定 ScheduledDate 来自 @PriceChangeDates table 的排名,其中它的排名是最接近的日期的排名比它自己能找到的还要小。然后采用给出的任何等级,我认为,将它乘以传入的增量参数(或一些数学运算,例如对现有参数执行 *@PriceChangeDates.Count() 然后添加新等级以避免碰撞)。但是,那是 "loop" 逻辑而不是 "set" 逻辑,在 SQL 中我需要考虑集合。


非常感谢所有help/advice。谢谢:)


更新:

SQLFiddle 上的示例数据和示例:http://www.sqlfiddle.com/#!18/af568/1

数据所在位置:

CREATE TABLE History
(
ProductId int,
DestinationId int,
ScheduledDate date,
Quantity float
);

INSERT INTO History (ProductId, DestinationId, ScheduledDate, Quantity)
VALUES
  (0, 1000, '20180401', 5),
  (0, 1000, '20180402', 10),
  (0, 1000, '20180403', 7),
  (3, 5000, '20180507', 15),
  (3, 5000, '20180508', 23),
  (3, 5000, '20180509', 52),
  (3, 5000, '20180510', 12),
  (3, 5000, '20180511', 14);

CREATE TABLE PriceChange
(
  ProductId int,
  DestinationId int,
  EffectiveDate date,
  Price float
);

INSERT INTO PriceChange (ProductId, DestinationId, EffectiveDate, Price)
VALUES
  (0, 1000, '20180201', 1),
  (0, 1000, '20180402', 2),
  (3, 5000, '20180101', 5),
  (3, 5000, '20180510', 20);

期望的结果是有一个生成结果的 SQL 语句:

StartDate   EndDate     ProductId   DestinationId   TotalQuantity
2018-04-01  2018-04-01  0           1000            5
2018-04-02  2018-04-03  0           1000            17
2018-05-07  2018-05-09  3           5000            90
2018-05-10  2018-05-11  3           5000            26

澄清一下,最终结果确实需要每个拆分数量的总数量,因此操纵结果和应用定价的程序代码知道价格变化的每一侧每个产品有多少,以准确确定值。

不确定我是否理解正确,但这只是我的想法:

Select concat_ws(',',view2.StartDate,  string_agg(view1.splitDate, ','), 
 view2.EndDate), view2.productId, view2.DestinationId from (
 SELECT DENSE_RANK() OVER (ORDER BY EffectiveDate) as Rank, EffectiveDate as 
  SplitDate FROM PriceChange GROUP BY EffectiveDate) view1 join 
 (
     SELECT MIN(ScheduledDate) as StartDate, MAX(ScheduledDate) as 
       EndDate,ProductId, DestinationId, SUM(Quantity) as TotalQuantity
     FROM (
      SELECT ScheduledDate, DestinationId, ProductId, PartitionGroup = 
      DATEADD(DAY ,-1 * DENSE_RANK() OVER (ORDER BY ScheduledDate), 
       ScheduledDate), Quantity
       FROM History
   ) tmp
      GROUP BY PartitionGroup, DestinationId, ProductId
    ) view2 on view1.SplitDate >= view2.StartDate 
      and view1.SplitDate <=view2.EndDate 
      group by view2.startDate, view2.endDate, view2.productId, 
      view2.DestinationId

此查询的结果将是:

| ranges                                      | productId | DestinationId |
|---------------------------------------------|-----------|---------------|
| 2018-04-01,2018-04-02,2018-04-03            | 0         | 1000          |
| 2018-05-07,2018-05-10,2018-05-11            | 3         | 5000          |

然后,对于任何过程语言,对于每一行,您都可以拆分字符串(对每个边界使用适当的包含或排除规则)以找出条件列表(:from, :to, :productId, : destinationId).

最后,您可以遍历条件列表并使用 Union all 子句来构建一个查询(它是所有查询的并集,它说明了一个条件)以找出最终结果。例如,

Select * from History where ScheduledDate >= '2018-04-01' and ScheduledDate <'2018-04-02' and productId = 0 and destinationId = 1000 
union all
Select * from History where ScheduledDate >= '2018-04-02' and ScheduledDate <'2018-04-03' and productId = 0 and destinationId = 1000

----更新--------

基于以上想法,我做了一些快速更改以提供您的结果集。也许你可以稍后优化它

 with view3 as 
(Select concat_ws(',',view2.StartDate,  string_agg(view1.splitDate, ','), 
 dateadd(day, 1, view2.EndDate)) dateRange, view2.productId, view2.DestinationId from (
 SELECT DENSE_RANK() OVER (ORDER BY EffectiveDate) as Rank, EffectiveDate as 
  SplitDate FROM PriceChange GROUP BY EffectiveDate) view1 join 
 (
     SELECT MIN(ScheduledDate) as StartDate, MAX(ScheduledDate) as 
       EndDate,ProductId, DestinationId, SUM(Quantity) as TotalQuantity
     FROM (
      SELECT ScheduledDate, DestinationId, ProductId, PartitionGroup = 
      DATEADD(DAY ,-1 * DENSE_RANK() OVER (ORDER BY ScheduledDate), 
       ScheduledDate), Quantity
       FROM History
   ) tmp
      GROUP BY PartitionGroup, DestinationId, ProductId
    ) view2 on view1.SplitDate >= view2.StartDate 
      and view1.SplitDate <=view2.EndDate 
      group by view2.startDate, view2.endDate, view2.productId, 
      view2.DestinationId
),
 view4 as
(
select productId, destinationId, value from view3 cross apply string_split(dateRange, ',')
 ),
 view5 as(
   select *, row_number() over(partition by productId, destinationId order by value) rn from view4
 ),
 view6 as (
   select v52.value fr, v51.value t, v51.productid, v51. destinationid from view5 v51 join view5 v52
 on v51.productid = v52.productid
 and v51.destinationid = v52.destinationid
 and v51.rn = v52.rn+1
 )
 select min(h.ScheduledDate) StartDate, max(h.ScheduledDate) EndDate, v6.productId, v6.destinationId, sum(h.quantity) TotalQuantity from view6 v6 join History h 
 on v6.destinationId = h.destinationId
 and v6.productId = h.productId
 and h.ScheduledDate >= v6.fr
 and h.ScheduledDate <v6.t
 group by v6.fr, v6.t, v6.productId, v6.destinationId

结果和你给的一模一样

| StartDate  | EndDate    | productId | destinationId | TotalQuantity |
|------------|------------|-----------|---------------|---------------|
| 2018-04-01 | 2018-04-01 | 0         | 1000          | 5             |
| 2018-04-02 | 2018-04-03 | 0         | 1000          | 17            |
| 2018-05-07 | 2018-05-09 | 3         | 5000          | 90            |
| 2018-05-10 | 2018-05-11 | 3         | 5000          | 26            |

straight-forward方法是对History的每一行取有效价格,然后在考虑价格的情况下生成缺口和孤岛。

从问题中不清楚DestinationID的作用是什么。样本数据在这里没有帮助。 我假设我们需要在 ProductIDDestinationID 上加入和分区。

以下查询 returns 对 History 中的每一行有效 Price。 您需要将索引添加到 PriceChange table

CREATE NONCLUSTERED INDEX [IX] ON [dbo].[PriceChange]
(
    [ProductId] ASC,
    [DestinationId] ASC,
    [EffectiveDate] DESC
)
INCLUDE ([Price])

为了使此查询有效地工作。

查询价格

SELECT
    History.ProductId
    ,History.DestinationId
    ,History.ScheduledDate
    ,History.Quantity
    ,A.Price
FROM
    History
    OUTER APPLY
    (
        SELECT TOP(1)
            PriceChange.Price
        FROM
            PriceChange
        WHERE
            PriceChange.ProductID = History.ProductID
            AND PriceChange.DestinationId = History.DestinationId
            AND PriceChange.EffectiveDate <= History.ScheduledDate
        ORDER BY
            PriceChange.EffectiveDate DESC
    ) AS A
ORDER BY ProductID, ScheduledDate;

对于 History 中的每一行,将在此索引中进行一次搜索以选择正确的价格。

这个查询returns:

价格

+-----------+---------------+---------------+----------+-------+
| ProductId | DestinationId | ScheduledDate | Quantity | Price |
+-----------+---------------+---------------+----------+-------+
|         0 |          1000 | 2018-04-01    |        5 |     1 |
|         0 |          1000 | 2018-04-02    |       10 |     2 |
|         0 |          1000 | 2018-04-03    |        7 |     2 |
|         3 |          5000 | 2018-05-07    |       15 |     5 |
|         3 |          5000 | 2018-05-08    |       23 |     5 |
|         3 |          5000 | 2018-05-09    |       52 |     5 |
|         3 |          5000 | 2018-05-10    |       12 |    20 |
|         3 |          5000 | 2018-05-11    |       14 |    20 |
+-----------+---------------+---------------+----------+-------+

现在是一个标准的 gaps-and-island 步骤,可以将相同价格的连续几天合并在一起。我在这里使用了两个行号序列的差异。

我在您的示例数据中添加了更多行以查看相同 ProductId.

中的差距
INSERT INTO History (ProductId, DestinationId, ScheduledDate, Quantity)
VALUES
  (0, 1000, '20180601', 5),
  (0, 1000, '20180602', 10),
  (0, 1000, '20180603', 7),
  (3, 5000, '20180607', 15),
  (3, 5000, '20180608', 23),
  (3, 5000, '20180609', 52),
  (3, 5000, '20180610', 12),
  (3, 5000, '20180611', 14);

如果你运行这个中间查询你会看到它是如何工作的:

WITH
CTE_Prices
AS
(
    SELECT
        History.ProductId
        ,History.DestinationId
        ,History.ScheduledDate
        ,History.Quantity
        ,A.Price
    FROM
        History
        OUTER APPLY
        (
            SELECT TOP(1)
                PriceChange.Price
            FROM
                PriceChange
            WHERE
                PriceChange.ProductID = History.ProductID
                AND PriceChange.DestinationId = History.DestinationId
                AND PriceChange.EffectiveDate <= History.ScheduledDate
            ORDER BY
                PriceChange.EffectiveDate DESC
        ) AS A
)
,CTE_rn
AS
(
    SELECT
        ProductId
        ,DestinationId
        ,ScheduledDate
        ,Quantity
        ,Price
        ,ROW_NUMBER() OVER (PARTITION BY ProductId, DestinationId, Price ORDER BY ScheduledDate) AS rn1
        ,DATEDIFF(day, '20000101', ScheduledDate) AS rn2
    FROM
        CTE_Prices
)
SELECT *
    ,rn2-rn1 AS Diff
FROM CTE_rn

中间结果

+-----------+---------------+---------------+----------+-------+-----+------+------+
| ProductId | DestinationId | ScheduledDate | Quantity | Price | rn1 | rn2  | Diff |
+-----------+---------------+---------------+----------+-------+-----+------+------+
|         0 |          1000 | 2018-04-01    |        5 |     1 |   1 | 6665 | 6664 |
|         0 |          1000 | 2018-04-02    |       10 |     2 |   1 | 6666 | 6665 |
|         0 |          1000 | 2018-04-03    |        7 |     2 |   2 | 6667 | 6665 |
|         0 |          1000 | 2018-06-01    |        5 |     2 |   3 | 6726 | 6723 |
|         0 |          1000 | 2018-06-02    |       10 |     2 |   4 | 6727 | 6723 |
|         0 |          1000 | 2018-06-03    |        7 |     2 |   5 | 6728 | 6723 |
|         3 |          5000 | 2018-05-07    |       15 |     5 |   1 | 6701 | 6700 |
|         3 |          5000 | 2018-05-08    |       23 |     5 |   2 | 6702 | 6700 |
|         3 |          5000 | 2018-05-09    |       52 |     5 |   3 | 6703 | 6700 |
|         3 |          5000 | 2018-05-10    |       12 |    20 |   1 | 6704 | 6703 |
|         3 |          5000 | 2018-05-11    |       14 |    20 |   2 | 6705 | 6703 |
|         3 |          5000 | 2018-06-07    |       15 |    20 |   3 | 6732 | 6729 |
|         3 |          5000 | 2018-06-08    |       23 |    20 |   4 | 6733 | 6729 |
|         3 |          5000 | 2018-06-09    |       52 |    20 |   5 | 6734 | 6729 |
|         3 |          5000 | 2018-06-10    |       12 |    20 |   6 | 6735 | 6729 |
|         3 |          5000 | 2018-06-11    |       14 |    20 |   7 | 6736 | 6729 |
+-----------+---------------+---------------+----------+-------+-----+------+------+

现在只需按 Diff 分组,每个间隔得到一行。

最终查询

WITH
CTE_Prices
AS
(
    SELECT
        History.ProductId
        ,History.DestinationId
        ,History.ScheduledDate
        ,History.Quantity
        ,A.Price
    FROM
        History
        OUTER APPLY
        (
            SELECT TOP(1)
                PriceChange.Price
            FROM
                PriceChange
            WHERE
                PriceChange.ProductID = History.ProductID
                AND PriceChange.DestinationId = History.DestinationId
                AND PriceChange.EffectiveDate <= History.ScheduledDate
            ORDER BY
                PriceChange.EffectiveDate DESC
        ) AS A
)
,CTE_rn
AS
(
    SELECT
        ProductId
        ,DestinationId
        ,ScheduledDate
        ,Quantity
        ,Price
        ,ROW_NUMBER() OVER (PARTITION BY ProductId, DestinationId, Price ORDER BY ScheduledDate) AS rn1
        ,DATEDIFF(day, '20000101', ScheduledDate) AS rn2
    FROM
        CTE_Prices
)
SELECT
    ProductId
    ,DestinationId
    ,MIN(ScheduledDate) AS StartDate
    ,MAX(ScheduledDate) AS EndDate
    ,SUM(Quantity) AS TotalQuantity
    ,Price
FROM
    CTE_rn
GROUP BY
    ProductId
    ,DestinationId
    ,Price
    ,rn2-rn1
ORDER BY
    ProductID
    ,DestinationId
    ,StartDate
;

最终结果

+-----------+---------------+------------+------------+---------------+-------+
| ProductId | DestinationId | StartDate  |  EndDate   | TotalQuantity | Price |
+-----------+---------------+------------+------------+---------------+-------+
|         0 |          1000 | 2018-04-01 | 2018-04-01 |             5 |     1 |
|         0 |          1000 | 2018-04-02 | 2018-04-03 |            17 |     2 |
|         0 |          1000 | 2018-06-01 | 2018-06-03 |            22 |     2 |
|         3 |          5000 | 2018-05-07 | 2018-05-09 |            90 |     5 |
|         3 |          5000 | 2018-05-10 | 2018-05-11 |            26 |    20 |
|         3 |          5000 | 2018-06-07 | 2018-06-11 |           116 |    20 |
+-----------+---------------+------------+------------+---------------+-------+

使用outer apply选择最接近的价格,然后group by:

现场测试:http://www.sqlfiddle.com/#!18/af568/65

select 
    StartDate = min(h.ScheduledDate),
    EndDate = max(h.ScheduledDate),
    h.ProductId,
    h.DestinationId,
    TotalQuantity = sum(h.Quantity)
from History h
outer apply
(
    select top 1 pc.*
    from PriceChange pc
    where 
        pc.ProductId = h.ProductId
        and pc.Effectivedate <= h.ScheduledDate
    order by pc.EffectiveDate desc
) UpToDate
group by UpToDate.EffectiveDate,
    h.ProductId,
    h.DestinationId
order by StartDate, EndDate, ProductId    

输出:

|  StartDate |    EndDate | ProductId | DestinationId | TotalQuantity |
|------------|------------|-----------|---------------|---------------|
| 2018-04-01 | 2018-04-01 |         0 |          1000 |             5 |
| 2018-04-02 | 2018-04-03 |         0 |          1000 |            17 |
| 2018-05-07 | 2018-05-09 |         3 |          5000 |            90 |
| 2018-05-10 | 2018-05-11 |         3 |          5000 |            26 |

这是一个可能比我的第一个答案表现更好的变体。我决定把它作为第二个答案,因为方法很不一样,而且答案会太长。您应该将所有变体的性能与硬件上的真实数据进行比较,并且不要忘记索引。

在第一个变体中,我使用 APPLYHistory table 中的每一行选择相关价格。对于 History table 中的每一行,引擎正在搜索 PriceChange table 中的相关行。即使在 PriceChange table 上有适当的索引,当这是通过单个搜索完成时,它仍然意味着在循环连接中有 370 万次搜索。

我们可以简单地将 HistoryPriceChange table 连接在一起,并在两个 table 上使用适当的索引,这将是一个有效的合并连接。

在这里,我还使用扩展样本数据集来说明差距。我将这些行添加到问题的示例数据中。

INSERT INTO History (ProductId, DestinationId, ScheduledDate, Quantity)
VALUES
  (0, 1000, '20180601', 5),
  (0, 1000, '20180602', 10),
  (0, 1000, '20180603', 7),
  (3, 5000, '20180607', 15),
  (3, 5000, '20180608', 23),
  (3, 5000, '20180609', 52),
  (3, 5000, '20180610', 12),
  (3, 5000, '20180611', 14);

中间查询

我们在这里做 FULL JOIN,而不是 LEFT JOIN,因为价格更改的日期可能不会出现在 History table 中完全没有。

WITH
CTE_Join
AS
(
    SELECT
        ISNULL(History.ProductId, PriceChange.ProductID) AS ProductID
        ,ISNULL(History.DestinationId, PriceChange.DestinationId) AS DestinationId
        ,ISNULL(History.ScheduledDate, PriceChange.EffectiveDate) AS ScheduledDate
        ,History.Quantity
        ,PriceChange.Price
    FROM
        History
        FULL JOIN PriceChange
            ON  PriceChange.ProductID = History.ProductID
            AND PriceChange.DestinationId = History.DestinationId
            AND PriceChange.EffectiveDate = History.ScheduledDate
)
,CTE2
AS
(
    SELECT
        ProductID
        ,DestinationId
        ,ScheduledDate
        ,Quantity
        ,Price
        ,MAX(CASE WHEN Price IS NOT NULL THEN ScheduledDate END)
            OVER (PARTITION BY ProductID, DestinationId ORDER BY ScheduledDate 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS grp
    FROM CTE_Join
)
SELECT *
FROM CTE2
ORDER BY
    ProductID
    ,DestinationId
    ,ScheduledDate

创建以下索引

CREATE UNIQUE NONCLUSTERED INDEX [IX_History] ON [dbo].[History]
(
    [ProductId] ASC,
    [DestinationId] ASC,
    [ScheduledDate] ASC
)
INCLUDE ([Quantity])

CREATE UNIQUE NONCLUSTERED INDEX [IX_Price] ON [dbo].[PriceChange]
(
    [ProductId] ASC,
    [DestinationId] ASC,
    [EffectiveDate] ASC
)
INCLUDE ([Price])

并且联接将是执行计划中的高效 MERGE 联接(不是 LOOP 联接)

中间结果

+-----------+---------------+---------------+----------+-------+------------+
| ProductID | DestinationId | ScheduledDate | Quantity | Price |    grp     |
+-----------+---------------+---------------+----------+-------+------------+
|         0 |          1000 | 2018-02-01    | NULL     | 1     | 2018-02-01 |
|         0 |          1000 | 2018-04-01    | 5        | NULL  | 2018-02-01 |
|         0 |          1000 | 2018-04-02    | 10       | 2     | 2018-04-02 |
|         0 |          1000 | 2018-04-03    | 7        | NULL  | 2018-04-02 |
|         0 |          1000 | 2018-06-01    | 5        | NULL  | 2018-04-02 |
|         0 |          1000 | 2018-06-02    | 10       | NULL  | 2018-04-02 |
|         0 |          1000 | 2018-06-03    | 7        | NULL  | 2018-04-02 |
|         3 |          5000 | 2018-01-01    | NULL     | 5     | 2018-01-01 |
|         3 |          5000 | 2018-05-07    | 15       | NULL  | 2018-01-01 |
|         3 |          5000 | 2018-05-08    | 23       | NULL  | 2018-01-01 |
|         3 |          5000 | 2018-05-09    | 52       | NULL  | 2018-01-01 |
|         3 |          5000 | 2018-05-10    | 12       | 20    | 2018-05-10 |
|         3 |          5000 | 2018-05-11    | 14       | NULL  | 2018-05-10 |
|         3 |          5000 | 2018-06-07    | 15       | NULL  | 2018-05-10 |
|         3 |          5000 | 2018-06-08    | 23       | NULL  | 2018-05-10 |
|         3 |          5000 | 2018-06-09    | 52       | NULL  | 2018-05-10 |
|         3 |          5000 | 2018-06-10    | 12       | NULL  | 2018-05-10 |
|         3 |          5000 | 2018-06-11    | 14       | NULL  | 2018-05-10 |
+-----------+---------------+---------------+----------+-------+------------+

可以看到Price列有很多NULL个值。我们需要 "fill" 这些 NULL 值与前面的 non-NULL 值。

Itzik Ben-Gan 写了一篇很好的文章,展示了如何有效地解决这个问题 The Last non NULL Puzzle. Also see Best way to replace NULL with most recent non-null value

这是在 CTE2 中使用 MAX window 函数完成的,您可以看到它如何填充 grp 列。这需要 SQL Server 2012+。确定组后,我们应该删除 Quantity 为 NULL 的行,因为这些行不是来自 History table.

现在我们可以使用 grp 列作为附加分区来执行相同的 gaps-and-islands 步骤。

查询的其余部分与第一个变体几乎相同。

最终查询

WITH
CTE_Join
AS
(
    SELECT
        ISNULL(History.ProductId, PriceChange.ProductID) AS ProductID
        ,ISNULL(History.DestinationId, PriceChange.DestinationId) AS DestinationId
        ,ISNULL(History.ScheduledDate, PriceChange.EffectiveDate) AS ScheduledDate
        ,History.Quantity
        ,PriceChange.Price
    FROM
        History
        FULL JOIN PriceChange
            ON  PriceChange.ProductID = History.ProductID
            AND PriceChange.DestinationId = History.DestinationId
            AND PriceChange.EffectiveDate = History.ScheduledDate
)
,CTE2
AS
(
    SELECT
        ProductID
        ,DestinationId
        ,ScheduledDate
        ,Quantity
        ,Price
        ,MAX(CASE WHEN Price IS NOT NULL THEN ScheduledDate END)
            OVER (PARTITION BY ProductID, DestinationId ORDER BY ScheduledDate 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS grp
    FROM CTE_Join
)
,CTE_RN
AS
(
    SELECT
        ProductID
        ,DestinationId
        ,ScheduledDate
        ,grp
        ,Quantity
        ,ROW_NUMBER() OVER (PARTITION BY ProductId, DestinationId, grp ORDER BY ScheduledDate) AS rn1
        ,DATEDIFF(day, '20000101', ScheduledDate) AS rn2
    FROM CTE2
    WHERE Quantity IS NOT NULL
)
SELECT
    ProductId
    ,DestinationId
    ,MIN(ScheduledDate) AS StartDate
    ,MAX(ScheduledDate) AS EndDate
    ,SUM(Quantity) AS TotalQuantity
FROM
    CTE_RN
GROUP BY
    ProductId
    ,DestinationId
    ,grp
    ,rn2-rn1
ORDER BY
    ProductID
    ,DestinationId
    ,StartDate
;

最终结果

+-----------+---------------+------------+------------+---------------+
| ProductId | DestinationId | StartDate  |  EndDate   | TotalQuantity |
+-----------+---------------+------------+------------+---------------+
|         0 |          1000 | 2018-04-01 | 2018-04-01 |             5 |
|         0 |          1000 | 2018-04-02 | 2018-04-03 |            17 |
|         0 |          1000 | 2018-06-01 | 2018-06-03 |            22 |
|         3 |          5000 | 2018-05-07 | 2018-05-09 |            90 |
|         3 |          5000 | 2018-05-10 | 2018-05-11 |            26 |
|         3 |          5000 | 2018-06-07 | 2018-06-11 |           116 |
+-----------+---------------+------------+------------+---------------+

此变体不输出相关价格(作为第一个变体),因为我简化了 "last non-null" 查询。问题中不需要它。无论如何,如果需要,添加价格是很容易的。