Is there a good way to do this in SQL?

你。 提交于 2019-12-06 05:20:42

问题


I am trying to solve the following problem entirely in SQL (ANSI or TSQL, in Sybase ASE 12), without relying on cursors or loop-based row-by-row processing.

NOTE: I already created a solution that accomplishes the same goal in application layer (therefore please refrain from "answering" with "don't do this in SQL"), but as a matter of principle (and hopefully improved performance) I would like to know if there is an efficient (e.g. no cursors) pure SQL solution.

Setup:

  • I have a table T with the following 3 columns (all NOT NULL):

    ---- Table T -----------------------------
    | item  | tag           | value          | 
    | [int] | [varchar(10)] | [varchar(255)] | 
    
  • The table has unique index on item, tag

  • Every tag has a form of a string "TAG##" where "##" is a number 1-99

  • Existing tags are not guaranteed to be contiguous, e.g. item 13 may have tags "TAG1", "TAG3", "TAG10".

  • TASK: I need to insert a bunch of new rows into the table from another table T_NEW, which only have items and values, and assign new tag to them so they don't violate unique index on item, tag.

    Uniqueness of values is irrelevant (assume that item+value is always unique already).

    ---- Table T_NEW --------------------------
    | item  | tag            | value          | 
    | [int] | STARTS AS NULL | [varchar(255)] | 
    
  • QUESTION: How can I assign new tags to all rows in table T_NEW, such that:

    • All item+tag combinations in a union of T and T_NEW are unique

    • Newly assigned tags should all be in the form "TAG##"

    • Newly assigned tags should ideally be the smallest available for a given item.

  • If it helps, you can assume that I already have a temp table #tags, with a "tag" column that contains 99 rows containing all the valid tags (TAG1..TAG99, one per row)


回答1:


I started a fiddle that will get you the list of available "open" tags by item. It does this using the #tags (AllTags) and doing an outer-join-where-null. You could use that to insert new tags from 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

or see this updated fiddle to do an update on T_New table. Essentially adds a row_number so we can pick the correct open tag to use in a single update statement. I padded the Tag names with a leading zero to simplify the sorting.

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

updated fiddle with poor mans row_number replacement that only works with distinct T_New values




回答2:


Try this:

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



回答3:


OK, here's a correct solution, tested to work on Sybase (H/T: big thanks to @ypercube for providing a solid basis for it)

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



回答4:


Inserting directly to 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 ;

Updating 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
    ) ;

Correction

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 ;


来源:https://stackoverflow.com/questions/14859134/is-there-a-good-way-to-do-this-in-sql

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!