【问题标题】:How to replace CURSOR when it has effect on functions in SELECT statement对 SELECT 语句中的函数有影响时如何替换 CURSOR
【发布时间】:2018-02-14 12:14:16
【问题描述】:

请考虑以下代码:

Declare @MyMinMaxTable Table
(
    [Min]    int,
    [Max]    int,
    [Desc]   NVARCHAR(50)
)

Insert into @MyMinMaxTable
values (0,12,N'Child'),
       (13,19,N'Teenager'),
       (20,25,N'Youth'),
       (25,40,N'Middle-aged'),
       (40,99,N'Old')

Declare @MyTable Table
(
    Id         int identity(1,1),
    [Year]     int,
    Age        int,
    MyCol2     int,
    MyCol3     int null
)

Insert into @MyTable
([Year], Age, MyCol2, MyCol3)
values 
(2012, 10, 1 , 1),
(2012, 28, 2 , 3),
(2012, 14, 1 , 7),
(2012, 24, 3 , 3),
(2012, 80, 1 , 6),
(2012, 39, 1 , 3),
(2012, 45, 1 , 5),
(2012, 23, 2 , 6),
(2012, 72, 3 , 8),
(2012, 17, 1 , null),
(2012, 62, 4 , 9),
(2012, 20, 1 , null),
(2012, 5, 1 , 9),
(2012, 8, 1 , 9),
(2012, 25, 1 , null),
(2012, 41, 2 , 2),
(2012, 26, 1 , 2),
(2012, 33, 4 , 2),
(2012, 40, 1 , 2),
(2012, 33, 2 , 3),
(2012, 41, 1 , 5),
(2012, 53, 1 , null),
(2012, 37, 1 , 3)

Declare @Result Table
(
    C0         NVARCHAR(50),
    c1         decimal(5,2),
    C2         decimal(5,2),
    C3         decimal(5,2)
)

CURSOR 部分:

DECLARE @Min    int;
DECLARE @Max    int;
DECLARE @Desc   nvarchar(50);

DECLARE mycur CURSOR  
FOR
    SELECT [min],
           [max],
           [Desc]
    FROM   @MyMinMaxTable
OPEN mycur

FETCH NEXT FROM mycur INTO @Min, @Max, @Desc

WHILE (@@fetch_status = 0)
    BEGIN
        INSERT INTO @Result
        SELECT @Desc As c0,
               (Cast(COUNT(CASE when Age >= @Min AND Age <= @Max  THEN 1 END) as decimal(5,2)) / cast(COUNT(Id) as decimal(5,2))) As c1,
               (Cast(COUNT(CASE when MyCol2 = 1 AND MyCol3 IS NOT NULL THEN 1 END) as decimal(5,2))  / cast(COUNT(CASE when Age >= @Min AND Age <= @Max  THEN 1 END) as decimal(5,2))) As c2,
               (Cast(COUNT(CASE when Age >= @Min AND Age <= @Max  ANd MyCol2 = 1 THEN 1 END) as decimal(5,2)) / cast(COUNT(CASE when MyCol2 = 1 THEN 1 END) as decimal(5,2))) As c3
        FROM   @MyTable AS td

    FETCH NEXT FROM mycur INTO @Min, @Max, @Desc
END 

CLOSE mycur
DEALLOCATE mycur

SELECT * FROM @Result

问题是我想删除CURSOR 并编写一个没有它的查询。在这种情况下怎么可能?

【问题讨论】:

  • 编辑您的问题并提供示例数据和所需结果。我不想理解光标代码。
  • @GordonLinoff 我更新了我的问题。感谢您的关注
  • 你能把合理的、描述性的列名放在这里吗? C1 和 C2 并没有告诉读者任何事情,在这里很难看到这个概念。您似乎想在年龄范围内对人员进行分类,但这些表定义与概念没有具体联系。 'min' 和 'max' 之类的列名也没有帮助。另外,为什么 C3 可以为空,这是否与概念相关?
  • @PittsburghDBA 感谢您的关注。 C1 , C2 , ...在结果表中,它们的名称无关紧要。我觉得我的问题很简单。我有一张表,我想在每个班级中分类一个做一些计算
  • 查看 CASE 块中的 MyCol1、MyCol2 和 MyCol3 很难确定业务规则的逻辑。这些列确实有意义,因为它们正在驱动 CASE 逻辑。是年龄吗?是上课的次数吗?他们是旗帜吗?业务目标是什么?这里可能有许多有效的答案,但逻辑过于混乱,以至于几乎不值得研究。

