我如何替换 T-SQL 游标?

How could I replace a T-SQL cursor?

我想问你如何替换插入到我的存储过程中的游标。

实际上,我们发现游标是管理我的场景的唯一出路,但据我所知,这不是最佳做法。

这是我的 scenario:I 必须逐行递归计算库存并根据前几行中计算的内容设置季节。

我可以设置转会类型为"purchase"的赛季。其他转会应通过 T-SQL 查询设置正确的季节。

我应该计算季节的table有以下模板和假数据,但它们反映了真实情况:

Transfer Table Example

"FlgSeason"为null的行,按如下方式计算:按升序,游标从第3行开始往回走前几行,计算每个季节的存货量然后用库存的最小季节更新列季节。

这是我使用的代码:

CREATE TABLE [dbo].[transfers]
(
    [rowId] [int] NULL,
    [area] [int] NULL,
    [store] [int] NULL,
    [item] [int] NULL,
    [date] [date] NULL,
    [type] [nvarchar](50) NULL,
    [qty] [int] NULL,
    [season] [nvarchar](50) NULL,
    [FlagSeason] [int] NULL
) ON [PRIMARY]

INSERT INTO [dbo].[transfers]
           ([rowId]
           ,[area]
           ,[store]
           ,[item]
           ,[date]
           ,[type]
           ,[qty]
           ,[season]
           ,[FlagSeason])
      VALUES (1,1,20,300,'2015-01-01','Purchase',3,'2015-FallWinter',1)
     , (2,1,20,300,'2015-01-01','Purchase',4,'2016-SpringSummer',1)
     ,  (3,1,20,300,'2015-01-01','Sales',-1,null,null)
     ,  (4,1,20,300,'2015-01-01','Sales',-2,null,null)
     ,  (5,1,20,300,'2015-01-01','Sales',-1,null,null)
     ,  (6,1,20,300,'2015-01-01','Sales',-1,null,null)
     ,  (7,1,20,300,'2015-01-01','Purchase',4,'2016-FallWinter',1)
     ,  (8,1,20,300,'2015-01-01','Sales',-1,null,null)

DECLARE @RowId as int
DECLARE db_cursor CURSOR FOR 
Select    RowID  
from Transfers
where [FlagSeason] is null
order by RowID

OPEN db_cursor   
FETCH NEXT FROM db_cursor INTO @RowId   

WHILE @@FETCH_STATUS = 0   
BEGIN   


Update Transfers
set Season = (Select min  (Season) as Season
                      from (
                          Select 
                            Season
                          , SUM(QTY)  as Qty 
                          from Transfers
                          where RowID < @RowId
                            and [FlagSeason] = 1
                          group by Season
                          having Sum(QTY) > 0  
                          )S
                          where s.QTY >= 0 
                     )
, [FlagSeason] = 1

where rowId = @RowId

       FETCH NEXT FROM db_cursor INTO @RowId   

    end

在这种情况下,查询将提取:

比更新声明将设置 2015-fw(两个赛季的最小数量)。

然后 courson 前进到第 4 行,并再次运行查询以提取考虑到第 3 行的计算而更新的股票。所以结果应该是

然后更新将设置 2015 FW。 等等。

最终输出应该是这样的:

Output

实际上,唯一的出路是实现游标,现在扫描和更新大约 250 万行需要 30/40 分钟以上。有没有人知道不重复游标的解决方案?

提前致谢!

2008 年更新为 运行

IF OBJECT_ID('tempdb..#transfer') IS NOT NULL
  DROP TABLE #transfer;
GO

CREATE TABLE #transfer (
                         RowID INT IDENTITY(1, 1) PRIMARY KEY NOT NULL,
                         Area INT,
                         Store INT,
                         Item INT,
                         Date DATE,
                         Type VARCHAR(50),
                         Qty INT,
                         Season VARCHAR(50),
                         FlagSeason INT
                       );

INSERT INTO #transfer ( Area,
                        Store,
                        Item,
                        Date,
                        Type,
                        Qty,
                        Season,
                        FlagSeason
                      )
