SQL Server:在不使用CURSOR的情况下改进PROCEDURE

时间:2010-02-05 19:23:04

标签: sql-server performance tsql

我正在寻找一种方法来编写以下程序而不使用CURSOR或只是为了找到性能更好的查询。

CREATE TABLE #OrderTransaction (OrderTransactionId int, ProductId int, Quantity int);
CREATE TABLE #Product (ProductId int, MediaTypeId int);
CREATE TABLE #OrderDelivery (OrderTransactionId int, MediaTypeId int);

INSERT INTO #Product (ProductId, MediaTypeId) VALUES (1,1);
INSERT INTO #Product (ProductId, MediaTypeId) VALUES (2,2);
INSERT INTO #OrderTransaction(OrderTransactionId, ProductId, Quantity) VALUES (1,1,1);
INSERT INTO #OrderTransaction(OrderTransactionId, ProductId, Quantity) VALUES (2,2,6);

DECLARE @OrderTransactionId int, @MediaTypeId int, @Quantity int; 

DECLARE ordertran CURSOR FAST_FORWARD FOR 
    SELECT  OT.OrderTransactionId, P.MediaTypeId, OT.Quantity
    FROM    #OrderTransaction OT WITH (NOLOCK)
        INNER JOIN  #Product P WITH (NOLOCK)
            ON OT.ProductId = P.ProductId


OPEN ordertran;
FETCH NEXT FROM ordertran INTO @OrderTransactionId, @MediaTypeId, @Quantity;

WHILE @@FETCH_STATUS = 0 
BEGIN
    WHILE @Quantity > 0 
    BEGIN
        INSERT INTO #OrderDelivery ([OrderTransactionId], [MediaTypeId])
        VALUES (@OrderTransactionId, @MediaTypeId)

        SELECT @Quantity = @Quantity - 1;
    END 

    FETCH NEXT FROM ordertran INTO @OrderTransactionId, @MediaTypeId, @Quantity;
END 

CLOSE ordertran;
DEALLOCATE ordertran;


SELECT  *   FROM    #OrderTransaction 
SELECT  *   FROM    #Product
SELECT  *   FROM    #OrderDelivery

DROP TABLE #OrderTransaction;
DROP TABLE #Product;
DROP TABLE #OrderDelivery;

5 个答案:

答案 0 :(得分:3)

从Number表开始,该表足以处理最大订单金额:

CREATE TABLE Numbers (
   Num int NOT NULL PRIMARY KEY CLUSTERED
)

-- SQL 2000 version
INSERT Numbers VALUES (1)
SET NOCOUNT ON
GO
INSERT Numbers (Num) SELECT Num + (SELECT Max(Num) FROM Numbers) FROM Numbers
GO 15

-- SQL 2005 and up version
WITH
   L0 AS (SELECT c = 1 UNION ALL SELECT 1),
   L1 AS (SELECT c = 1 FROM L0 A, L0 B),
   L2 AS (SELECT c = 1 FROM L1 A, L1 B),
   L3 AS (SELECT c = 1 FROM L2 A, L2 B),
   L4 AS (SELECT c = 1 FROM L3 A, L3 B),
   L5 AS (SELECT c = 1 FROM L4 A, L4 B),
   N AS (SELECT Num = ROW_NUMBER() OVER (ORDER BY c) FROM L5)
INSERT Numbers(Num)
SELECT Num FROM N
WHERE Num <= 32768;

然后,在INSERT语句之后立即:

INSERT #OrderDelivery (OrderTransactionId, MediaTypeId)
SELECT
   OT.OrderTransactionId,
   P.MediaTypeId
FROM
   #OrderTransaction OT
   INNER JOIN #Product P ON OT.ProductId = P.ProductId
   INNER JOIN Numbers N ON N.Num BETWEEN 1 AND OT.Quantity

应该这样做!

如果由于某种原因你有疑虑在你的数据库中放置一个永久的Numbers表(我不明白,因为它是一个很好的工具),那么你可以简单地加入CTE而不是表本身。在SQL 2000中,您可以创建临时表并使用循环,但我会强烈反对这一点。

强烈建议使用Numbers表。没有人担心未来的某些变化会破坏它(整数的集合不会很快改变)。有些人使用带有一百万个数字的Numbers表,只有大约4MB的存储空间。