标签: sql sql-server cursor sql-server-2014


【解决方案1】:

尝试使用 CROSS JOIN 并在 where 子句或条件 CASE 语句中进行过滤。目前还不清楚您的最终目标是什么,因此必须调整以下汇总数据,但这可能是一个好的开始:

SELECT 
  mm.[Desc],
  (CAST(SUM(CASE WHEN MyCol1 >= mm.Min AND MyCol1 <= mm.Max THEN 1 ELSE 0 END) AS DECIMAL(5,2)) / CAST(COUNT(Id) AS DECIMAL(5,2))) AS C1,
  (CAST(SUM(CASE WHEN MyCol2 = 1 AND MyCol3 IS NOT NULL THEN 1 END) AS DECIMAL(5,2)) / CAST(SUM(CASE WHEN MyCol1 >= mm.Min AND MyCol1 <= mm.Max THEN 1 ELSE 0 END) AS DECIMAL(5,2))) AS C2,
  (CAST(SUM(CASE WHEN MyCol1 >= mm.Min AND MyCol1 <= mm.Max AND MyCol2 = 1 THEN 1 ELSE 0 END) AS DECIMAL(5,2)) / CAST(SUM(CASE WHEN MyCol2 = 1 THEN 1 ELSE 0 END) AS DECIMAL(5,2))) AS C3
FROM MyTable td
  CROSS JOIN MyMinMaxTable mm
GROUP BY mm.[Desc]

【讨论】:

  • 您要求替代方案并得到了替代方案,但执行计划是另一回事,因为我们不知道您的数据库是什么样的以及它的索引方式。那部分取决于你。您可以随时使用此示例并对其进行调整,但我们不负责为您执行您的工作。
  • @Arian: 交叉连接的执行是基于集合的操作,它比你的逐行方法要好得多。
  • 以上答案替换了您的光标,不是吗? ,比逐行操作好很多
  • 这是个问题,但这个方案绝对比你的好
  • @TheGameiswar,这个答案中的查询确实产生了与问题中的光标相同的结果。不同之处在于四舍五入,因为游标代码插入到带有 decimal(5,2) 列的 @Result 表中,并且此答案中的查询不会将比率的最终结果转换为 decimal(5,2) 并且结果有更多小数位。
【解决方案2】:

这里有一个解决方案。我解开您的查询并重写它。 如果您添加其他范围,此选项将继续工作,并且不需要 CROSS APPLY、硬编码子查询范围或其他任何内容。

排序对你来说是一项练习:-)

Declare @MyMinMaxTable Table
(
    [Min]    int,
    [Max]    int,
    [Desc]   NVARCHAR(50)
)

Insert into @MyMinMaxTable
values (0,12,N'Child'),
       (13,19,N'Teenager'),
       (20,25,N'Youth'),
       (25,40,N'Middle-aged'),
       (40,99,N'Old')

Declare @MyTable Table
(
    Id         int identity(1,1),
    [Year]     int,
    MyCol1     int,
    MyCol2     int,
    MyCol3     int null
)

Insert into @MyTable
([Year], MyCol1, MyCol2, MyCol3)
values 
(2012, 10, 1 , 1),
(2012, 28, 2 , 3),
(2012, 14, 1 , 7),
(2012, 24, 3 , 3),
(2012, 80, 1 , 6),
(2012, 39, 1 , 3),
(2012, 45, 1 , 5),
(2012, 23, 2 , 6),
(2012, 72, 3 , 8),
(2012, 17, 1 , null),
(2012, 62, 4 , 9),
(2012, 20, 1 , null),
(2012, 5, 1 , 9),
(2012, 8, 1 , 9),
(2012, 25, 1 , null),
(2012, 41, 2 , 2),
(2012, 26, 1 , 2),
(2012, 33, 4 , 2),
(2012, 40, 1 , 2),
(2012, 33, 2 , 3),
(2012, 41, 1 , 5),
(2012, 53, 1 , null),
(2012, 37, 1 , 3)