VALUES (1, 20, 300, '20150101', 'Purchase', 3, '2015-SpringSummer', 1),
(1, 20, 300, '20150601', 'Purchase', 4, '2016-SpringSummer', 1),
(1, 20, 300, '20150701', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20150721', 'Sales', -2, NULL, NULL),
(1, 20, 300, '20150901', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20160101', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170101', 'Purchase', 4, '2017-SpringSummer', 1),
(1, 20, 300, '20170125', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170201', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170225', 'Sales', -1, NULL, NULL),
(1, 21, 301, '20150801', 'Purchase', 4, '2017-SpringSummer', 1),
(1, 21, 301, '20150901', 'Sales', -1, NULL, NULL),
(1, 21, 301, '20151221', 'Sales', -2, NULL, NULL),
(1, 21, 302, '20150801', 'Purchase', 1, '2016-SpringSummer', 1),
(1, 21, 302, '20150901', 'Purchase', 1, '2017-SpringSummer', 1),
(1, 21, 302, '20151101', 'Sales', -1, NULL, NULL),
(1, 21, 302, '20151221', 'Sales', -1, NULL, NULL),
(1, 20, 302, '20150801', 'Purchase', 1, '2016-SpringSummer', 1),
(1, 20, 302, '20150901', 'Purchase', 1, '2017-SpringSummer', 1),
(1, 20, 302, '20151101', 'Sales', -1, NULL, NULL),
(1, 20, 302, '20151221', 'Sales', -1, NULL, NULL);
WITH Purchases
AS (SELECT  t1.RowID,
            t1.Area,
            t1.Store,
            t1.Item,
            t1.Date,
            t1.Type,
            t1.Qty,
            t1.Season,
            RunningInventory = ( SELECT SUM(t2.Qty)
                                 FROM   #transfer AS t2
                                 WHERE  t1.Type = t2.Type
                                        AND t1.Area = t2.Area
                                        AND t1.Store = t2.Store
                                        AND t1.Item = t2.Item
                                        AND t2.Date <= t1.Date
                               )
    FROM    #transfer AS t1
    WHERE   t1.Type = 'Purchase'
   ),
     Sales
AS (SELECT  t1.RowID,
            t1.Area,
            t1.Store,
            t1.Item,
            t1.Date,
            t1.Type,
            t1.Qty,
            t1.Season,
            RunningSales = ( SELECT SUM(ABS(t2.Qty))
                             FROM   #transfer AS t2
                             WHERE  t1.Type = t2.Type
                                    AND t1.Area = t2.Area
                                    AND t1.Store = t2.Store
                                    AND t1.Item = t2.Item
                                    AND t2.Date <= t1.Date
                           )
    FROM    #transfer AS t1
    WHERE   t1.Type = 'Sales'
   )
SELECT  Sales.RowID,
        Sales.Area,
        Sales.Store,
        Sales.Item,
        Sales.Date,
        Sales.Type,
        Sales.Qty,
        Season = ( SELECT TOP 1
                        Purchases.Season
                   FROM Purchases
                   WHERE Purchases.Area = Sales.Area
                         AND Purchases.Store = Sales.Store
                         AND Purchases.Item = Sales.Item
                         AND Purchases.RunningInventory >= Sales.RunningSales
                   ORDER BY Purchases.Date, Purchases.Season
                 )
FROM    Sales
UNION ALL
SELECT  Purchases.RowID ,
        Purchases.Area ,
        Purchases.Store ,
        Purchases.Item ,
        Purchases.Date ,
        Purchases.Type ,
        Purchases.Qty ,
        Purchases.Season 
FROM    Purchases
ORDER BY Sales.Area, Sales.Store, item, Sales.Date

*下面是原始答案**

我不明白 flagseason 专栏的目的,所以我没有包含它。从本质上讲,这会计算采购和销售的 运行 总和,然后找到每个销售交易的 purchase_to_date 库存至少 sales_to_date 流出量的季节。

IF OBJECT_ID('tempdb..#transfer') IS NOT NULL
  DROP TABLE #transfer;
GO

CREATE TABLE #transfer (
                         RowID INT IDENTITY(1, 1) PRIMARY KEY NOT NULL,
                         Area INT,
                         Store INT,
                         Item INT,
                         Date DATE,
                         Type VARCHAR(50),
                         Qty INT,
                         Season VARCHAR(50),
                         FlagSeason INT
                       );

