【问题标题】:How to effeciently update table that depends on previous records如何有效地更新依赖于先前记录的表
【发布时间】:2022-01-10 19:11:36
【问题描述】:

我的 SQL Server 表如下所示:

id  Distance        a   b   Grp
--------------------------------
1   0.0000000000    100 114 NULL
2   0.1000000000    64  125 NULL
3   0.1000000000    88  100 NULL
4   0.1000000000    65  125 NULL
5   0.1000000000    63  64  NULL
6   0.1000000000    65  66  NULL
7   0.2000000000    63  66  NULL
8   0.2000000000    10  61  NULL
9   0.2000000000    19  61  NULL
10  0.2000000000    30  61  NULL
11  0.2000000000    10  65  NULL
12  0.2000000000    10  94  NULL
13  0.2000000000    19  65  NULL
14  0.2000000000    19  94  NULL
15  0.2000000000    30  94  NULL
16  0.2000000000    60  94  NULL
17  0.2000000000    61  94  NULL

Grp栏应填写如下

  • 第一条记录Grp是1

  • 如果 a & b 的下一行的值在前面的任何行中,那么它将采用第一行 Grp

  • 如果下一行的 a 和 b 的值不在前面的任何行中,则 Grp 的值将是 max Grp + 1

  • 如果记录 id = 3,则 b = 100 的值存在于前几行中,它出现的第一个是 id = 1,即 Grp = 1,因此对于 id 3,Grp 将是 1

我的桌子应该是这样的:

id  Distance        a   b   Grp
--------------------------------
1   0.0000000000    100 114 1
2   0.1000000000    64  125 2
3   0.1000000000    88  100 1
4   0.1000000000    65  125 2
5   0.1000000000    63  64  2
6   0.1000000000    65  66  2
7   0.2000000000    63  66  2
8   0.2000000000    10  61  3
9   0.2000000000    19  61  3
10  0.2000000000    30  61  3
11  0.2000000000    10  65  2
12  0.2000000000    10  94  3
13  0.2000000000    19  65  2
14  0.2000000000    19  94  3
15  0.2000000000    30  94  3
16  0.2000000000    60  94  3
17  0.2000000000    61  94  3

我已经构建了这个运行良好的脚本,但是它非常慢,有什么方法可以让它变得更好(没有循环)?

DECLARE @T AS TABLE  
              (
                  id int IDENTITY, 
                  Distance decimal(18, 10), 
                  a int, 
                  b int, 
                  Grp int
              )

INSERT INTO @T(Distance, a, b)
    SELECT Distance, a, b 
    FROM MyTable 
    ORDER BY Distance

UPDATE @T
SET Grp = 1 
WHERE id = 1

DECLARE @i int = 2, @max int, @min int, 
        @grp int, @a int, @b int, @maxgrp int = 1

SELECT @max = MAX(id) FROM @T

WHILE @i <= @max 
BEGIN
    SELECT @a = a, @b = b 
    FROM @T 
    WHERE id = @i

    SELECT @min = MIN(id) 
    FROM @T 
    WHERE id < @i AND a IN (@a, @b) OR b IN (@a, @b)

    SELECT @grp = grp 
    FROM @T 
    WHERE id = @min

    IF @grp IS NULL
    BEGIN
        SET @maxgrp = @maxgrp  + 1 
        SET @grp = @maxgrp
    END
    
    UPDATE @T 
    SET Grp = @grp 
    WHERE id = @i

    SET @i = @i + 1
END

SELECT * FROM @T

【问题讨论】:

  • 为什么第 12 行是第 3 组?它有一个 10,到那时它是第 2 组和第 3 组的成员?
  • 第 12 行具有 a=10 和 b=94.. 显示的这些值中的任何一个(最小 ID)的第一条记录是 id = 8 的记录,其 Grp = 3

标签: sql sql-server tsql sql-update


【解决方案1】:

使用递归而不是循环的答案。


首先,确定第一行出现的任何值(来自 a 或 b)...

CREATE TABLE #node(
  row_id       INT,
  old_val      INT,
  new_val      INT,
  link_count   INT
  INDEX node_old_new CLUSTERED (link_count, old_val, new_val, row_id),
);

INSERT INTO
  #node
SELECT
  e.id, twinned.*, COUNT(*) OVER (PARTITION BY e.id)
FROM
  #example  AS e
CROSS APPLY
(
  SELECT e.a AS old_val, e.b AS new_val
  UNION ALL
  SELECT e.b AS old_val, e.a AS new_val
)
  AS twinned
WHERE
  NOT EXISTS (
    SELECT *
      FROM #example AS lookup
     WHERE twinned.new_val IN (lookup.a, lookup.b)
       AND lookup.id < e.id
  )
;

link_count = 2bothab第一次出现在此行,表示此行将开始一个新组。

以前从未见过link_count = 1new_val,但old_val 出现过。因此,一旦为 old_val 分配了一个组,我们就可以将该组传播到 new_val

这只是创建闭包表的树遍历。

WITH
  closure AS
