T-SQL-遍历表直到满足条件的最有效方法是什么


10

在中进行了编程任务T-SQL

任务:

  1. 人们想进入电梯,每个人都有一定的体重。
  2. 排队等候的人的顺序由立柱转弯确定。
  3. 电梯的最大容量为<= 1000磅。
  4. 返回在电梯沉重之前能够进入电梯的最后一个人的名字!
  5. 返回类型应为表格

在此处输入图片说明

问题: 解决此问题的最有效方法是什么?如果循环正确,是否还有改进的空间?

我使用了一个循环和#临时表,这是我的解决方案:

set rowcount 0
-- THE SOURCE TABLE "LINE" HAS THE SAME SCHEMA AS #RESULT AND #TEMP
use Northwind
go

declare @sum int
declare @curr int
set @sum = 0
declare @id int

IF OBJECT_ID('tempdb..#temp','u') IS NOT NULL
    DROP TABLE #temp

IF OBJECT_ID('tempdb..#result','u') IS NOT NULL
    DROP TABLE #result

create table #result( 
    id int not null,
    [name] varchar(255) not null,
    weight int not null,
    turn int not null
)

create table #temp( 
    id int not null,
    [name] varchar(255) not null,
    weight int not null,
    turn int not null
)

INSERT into #temp SELECT * FROM line order by turn

 WHILE EXISTS (SELECT 1 FROM #temp)
  BEGIN
   -- Get the top record
   SELECT TOP 1 @curr =  r.weight  FROM  #temp r order by turn  
   SELECT TOP 1 @id =  r.id  FROM  #temp r order by turn

    --print @curr
    print @sum

    IF(@sum + @curr <= 1000)
    BEGIN
    print 'entering........ again'
    --print @curr
      set @sum = @sum + @curr
      --print @sum
      INSERT INTO #result SELECT * FROM  #temp where [id] = @id  --id, [name], turn
      DELETE FROM #temp WHERE id = @id
    END
     ELSE
    BEGIN    
    print 'breaaaking.-----'
      BREAK
    END 
  END

   SELECT TOP 1 [name] FROM #result r order by r.turn desc 

这是我使用Northwind测试的表的Create脚本:

USE [Northwind]
GO

/****** Object:  Table [dbo].[line]    Script Date: 28.05.2018 21:56:18 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[line](
    [id] [int] NOT NULL,
    [name] [varchar](255) NOT NULL,
    [weight] [int] NOT NULL,
    [turn] [int] NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
UNIQUE NONCLUSTERED 
(
    [turn] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[line]  WITH CHECK ADD CHECK  (([weight]>(0)))
GO

INSERT INTO [dbo].[line]
    ([id], [name], [weight], [turn])
VALUES
    (5, 'gary', 800, 1),
    (3, 'jo', 350, 2),
    (6, 'thomas', 400, 3),
    (2, 'will', 200, 4),
    (4, 'mark', 175, 5),
    (1, 'james', 100, 6)
;

Answers:


16

您应该尽量避免循环。它们通常不如基于集合的解决方案有效,并且可读性较低。

下面应该是非常有效的。

如果INCLUDE-在索引中可以包含name和weight列以避免进行键查找,则更是如此。

它可以按顺序扫描唯一索引turn并计算该Weight列的运行总计-然后使用LEAD相同的排序条件来查看下一行的运行总计。

一旦发现第一行超过或等于1000 NULL(表明没有下一行),它便可以停止扫描。

WITH T1
     AS (SELECT *,
                SUM(Weight) OVER (ORDER BY turn ROWS UNBOUNDED PRECEDING) AS cume_weight
         FROM   [dbo].[line]),
     T2
     AS (SELECT LEAD(cume_weight) OVER (ORDER BY turn) AS next_cume_weight,
                *
         FROM   T1)
SELECT TOP 1 name
FROM   T2
WHERE  next_cume_weight > 1000
        OR next_cume_weight IS NULL
ORDER  BY turn 

执行计划

在此处输入图片说明

在实践中,似乎要在严格必要的位置之前读取几行-看起来每个窗口假脱机/流聚合对都导致要读取另外两个行。

对于问题中的样本数据,理想情况下,它只需要从索引扫描中读取两行,但实际上它的读取值为6,但这不是一个显着的效率问题,并且不会随着向表中添加更多行而降低性能(例如这个演示

对于那些有兴趣在这个问题上与由每个操作者(如由输出的行的图像query_trace_column_values扩展事件)低于,行是在输出row_id顺序(开始于47第一行读出由索引扫描,并在精加工113TOP

单击下面的图像将其放大,或者查看动画版本以使流程更易于遵循

在右手流集合发出第一行的位置暂停动画(对于gary-turn = 1)。显然,它正在等待接收具有不同WindowCount的第一行(对于Jo-turn = 2)。并且窗口假脱机在读取具有不同turn字符的下一行之前,不会释放第一行“ Jo” (对于托马斯-turn = 3)

因此,窗口假脱机和流聚合都导致要读取另外一行,并且在计划中有四个,因此又增加了4行。

在此处输入图片说明

上面显示的各列的说明如下(基于此处的信息)

  • NodeName:索引扫描,NodeId:15,ColumnName:索引覆盖的id基表列
  • NodeName:索引扫描,NodeId:15,ColumnName:转向索引覆盖的基本表列
  • NodeName:聚集索引查找,NodeId:17,ColumnName:从查找中检索的权重基表列
  • NodeName:聚集索引查找,NodeId:17,ColumnName:从查找中检索到的名称基表列
  • NodeName:细分,NodeId:13,ColumnName:Segment1010在新组开始时返回1,否则返回null。由于仅第一行Partition By中的否SUM获得1
  • NodeName:序列项目,NodeId:12,ColumnName: row_number() Segment1010标志指示的组内的RowNumber1009。由于所有行都在同一组中,因此这是从1到6的升序整数rows between 5 preceding and 2 following。在类似的情况下,将用于过滤右帧行。(或LEAD以后)
  • NodeName:细分,NodeId:11,ColumnName:Segment1011在新组开始时返回1,否则返回null。由于仅第一行Partition By中的否SUM获得1(与Segment1010相同)
  • NodeName:窗口假脱机,NodeId:10,ColumnName:WindowCount1012该属性将属于窗口框架的行组合在一起。该窗口后台处理程序使用的“快速跟踪”案例UNBOUNDED PRECEDING。它在每个源行中发出两行。一种是累积值,另一种是详细值。尽管暴露的行之间没有明显的区别,但query_trace_column_values我认为实际存在累积列。
  • NodeName:流聚合,NodeId:9,ColumnName: Count(*) 根据计划由WindowCount1012分组的Expr1004,但实际上是运行计数
  • NodeName:流聚合,NodeId:9,ColumnName: SUM(weight) 根据计划由WindowCount1012分组的Expr1005,但实际上是运行权重之和(即cume_weight
  • NodeName:段,NodeId:7,ColumnName:Expr1002- CASE WHEN [Expr1004]=(0) THEN NULL ELSE [Expr1005] END不知道如何COUNT(*)将其设为0,因此将始终为sum(cume_weight
  • NodeName:段,NodeId:7,ColumnName:Segment1013partition byLEAD第一行上的否为1。所有剩余的为null
  • row_number()由Segment1013标志指示的组内的NodeName:序列项目,NodeId:6,ColumnName:RowNumber1006。由于所有行都在同一组中,因此这是从1到4的整数递增
  • NodeName:段,NodeId:4,ColumnName:BottomRowNumber1008 RowNumber1006 +1作为LEAD要求的下一行
  • NodeName:细分,NodeId:4,ColumnName:TopRowNumber1007 RowNumber1006 + 1,因为LEAD需要下一行
  • NodeName:段,NodeId:4,ColumnName:Segment1014partition byLEAD第一行上的否为1。所有剩余的为null
  • NodeName:窗口假脱机,NodeId:3,ColumnName:WindowCount1015该属性使用先前的行号将属于窗口框架的行组合在一起。的窗框LEAD最多有2行(当前一行和下一行)
  • 节点名:流聚合,节点Id:2的ColumnName:Expr1003 LAST_VALUE([Expr1002])LEAD(cume_weight)

6

出于好奇(因为问题指出T-SQL),也可以使用SQLCLR有效地解决此问题。

这个想法是一次读取一行,turn直到weight超过1000(或者我们用完了行),然后返回最后一次name读取。

源代码是:

using Microsoft.SqlServer.Server;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;

public partial class UserDefinedFunctions
{
    [SqlFunction(DataAccess = DataAccessKind.Read,
        SystemDataAccess = SystemDataAccessKind.None,
        IsDeterministic = true, IsPrecise = true)]
    [return: SqlFacet(IsFixedLength = false, IsNullable = true, MaxSize = 255)]
    public static SqlString Elevator()
    {
        const string query =
            @"SELECT L.[name], L.[weight]
            FROM dbo.line AS L
            ORDER BY L.turn;";

        using (var con = new SqlConnection("context connection = true"))
        {
            con.Open();
            using (var cmd = new SqlCommand(query, con))
            {
                var rdr = cmd.ExecuteReader(CommandBehavior.SingleResult);
                var name = SqlString.Null;
                var total = 0;

                while (rdr.Read() && (total += rdr.GetInt32(1)) <= 1000)
                {
                    name = rdr.GetSqlString(0);
                }
                return name;
            }
        }
    }
}

编译后的程序集和T-SQL函数:

CREATE ASSEMBLY Elevator AUTHORIZATION [dbo]
FROM 0x4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C010300606C0E5B0000000000000000E00022200B013000000C000000060000000000004A2A0000002000000040000000000010002000000002000004000000000000000600000000000000008000000002000000000000030060850000100000100000000010000010000000000000100000000000000000000000F82900004F00000000400000A802000000000000000000000000000000000000006000000C000000C02800001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000500A000000200000000C000000020000000000000000000000000000200000602E72737263000000A80200000040000000040000000E0000000000000000000000000000400000402E72656C6F6300000C00000000600000000200000012000000000000000000000000000040000042000000000000000000000000000000002C2A0000000000004800000002000500F0200000D007000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001B30030070000000010000117201000070730600000A0A066F0700000A723500007006730800000A0B07176F0900000A0C7E0A00000A0D1613042B0808166F0B00000A0D086F0C00000A2C14110408176F0D00000A5825130420E803000031DC091305DE14072C06076F0E00000ADC062C06066F0E00000ADC11052A011C000002001D003C59000A0000000002000B005863000A000000001E02280F00000A2A42534A4201000100000000000C00000076342E302E33303331390000000005006C000000AC010000237E0000180200002002000023537472696E677300000000380400008801000023555300C0050000100000002347554944000000D00500000002000023426C6F620000000000000002000001471502000900000000FA01330016000001000000110000000200000002000000010000000F000000050000000200000001000000020000000000E70001000000000006008A00A9010600BC00A9010600610096010F00C901000006000202F9000A0075005C010A003E005C010A0038005C010A00AA005C010A00DD00D8010A00120109020A002D0009020A00400109020A00050120010A00770113000A003301200106004D00F900000000000A00000000000100010001001000ED0100001500010001005020000000009600870150000100E820000000008618900106000200000000000000090090010100110090010600190090010A00310090010600490090010600590090011E0071000001060061009001230061004E012A005100F40031006900DA003500810028003B00810001003F0089005900060029009001060020002300860024002B00CE012E000B0055002E0013005E002E001B007D00100044000480000000000000000000000000000000008701000004000000000000000000000047001F000000000004000000000000000000000047001300000000000000000000476574496E743332003C4D6F64756C653E0053797374656D2E44617461006D73636F726C696200526561640053716C436F6D6D616E640053797374656D446174614163636573734B696E640049446973706F7361626C6500446973706F73650044656275676761626C654174747269627574650053716C46756E6374696F6E41747472696275746500436F6D70696C6174696F6E52656C61786174696F6E734174747269627574650053716C46616365744174747269627574650052756E74696D65436F6D7061746962696C6974794174747269627574650047657453716C537472696E6700456C657661746F722E646C6C004E756C6C0053797374656D004F70656E004462436F6E6E656374696F6E0053716C436F6E6E656374696F6E0053797374656D2E446174612E436F6D6D6F6E004462446174615265616465720053716C446174615265616465720045786563757465526561646572004D6963726F736F66742E53716C5365727665722E53657276657200436F6D6D616E644265686176696F7200456C657661746F72002E63746F720053797374656D2E446961676E6F73746963730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300446562756767696E674D6F6465730053797374656D2E446174612E53716C54797065730055736572446566696E656446756E6374696F6E73004F626A6563740053797374656D2E446174612E53716C436C69656E740000003363006F006E007400650078007400200063006F006E006E0065006300740069006F006E0020003D00200074007200750065000081510D000A00200020002000200020002000200020002000200020002000530045004C004500430054000D000A0020002000200020002000200020002000200020002000200020002000200020004C002E005B006E0061006D0065005D002C000D000A0020002000200020002000200020002000200020002000200020002000200020004C002E005B007700650069006700680074005D000D000A00200020002000200020002000200020002000200020002000460052004F004D002000640062006F002E006C0069006E00650020004100530020004C000D000A002000200020002000200020002000200020002000200020004F0052004400450052002000420059000D000A0020002000200020002000200020002000200020002000200020002000200020004C002E007400750072006E003B000D000A00200020002000200020002000200020002000200020002000006C68AC85076C6945BCECB2CE73B9787F000420010108032000010520010111110D0706122D123112351129081129042001010E062002010E122D0620011235113D0306112905200111290803200002042001080802060E08B77A5C561934E08904000011290801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F7773010801000200000000008146010004005455794D6963726F736F66742E53716C5365727665722E5365727665722E446174614163636573734B696E642C2053797374656D2E446174612C2056657273696F6E3D342E302E302E302C2043756C747572653D6E65757472616C2C205075626C69634B6579546F6B656E3D623737613563353631393334653038390A446174614163636573730100000054557F4D6963726F736F66742E53716C5365727665722E5365727665722E53797374656D446174614163636573734B696E642C2053797374656D2E446174612C2056657273696F6E3D342E302E302E302C2043756C747572653D6E65757472616C2C205075626C69634B6579546F6B656E3D623737613563353631393334653038391053797374656D446174614163636573730000000054020F497344657465726D696E69737469630154020949735072656369736501310100030054020D497346697865644C656E6774680054020A49734E756C6C61626C65015408074D617853697A65FF00000000000000606C0E5B00000000020000001C010000DC280000DC0A000052534453F8D0A90B6069F74DB5FBFABECC9D213901000000633A5C75736572735C7061756C775C736F757263655C7265706F735C4461746162617365325C4461746162617365325C6F626A5C52656C656173655C456C657661746F722E7064620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202A000000000000000000003A2A00000020000000000000000000000000000000000000000000002C2A0000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF2500200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001001000000018000080000000000000000000000000000001000100000030000080000000000000000000000000000001000000000048000000584000004C02000000000000000000004C0234000000560053005F00560045005200530049004F004E005F0049004E0046004F0000000000BD04EFFE00000100000000000000000000000000000000003F000000000000000400000002000000000000000000000000000000440000000100560061007200460069006C00650049006E0066006F00000000002400040000005400720061006E0073006C006100740069006F006E00000000000000B004AC010000010053007400720069006E006700460069006C00650049006E0066006F0000008801000001003000300030003000300034006200300000002C0002000100460069006C0065004400650073006300720069007000740069006F006E000000000020000000300008000100460069006C006500560065007200730069006F006E000000000030002E0030002E0030002E00300000003A000D00010049006E007400650072006E0061006C004E0061006D006500000045006C0065007600610074006F0072002E0064006C006C00000000002800020001004C006500670061006C0043006F00700079007200690067006800740000002000000042000D0001004F0072006900670069006E0061006C00460069006C0065006E0061006D006500000045006C0065007600610074006F0072002E0064006C006C0000000000340008000100500072006F006400750063007400560065007200730069006F006E00000030002E0030002E0030002E003000000038000800010041007300730065006D0062006C0079002000560065007200730069006F006E00000030002E0030002E0030002E0030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000C0000004C3A00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
WITH PERMISSION_SET = SAFE;
GO
CREATE FUNCTION dbo.Elevator ()
RETURNS nvarchar(255)
AS EXTERNAL NAME Elevator.UserDefinedFunctions.Elevator;

得到结果:

SELECT dbo.Elevator();

1

Martin Smith的解决方案略有不同

SELECT top 1 name
FROM (
    SELECT id, name, weight, turn
         , SUM(weight) OVER (ORDER BY turn) AS cumulative_weight
    FROM line                               
) as T
WHERE cumulative_weight <= 1000
ORDER BY turn DESC 

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 是默认的窗口框架,因此我没有声明。

使用当前累积权重的谓词代替下一个累积权重。

我没有检查任何计划,所以我无法确定这方面是否有所不同。


我知道,我被DB极客包围了:-)。我必须检查你们提到的所有关键字,以掌握它们的作用。我只看了一下Client statistics --> Total Execution Time,而不是Actual execution plan这里最有趣的地方。作为Client Statistics您的解决方案,它的速度要比Martin的慢一点。感谢您提供其他信息。哪种方法可以用来衡量不同方法之间的性能差异?
传奇'18

1
恐怕我对SQL Server的了解非常有限,因此在使用何种度量标准方面我没有太多的见识。马丁的回答中有db <> fiddle链接,也许您可​​以看一下那里的计划。
Lennart '18

1
我也没有检查计划,但可以想象这可能会计算整个表的运行总计,然后对与WHERE匹配的结果行​​进行排序。我怀疑它将使用检查约束来知道运行总量严格上升并且可以提前停止。同样在SQL Server中,除了使用批处理模式窗口聚合时,指定ROWS而不是RANGE更为可取,即使在没有重复的情况下也是如此,因为窗口假脱机不在内存中而不是光盘
Martin Smith,

@MartinSmith,有趣。在您的解决方案中,LEAD是否可以将T1内的next_cume_weight <10000谓词推入并从索引扫描中提早退出?我检查了查询计划,并ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW引入了Sequence Project (Compute Scalar)运算符。不用说,我不知道这意味着什么:-)
Lennart

1
索引按总和,前导和顶部所需的顺序交付行。top收到第一行后,它可以停止请求更多行,并且执行可以停止。
马丁·史密斯

0

您可以对自己进行联接:

select 
    a.id, a.turn, a.game, 
    coalesce(sum(b.weight), 0) as cumulative_weight
from
    table a
left join 
    table b
on
    a.turn > b.turn
group by
    a.id, a.turn, a.game ;

这种事情不是很有效,因为它会导致每行的选择。但是至少它表达为单个声明。

如果您不必完全用SQL进行操作,则只需选择所有行并遍历它们,然后就可以进行累加。

您也可以在没有临时表的情况下在存储过程中执行相同的操作。只需将总和和最后一行名称保存在变量中即可。


抱歉,我不知道如何使它与一起使用self-join,如果您可以制作一个可重现的示例,我已将表定义添加到我的问题中。我的SQL错误。...我需要最接近<= 1000磅的人员的姓名。
传说

看起来您的更新工作正常,如果您希望它产生的只是精确的输出,则需要进行一些调整。但是就像我说的那样,它并不是超级有效

好?我的ID为5的人为空...
传奇

这很奇怪,我希望sum()在0行中的总和返回0

超过0行的SUM不为0(不幸的是)。您需要使用COALESCE()ISNULL()函数或CASE表达式将其
设为
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.