На основе строки в одном столбце преобразовать одну строку в несколько строк и столбцов

#sql #sql-server

#sql #sql-сервер

Вопрос:

Я должен преобразовать одну строку в несколько строк на основе числовых значений в строке в одном столбце

Пример ввода:

 EmpId | work date  | String 
------ ------------ --------------------------------------------------------
 1234 | 12/10/2020 | The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters
 

Пример вывода:

 Empid | Work Date  | Clock | Radius 
------ ------------ ------- --------
1234  | 12/10/2020 | 12:03 | 209759
1234  | 12/10/2020 | 12:04 | 209758
 

Может быть n значений, основанных на числах в строке, которые необходимо разделить на два столбца и строки.

Пожалуйста, помогите мне с этим — спасибо

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

1. Пожалуйста, укажите правила для этого. Например, 12:03 это не «числовое значение», как я бы определил «числовое». Их всегда два? Что, если существует несколько групп? И почему вы используете неподдерживаемую версию SQL Server?

2. SQL действительно плох в подобных манипуляциях со строками. Сделайте это на C # или подобном. Лучшая идея, сначала поместите ее в базу данных в отдельные строки и столбцы

3. Привет, я новичок в Microsoft Sql server. версия сервера, которую я использую, равна 15, их будет много, не ограничиваясь двумя, в строке будет больше. мне нужно разделить его на основе, и он должен отображаться в двух отдельных столбцах

4. Если вы знаете, что текст всегда имеет форму The following clocks [list of times] are outside of the allowed radius by [list of integers] meters , то это вполне выполнимо в T-SQL, используя CHARINDEX и любое количество хорошо известных подходов разделения строк, если STRING_SPLIT оно недоступно. Для любого шаблона, более общего, чем этот, он становится сомнительным. Я согласен с другими, что вам гораздо лучше определить процесс, который генерирует этот текст в первую очередь, и заставить его создавать строки вместо текста.

Ответ №1:

Пожалуйста, попробуйте следующее решение.

Это грязно, но работает:

  1. Первый CTE маркирует столбцы free_text с помощью XML и XQuery и отфильтровывает токены без запятой.
  2. 2-й CTE получает столбец часов из XML.
  3. 3-й CTE получает столбец Radius из XML.
  4. Окончательный ВЫБОР объединяет все это.

SQL

 -- DDL and sample data population, start
DECLARE @tbl TABLE (emp_id INT, work_date DATE, free_text NVARCHAR(MAX))
INSERT INTO @tbl (emp_id, work_date, free_text) VALUES
(1234, '12/10/2020',N'The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters');
-- DDL and sample data population, end

DECLARE @separator CHAR(1) = SPACE(1)
    , @comma CHAR(1) = ',';

WITH rs AS
(
    SELECT emp_id, work_date
        , CAST('<root><r><![CDATA['   
            REPLACE(free_text COLLATE Czech_BIN2, @separator, ']]></r><r><![CDATA[')   ']]></r></root>' AS XML)
        .query('
        for $x in /root/r
        where contains($x, sql:variable("@comma"))
        return $x
        ') AS result
    FROM @tbl
), clock AS
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS seq
        , rs.*
        , z.value AS clock
    FROM rs
        CROSS APPLY result.nodes('/r[1]') AS t(c)
        CROSS APPLY STRING_SPLIT(c.value('.', 'VARCHAR(20)'), @comma) AS z
), Radius AS
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS seq
        , rs.*
        , z.value AS Radius
    FROM rs
        CROSS APPLY result.nodes('/r[2]') AS t(c)
        CROSS APPLY STRING_SPLIT(c.value('.', 'VARCHAR(20)'), @comma) AS z
)
SELECT c.emp_id, c.work_date, c.clock, r.Radius
FROM clock AS c
    INNER JOIN Radius AS r ON r.seq = c.seq
        AND r.emp_id = c.emp_id;
 

Вывод

  -------- ------------ ------- -------- 
| emp_id | work_date  | clock | Radius |
 -------- ------------ ------- -------- 
|   1234 | 2020-12-10 | 12:03 | 209759 |
|   1234 | 2020-12-10 | 12:04 | 209758 |
 -------- ------------ ------- -------- 
 

Ответ №2:

Еще один беспорядочный вариант — без XML, но с использованием заимствованного табличного решения «Tally», которое может оказаться полезным для повышения производительности при больших объемах исходных данных

