Обновление столбцов строки из столбца, разделенного запятыми, из той же таблицы - PullRequest
0 голосов
/ 17 мая 2018

У меня есть следующее определение таблицы

CREATE TABLE _Table 
(
    [Pat] NVARCHAR(8), 
    [Codes] NVARCHAR(50), 
    [C1] NVARCHAR(6), 
    [C2] NVARCHAR(6), 
    [C3] NVARCHAR(6), 
    [C4] NVARCHAR(6), 
    [C5] NVARCHAR(6)
);
GO

INSERT INTO _Table ([Pat], [Codes], [C1], [C2], [C3], [C4], [C5])
VALUES
    ('Pat1', 'U212,Y973,Y982', null, null, null, null, null),
    ('Pat2', 'M653', null, null, null, null, null), 
    ('Pat3', 'U212,Y973,Y983,Z924,Z926', null, null, null, null, null);
GO  

SQL Fiddle здесь .

Теперь я хотел бы разделить коды для каждой строки и заполнить столбцы Cn, чтобы мы получили

Pat     Codes                       C1      C2      C3      C4      C5
Pat1    'U212,Y973,Y982'            U212    Y973    Y982    NULL    NULL
Pat2    'M653'                      M653    NULL    NULL    NULL    NULL
Pat3    'U212,Y973,Y983,Z924,Z926'  U212    Y973    Y983    Z924    Z926

Я смотрю на динамический SQL, но есть ли лучший способ ...

Я начал спускаться по маршруту CTE, но я слаб здесь. Я, по сути, зацикливаюсь, удаляя первый разделенный запятыми код и использую left, чтобы получить этот код и выбрал его как C1.

;WITH tmp([Pat], [Codes], [C1], [C2], [C3], [C4], [C5]) AS
(
    SELECT
        Pat,
        STUFF(Codes, 1, CHARINDEX(',', Codes + ','), ''), 
        LEFT(Codes, CHARINDEX(',', Codes + ',') - 1), 
        [C2], 
        [C3], 
        [C4], 
        [C5]
    FROM _Table 
    UNION all
    SELECT
        Pat,
        STUFF(Codes, 1, CHARINDEX(',', Codes + ','), ''), 
        LEFT(Codes, CHARINDEX(',', Codes + ',') - 1), 
        [C2], 
        [C3], 
        [C4], 
        [C5]
    FROM _Table 
    WHERE
        Codes > ''
)
SELECT Pat, Codes,  [C1], [C2], [C3], [C4], [C5] 
FROM tmp 
ORDER BY Pat

Это работает для одного кода, но как мне сделать все 5? Обратите внимание, на практике это может увеличиться до N кодов.

Ответы [ 6 ]

0 голосов
/ 23 мая 2018

Мое предложение использует обновляемый CTE и XML в качестве разделителя. XML позволяет адресовать каждый фрагмент по его позиции:

WITH updatableCTE AS
(
    SELECT *
          ,CAST('<x>' + REPLACE(Codes,',','</x><x>') + '</x>' AS XML) AS CastedToXml
    FROM _Table
)
UPDATE updatableCTE SET  C1=CastedToXml.value('/x[1]','nvarchar(6)')
                        ,C2=CastedToXml.value('/x[2]','nvarchar(6)')  
                        ,C3=CastedToXml.value('/x[3]','nvarchar(6)')  
                        ,C4=CastedToXml.value('/x[4]','nvarchar(6)')  
                        ,C5=CastedToXml.value('/x[5]','nvarchar(6)'); 

SELECT * FROM _Table

Это очень легко масштабировать до любого необходимого количества фрагментов.

0 голосов
/ 23 мая 2018

Хотя мне очень нравится ответ Алана Бурштейна, другой вариант предполагает использование DelimitedSplit8K Джеффа Модена, условное агрегирование и набор нескольких общих табличных выражений, чтобы получить нужные значения:

;WITH CTE1 AS
(
    SELECT Pat, Codes, ItemNumber, Item
    FROM _Table
    CROSS APPLY [dbo].[DelimitedSplit8K](Codes, ',')
), CTE2 AS
(
    SELECT  Pat, 
            Codes, 
            MAX(CASE WHEN ItemNumber = 1 THEN Item END) As V1, 
            MAX(CASE WHEN ItemNumber = 2 THEN Item END) As V2, 
            MAX(CASE WHEN ItemNumber = 3 THEN Item END) As V3, 
            MAX(CASE WHEN ItemNumber = 4 THEN Item END) As V4, 
            MAX(CASE WHEN ItemNumber = 5 THEN Item END) As V5
    FROM CTE1
    GROUP BY [Pat], [Codes]
)