回答Numbers表的批评:如果数据库设计使用数字表,那么该表不需要更改。它就像数据库中的任何其他表一样,可以依赖它。你不必太担心Orders表的查询失败,因为某些表可能不存在,所以我不明白为什么对于另一个需要和依赖的表有任何类似的担忧。

<强>更新

在撰写此答案后的那段时间里,我了解到master.dbo.spt_values表格中有number列。当使用where type='P'查询时,在SQL 2000中得到0 - 255,在SQL 2005及以上得到0 - 8191。 (还有一些可能有用的lowhigh列。)如果有必要,即使在SQL 2000中,也可以非常快速地将此表交叉连接到自身。 / p>

答案 1 :(得分:1)

诀窍是引入一个值表(在下面的示例中命名为MyTableOfIntegers),其中包含1和(至少)某个值之间的所有整数值(在手边的情况下) ,这将是OrderTransaction表中可能的最大数量值。)

INSERT INTO #OrderDelivery ([OrderTransactionId], [MediaTypeId])
  SELECT  OT.OrderTransactionId, P.MediaTypeId
  FROM  #OrderTransaction OT WITH (NOLOCK)
  INNER JOIN  #Product P WITH (NOLOCK)
    ON OT.ProductId = P.ProductId
  JOIN MyTableOfIntegers I ON I.Num <= OT.Quantity
  --WHERE some optional conditions 

基本上MyTableOfIntegers上的额外JOIN产生与OT.Quantity一样多的重复行,这似乎是游标的目的:在OrderDelivery表中插入那么多重复的行。

我没有用临时表和所有来检查剩下的逻辑(我假设这些是临时表,用于检查逻辑而不是正确处理过程的一部分),但它似乎是上面是仅以声明方式表达所需逻辑所需的构造类型,没有任何游标甚至任何循环。

答案 2 :(得分:1)

以前的答案略有不同,避免使用永久数字表(虽然我不确定为什么人们如此害怕这个结构),并允许你构建一个包含完全集合的运行时CTE您需要执行正确数量的插入数字(通过检查最高数量)。我在初始CTE中注释掉了CROSS JOIN,但如果任何给定订单的数量超过sys.columns中的行数,您可以使用它。希望这是不太可能的情况。请注意,这适用于SQL Server 2005及更高版本...告诉我们您要定位的特定版本总是有用的。

DECLARE @numsNeeded INT;

SELECT @numsNeeded = MAX(Quantity) FROM #OrderTransaction;

WITH n AS 
(
    SELECT TOP (@numsNeeded) i = ROW_NUMBER()
    OVER (ORDER BY c.[object_id])
    FROM sys.columns AS c  --CROSS JOIN sys.columns AS c2
)
INSERT #OrderDelivery
(
    OrderTransactionID,
    MediaTypeID
)
SELECT t.OrderTransactionID, p.MediaTypeID
    FROM #OrderTransaction AS t
    INNER JOIN #Product AS p
    ON t.ProductID = p.ProductID
    INNER JOIN n
    ON n.i <= t.Quantity;

答案 3 :(得分:0)

INSERT INTO #OrderDelivery ([OrderTransactionId], [MediaTypeId])
SELECT  OT.OrderTransactionId, P.MediaTypeId,
FROM    #OrderTransaction OT
INNER JOIN  #Product P
ON OT.ProductId = P.ProductId
WHERE OT.Quantity > 0

我觉得我在这里误读了这个逻辑,但这不是公平的吗?

答案 4 :(得分:0)

这仍然使用循环,但它已经摆脱了光标。如果没有创建一个数字表来加入,我认为这是最好的答案。

DECLARE @Count AS INTEGER

SET @Count = 1

WHILE (1 = 1)
BEGIN

    INSERT INTO #OrderDelivery ([OrderTransactionId], [MediaTypeId]) 
    SELECT  OT.OrderTransactionId, P.MediaTypeId, OT.Quantity 
    FROM    #OrderTransaction OT WITH (NOLOCK) 
        INNER JOIN  #Product P WITH (NOLOCK) 
            ON OT.ProductId = P.ProductId 
    WHERE OT.Quantity > @Count

    IF @@ROWCOUNT = 0 
        BREAK

    SET @COUNT = @COUNT + 1
END