Спасибо Ицхаку Хабинскому, поскольку его версия абсолютно верна и, вероятно, проще в обслуживании — я заблудился со своей версией XML, поэтому перешел к таблице подсчета. Просто предостережение от решения от Ицхака, если предоставлено больше показаний radius, чем предоставлено показаний часов — мы не узнаем об этом из результатов напрямую — вся запись исключается.

Вот мое (возможно, длинное) решение

Шаг 1) Создайте таблицу подсчета

Шаг 2) Создайте табличную функцию, которая связывает и преобразует значение с разделителями в таблицу с числом строк

Шаг 3 — Развертывание табличной функции, которая соответствующим образом анализирует строку и возвращает таблицу с преобразованными записями

Шаг 4 Тестовые примеры — и пример использования

Шаг 5 — Измените функцию main, чтобы скрыть все столбцы справа от столбца «Radius»

Шаг 1) Создайте таблицу подсчета из https://www.sqlservercentral.com/articles/the-numbers-or-tally-table-what-it-is-and-how-it-replaces-a-loop-1

 --=============================================================================
--      Setup Tally Table
--=============================================================================
    USE TempDB     --DB that everyone has where we can cause no harm
    SET NOCOUNT ON --Supress the auto-display of rowcounts for appearance/speed
DECLARE @StartTime DATETIME    --Timer to measure total duration
    SET @StartTime = GETDATE() --Start the timer
--=============================================================================
--      Create and populate a Tally table
--=============================================================================
--===== Conditionally drop and create the table/Primary Key
     IF OBJECT_ID('dbo.Tally') IS NOT NULL 
        DROP TABLE dbo.Tally
 CREATE TABLE dbo.Tally 
        (N INT, 
         CONSTRAINT PK_Tally_N PRIMARY KEY CLUSTERED (N))
--===== Create and preset a loop counter
DECLARE @Counter INT
    SET @Counter = 1
--===== Populate the table using the loop and couner
  WHILE @Counter <= 11000
  BEGIN
         INSERT INTO dbo.Tally
                (N)
         VALUES (@Counter)
            SET @Counter = @Counter   1
    END
--===== Display the total duration
 SELECT STR(DATEDIFF(ms,@StartTime,GETDATE()))   ' Milliseconds duration'
 

Шаг 2) Создайте табличную функцию, которая связывает и преобразует значение с разделителями в таблицу с числом строк
Это гарантирует, что даже при наличии сотен показаний radius и clock — каждое из них будет быстро преобразовано в вложенную таблицу.

https://www.sqlservercentral.com/articles/tally-oh-an-improved-sql-8k-“csv-splitter”-function

Функция использует таблицу подсчета, созданную на шаге 1

 create function [dbo].[SplitAndSequence] (@pString nvarchar(4000), @pDelimiter char(1)) returns table return
 SELECT ItemNumber = ROW_NUMBER() OVER (ORDER BY t.N),
        Item       = SUBSTRING(@pString, t.N, CHARINDEX(@pDelimiter, @pString   @pDelimiter, t.N) - t.N)
   FROM TempDB.dbo.Tally t
  WHERE t.N <= DATALENGTH(@pString) 1 --DATATLENGTH allows for trailing space delimiters
    AND SUBSTRING(@pDelimiter   @pString, t.N, 1) = @pDelimiter
 

Шаг 3 — Развертывание табличной функции, которая соответствующим образом анализирует строку и возвращает таблицу с преобразованными записями

 CREATE function [dbo].[fn_ParseAndReturnObservations] (@Emp_ID varchar(50), @WorkDateStr varchar(10), @String nvarchar(300)) returns table 