(
  SELECT
    new_val                               AS val,
    DENSE_RANK() OVER (ORDER BY row_id)   AS grp,
    row_id                                AS row_id,
    0                                     AS depth
  FROM
    #node
  WHERE
    link_count = 2
  
  UNION ALL
  
  SELECT
    r.new_val,
    c.grp,
    r.row_id,
    c.depth + 1
  FROM
    closure   AS c
  INNER JOIN
    #node     AS r
      ON  r.old_val    = c.val
      AND r.link_count = 1
)

现在,对于ab 中的任何值,我们可以在闭包表中查找该值的组。我们可能会得到两个不同的组,一个来自查找a,另一个来自查找b;因此,我们采用从最早的行开始分配的组。

SELECT
  e.*, g.grp
FROM
  #example   e
CROSS APPLY
(
  SELECT TOP 1
    c.grp
  FROM
    #closure  AS c
  WHERE
    c.val IN (e.a, e.b)
  ORDER BY
    c.row_id
)
  AS g
ORDER BY
  e.id

【讨论】:

  • 太棒了.. 谢谢大家.. 我不得不在 10 小时后停止循环并更新 25% 的记录。您的查询是 10 分钟
  • @asmgx 不客气,欢迎投票和/或接受。出于兴趣,这三个步骤中的每一个都需要多长时间?
【解决方案2】:

以下查询产生您想要的输出。

WITH t3 as(
  SELECT *
  FROM 
    (SELECT id,Distance,a,b,rnk,
     CASE WHEN rnk > 0 THEN NULL ELSE grp END AS Grp,
     Row_Number() OVER(ORDER BY id) AS seq
     FROM
       (SELECT id,Distance,a,b,rnk,ROW_NUMBER() OVER(PARTITION BY rnk ORDER BY id) AS grp
        FROM
         (SELECT id,Distance,a,b,
          ISNULL((SELECT top 1 id FROM tb s2 WHERE s2.id < s1.id AND (s2.a = s1.a OR s2.b = s1.b OR s2.b = s1.a OR s2.a = s1.b)),0) AS rnk
          FROM tb s1) T) T) T
  WHERE Grp > 0
)

SELECT id,Distance,a,b,min(grp)
FROM
  (SELECT distinct * 
  FROM
   (SELECT t1.id,t1.Distance,t1.a,t1.b,t3.grp
   FROM 
     (SELECT id,Distance,a,b,grp,
        ISNULL((SELECT top 1 id FROM tb s2 WHERE s2.id < s1.id AND (s2.a = s1.a OR s2.b = s1.b OR s2.b = s1.a OR s2.a = s1.b)),0) AS rnk
      FROM tb s1) t1
   JOIN tb t2 ON t1.rnk = t2.id

   JOIN t3 ON
     t1.a = t3.a OR t1.a = t3.b OR 

     t1.b = t3.b OR t1.b = t3.a OR
   
     t2.b = t3.b OR t2.b = t3.a OR
   
     t2.a = t3.a OR t2.a = t3.b) t1
     
  UNION ALL  
     
  SELECT id,Distance,a,b,grp
  FROM t3) T
GROUP BY id,Distance,a,b
ORDER BY id

db<>fiddle中的演示

【讨论】:

    【解决方案3】:

    如果只需要Grp对其进行分组,可以这样简化:

    declare @t table (id int identity, Distance decimal(18,10)
                    , a int, b int, Grp int)
    insert @t (Distance, a, b)
    -- select Distance, a, b From MyTable order by Distance
    values
        (0.0,100,114),(0.1, 64,125),(0.1, 88,100),(0.1, 65,125),
        (0.1, 63, 64),(0.1, 65, 66),(0.2, 63, 66),(0.2, 10, 61),
        (0.2, 19, 61),(0.2, 30, 61),(0.2, 10, 65),(0.2, 10, 94),
        (0.2, 19, 65),(0.2, 19, 94),(0.2, 30, 94),(0.2, 60, 94),
        (0.2, 61, 94)
    
    declare @i int, @d float, @a int, @b int, @g int
    
    declare c1 cursor for select * from @t for update of Grp
    open c1
    fetch next from c1 into @i, @d, @a, @b, @g
    update @t set Grp = 1 where current of c1
    fetch next from c1 into @i, @d, @a, @b, @g
    while @@fetch_status = 0
    begin
        update @t set Grp =
            isNull((select top 1 (Grp)
                    from @t t2
                    where t2.id < @i
                      and (@a in (t2.a , t2.b)
                       or  @b in (t2.a , t2.b)))
                    , @i)
        where current of c1
        fetch next from c1 into @i, @d, @a, @b, @g
    end
    close c1
    deallocate c1
    
    -- If you need consecutive Grp numbers ..
    declare @u table (id int identity, Grp int)
    insert @u (Grp)
    select distinct Grp from @t order by Grp
    
    update @t set Grp = u.id
    from @t t
    join @u u on (u.Grp = t.Grp and u.Grp<>u.id)
    
    select * from @t
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-01-13
      • 2021-12-21
      • 2018-07-28
      • 1970-01-01
      • 2017-11-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多