SELECT
    MMT.[Desc]
    --Ratio of (Age Bracket Matches) / (ALL Rows)
    , CAST(COUNT(1) / (SELECT CAST(COUNT(1) AS DECIMAL(15,2)) AS TotalRows FROM @MyTable) AS DECIMAL(15,2)) AS C1
    --Ratio of (Rows WHERE MyCol2 = 1 and MyCol3 = NULL) / (Age Bracket Matches)
    , CAST((SELECT COUNT(1) FROM @MyTable WHERE MyCol2 = 1 AND MyCol3 IS NOT NULL) / CAST(COUNT(1) AS DECIMAL(15,2)) AS DECIMAL(15,2)) AS C2
    --Ratio of (Age Bracket Matches WHERE MyCol2 = 1) / (Rows WHERE WHERE MyCol2 = 1)
    , CAST(SUM(CASE WHEN T.MyCol2 = 1 THEN 1 ELSE 0 END) / (SELECT CAST(COUNT(1) AS DECIMAL(15,2)) FROM @MyTable WHERE MyCol2 = 1) AS DECIMAL(15,2)) AS C3
FROM
    @MyMinMaxTable AS MMT
    INNER JOIN @MyTable AS T ON
        T.MyCol1 BETWEEN MMT.[Min] AND MMT.[Max]
GROUP BY
    MMT.[Desc]

【讨论】:

  • 太好了。您还可以将联接更改为 LEFT,并针对特定年龄范围内没有集合成员的情况进行一些小的修改。
  • 感谢您的赏金!确保将 Middle-aged 的​​开始改为 26,Old 的开始改为 41,这样你的范围就不会重叠。如果数据集中出现任何百岁老人,您可能还需要 100 人及以上的人。
【解决方案3】:

您似乎正在尝试将数据分组。试试这个查询。但我不确定第三列,尤其是没有样本数据

INSERT INTO @outTable
select
    mt.[Desc], count(*) / count([Id]) over ()
    , COUNT(CASE when MyCol2 = '1' AND MyCol3 IS NOT NULL THEN 1 END) / count(*)
from
    MyTable td
    join MyMinMaxTable mt on td.MyCol1 between mt.[min] and mt.[max]
where
    td.[YEAR] = @Year
    and td.[Status] = 1
group by mt.[Desc]

【讨论】:

  • 您的查询有错误,因为Id 没有出现在GROUP BY 子句中
  • 可以通过将count([Id]) over () 更改为count(*) over () 来消除错误。但是您能解释一下您在 C1、C2、C3 列中计算的内容吗?
  • 我看到了更新。但是这些字段没有解释
  • 这些字段需要什么解释? Mycol1 适用于年龄,其余字段无关紧要
  • 不要让别人解开你的代码。添加一些解释。你想计算什么。可能有错误。例如,对于列c1,您似乎正在尝试计算某种比率。但是所有值的总和都大于1。正确吗?
【解决方案4】:

另一种方法是使用CROSS APPLY

   SELECT MM.[Desc], 
       CAST(C1.C1Count AS DECIMAL(15, 2)) / CAST(Tot.TotCount AS decimal (15, 2))  AS C1, 
       CAST(C2.C2Count AS DECIMAL(15, 2)) / CAST(C1.C1Count AS decimal (15, 2))    AS C2, 
       CAST(C3.C3Count AS DECIMAL(15, 2)) / CAST(C4.C4Count AS decimal (15, 2))    AS C2 
   FROM   @MyMinMaxTable MM 
       CROSS APPLY (SELECT COUNT(MyCol1) AS C1Count 
                    FROM   @MyTable 
                    WHERE  ( MyCol1 BETWEEN MM.Min AND MM.Max )) C1 
       CROSS APPLY (SELECT COUNT(*) C2Count 
                    FROM   @MyTable 
                    WHERE  MyCol2 = 1 
                           AND MyCol3 IS NOT NULL)C2 
       CROSS APPLY (SELECT COUNT(*) C3Count 
                    FROM   @MyTable 
                    WHERE  MyCol1 >= mm.Min 
                           AND MyCol1 <= mm.Max 
                           AND MyCol2 = 1)C3 
       CROSS APPLY (SELECT COUNT(*) C4Count 
                    FROM   @MyTable 
                    WHERE  MyCol2 = 1)C4 
       CROSS APPLY (SELECT COUNT(*) TotCount 
                    FROM   @MyTable)Tot 

以下是上述查询的输出。

Desc                        C1                      C2                  C2
----------          --------------------    ---------------------   ---------------------
Child               0.130434782608695652    3.666666666666666666    0.200000000000000000
Teenager            0.086956521739130434    5.500000000000000000    0.133333333333333333
Youth               0.173913043478260869    2.750000000000000000    0.133333333333333333
Middle-aged         0.347826086956521739    1.375000000000000000    0.333333333333333333
Old                 0.347826086956521739    1.375000000000000000    0.333333333333333333