UPDATE t
SET C1 = V1,
    C2 = V2,
    C3 = V3,
    C4 = V4,
    C5 = V5
FROM _Table t
JOIN CTE2 ON t.Pat = CTE2.Pat

Вы можете посмотреть онлайн-образец или тестер.

0 голосов
/ 22 мая 2018

Если я правильно понимаю требование, это предельно просто.Нет необходимости в разделении или другом типе функции, динамическом SQL, рекурсивных CTE, PIVOTING или любой другой skulduggery.

Для выполнения «разделения» вы можете использовать CROSS APPLY, например, так:

SELECT 
  Pat,
  Codes,
  C1 = SUBSTRING(Codes,1,ISNULL(d1.d-1,8000)),
  C2 = SUBSTRING(Codes,d1.d+1, d2.d-d1.d-1),
  C3 = SUBSTRING(Codes,d2.d+1, d3.d-d2.d-1),
  C4 = SUBSTRING(Codes,d3.d+1, d4.d-d3.d-1),
  C5 = SUBSTRING(Codes,d4.d+1, 8000)
FROM _Table
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes),0)))        d1(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d1.d+1),0))) d2(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d2.d+1),0))) d3(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d3.d+1),0))) d4(d);

Возвращает

Pat      Codes                         C1    C2     C3    C4    C5    
-------- ----------------------------- ----- ------ ----- ----- ------
Pat1     U212,Y973,Y982                U212  Y973   NULL  NULL  NULL
Pat2     M653                          M653  NULL   NULL  NULL  NULL
Pat3     U212,Y973,Y983,Z924,Z926      U212  Y973   Y983  Z924  Z926

Обратите внимание на супер-простой и очень эффективный план выполнения:

enter image description here

Вы можете упростить это еще больше, если коды всегда состоят из четырех символов, например:

SELECT
  Pat,
  Codes,
  C1 = NULLIF(SUBSTRING(Codes,1,4),''),
  C2 = NULLIF(SUBSTRING(Codes,6,4),''),
  C3 = NULLIF(SUBSTRING(Codes,11,4),''),
  C4 = NULLIF(SUBSTRING(Codes,16,4),''),
  C2 = NULLIF(SUBSTRING(Codes,21,4),'')
FROM _Table;

Чтобы выполнить обновление, вы должны сделать это для первого решения:

UPDATE _Table
SET 
  C1 = SUBSTRING(Codes,1,ISNULL(d1.d-1,8000)),
  C2 = SUBSTRING(Codes,d1.d+1, d2.d-d1.d-1),
  C3 = SUBSTRING(Codes,d2.d+1, d3.d-d2.d-1),
  C4 = SUBSTRING(Codes,d3.d+1, d4.d-d3.d-1),
  C5 = SUBSTRING(Codes,d4.d+1, 8000)
FROM _Table
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes),0)))        d1(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d1.d+1),0))) d2(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d2.d+1),0))) d3(d)
CROSS APPLY (VALUES (NULLIF(CHARINDEX(',',Codes,d3.d+1),0))) d4(d);

Если сновакоды состоят всего из четырех символов, тогда обновление так просто, что кажется обманом:

UPDATE _Table
SET C1 = NULLIF(SUBSTRING(Codes,1,4),''),
    C2 = NULLIF(SUBSTRING(Codes,6,4),''),
    C3 = NULLIF(SUBSTRING(Codes,11,4),''),
    C4 = NULLIF(SUBSTRING(Codes,16,4),''),
    C5 = NULLIF(SUBSTRING(Codes,21,4),'');
0 голосов
/ 22 мая 2018

Вот одно из возможных решений: SQL Fiddle

with cte (PAT, CNum, Indx) as 
(
  select PAT
  , 1
  , 1
  from _table

  union all

  select a.PAT
  , b.CNum + 1
  , charindex(',', a.CODES, b.Indx+1)+1
  from _table a
  inner join cte b
  on b.PAT = a.PAT
  where charindex(',', a.CODES, b.Indx+1) > 0
)
select t.PAT
--, t.CODES --include to see the original codes value
, max(case when c1.CNUM = 1 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end) C1
, max(case when c1.CNUM = 2 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end) C2
, max(case when c1.CNUM = 3 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end) C3
, max(case when c1.CNUM = 4 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end) C4
, max(case when c1.CNUM = 5 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end) C5
from _Table t
left outer join cte c1
on c1.PAT = t.PAT
left outer join cte c2
on c2.PAT = c1.PAT
and c2.CNum = c1.CNum + 1
group by t.PAT
--, t.CODES --include to see the original codes value

