С помощью SSIS ИЛИ T-SQL разделить столбец значений, разделенных запятыми и кавычками, на несколько столбцов

#sql #sql-server #tsql #parsing #ssis

#sql #sql-server #tsql #синтаксический анализ #ssis

Вопрос:

У меня есть данные, разделенные запятыми, в столбце с именем C0.
Данные в C0 выглядят следующим образом:

C0
«Pacey LLC.», 213830ZZ,11/1/2017,11/1/2017,»297,311.74″,»2,371.40″,0.00,»1,325.18″,0.00,42.22,»123,986.56″
Майк Miker,9814140VCD,12/1/2018,12/1/2018,»3,917,751.99″,»419,743.54″,»36,642.66″,»344,090.43″,0.00,10.00,»2,434,671.06″

И я хочу, чтобы это закончилось так:

F1 F1 F3 F4 F5 F6 F7 F8 F9 F10 F11
«Pacey LLC». 213830ZZ 11/1/2017 11/1/2017 297,311.74 2,371.40 0.00 1,325.18 0.00 42.22 123,986.56
Майк Майкер 9814140VCD 12/1/2018 12/1/2018 3,917,751.99 419,743.54 36,642.66 344,090.43 0.00 10.00 2,434,671.06

Я пробовал вложенные замены, но не смог найти шаблон для надежного поиска без регулярных выражений, который является T / SQL? Я также пробовал использовать ТОКЕНОВЫЙ подход в SSIS от этого разработчика, но ни один из них не принес результатов.

Подходы вложенной замены застряли в денежных полях, которые меньше 1000 (например, 0.00), а подход с использованием ТОКЕНА SSIS предполагает, что все поля разделены кавычками, чего в моем примере нет.

Комментарии:

1. Этот тип вещей чрезвычайно сложен в TSQL. Вы можете обнаружить, что функция SQLCLR, использующая регулярное выражение, является лучшим выбором. Мораль истории: не храните несколько фрагментов информации в одном столбце

2. Вы не можете надежно использовать регулярные выражения для данных CSV. CSV — это потоковый протокол, который правильно реализован с помощью конечного автомата. Я согласен с @Charlieface в том, что для этого следует написать синтаксический анализатор в среде SQL CLR, но не пытайтесь его регулярно выражать, используйте RFC 4180 Common Format и MIME-тип для файлов значений, разделенных запятыми (CSV) , в качестве ссылки.

Ответ №1:

Как вам уже говорили, TSQL — неправильный инструмент для этого. Тем не менее, это можно сделать (по крайней мере, для заданного набора). Если это одноразовое действие, вы можете попробовать. Если это повторяющаяся задача в реальном сценарии, я бы попытался получить данные в соответствующем формате.

Однако это будет работать для заданных строк:

 DECLARE @t1 TABLE(ID INT IDENTITY, YourString NVARCHAR(1000));
INSERT INTO @t1 VALUES(N'"Pacey LLC.",213830ZZ,11/1/2017,11/1/2017,"297,311.74","2,371.40",0.00,"1,325.18",0.00,42.22,"123,986.56"')
                     ,(N'Mike The Miker,9814140VCD,12/1/2018,12/1/2018,"3,917,751.99","419,743.54","36,642.66","344,090.43",0.00,10.00,"2,434,671.06"');
 

—Ваши данные включают даты в формате, зависящем от конкретной культуры (что-то действительно! плохо)
—Лучше переключиться на ISO8601
—Установка формата даты поможет, но НЕ рекомендуется

 SET DATEFORMAT dmy;
 

—первый cte будет использоваться APPLY вместе с вычисляемым TOP()
— Это позволит получить каждый отдельный символ, один за другим.

 WITH singleChars AS