-- select * from [fn_ParseAndReturnObservations]('1234', '12/10/2020', 'The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters')
return
with [cte_SourceData] as (
select [Emp_ID] = @Emp_Id -- '1234'
     , [Work Date] = convert(date,@WorkDateStr,103) -- convert(date,'12/10/2020',103)
     , [String] = @String -- 'The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters'
) -- end [cte_SourceData]
, [cte_Parse1] as (
select [Emp_ID], [Work Date]
     , [Clocks Position] = charindex('clocks ', [String])   len('clocks')
     , [String] 
  from [cte_SourceData] 
)  -- end [cte_Parse1]
, [cte_Parse2] as (
select [Emp_ID], [Work Date] , [Clocks Position]
     , [String From Clock Observations] = substring([String],[Clocks Position] 1, len([String]))
     , [String]
  from [cte_Parse1]
)  -- end [cte_Parse2]
, [cte_Parse3] as (
select [Emp_ID], [Work Date]
     , [Clock Observations] = substring([String From Clock Observations],1, charindex(' ',[String From Clock Observations])-1)
     , [Clocks Position], [String From Clock Observations]
     , [Radius Verbosity Position Pre Ends] = charindex(' are outside of the allowed radius by ', [String From Clock Observations])   len(' are outside of the allowed radius by ')
         --- Assumes no spaces between Clock Observations
     , [String]
  from [cte_Parse2]
) -- end [cte_Parse3]
, [cte_Parse4] as (
select [Emp_ID], [Work Date], [Clock Observations]
     , [String From Meters] = substring([String From Clock Observations],[Radius Verbosity Position Pre Ends] 1,len([String From Clock Observations]))
     , [Clocks Position] , [String From Clock Observations], [String]
  from [cte_Parse3]
) -- end [cte_Parse4]
, [cte_ReadyForDelimiting] as (
select [Emp_ID], [Work Date], [Clock Observations]
     , [Radius Observations] = substring([String From Meters],1, charindex(' meters', [String From Meters]))
--     , [String From Meters]     , [Clocks Position] , [String From Clock Observations], [String]
  from [cte_Parse4]
) -- end [cte_ReadyForDelimiting] 
, [cte_CountNumberOfSubColumns] as (
select [rdy].[Emp_ID], [rdy].[Work Date], [rdy].[Clock Observations], [rdy].[Radius Observations]
     , [Clock Observation Count] =  (len([rdy].[Clock Observations]) - len(replace([rdy].[Clock Observations],',',''))) / (len([rdy].[Clock Observations]) - len(replace([rdy].[Clock Observations],';',''))   1)   1
     , [Radius Observation Count] =  (len([Radius Observations]) - len(replace([Radius Observations],',',''))) / (len([Radius Observations]) - len(replace([Radius Observations],';',''))   1)   1
  from [cte_ReadyForDelimiting] [rdy]
) -- end [cte_CheckNumberOfColumns] 
, [cte_ParsingClockObservations] as (
select [src].[Emp_ID], [src].[Work Date], [src].[Clock Observations], [src].[Radius Observations]
       ,  [src].[Clock Observation Count] , [src].[Radius Observation Count]
       , [Clock Record Sequence] = [Times].[ItemNumber]
       , [Clock] = [Times].[Item]
from [cte_CountNumberOfSubColumns] [src]
  outer apply (select * from [tempDB].[dbo].[SplitAndSequence] ([src].[Clock Observations],','))  [Times]
) -- [cte_ParsingClockObservations] 
, [cte_ParsingRadiusObservations] as (
select [src].[Emp_ID], [src].[Work Date], [src].[Clock Observations], [src].[Radius Observations]
       ,  [src].[Clock Observation Count] , [src].[Radius Observation Count]
       , [Radius Record Sequence] = [Radius].[ItemNumber]
       , [Radius] = [Radius].[Item]
  from [cte_CountNumberOfSubColumns] [src]
  outer apply (select * from [tempDB].[dbo].[SplitAndSequence] ([src].[Radius Observations],','))  [Radius] --  on [Radius].[ItemNumber] = [Times].[ItemNumber]  */
 ) -- end [cte_ParsingRadiusObservations]
, [cte_TransposedRecords] as (
select [Emp_ID] = cast(isnull([times].[Emp_ID],[radius].[Emp_ID]) as varchar(10))
      , [Work Date] = isnull([times].[Work Date],[radius].[Work Date])
      , [Clock] = cast(case when isnull([times].[Clock],'N/A') in ('') then 'N/A' else isnull([times].[Clock],'N/A') end as varchar(10))
      , [Radius] = cast(case when isnull([radius].[Radius], 'N/A') in ('') then 'N/A' else isnull([radius].[Radius],'N/A') end as varchar(20))
      , [Stub] = 'StubStubStub'
      , [times].[Clock Record Sequence]
      , [radius].[Radius Record Sequence]
      , [times].[Clock Observations]
      , [times].[Radius Observations]
      , [times].[Clock Observation Count]
      , [times].[Radius Observation Count]
  from [cte_ParsingClockObservations] [times]
  full outer join [cte_ParsingRadiusObservations] [radius] on [radius].[Emp_ID] = [times].[Emp_ID] and [radius].[Work Date] = [times].[Work Date] and [radius].[Radius Record Sequence] = [times].[Clock Record Sequence] 
)
-- Original
select * from [cte_TransposedRecords]
 

Запустите тест с этой строкой кода (используя исходный набор данных запроса)

 select * from [fn_ParseAndReturnObservations]
   ('1234', 
   '12/10/2020', 
   'The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters')