При этом используется рекурсивный CTE для извлечения начальных позиций каждого из значений в CODES. Первая позиция принимается за 1; последующие позиции являются индексом следующей запятой плюс одна (поэтому мы получаем символ после запятой). Мы записываем эти позиции в значение INDX.

У нас также есть CNum в нашем рекурсивном CTE, чтобы записать, с каким (C#) полем будет связана связанная запись; это простой счетчик для каждого возвращаемого результата.

т.е. Для примера данных по ссылке SQL Fiddle результат нашего CTE выглядит следующим образом:

PAT   | CNum    |   Indx
------+---------+-------
Pat1  |    1    |    1
Pat1  |    2    |    6
Pat1  |    3    |   11
Pat1  |    4    |   16
Pat1  |    5    |   21
Pat2  |    1    |    1
Pat3  |    1    |    1
Pat3  |    2    |    6
Pat3  |    3    |   11
Pat3  |    4    |   16
Pat3  |    5    |   21
Pat3  |    6    |   26
Pat3  |    7    |   31
Pat3  |    8    |   36
Pat3  |    9    |   41
Pat3  |   10    |   46
Pat4  |    1    |   1

Затем мы свернем это, используя наше выражение group by; по сути, делает круг, помещая каждое значение CNum в соответствующий столбец.

max только для того, чтобы мы игнорировали все нулевые значения / принимали единственный результат со значением для данного CNum.

Код case when c1.CNUM = 1 then substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) end выполняет подстроку между первым символом текущего элемента, принимая его длину до 1 перед следующим символом; или, если следующего символа нет, длина строки CODES.


Обновление

Вот обновленная версия, которая использует функцию pivot / вся остальная логика такая же, как указано выше: SQL Fiddle

with cte (PAT, CNum, Indx) as 
(
  select PAT
  , 1
  , 1
  from @table
  where CODES != ''
  and CODES is not null

  union all

  select a.PAT
  , b.CNum + 1
  , charindex(',', a.CODES, b.Indx+1)+1
  from @table a
  inner join cte b
  on b.PAT = a.PAT
  where charindex(',', a.CODES, b.Indx+1) > 0
)
select PAT
, CODES
, [1] C1
, [2] C2
, [3] C3
, [4] C4
, [5] C5
from 
(
  select t.PAT
  , t.CODES
  , c1.CNum CNum1
  , substring(t.CODES,c1.INDX,coalesce(c2.INDX,LEN(t.CODES)+2)-c1.INDX-1) value
  from @table t
  left outer join cte c1
  on c1.PAT = t.PAT
  left outer join cte c2
  on c2.PAT = c1.PAT
  and c2.CNum = c1.CNum + 1
) x
pivot
(
  max(value) 
  for CNum1 in ([1],[2],[3],[4],[5])
) pvt
order by PAT
0 голосов
/ 22 мая 2018

Я начал движение по маршруту CTE, но здесь я слаб.По сути, я зацикливаюсь, удаляя первый разделенный запятыми код и использую левую, чтобы получить этот код, и выбрал его как C1.

В SQL Server существует множество возможных ' CSV-сплиттеров .Я просто хочу дополнить вашу идею cte (рекурсивная версия в сочетании с условным агрегированием):

;WITH tmp([Pat], [Codes],x, lvl) AS
(
  SELECT
   Pat,
   Codes = CAST(STUFF(Codes,1,CHARINDEX(',', Codes + ','), '')AS NVARCHAR(MAX)), 
   x = CAST(LEFT(Codes, CHARINDEX(',', Codes + ',') - 1) AS NVARCHAR(MAX)),
   lvl = 1
  FROM _Table
  -- WHERE c1 IS NULL AND c2 IS NULL AND ...
  -- to avoid recalculating the same rows if run many times
  UNION ALL
  SELECT  Pat,
    Codes = STUFF(Codes,1,CHARINDEX(',', Codes + ','), ''), 
    x= LEFT(Codes, CHARINDEX(',', Codes + ',') - 1),
    lvl = lvl+1
  FROM tmp
  WHERE Codes > ''
)
SELECT Pat
    ,c1 = MAX(IIF(lvl=1, x, NULL))
    ,c2 = MAX(IIF(lvl=2, x, NULL))
    ,c3 = MAX(IIF(lvl=3, x, NULL))
    ,c4 = MAX(IIF(lvl=4, x, NULL))
    ,c5 = MAX(IIF(lvl=5, x, NULL))
    -- add more if necessary