(                    
SELECT t.ID
      ,A.Pos
      ,SUBSTRING(t.YourString,A.POs,1) AS CharOnPos
FROM @t1 t
CROSS APPLY(SELECT TOP (LEN(t.YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Pos) --master..spt_values can be any table with sufficient rows
)
 

—мы продолжаем с рекурсивным cte
—он будет проходить по строке и определять, находимся ли мы в области кавычек или нет

 ,recCTE AS
(
    SELECT *
          ,CASE WHEN CharOnPos='"' THEN 1 ELSE 0 END AS QuoteIsOpen
          ,CAST(CharOnPos AS NVARCHAR(MAX)) AS GrowingString
    FROM singleChars WHERE Pos=1

    UNION ALL

    SELECT sc.ID,sc.Pos,sc.CharOnPos
          ,A.QuoteIsStillOpen
          ,CONCAT(GrowingString,CASE WHEN sc.CharOnPos=N',' AND A.QuoteIsStillOpen=0 THEN N'$%amp;' ELSE sc.CharOnPos END)
    FROM singleChars sc
    INNER JOIN recCTE r ON sc.ID = r.ID AND sc.Pos=r.Pos 1 
    CROSS APPLY(VALUES(CASE WHEN sc.CharOnPos='"' THEN CASE WHEN r.QuoteIsOpen=1 THEN 0 ELSE 1 END ELSE r.QuoteIsOpen END )) A(QuoteIsStillOpen)
)
 

—этот CTE выполняет трюк с TOP 1 WITH TIES разделенным ORDER BY ROW_NUMBER()
—Результат будет включать последнюю строку рекурсии по идентификатору

 ,newlySeparated AS
(
    SELECT TOP 1 WITH TIES * FROM recCTE
    ORDER BY ROW_NUMBER() OVER(PARTITION BY ID ORDER BY Pos DESC)
)
 

—В финале SELECT используется трюк для разделения строк с сохранением позиции и типа

 SELECT A.*
FROM newlySeparated ns
CROSS APPLY OPENJSON(CONCAT(N'[["',REPLACE(REPLACE(ns.GrowingString,'"',''),'$%amp;','","'),N'"]]'))
WITH(Company    NVARCHAR(100)        '$[0]'
    ,Code1      NVARCHAR(100)        '$[1]'
    ,Date1      DATE                 '$[2]'
    ,Date2      DATE                 '$[3]'
    ,Decimal1   NVARCHAR(100)        '$[4]' --Using a numbers type might work here, this depends on your machine
    ,Decimal2   NVARCHAR(100)        '$[5]'
    ,Decimal3   NVARCHAR(100)        '$[6]'
    ,Decimal4   NVARCHAR(100)        '$[7]'
    ,Decimal5   NVARCHAR(100)        '$[8]'
    ,Decimal6   NVARCHAR(100)        '$[9]'
    ,Decimal7   NVARCHAR(100)        '$[10]') A
OPTION(MAXRECURSION 0);
 

Результат

  ---------------- ------------ ------------ ------------ -------------- ------------ ----------- ------------ ------ ------- -------------- 
| Pacey LLC.     | 213830ZZ   | 2017-01-11 | 2017-01-11 | 297,311.74   | 2,371.40   | 0.00      | 1,325.18   | 0.00 | 42.22 | 123,986.56   |
 ---------------- ------------ ------------ ------------ -------------- ------------ ----------- ------------ ------ ------- -------------- 
| Mike The Miker | 9814140VCD | 2018-01-12 | 2018-01-12 | 3,917,751.99 | 419,743.54 | 36,642.66 | 344,090.43 | 0.00 | 10.00 | 2,434,671.06 |
 ---------------- ------------ ------------ ------------ -------------- ------------ ----------- ------------ ------ ------- -------------- 
 

Комментарии:

1. В чем опасность использования дат, специфичных для конкретной культуры? И спасибо.

2. @MikeG данные, относящиеся к конкретной культуре (даже хуже, чем данные, относящиеся к конкретному языку ), зависят от настройки cutlure на исполняющей машине. Тот же код с тем же вводом может прерваться на другом сервере. Есть культуры, где 11/1/2017 первое ноября. Подобная дата 31/1/2017 приведет к ошибке (что на самом деле лучше), в то время как эта дата просто пройдет с ложным результатом. То же самое с 1-Dec-2017 , который сломался бы на немецком сервере, поскольку там был «Dez». То же самое с числами, так как 10.123 может быть больше 10 кб или всего 10 с некоторыми десятичными знаками. Понимаете, что я имею в виду?

3. @MikeG На самом деле я не знаю, верен ли мой результат… Я взял вашу дату как 11 января и 12 января. Еще раз подумав об этом, я предполагаю, что вы действительно хотели первое ноября и декабря??? Именно поэтому … 🙂

4. @MikeG Попробуйте выполнить вышеуказанное с dmy помощью и mdy после SET DATEFORMAT .

5. Это сработало для меня до этих изменений, поскольку я имел дело с датами дальше по течению.