使用(递归?)CTE + Window 函数将销售订单归零?

Using a (Recursive?) CTE + Window Functions to zero out sales orders?

我正在尝试使用递归 CTE + window 函数来查找一系列 buy/sell 订单的最后结果。

首先,这里有一些术语:

不幸的是,由于系统的工作方式,退回商品时我无法获得成本,因此弄清楚订单的最后结果很复杂(我们最终是否卖出了任何商品)。我需要将购买与销售相匹配,这通常效果很好。但是,在某些情况下失败时会出现以下情况,我正试图找到一种一次性完成此操作的方法,可能使用递归 CTE。

这是一些代码。

DECLARE @tablea TABLE (field_id int, field_number CHAR(3), field_date datetime, field_inserted DATETIME, field_sale varchar(4))
INSERT INTO @tablea
VALUES 
(1, 100, '20170311','20170311 01:00:00', 'Buy'), 
(1, 100, '20170311','20170311 01:01:00', 'Retu'),
(1, 100, '20170311','20170311 01:02:00', 'Buy'),
(1, 100, '20170311','20170311 01:03:00', 'Retu'),
(1, 100, '20170311','20170311 01:02:01', 'buy'),
(2, 100, '20170311','20170311 01:03:00', 'REtu'),
(1, 110, '20170311','20170311 01:03:00', 'Buy');

现在删除随后退回的购买。 ISNULL 是因为我是 NOT IN 将忽略 _lead/_lag 值具有 NULL 的所有行。

WITH cte AS 
(SELECT 
        ROW_NUMBER() OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS row_num,
        field_id,
        field_number, 
        field_date,
        field_sale,
       lead(field_sale) OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lead,      
       lag(field_sale)  OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lag    
FROM   @tablea
)
SELECT * FROM cte
WHERE NOT (cte.field_sale = 'Buy'  AND ISNULL(field_sale_lead,'') = 'Retu')--AND field_sale_lead IS NOT null)
  AND NOT (cte.field_sale = 'Retu' AND ISNULL(field_sale_lag,'') =  'buy' )--AND field_sale_lag  IS NOT NULL)

我觉得很自鸣得意,以为我做到了。然而,这是简单的情况。买入,Return,买入,Return。让我们尝试另一种情况,Buy Buy Return Return,它仍然有效,但显然会导致净值为 0..

DECLARE @tablea TABLE (field_id int, field_number CHAR(3), field_date datetime, field_inserted DATETIME, field_sale varchar(4))
INSERT INTO @tablea
VALUES 
(1, 100, '20170311','20170311 01:00:00', 'Buy'), 
(1, 100, '20170311','20170311 01:01:00', 'Buy'),
(1, 100, '20170311','20170311 01:02:00', 'Retu'),
(1, 100, '20170311','20170311 01:03:00', 'Retu'),
(2, 100, '20170311','20170311 01:03:00', 'Buy'),
(1, 110, '20170311','20170311 01:03:00', 'Buy');


WITH cte AS 
(SELECT 
        ROW_NUMBER() OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS row_num,
        field_id,
        field_number, 
        field_date,
        field_sale,
       lead(field_sale) OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lead,      
       lag(field_sale)  OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lag    
FROM   @tablea
)
SELECT * FROM cte
WHERE NOT (cte.field_sale = 'Buy'  AND ISNULL(field_sale_lead,'') = 'sell')--AND field_sale_lead IS NOT null)
  AND NOT (cte.field_sale = 'sell' AND ISNULL(field_sale_lag,'') =  'buy' )--AND field_sale_lag  IS NOT NULL)

不过,当您执行此操作时,您会发现它找到了直接匹配项,但现在仍然存在 Buy/Return 对,我想将其取消。

此时我被卡住了。我以前做过递归 CTE,但出于某种原因,我无法弄清楚如何递归并使其抵消 1/1/100 和 4/1/100。我所做的就是让它在递归时窒息。

DECLARE @tablea TABLE (field_id int, field_number CHAR(3), field_date datetime, field_inserted DATETIME, field_sale varchar(4))
INSERT INTO @tablea
VALUES 
(1, 100, '20170311','20170311 01:00:00', 'Buy'), 
(1, 100, '20170311','20170311 01:01:00', 'Buy'),
(1, 100, '20170311','20170311 01:02:00', 'Retu'),
(1, 100, '20170311','20170311 01:03:00', 'Retu'),
(2, 100, '20170311','20170311 01:03:00', 'Buy'),
(1, 110, '20170311','20170311 01:03:00', 'Buy');