FROM tmp 
GROUP BY Pat
-- OPTION (MAXRECURSION 0);

DBFiddle Demo


И UPDATE:

;WITH tmp([Pat], [Codes],x, lvl) AS
(
    SELECT
        Pat,
        Codes=CAST(STUFF(Codes,1,CHARINDEX(',',Codes+','),'')AS NVARCHAR(MAX)), 
        x = CAST(LEFT(Codes, CHARINDEX(',', Codes + ',') - 1) AS NVARCHAR(MAX)),
        lvl = 1
    FROM _Table
    UNION ALL
    SELECT  Pat,
        Codes = STUFF(Codes, 1, CHARINDEX(',', Codes + ','), ''), 
        x= LEFT(Codes, CHARINDEX(',', Codes + ',') - 1),
        lvl = lvl+1
    FROM tmp
    WHERE Codes > ''
), cte2 AS (
SELECT Pat
    ,c1 = MAX(IIF(lvl=1, x, NULL))
    ,c2 = MAX(IIF(lvl=2, x, NULL))
    ,c3 = MAX(IIF(lvl=3, x, NULL))
    ,c4 = MAX(IIF(lvl=4, x, NULL))
    ,c5 = MAX(IIF(lvl=5, x, NULL))
FROM tmp 
GROUP BY Pat
)
UPDATE _Table
SET c1 = c.c1
   ,c2 = c.c2
   ,c3 = c.c3
   ,c4 = c.c4
   ,c5 = c.c5
FROM _Table t
JOIN cte2 c
  ON t.Pat = c.Pat
 OPTION (MAXRECURSION 0);

DBFiddle Demo

Выход:

╔══════╦══════════════════════════╦══════╦══════╦══════╦══════╦══════╗
║ Pat  ║          Codes           ║  C1  ║  C2  ║  C3  ║  C4  ║  C5  ║
╠══════╬══════════════════════════╬══════╬══════╬══════╬══════╬══════╣
║ Pat1 ║ U212,Y973,Y982           ║ U212 ║ Y973 ║ Y982 ║ null ║ null ║
║ Pat2 ║ M653                     ║ M653 ║ null ║ null ║ null ║ null ║
║ Pat3 ║ U212,Y973,Y983,Z924,Z926 ║ U212 ║ Y973 ║ Y983 ║ Z924 ║ Z926 ║
╚══════╩══════════════════════════╩══════╩══════╩══════╩══════╩══════╝

Заключительная мысль: Правильный способ - нормализовать схему.

0 голосов
/ 17 мая 2018

Что MatBailie пытается заставить вас увидеть, так это то, что иметь список в виде столбца, разделенный запятыми, или столбцы C1 - C (n) - плохая идея.

Лучше сделать второй стол. Он будет иметь внешний ключ для родительской таблицы (_Table в вашем примере).

Для этого вам нужно сделать пару вещей.

Сначала добавьте столбец идентификаторов в _Table. Это даст вам уникальный ключ для использования в других таблицах. Идентификация - это простое решение без знания всего объема ваших данных. Вы можете сделать это другим, уникальным столбцом.

Затем создайте дочернюю таблицу, например, PatCodes. Это столбцы должны включать:

Id int not null (identity)
_TableId int not null (fk to _Table)
Code varchar(xx) not null 

(optional that might be nice to have)
Description varchar(xxx) null 
DateUpdated datetime null
Active bit not null default 1 

Таким образом, вы можете иметь коды от нуля до множества для каждой записи в _Table. Вам не нужно знать максимальное (которое может даже измениться в будущем) количество столбцов Cx, добавляемых в _Table. Вы не будете залиты большим количеством значений столбца NULL в _Table. У вас есть масса возможностей для управления обновлениями изменяющихся кодов для данной записи _Table. Я мог бы продолжать и продолжать о преимуществах правильной нормализации этого с самого начала.

Также легко изменить код для заполнения этой таблицы вместо неизвестного числа столбцов в _Table.

Просто сделайте это сейчас, прежде чем вы получите кучу неприятных данных в базе данных и / или приложении с множеством беспорядка вокруг ваших объектов _Table, это сэкономит вам много времени и усилий.

...