INSERT INTO #transfer ( Area,
                        Store,
                        Item,
                        Date,
                        Type,
                        Qty,
                        Season,
                        FlagSeason
                      )
VALUES (1, 20, 300, '20150101', 'Purchase', 3, '2015-FallWinter', 1),
(1, 20, 300, '20150601', 'Purchase', 4, '2016-SpringSummer', 1),
(1, 20, 300, '20150701', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20150721', 'Sales', -2, NULL, NULL),
(1, 20, 300, '20150901', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20160101', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170101', 'Purchase', 4, '2016-FallWinter', 1),
(1, 20, 300, '20170201', 'Sales', -1, NULL, NULL);

WITH Inventory
AS (SELECT  *,
            PurchaseToDate = SUM(CASE WHEN Type = 'Purchase' THEN Qty ELSE 0 END) OVER (ORDER BY Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
            SalesToDate = ABS(SUM(CASE WHEN Type = 'Sales' THEN Qty ELSE 0 END) OVER (ORDER BY Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW))
    FROM    #transfer
   )
SELECT  Inventory.RowID,
        Inventory.Area,
        Inventory.Store,
        Inventory.Item,
        Inventory.Date,
        Inventory.Type,
        Inventory.Qty,
        Season = CASE
                   WHEN Inventory.Season IS NULL
                     THEN ( SELECT TOP 1
                                PurchaseToSales.Season
                            FROM    Inventory AS PurchaseToSales
                            WHERE   PurchaseToSales.PurchaseToDate >= Inventory.SalesToDate
                            ORDER BY Inventory.Date
                          )
                   ELSE
                     Inventory.Season
                 END,
        Inventory.PurchaseToDate,
        Inventory.SalesToDate
FROM    Inventory;

*更新*******************************

您需要一个数据索引来帮助排序才能执行此操作。

可能:

CREATE  NONCLUSTERED INDEX IX_Transfer ON #transfer(Store, Item, Date) INCLUDE(Area,Qty,Season,Type)

您应该会看到对指定索引的索引扫描。它不会是一个搜索,因为示例查询不过滤任何数据并且包含所有数据。

此外,您需要从 SalesToDate 的 Partition By 子句中删除 Season。重置每个季节的销售额会使您的比较失败,因为滚动销售额需要与滚动库存进行比较,以便您确定销售库存的来源。

分区子句的其他两个提示:

  1. 不要重复分区依据和排序依据之间的字段。分区字段的顺序无关紧要,因为每个分区都会重置聚合。最好的情况是,有序的分区字段将被忽略,最坏的情况是它可能导致优化器以特定顺序聚合字段。这对结果没有任何影响,但会增加不必要的开销。

  2. 确保您的索引与分区 by/order 的定义匹配。

索引应该是[分区字段,顺序无所谓] + [排序字段,顺序需要匹配order by clause]。 在您的场景中,索引列应该在商店、项目和日期上。如果日期在商店或项目之前,则不会使用索引,因为优化器需要先按商店和项目处理分区,然后再按日期排序。

如果您的数据中可能有多个区域,则索引和分区子句需要

索引:地区、店铺、项目、日期

分区依据:区域、商店、项目按日期排序

参考Wes的回答,提出的解决方案已经差不多了。它运作良好,但我注意到季节分配无法正常工作,因为在我的场景中,库存应该由商店和商品本身计算和更新。我更新了脚本,添加了一些调整。此外,我添加了一些新的 "Fake" 数据以更好地理解我的场景及其工作原理。

IF OBJECT_ID('tempdb..#transfer') IS NOT NULL
  DROP TABLE #transfer;
GO

CREATE TABLE #transfer (
                         RowID INT IDENTITY(1, 1) PRIMARY KEY NOT NULL,
                         Area INT,
                         Store INT,
                         Item INT,
                         Date DATE,
                         Type VARCHAR(50),
                         Qty INT,
                         Season VARCHAR(50),
                         FlagSeason INT
                       );

INSERT INTO #transfer ( Area,
                        Store,
                        Item,
                        Date,
                        Type,
                        Qty,
                        Season,
                        FlagSeason
                      )
