我怎么能替换T-SQL游标?

时间:2017-04-13 13:31:58

标签: sql-server tsql cursor

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

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

这是我的场景:我必须逐行递归计算股票,并根据前一行计算的内容设置季节。

我可以设置转移类型为“购买”的季节。其他转移应通过T-SQL查询设置正确的季节。

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

Transfer Table Example

enter image description here

将“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
  • 4 for 2016 SS。

比更新声明将设定2015-fw(两个赛季的分数与数量相比)。

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

  • QTY 2 for 2015 FW
  • 2016年第4期SS

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

最终输出应该是这样的:

Output

enter image description here

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

提前致谢!

2 个答案:

答案 0 :(得分:1)

已更新为在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专栏的目的,所以我没有把它包括在内。从本质上讲,这会计算购买和销售的运行总和,然后查找每个销售交易至少具有sales_to_date流出量的purchase_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. 确保您的索引与/ order by子句的分区定义相匹配。

  3. 索引应该是[分区字段,顺序无关紧要] + [排序字段,顺序需要匹配order by子句]。 在您的方案中,索引列应该在store,item和date上。如果date在store或item之前,则不会使用索引,因为优化器需要首先按store&amp ;;处理分区。按日期排序前的项目。

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

    索引:区域,商店,项目,日期

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

答案 1 :(得分:0)

参考Wes的答案,提出的解决方案几乎没问题。它运作良好,但我注意到季节的分配不能正常工作,因为在我的场景中,库存应该由商店和商品本身计算和更新。我已经更新了添加一些调整的脚本。此外,我添加了一些新的“假”数据,以更好地了解我的场景以及它应该如何工作。

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

经过这些调整后,它运行正常,但是如果我使用6百万行数据表中的实际数据切换假数据,则查询变得非常慢(每分钟提取约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

我尝试用“交叉应用”功能替换子查询,但没有任何改变。我想念一些事吗?

提前致谢