【讨论】:

  • 感谢@PSK 结果是一样的。问题在于更改边界并添加另一个分组边界,我们应该更改该查询
  • 我觉得任何实现都是如此,一个新的分组将导致任何实现的变化,直到你没有动态地形成你的查询。
【解决方案5】:

我认为在这种情况下可以预先计算“@MyTable 中的所有记录数”、“@MyTable 中 MyCol2 = 1 且 MyCol3 不为空的记录数”和“@MyTable 中的记录数其中 MyCol2 = 1” 并将它们存储到变量中。

这将使脚本更具可读性和更易于理解。

来了。希望这会有所帮助。

Declare @MyMinMaxTable Table
(
    [Min]    int,
    [Max]    int,
    [Desc]   NVARCHAR(50)
)

Insert into @MyMinMaxTable
values (0,12,N'Child'),
       (13,19,N'Teenager'),
       (20,25,N'Youth'),
       (25,40,N'Middle-aged'),
       (40,99,N'Old')

Declare @MyTable Table
(
    Id         int identity(1,1),
    [Year]     int,
    Age        int,
    MyCol2     int,
    MyCol3     int null
)

Insert into @MyTable
([Year], Age, MyCol2, MyCol3)
values 
(2012, 10, 1 , 1),
(2012, 28, 2 , 3),
(2012, 14, 1 , 7),
(2012, 24, 3 , 3),
(2012, 80, 1 , 6),
(2012, 39, 1 , 3),
(2012, 45, 1 , 5),
(2012, 23, 2 , 6),
(2012, 72, 3 , 8),
(2012, 17, 1 , null),
(2012, 62, 4 , 9),
(2012, 20, 1 , null),
(2012, 5, 1 , 9),
(2012, 8, 1 , 9),
(2012, 25, 1 , null),
(2012, 41, 2 , 2),
(2012, 26, 1 , 2),
(2012, 33, 4 , 2),
(2012, 40, 1 , 2),
(2012, 33, 2 , 3),
(2012, 41, 1 , 5),
(2012, 53, 1 , null),
(2012, 37, 1 , 3)

Declare @Result Table
(
    C0         NVARCHAR(50),
    c1         decimal(5,2),
    C2         decimal(5,2),
    C3         decimal(5,2)
)


-- ANSWER BEGIN HERE

-- The count all record in @MyTable
DECLARE @CountAll DECIMAL(5,2)
SET @CountAll = (SELECT COUNT(1) FROM @MyTable)

-- The count record in @MyTable where MyCol2 = 1 and MyCol3 is not null
DECLARE @CountMyCol2Eq1AndMyCol3NotNull DECIMAL(5,2)
SET @CountMyCol2Eq1AndMyCol3NotNull = (SELECT COUNT(1) FROM @MyTable WHERE MyCol2 = 1 AND MyCol3 IS NOT NULL)

-- The count record in @MyTable where MyCol2 = 1
DECLARE @CountMyCol2Eq1 DECIMAL(5,2)
SET @CountMyCol2Eq1 = (SELECT COUNT(1) FROM @MyTable WHERE MyCol2 = 1)

INSERT INTO @Result
SELECT mm.[Desc],
    COUNT(1) / @CountAll,
    @CountMyCol2Eq1AndMyCol3NotNull / COUNT(1),
    SUM(CASE WHEN tt.MyCol2 = 1 THEN 1 ELSE 0 END) / @CountMyCol2Eq1
FROM @MyMinMAxTable mm
JOIN @MyTable tt
    ON mm.[Min] <= tt.Age
    AND mm.[Max] >= tt.Age
GROUP BY mm.[Desc]

SELECT * FROM @Result

【讨论】:

  • 感谢您的解决方案与@Pittsburgh DBA 解决方案相同。
  • 查询优化器已经将这些子查询计算为标量值,因此没有理由将它们“预计算”为变量。这种方法在没有增加价值的情况下使情况复杂化。 @Arian 注意到这是将我的查询重构为更复杂的东西。
  • @PittsburghDBA 是的,我们的解决方案看起来非常相似。对不起,在开始我的之前我没有看到你的答案。我也会点赞你的回答。无论如何,我知道查询优化器已经将子查询计算为标量值。我只是认为它会更具可读性。
  • @JeremiahWidjaja 这两种方法在某些方面都有优点。正如您所说,您的可能更容易阅读,尤其是对于新开发人员或具有程序倾向的人。我的可以用作视图。生活充满了选择!
猜你喜欢
  • 2022-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-14
  • 1970-01-01
  • 2020-03-15
  • 1970-01-01
  • 2023-04-04
相关资源
最近更新 更多