WITH cte AS 
(SELECT 
        ROW_NUMBER() OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS row_num,
        field_id,
        field_number, 
        field_date,
        field_sale,
        field_inserted,
       lead(field_sale) OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lead,      
       lag(field_sale)  OVER (PARTITION BY field_id, field_number, field_date ORDER BY field_inserted) AS field_sale_lag    
FROM   @tablea
--) 
--SELECT * FROM cte
--WHERE NOT (cte.field_sale = 'Buy'  AND ISNULL(field_sale_lead,'') = 'Retu')--AND field_sale_lead IS NOT null)
--AND NOT (cte.field_sale = 'Retu' AND ISNULL(field_sale_lag,'') =  'buy' )--AND field_sale_lag  IS NOT NULL)

UNION ALL
SELECT 
        ROW_NUMBER() OVER (PARTITION BY  cte.field_id, cte.field_number, cte.field_date ORDER BY cte.field_inserted) AS row_num,
        cte.field_id,
        cte.field_number, 
        cte.field_date,
        cte.field_sale,
        cte.field_inserted,
       lead(cte.field_sale) OVER (PARTITION BY cte.field_id, cte.field_number, cte.field_date ORDER BY cte.field_inserted) AS field_sale_lead,      
       lag(cte.field_sale)  OVER (PARTITION BY cte.field_id, cte.field_number, cte.field_date ORDER BY cte.field_inserted) AS field_sale_lag    
FROM   @tablea INNER JOIN cte ON cte.field_date = [@tablea].field_date AND cte.field_id = [@tablea].field_id AND cte.field_number = [@tablea].field_number
)
SELECT * FROM cte
WHERE NOT (cte.field_sale = 'Buy'  AND ISNULL(field_sale_lead,'') = 'Retu')--AND field_sale_lead IS NOT null)
  AND NOT (cte.field_sale = 'Retu' AND ISNULL(field_sale_lag,'') =  'buy' )--AND field_sale_lag  IS NOT NULL)

有一件事我可以建议在可能的情况下删除成对的顺序 buy/return。尝试

DECLARE @tablea TABLE (field_id int, field_number CHAR(3), field_date datetime, field_inserted DATETIME, field_sale varchar(4))
INSERT INTO @tablea
VALUES 
(1, 100, '20170311','20170311 01:01:00', 'Buy'),
(1, 100, '20170311','20170311 01:02:00', 'Buy'), 
(1, 100, '20170311','20170311 01:03:00', 'Buy'), 
(1, 100, '20170311','20170311 01:04:00', 'Retu'),
(1, 100, '20170311','20170311 01:05:00', 'Buy'), 
(1, 100, '20170311','20170311 01:06:00', 'Retu'),
(1, 100, '20170311','20170311 01:07:00', 'Retu'),
(2, 100, '20170311','20170311 01:03:00', 'Buy'),
(1, 110, '20170311','20170311 01:03:00', 'Buy');

select * from @tablea
order by field_id,
        field_number, 
        field_inserted 

declare @eoj int =1;
while @eoj > 0
begin
    WITH cte AS 
    (
        SELECT 
            case field_sale when 'Buy' then 
                  lead (field_sale)  OVER (PARTITION BY field_id, field_number  ORDER BY field_inserted)
                  when 'Retu' then 
                  lag (field_sale)  OVER (PARTITION BY field_id, field_number  ORDER BY field_inserted)
                  end nbr_type,
            field_id,
            field_number, 
            field_date,
            field_sale,
            field_inserted 
    FROM   @tablea 
    ) 
    delete  
    from cte 
    where nbr_type is not null and nbr_type <> field_sale;
    set @eoj = @@rowcount;
    -- check it
    select * from @tablea
    order by field_id,
            field_number, 
            field_inserted; 
end;

它将重复 N+1 次,其中 N 是 returns 的最长序列的长度。上例中的 N=2.

我们可以通过使用 common table expression and row_number() 来解决这个 而无需循环或递归 ,如下所示:

如果我对你的问题的理解正确,你想删除已退回的销售 ,并且对于每个 'retu' 它应该删除最近的 'buy'.

首先,我们将使用 row_number() 添加 id 到我们的行集中,以便我们可以唯一地标识我们的行。

接下来,我们添加 br_rn(Buy/Return RowNumber 的缩写)按 field_id, field_number, field_date 分区,但我们将 还添加 field_sale 到分区;我们将在 field_inserted desc 之前订购。 这将使我们将每个 'retu' 与最近的 'buy' 进行匹配,一旦我们可以做到这一点,我们就可以消除所有 not exists():

的对
;with cte as (
  select 
      id = row_number() over (
        order by field_id, field_number, field_date, field_inserted asc
        ) 
    , field_id
    , field_number
    , field_date 
    , field_inserted 
    , field_sale
    , br_rn = row_number() over (
        partition by field_id, field_number, field_date, field_sale
        order by field_inserted desc
        ) 
  from @tablea
)
select 
    id 
  , field_number
  , field_date
  , field_inserted
  , field_sale
