【问题标题】:Is there a good way to do this in SQL?有没有在 SQL 中执行此操作的好方法?
【发布时间】:2013-01-29 07:53:11
【问题描述】:

我试图完全在 SQL(ANSI 或 TSQL,在 Sybase ASE 12 中)解决以下问题,而不依赖于游标或基于循环的逐行处理。

注意:我已经创建了一个在应用程序层实现相同目标的解决方案(因此请不要用“不要在 SQL 中执行此操作”来“回答”),但作为一个原则问题(希望提高性能)我想知道是否有一个有效的(例如无游标)纯 SQL 解决方案。

设置

  • 我有一个表 T,其中包含以下 3 列(全部不是 NULL):

    ---- Table T -----------------------------
    | item  | tag           | value          | 
    | [int] | [varchar(10)] | [varchar(255)] | 
    
  • 表在item, tag上具有唯一索引

  • 每个标签都有一个字符串“TAG##”的形式,其中“##”是一个数字 1-99

  • 不保证现有标签是连续的,例如项目 13 可能有标签“TAG1”、“TAG3”、“TAG10”。

  • TASK:我需要将一堆新行从另一个表 T_NEW 插入到表中,该表只有项目和值,并为它们分配新标签,这样它们就不会违反唯一索引在item, tag

    值的唯一性无关紧要(假设 item+value 始终是唯一的)。

    ---- Table T_NEW --------------------------
    | item  | tag            | value          | 
    | [int] | STARTS AS NULL | [varchar(255)] | 
    
  • 问题:如何为表 T_NEW 中的所有行分配新标签,例如:

    • T 和 T_NEW 联合中的所有项目+标签组合都是唯一的

    • 新分配的标签都应采用“TAG##”形式

    • 理想情况下,新分配的标签应该是给定项目可用的最小标签。

  • 如果有帮助,您可以假设我已经有一个临时表#tags,其中的“标签”列包含 99 行包含所有有效标签(TAG1..TAG99,每行一个)

【问题讨论】:

  • 所以你想从最低的可用标签号开始并填补空白?
  • @dotjoe - 差不多。不一定要 100% 完美,但如果 TAG1-96 是免费的,我不想从“TAG97”开始并在 4 行之后用完该项目的标签。

标签: sql tsql unique-constraint sap-ase


【解决方案1】:

直接插入t:

INSERT INTO t
    (item, tag, value) 
SELECT 
    item, 
    ( SELECT MIN(tags.tag)
      FROM #tags AS tags
        LEFT JOIN t AS o
          ON  tags.tag = o.tag
          AND o.item_id = n.item_id 
      WHERE o.tag IS NULL
    ) AS tag,
    value  
FROM
    t_new AS n ;

更新t_new

UPDATE
    t_new AS n
SET
    tag =  
    ( SELECT MIN(tags.tag)
      FROM #tags AS tags
        LEFT JOIN t AS o
          ON  tags.tag = o.tag
          AND o.item_id = n.item_id 
      WHERE o.tag IS NULL
    ) ;

更正

UPDATE
    n
SET
    n.tag = w.tag
FROM
    ( SELECT item_id,
             tag,
             ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY value) AS rn
      FROM t_new
    ) AS n
  JOIN
    ( SELECT di.item_id,
             tags.tag,
             ROW_NUMBER() OVER (PARTITION BY di.item_id ORDER BY tags.tag) AS rn
      FROM 
          ( SELECT DISTINCT item_id
            FROM t_new
          ) AS di
        CROSS JOIN 
          #tags AS tags
        LEFT JOIN
          t AS o
            ON  tags.tag = o.tag
            AND o.item_id = di.item_id 
      WHERE o.tag IS NULL
    ) AS w
    ON  w.item_id = n.item_id
    AND w.rn = n.rn ;

【讨论】:

  • 抱歉,我没有要测试的 Sybase 安装。
  • 它可能不喜欢它们作为内联子查询。我们可以在FROM 子句中移动它们,可能使用GROUP BY
  • 是的,我认为 TOP 在 Sybase 中可用。我之所以这么说是因为它比MAX()更灵活(TOP+ORDER BY)。
  • 我很抱歉 - 我不接受和反对。我刚刚意识到,当 t_new 表的 >1 行具有相同的项目时,这不起作用(它应该为这些行中的每一行分配不同的标签,但它没有)。正如 Q 所说:“T 和 T_NEW 联合中的所有项目+标签组合都是唯一的
  • @DVK:啊,是的,你是对的。我会更新一个新版本。