Emp_ID     Work Date  Clock      Radius              
---------- ---------- ---------- --------------------
1234       2020-10-12 12:03      209759              
1234       2020-10-12 12:04      209758              
 

Шаг 4 Тестовые примеры — и пример использования
Замените последние три строки тестовых примеров для рабочей таблицы, которую вы хотите профилировать, на тактовое время и радиус re

 with cte_SampleTests as (
    select TestCase = 'Original Case with a break down', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04 are outside of the allowed radius by 209759,209758 meters'
  union  select TestCase = 'Three Readings', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05 are outside of the allowed radius by 209759,209758,209757 meters'
  union select TestCase = 'Forth Clock Position Listed but no value', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05, are outside of the allowed radius by 209759,209758,209757,209755 meters' 
  union select TestCase = 'Four Radius and Three Clock Readings', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05 are outside of the allowed radius by 209759,209758,209757,209755 meters'
  union select TestCase = 'Four Clock Readings and 3 Radius Readings', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05,12:06 are outside of the allowed radius by 209759,209758,209757 meters'
  union select TestCase = 'Four Clock Readings and Fourth Radius Reading missing', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05,12:06 are outside of the allowed radius by 209759,209758,209757, meters'
  union select TestCase = 'Four Clock Readings and third Radius Reading missing', [Test_Emp_ID] = '1234', [Test_WorkDateStr] = '12/10/2020', [Test_String] = 'The following clocks 12:03,12:04,12:05,12:06 are outside of the allowed radius by 209759,209758,,209757 meters'
)
select cte_SampleTests.[TestCase], x.*
      from [cte_SampleTests]
      cross apply (select * from [dbo].[fn_ParseAndReturnObservations]([cte_SampleTests].[Test_Emp_ID], [cte_SampleTests].[Test_WorkDateStr], [cte_SampleTests].[Test_String])) x

TestCase                                              Emp_ID     Work Date  Clock      Radius            
----------------------------------------------------- ---------- ---------- ---------- ------------------
Original Case with a break down                       1234       2020-10-12 12:03      209759            
Original Case with a break down                       1234       2020-10-12 12:04      209758            
Three Readings                                        1234       2020-10-12 12:03      209759            
Three Readings                                        1234       2020-10-12 12:04      209758            
Three Readings                                        1234       2020-10-12 12:05      209757            
Forth Clock Position Listed but no value              1234       2020-10-12 12:03      209759            
Forth Clock Position Listed but no value              1234       2020-10-12 12:04      209758            
Forth Clock Position Listed but no value              1234       2020-10-12 12:05      209757            
Forth Clock Position Listed but no value              1234       2020-10-12 N/A        209755            
Four Radius and Three Clock Readings                  1234       2020-10-12 12:03      209759            
Four Radius and Three Clock Readings                  1234       2020-10-12 12:04      209758            
Four Radius and Three Clock Readings                  1234       2020-10-12 12:05      209757            
Four Radius and Three Clock Readings                  1234       2020-10-12 N/A        209755            
Four Clock Readings and 3 Radius Readings             1234       2020-10-12 12:03      209759            
Four Clock Readings and 3 Radius Readings             1234       2020-10-12 12:04      209758            
Four Clock Readings and 3 Radius Readings             1234       2020-10-12 12:05      209757            
Four Clock Readings and 3 Radius Readings             1234       2020-10-12 12:06      N/A               
Four Clock Readings and Fourth Radius Reading missing 1234       2020-10-12 12:03      209759            
Four Clock Readings and Fourth Radius Reading missing 1234       2020-10-12 12:04      209758            
Four Clock Readings and Fourth Radius Reading missing 1234       2020-10-12 12:05      209757            
Four Clock Readings and Fourth Radius Reading missing 1234       2020-10-12 12:06      N/A               
Four Clock Readings and third Radius Reading missing  1234       2020-10-12 12:03      209759            
Four Clock Readings and third Radius Reading missing  1234       2020-10-12 12:04      209758            
Four Clock Readings and third Radius Reading missing  1234       2020-10-12 12:05      N/A               
Four Clock Readings and third Radius Reading missing  1234       2020-10-12 12:06      209757            
 

Шаг 5 — Измените функцию main, чтобы скрыть все столбцы справа от столбца «Radius»
Столбцы Stub и all справа от Stub перечислены для отображения «работы» — Может оказаться полезным иметь количество наблюдений Radius и Clock для последующего анализа и профилирования решений [Последовательность записей часов] и [Последовательность записей Radius]

Шаг 6 — вернитесь к исходному автору данных … и попросите их предоставить вам альтернативную структуру — XML не должен быть сложным