from cte
where not exists (
  select 1
  from cte as i
  where i.field_id = cte.field_id
    and i.field_number = cte.field_number
    and i.field_date = cte.field_date
    and i.br_rn = cte.br_rn
    and i.id <> cte.id
    )
order by id

rextester 演示:http://rextester.com/TKXOC61533

对于此输入:

  (1, 100, '20170311','20170311 01:00:00', 'Buy') 
, (1, 100, '20170311','20170311 01:01:00', 'Buy')
, (1, 100, '20170311','20170311 01:02:00', 'Retu')
, (1, 100, '20170311','20170311 01:03:00', 'Retu')
, (2, 100, '20170311','20170311 01:03:00', 'Buy')
, (1, 110, '20170311','20170311 01:03:00', 'Buy');

returns:

+----+----------+--------------+------------+---------------------+------------+
| id | field_id | field_number | field_date |   field_inserted    | field_sale |
+----+----------+--------------+------------+---------------------+------------+
|  5 |        1 |          110 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
|  6 |        2 |          100 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
+----+----------+--------------+------------+---------------------+------------+

对于此输入:

  (1, 100, '20170311','20170311 01:01:00', 'Buy')
, (1, 100, '20170311','20170311 01:02:00', 'Buy')
, (1, 100, '20170311','20170311 01:03:00', 'Buy') 
, (1, 100, '20170311','20170311 01:04:00', 'Retu')
, (1, 100, '20170311','20170311 01:05:00', 'Buy') 
, (1, 100, '20170311','20170311 01:06:00', 'Retu')
, (1, 100, '20170311','20170311 01:07:00', 'Retu')
, (2, 100, '20170311','20170311 01:03:00', 'Buy')
, (1, 110, '20170311','20170311 01:03:00', 'Buy');

returns:

+----+----------+--------------+------------+---------------------+------------+
| id | field_id | field_number | field_date |   field_inserted    | field_sale |
+----+----------+--------------+------------+---------------------+------------+
|  1 |        1 |          100 | 2017-03-11 | 2017-03-11 01:01:00 | Buy        |
|  8 |        1 |          110 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
|  9 |        2 |          100 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
+----+----------+--------------+------------+---------------------+------------+

对于此输入:

  (1, 100, '20170311','20170311 01:01:00', 'Buy')
, (1, 100, '20170311','20170311 01:02:00', 'Buy')
, (1, 100, '20170311','20170311 01:04:00', 'Retu')
, (1, 100, '20170311','20170311 01:05:00', 'Retu')
, (1, 100, '20170312','20170311 01:06:00', 'Buy')
, (1, 100, '20170312','20170311 01:07:00', 'Buy')
, (2, 100, '20170311','20170311 01:03:00', 'Buy')
, (1, 110, '20170311','20170311 01:03:00', 'Buy')

returns:

+----+----------+--------------+------------+---------------------+------------+
| id | field_id | field_number | field_date |   field_inserted    | field_sale |
+----+----------+--------------+------------+---------------------+------------+
|  5 |        1 |          100 | 2017-03-12 | 2017-03-11 01:06:00 | Buy        |
|  6 |        1 |          100 | 2017-03-12 | 2017-03-11 01:07:00 | Buy        |
|  7 |        1 |          110 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
|  8 |        2 |          100 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |
+----+----------+--------------+------------+---------------------+------------+

这可能有助于说明我们正在做什么,以便在我们消除任何对之前查看 cte 返回的内容。

在我们过滤之前先看看需要过滤的集合:

+----+----------+--------------+------------+---------------------+------------+-------+
| id | field_id | field_number | field_date |   field_inserted    | field_sale | br_rn |
+----+----------+--------------+------------+---------------------+------------+-------+
|  1 |        1 |          100 | 2017-03-11 | 2017-03-11 01:01:00 | Buy        |     4 |
|  2 |        1 |          100 | 2017-03-11 | 2017-03-11 01:02:00 | Buy        |     3 |
|  3 |        1 |          100 | 2017-03-11 | 2017-03-11 01:03:00 | Buy        |     2 |
|  4 |        1 |          100 | 2017-03-11 | 2017-03-11 01:04:00 | Retu       |     3 |
|  5 |        1 |          100 | 2017-03-11 | 2017-03-11 01:05:00 | Buy        |     1 |
|  6 |        1 |          100 | 2017-03-11 | 2017-03-11 01:06:00 | Retu       |     2 |
|  7 |        1 |          100 | 2017-03-11 | 2017-03-11 01:07:00 | Retu       |     1 |
+----+----------+--------------+------------+---------------------+------------+-------+

这样看,我们很容易看出'buy'订单id1有一个br_rn4,没有关联 'retu'.