VALUES (1, 20, 300, '20150101', 'Purchase', 3, '2015-SpringSummer', 1),
(1, 20, 300, '20150601', 'Purchase', 4, '2016-SpringSummer', 1),
(1, 20, 300, '20150701', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20150721', 'Sales', -2, NULL, NULL),
(1, 20, 300, '20150901', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20160101', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170101', 'Purchase', 4, '2017-SpringSummer', 1),
(1, 20, 300, '20170125', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170201', 'Sales', -1, NULL, NULL),
(1, 20, 300, '20170225', 'Sales', -1, NULL, NULL),
(1, 21, 301, '20150801', 'Purchase', 4, '2017-SpringSummer', 1),
(1, 21, 301, '20150901', 'Sales', -1, NULL, NULL),
(1, 21, 301, '20151221', 'Sales', -2, NULL, NULL),
(1, 21, 302, '20150801', 'Purchase', 1, '2016-SpringSummer', 1),
(1, 21, 302, '20150901', 'Purchase', 1, '2017-SpringSummer', 1),
(1, 21, 302, '20151101', 'Sales', -1, NULL, NULL),
(1, 21, 302, '20151221', 'Sales', -1, NULL, NULL),
(1, 20, 302, '20150801', 'Purchase', 1, '2016-SpringSummer', 1),
(1, 20, 302, '20150901', 'Purchase', 1, '2017-SpringSummer', 1),
(1, 20, 302, '20151101', 'Sales', -1, NULL, NULL),
(1, 20, 302, '20151221', 'Sales', -1, NULL, NULL)




;

WITH Inventory
AS (SELECT  *,
            PurchaseToDate = SUM(CASE WHEN Type = 'Purchase' THEN Qty ELSE 0 END) OVER (partition by store, item ORDER BY store, item,Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
            SalesToDate = ABS(SUM(CASE WHEN Type = 'Sales' THEN Qty ELSE 0 END) OVER (partition by store, item,season ORDER BY store, item, Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW))
    FROM    #transfer
   )
SELECT  Inventory.RowID,
        Inventory.Area,
        Inventory.Store,
        Inventory.Item,
        Inventory.Date,
        Inventory.Type,
        Inventory.Qty,
        Season = CASE
                   WHEN Inventory.Season IS NULL
                     THEN ( SELECT TOP 1
                                PurchaseToSales.Season
                            FROM    Inventory AS PurchaseToSales
                            WHERE   PurchaseToSales.PurchaseToDate >= Inventory.SalesToDate
                                    and PurchaseToSales.Item = inventory.item   --//Added
                                    and PurchaseToSales.store = inventory.store --//Added
                                    and PurchaseToSales.Area = Inventory.area   --//Added

                            ORDER BY  Inventory.Date
                          )
                   ELSE
                     Inventory.Season
                 END,
        Inventory.PurchaseToDate,
        Inventory.SalesToDate
FROM    Inventory

此处输出:

enter image description here

经过这些调整后,它工作正常,但如果我将假数据与 600 万行数据中的真实数据切换 table,查询变得非常慢(每次提取约 400 行分钟)因为在子查询的 where 子句中插入了这些检查:

WHERE   PurchaseToSales.PurchaseToDate >= Inventory.SalesToDate
                                    and PurchaseToSales.Item = inventory.item   --//Added
                                    and PurchaseToSales.store = inventory.store --//Added
                                    and PurchaseToSales.Area = Inventory.area   --//Added

我尝试用 "Cross Apply" 函数替换子查询,但没有任何改变。我错过了什么吗?

提前致谢