【解决方案2】:

我创建了一个fiddle,它将按项目为您提供可用“打开”标签的列表。它使用#tags (AllTags) 并执行outer-join-where-null 来执行此操作。您可以使用它从 T_New 插入新标签...

with T_openTags as (
  select 
    items.item,
    openTagName = a.tag
  from
    (select distinct item from T) items
    cross join AllTags a
    left outer join T on 
      items.item = T.item
      and T.tag = a.tag
  where
    T.item is null
 )

select * from T_openTags

或查看updated fiddle 对 T_New 表进行更新。本质上添加了一个 row_number,因此我们可以选择正确的打开标记以在单个更新语句中使用。我用前导零填充标签名称以简化排序。

with T_openTags as (
  select 
    items.item,
    openTagName = a.tag,
    rn = row_number() over(partition by items.item order by a.tag)
  from
    (select distinct item from T) items
    cross join AllTags a
    left outer join T on 
      items.item = T.item
      and T.tag = a.tag
  where
    T.item is null

), T_New_numbered as (

  select *, 
     rn = row_number() over(partition by item order by value) 
  from T_New
)

update tnn set tag = openTagName
from T_New_numbered tnn
inner join T_openTags tot on 
  tot.item = tnn.item
  and tot.rn = tnn.rn


select * from T_New

更新了 fiddle,替换了可怜的 mans row_number,仅适用于不同的 T_New 值

【讨论】:

  • 我不确定...我以前从未使用过 sybase。
  • 我的问题是针对 Sybase 的。 Oraclish/MSSQL 解决方案对我没有真正的帮助:(
  • 对不起,我看到了 tsql,不再看标签了
  • 根据我的搜索,Sybase 支持 CTE 和窗口函数。
  • Sybase 12 似乎不支持 CROSS JOIN - 例如issues.jboss.org/browse/TEIID-1567?_sscc=t。我什至看到提到 Sybase 15 不支持但不确定
【解决方案3】:

好的,这是一个正确的解决方案,经过测试可以在 Sybase 上运行(H/T:非常感谢 @ypercube 为其提供了坚实的基础)

declare @c int
select @c = 1
WHILE (@c > 0)
BEGIN

    UPDATE
        t_new
    SET
        tag =  
        ( SELECT min(tags.tag)
          FROM #tags tags
            LEFT JOIN t o
              ON  tags.tag = o.tag
              AND o.item = t_new.item
            LEFT JOIN t_new n3
              ON  tags.tag = n3.tag
              AND n3.item = t_new.item
          WHERE o.tag IS NULL
          AND n3.tag IS NULL
        )
        WHERE tag IS NULL
        -- and here's the main magic for only updating one item at a time
        AND NOT EXISTS (SELECT 1 FROM t_new n2 WHERE t_new.value > n2.value 
                        and n2.tag IS NULL and n2.item=t_new.item)
        SELECT @c = @@rowcount
END

【讨论】:

  • @dotjoe - 如果您仔细阅读问题,请求不是避免循环,而是避免 基于循环的逐行处理(例如,没有光标或它的仿真)。这个循环离逐行很远——当按项目分组时,它的迭代次数是 max(count(*))。
  • IOW,如果每个 item 最多有 5 个重复项,则循环将总共执行 5 次,即使有 100000 个 item。最坏的情况是 100 次迭代,因为我们最多只能使用 100 个标签。
【解决方案4】:

试试这个:

DECLARE @T TABLE (ITEM  INT, TAG VARCHAR(10), VALUE VARCHAR(255))
INSERT INTO @T VALUES 
(1,'TAG1', '100'),
(2,'TAG2', '200')

DECLARE @T_NEW TABLE (ITEM  INT, TAG VARCHAR(10), VALUE VARCHAR(255))
INSERT INTO @T_NEW VALUES 
(3,NULL, '500'),
(4,NULL, '600')

INSERT INTO @T
SELECT
    ITEM,
    ('TAG' + CONVERT(VARCHAR(20),ITEM)) AS TAG,
    VALUE
FROM 
   @T_NEW

SELECT * FROM @T

【讨论】:

    猜你喜欢
    • 2021-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多