【问题标题】:TSQL Splitting Comma Separated Field into Multiple Rows with a Row per user ColumnSQL将逗号分隔的字段拆分为多行,每个用户列一行
【发布时间】:2015-02-17 22:00:14
【问题描述】:

我正在使用 MS SQL Server 2008 中的旧表,需要创建一个 view 以以新系统需要的方式显示数据。这是旧表。

表格

id    userid    sport1                        sport1level               sport2                        sport2level
----------------------------------------------------------------------------------------------------------------------------------
1     11        Baseball                      Varsity                   Baseball                      Recreational
2     22        Baseball,Basketball           Varsity,Junior Varsity    Baseball                      Varsity
3     33        Soccer                        Varsity                   Soccer,Track & Field          Recreational,Intramural
4     44        null                          null                      Tennis                        Varsity
5     55        Volleyball                    Varsity                   null                          null
6     66        Baseball,Basketball           Varsity,Varsity           Soccer,Football               Varsity,Varsity
7     77        Baseball,Basketball,Rowing    Varsity,Varsity,Varsity   Soccer,Football,Volleyball    Varsity,Varsity,Recreational

这是我们正在寻找的结果:

结果

id    userid    sport            sportlevel1       sportlevel2
---------------------------------------------------------------------------------------
1_1   11        Baseball         Varsity           Recreational
2_1   22        Baseball         Varsity           Varsity
2_2   22        Basketball       Junior Varsity    null
3_1   33        Soccer           Varsity           Recreational
3_2   33        Track & Field    null              Intramural
4_1   44        Tennis           null              Varsity
5_1   55        Volleyball       Varsity           null
6_1   66        Baseball         Varsity           null
6_2   66        Basketball       Varsity           null
6_3   66        Soccer           null              Varsity
6_4   66        Football         null              Varsity
7_1   77        Baseball         Varsity           null
7_2   77        Basketball       Varsity           null
7_3   77        Rowing           Varsity           null
7_4   77        Soccer           null              Varsity
7_5   77        Football         null              Varsity
7_6   77        Volleyball       null              Recreational

注意事项:

  • 原始表格可能包含超过 2 个逗号分隔值(我添加了第 7 行来显示这一点
  • 旧表中的idint,但在新表中不一定需要这种方式
  • 您可能已经注意到新表的id{original id}_{incremental sport count per user} 的串联。如果您愿意,{incremental sport count per user}sub id,对于用户选择的每项运动。例如:userid = 2 选择了 2 个distinct 运动:baseballbasketball,即使 baseball 分为两列。

如果我必须创建辅助函数或其他什么,请告诉我。

如果您有任何问题或需要更多信息,请告诉我。

请不要试图询问为什么采用这种结构或试图为新格式提供更好的结构。谢谢

【问题讨论】:

  • 这里有一些提示:stackoverflow.com/questions/5738834/… 您可以将其放入返回表或插入临时表等的函数中...
  • 我已经能够通过 CTE 获得结果,但无法以他们需要的方式获得 id 列。我会把它放在这里,但我想要一个更简洁的解决方案,因为我不喜欢我的同事自己编写 CTE 的方式。
  • 什么是 CME?你是说cte吗?如果有,可以分享一下吗?
  • 罗弗!是的对不起!固定。
  • 哈哈。可以分享一下cte吗?另外,您能否将其发布为可消耗的 ddl 和数据,以便我们提供帮助。

标签: sql sql-server sql-server-2008 tsql


【解决方案1】:

不太优雅,但它可以完成工作:

WITH 
Data AS(
    SELECT *
    FROM (
    VALUES ( 1, 11, 'Baseball           ',               'Varsity               ',                 'Baseball            ', 'Recreational           ' )
    ,      ( 2, 22, 'Baseball,Basketball',               'Varsity,Junior Varsity',                 'Baseball            ', 'Varsity                ' )
    ,      ( 3, 33, 'Soccer             ',               'Varsity               ',                 'Soccer,Track & Field', 'Recreational,Intramural' )
    ,      ( 4, 44, NULL,                                NULL,                                     'Tennis              ', 'Varsity                ' )
    ,      ( 5, 55, 'Volleyball         ',               'Varsity               ',                 NULL,                   NULL                      )
    ,      ( 6, 66, 'Baseball,Basketball',               'Varsity,Varsity       ',                 'Soccer,Football     ', 'Varsity,Varsity        ' )
    ,      ( 7, 77, 'Baseball,Football,Rugby,Wrestling', 'Varsity,Varsity,Varsity,Junior Varsity', 'Rugby',                'Recreational'            )
    ) AS T(id, userid, sport1, sport1level, sport2, sport2level)
),
SplitValues AS(
    -- Substring logic is in the Anchor record ommited to prevent repetition of 
    -- code, therefor level 0 needs to be ignored
    SELECT
          id
        , userid
        , [level] = 0
        , sport1                   = sport1       
        , sport1level              = sport1level  
        , sport2                   = sport2       
        , sport2level              = sport2level  
        , sport1Remainder          = sport1       
        , sport1levelRemainder     = sport1level  
        , sport2Remainder          = sport2       
        , sport2levelRemainder     = sport2level  
    FROM data
    UNION ALL
    SELECT
           id
         , userid
         , [level] = [level] + 1
         , sport1                   = SUBSTRING(sport1Remainder, 1, ISNULL(NULLIF(CHARINDEX(',', sport1Remainder)- 1, -1), LEN(sport1Remainder)))
         , sport1level              = SUBSTRING(sport1levelRemainder, 1, ISNULL(NULLIF(CHARINDEX(',', sport1levelRemainder)- 1, -1), LEN(sport1levelRemainder)))
         , sport2                   = SUBSTRING(sport2Remainder, 1, ISNULL(NULLIF(CHARINDEX(',', sport2Remainder)- 1, -1), LEN(sport2Remainder)))
         , sport2level              = SUBSTRING(sport2levelRemainder, 1, ISNULL(NULLIF(CHARINDEX(',', sport2levelRemainder)- 1, -1), LEN(sport2levelRemainder)))
         , sport1Remainder          = SUBSTRING(sport1Remainder, NULLIF(CHARINDEX(',', sport1Remainder)+1, 1), LEN(sport1Remainder))
         , sport1levelRemainder     = SUBSTRING(sport1levelRemainder, NULLIF(CHARINDEX(',', sport1levelRemainder)+1, 1), LEN(sport1levelRemainder))
         , sport2Remainder          = SUBSTRING(sport2Remainder, NULLIF(CHARINDEX(',', sport2Remainder)+1, 1), LEN(sport2Remainder))
         , sport2levelRemainder     = SUBSTRING(sport2levelRemainder, NULLIF(CHARINDEX(',', sport2levelRemainder)+1, 1), LEN(sport2levelRemainder))
    FROM SplitValues
    WHERE sport1Remainder IS NOT NULL
        OR sport2Remainder IS NOT NULL
),
SplitRowsWithDifferentSport AS(
    SELECT id
         , userid
         , sport1
         , sport1level
         , sport1level2 = CASE WHEN sport1 = sport2 THEN sport2level END
    FROM SplitValues
    WHERE [level] <> 0

    UNION ALL

    SELECT id
         , userid
         , sport2
         , null
         , sport1level2 = sport2level
    FROM SplitValues
    WHERE ISNULL(sport1, '') <> sport2
        AND [level] <> 0
)
SELECT id           = CAST(S.id AS VARCHAR(max)) + '_' + 
                      CAST(ROW_NUMBER() OVER (PARTITION BY S.userid ORDER BY s.id) AS VARCHAR(max)) 
     , S.sport1
     , sport1level1 = MAX(S.sport1level)
     , sport1level2 = MAX(S.sport1level2)
FROM SplitRowsWithDifferentSport AS S
WHERE S.sport1 IS NOT NULL 
GROUP BY S.ID, S.userid, S.sport1
ORDER BY id

编辑:更改了 SplitValues CTE 以允许在单个列中进行多项运动。现在支持每行最多 99 项运动。如果您需要更高,请添加OPTION(MAXRECURSION 0) 以完全没有限制。

EDIT2:添加分组依据以摆脱多行上的相同运动。

【讨论】:

  • 哇!你太棒了。我正在准备其他用户要求的 CTE,但你的效果很好!我会修改它以使用我们的字段名称,并让你知道它是怎么回事!
  • 您可能需要稍微调整row_number() 表达式,使其按运动名称或(列和位置)排序。我认为它可能会像现在写的那样发生变化。
  • @MWillemse 您的解决方案适用于真实数据。但是,我确实看到了一个由于空间不足而没有显示的问题,也没有提到它。我的数据显示,每个运动列都可以有多个运动,而不是最多 2 个。我会参考这个更新我的问题。
  • 您的解决方案非常快!我喜欢它!但是,我刚刚在验证数据时发现了它的问题。如果需要,我会更新我最初的问题,但是如果您将第 7 行更改为( 7, 77, 'Baseball,Football,Rugby,Wrestling','Varsity,Varsity,Varsity,Junior Varsity','Rugby','Recreational' ),您将获得两次 Rugby。我最喜欢您的解决方案,因为它非常快,只需 5 秒即可处理所有 130,000 行而没有 where 子句。任何进一步的帮助都会很棒!我也会尝试自己调整它。
  • @MWillemse,谢谢!这是完美的!运行速度非常快(2 秒有 130k 条记录)
【解决方案2】:

您期望的输出没有经过优化,因为它提供了一种在 SportLevel2 中生成空值的方法。您应该将每项运动和每个级别存储为单独的行,例如:

nid userid  SportName   SportLevel
1_1 11  Baseball        Varsity
1_2 11  Baseball        Recreational
2_1 22  Baseball        Varsity
2_2 22  Baseball        Varsity
3_1 33  Soccer          Varsity
3_2 33  Soccer          Recreational
3_3 33  Track & Field   Recreation
4_2 44  Tennis          Varsity
5_1 55  Volleyball      Varsity
6_1 66  Baseball        Varsity
6_2 66  Soccer          Varsity
6_2 66  Basketball      Varsity
6_3 66  Football        Varsity

为此,您可以按如下方式使用 CTE:

DECLARE @tmp TABLE(id INT IDENTITY(1,1), userid INT , sport1 VARCHAR(150), sport1level VARCHAR(150), sport2 VARCHAR(150), sport2level VARCHAR(150))

INSERT INTO @tmp (userid, sport1, sport1level, sport2, sport2level)
VALUES(11, 'Baseball', 'Varsity', 'Baseball', 'Recreational'),
(22, 'Baseball,Basketball', 'Varsity,Junior Varsity', 'Baseball', 'Varsity'),
(33, 'Soccer', 'Varsity', 'Soccer,Track & Field', 'Recreational,Intramural'),
(44, null, null, 'Tennis', 'Varsity'),
(55, 'Volleyball', 'Varsity', null, null),
(66, 'Baseball,Basketball', 'Varsity,Varsity', 'Soccer,Football', 'Varsity,Varsity')

;WITH Sports AS
(
    --1) initial value
    --   a) no commas in sport1
    SELECT id, userid, 1 AS sportid, sport1 AS SportName, sport1level  AS SportLevel, 
        NULL AS SportNameRemainder, NULL AS SportLevelRemainder 
    FROM @tmp 
    WHERE CHARINDEX(',', sport1)=0 AND CHARINDEX(',', sport1level)=0
    UNION ALL
    --   b) no commas in sport2
    SELECT id, userid, 2 AS sportid, sport2 AS SportName, sport2level  AS SportLevel, 
        NULL AS SportNameRemainder, NULL AS SportLevelRemainder 
    FROM @tmp 
    WHERE CHARINDEX(',', sport2)=0 AND CHARINDEX(',', sport2level)=0
    UNION ALL
    --   c) commas in sport1
    SELECT id, userid, 1 AS sportid, LEFT(sport1, CHARINDEX(',', sport1)-1) AS SportName, LEFT(sport1level , CHARINDEX(',', sport1level)-1) AS SportLevel, 
        RIGHT(sport1, LEN(sport1) - CHARINDEX(',', sport1)) AS SportNameRemainder, LEFT(sport1level , LEN(sport1level) - CHARINDEX(',', sport1level)) AS SportLevelRemainder 
    FROM @tmp 
    WHERE CHARINDEX(',', sport1)>0 AND CHARINDEX(',', sport1level)>0
    UNION ALL
    --   d) commas in sport2
    SELECT id, userid, 2 AS sportid, LEFT(sport2, CHARINDEX(',', sport2)-1) AS SportName, LEFT(sport2level , CHARINDEX(',', sport2level)-1) AS SportLevel, 
        RIGHT(sport2, LEN(sport2) - CHARINDEX(',', sport2)) AS SportNameRemainder, LEFT(sport2level , LEN(sport2level) - CHARINDEX(',', sport2level)) AS SportLevelRemainder 
    FROM @tmp 
    WHERE CHARINDEX(',', sport2)>0 AND CHARINDEX(',', sport2level)>0
    UNION ALL
    --2) recursive part
    SELECT id, userid, sportid +1 AS sportid, SportNameRemainder AS SportName, SportLevelRemainder AS SportLevel, NULL AS SportNameRemainder, NULL AS SportLevelRemainder
    FROM Sports
    WHERE CHARINDEX(',', SportNameRemainder)=0 AND CHARINDEX(',', SportLevelRemainder)=0
)
SELECT CONCAT(CONVERT(VARCHAR(5), id), '_', CONVERT(VARCHAR(5), sportid)) AS nid, userid, SportName, SportLevel 
FROM Sports
ORDER BY id, userid, sportid 

请根据您的需要随意更改。

注意:例如,我建议将 SportLevel 中的字符串值替换为其数值,并且不要将 idSportLevel 连接起来: Varsity 的值可能为 1,Recreational - 2 等。SportName 应使用相同的逻辑。可能需要连接来自 2 个表的数据。如果您需要帮助,请致电 ;)

【讨论】:

  • 酷。我会在早上尝试一下。这就是我设计这种特殊情况的方式:)
  • 谢谢。这不适用于我们的情况,因为运动 1 被定义为过去的运动,运动 2 被定义为未来的兴趣运动。诚然,我可能会在新系统中添加一列来定义哪种运动是哪种类型,但我无法控制。不过感谢您的意见!
【解决方案3】:

试试这个:

WITH    Data
          AS ( SELECT   *
               FROM     ( VALUES
                        ( 1, 11, 'Baseball', 'Varsity', 'Baseball', 'Recreational'),
                        ( 2, 22, 'Baseball,Basketball', 'Varsity,Junior Varsity', 'Baseball', 'Varsity'),
                        ( 3, 33, 'Soccer', 'Varsity', 'Soccer,Track & Field', 'Recreational,Intramural'),
                        ( 4, 44, NULL , NULL , 'Tennis', 'Varsity'),
                        ( 5, 55, 'Volleyball', 'Varsity', NULL , NULL ),
                        ( 6, 66, 'Baseball,Basketball', 'Varsity,Varsity', 'Soccer,Football', 'Varsity,Varsity'),
                        ( 7, 77, 'Baseball,Basketball,Rowing', 'Varsity,Varsity,Varsity', 'Soccer,Football,Volleyball', 'Varsity,Varsity,Recreational') )
                        AS T ( id, userid, sport1, sportlevel1, sport2, sportlevel2 )
             ),
        Tally
          AS ( SELECT   n = 1
               UNION ALL
               SELECT   n + 1
               FROM     Tally
               WHERE    n <= 100
             ),
        Sprt1
          AS ( SELECT   ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
                        id ,
                        userid ,
                        CASE WHEN CHARINDEX(',', sport1) = 0 THEN sport1
                             ELSE SUBSTRING(sport1, T.n,
                                            CHARINDEX(',',
                                                      SUBSTRING(sport1 + ',',
                                                              t.n, 100)) - 1)
                        END AS sport1
               FROM     data AS d
                        JOIN Tally AS T ON COALESCE(LEN(d.sport1), 1) >= t.n
                                           AND SUBSTRING(','
                                                         + COALESCE(d.sport1,
                                                              ''), n, 1) = ','
             ),
        lvl1
          AS ( SELECT   ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
                        id ,
                        userid ,
                        CASE WHEN CHARINDEX(',', sportlevel1) = 0
                             THEN sportlevel1
                             ELSE SUBSTRING(sportlevel1, T.n,
                                            CHARINDEX(',',
                                                      SUBSTRING(sportlevel1
                                                              + ',', t.n, 100))
                                            - 1)
                        END AS sportlevel1
               FROM     data AS d
                        JOIN Tally AS T ON COALESCE(LEN(d.sportlevel1), 1) >= t.n
                                           AND SUBSTRING(','
                                                         + COALESCE(d.sportlevel1,
                                                              ''), n, 1) = ','
             ),
        sprt2
          AS ( SELECT   ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
                        id ,
                        userid ,
                        CASE WHEN CHARINDEX(',', sport2) = 0 THEN sport2
                             ELSE SUBSTRING(sport2, T.n,
                                            CHARINDEX(',',
                                                      SUBSTRING(sport2 + ',',
                                                              t.n, 100)) - 1)
                        END AS sport2
               FROM     data AS d
                        JOIN Tally AS T ON COALESCE(LEN(d.sport2), 1) >= t.n
                                           AND SUBSTRING(','
                                                         + COALESCE(d.sport2,
                                                              ''), n, 1) = ','
             ),
        lvl2
          AS ( SELECT   ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
                        id ,
                        userid ,
                        CASE WHEN CHARINDEX(',', sportlevel2) = 0
                             THEN sportlevel2
                             ELSE SUBSTRING(sportlevel2, T.n,
                                            CHARINDEX(',',
                                                      SUBSTRING(sportlevel2
                                                              + ',', t.n, 100))
                                            - 1)
                        END AS sportlevel2
               FROM     data AS d
                        JOIN Tally AS T ON COALESCE(LEN(d.sportlevel2), 1) >= t.n
                                           AND SUBSTRING(','
                                                         + COALESCE(d.sportlevel2,
                                                              ''), n, 1) = ','
             ),
        final
          AS ( SELECT   COALESCE(sprt1.RN, lvl1.RN, sprt2.rn, lvl2.rn) AS RN ,
                        COALESCE(sprt1.id, lvl1.id, sprt2.id, lvl2.id) AS id ,
                        COALESCE(sprt1.userid, lvl1.userid, sprt2.userid,
                                 lvl2.userid) AS userid ,
                        sprt1.sport1 ,
                        lvl1.sportlevel1 ,
                        sprt2.sport2 ,
                        lvl2.sportlevel2
               FROM     sprt1
                        FULL JOIN lvl1 ON sprt1.id = lvl1.id
                                          AND sprt1.RN = lvl1.RN
                        FULL JOIN sprt2 ON COALESCE(sprt1.id, lvl1.id) = sprt2.id
                                           AND COALESCE(sprt1.RN, lvl1.RN) = sprt2.RN
                        FULL JOIN lvl2 ON COALESCE(sprt1.id, lvl1.id, sprt2.id) = lvl2.id
                                          AND COALESCE(sprt1.RN, lvl1.RN,
                                                       sprt2.rn) = lvl2.RN
             )
    SELECT  CONVERT(VARCHAR, id) + '_'
            + CONVERT(VARCHAR, ROW_NUMBER() OVER ( PARTITION BY id ORDER BY userid , part , sport )) AS id ,
            userid ,
            sport ,
            sportlevel1 ,
            sportlevel2
    FROM    ( SELECT    1 part ,
                        id ,
                        userid ,
                        sport1 AS sport ,
                        sportlevel1 ,
                        CASE WHEN sport1 = sport2 THEN sportlevel2
                             ELSE NULL
                        END sportlevel2
              FROM      final
              WHERE     sport1 IS NOT NULL
              UNION ALL
              SELECT    2 ,
                        id ,
                        userid ,
                        sport2 ,
                        NULL ,
                        sportlevel2
              FROM      final
              WHERE     sport2 IS NOT NULL
                        AND sport2 != COALESCE(sport1, '')
            ) FinalDataset
OPTION  ( MAXRECURSION 1000 )

输出

存储过程代码

--Create demo temp table for testing
IF OBJECT_ID('Tempdb..#Data') IS NOT NULL 
    DROP TABLE #Data
SELECT  *
INTO    #Data
FROM    ( VALUES ( 1, 11, 'Baseball', 'Varsity', 'Baseball', 'Recreational'),
        ( 2, 22, 'Baseball,Basketball', 'Varsity,Junior Varsity', 'Baseball', 'Varsity'),
        ( 3, 33, 'Soccer', 'Varsity', 'Soccer,Track & Field', 'Recreational,Intramural'),
        ( 4, 44, NULL , NULL , 'Tennis', 'Varsity'),
        ( 5, 55, 'Volleyball', 'Varsity', NULL , NULL ),
        ( 6, 66, 'Baseball,Basketball', 'Varsity,Varsity', 'Soccer,Football', 'Varsity,Varsity'),
        ( 7, 77, 'Baseball,Basketball,Rowing', 'Varsity,Varsity,Varsity', 'Soccer,Football,Volleyball', 'Varsity,Varsity,Recreational') )
        AS T ( id, userid, sport1, sportlevel1, sport2, sportlevel2 );

----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
---      CODE BELOW CAN BE USED THRO STORED PROCEDURE, VIEW DOES NOT ALLOW TEMP TABLE USAGE      ---
----------------------------------------------------------------------------------------------------
--Create temp table with sequence num
IF OBJECT_ID('Tempdb..#Tally') IS NOT NULL --<<<~this code not required in Stored procedure, can be deleted
    DROP TABLE #Tally --<<<~this code not required in Stored procedure, can be deleted
CREATE TABLE #Tally ( N INT PRIMARY KEY )
DECLARE @i INT = 1
WHILE @i < 1000 
    BEGIN
        INSERT  INTO #Tally
                SELECT  @i
        SET @i = @i + 1
    END      
----------------------------------------------------------------------------------------------------
--split sport2 field and create temp table for final result  
IF OBJECT_ID('Tempdb..#sprt1') IS NOT NULL 
    DROP TABLE #sprt1         
SELECT  ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
        id ,
        userid ,
        CASE WHEN CHARINDEX(',', sport1) = 0 THEN sport1
             ELSE SUBSTRING(sport1, T.n,
                            CHARINDEX(',', SUBSTRING(sport1 + ',', t.n, 100))
                            - 1)
        END AS sport1
INTO    #sprt1
FROM    #data AS d
        JOIN #Tally AS T ON COALESCE(LEN(d.sport1), 1) >= t.n
                            AND SUBSTRING(',' + COALESCE(d.sport1, ''), n, 1) = ','
----------------------------------------------------------------------------------------------------
--split sportlevel1 field and create temp table for final result         
IF OBJECT_ID('Tempdb..#lvl1') IS NOT NULL 
    DROP TABLE #lvl1      
SELECT  ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
        id ,
        userid ,
        CASE WHEN CHARINDEX(',', sportlevel1) = 0 THEN sportlevel1
             ELSE SUBSTRING(sportlevel1, T.n,
                            CHARINDEX(',',
                                      SUBSTRING(sportlevel1 + ',', t.n, 100))
                            - 1)
        END AS sportlevel1
INTO    #lvl1
FROM    #data AS d
        JOIN #Tally AS T ON COALESCE(LEN(d.sportlevel1), 1) >= t.n
                            AND SUBSTRING(',' + COALESCE(d.sportlevel1, ''), n,
                                          1) = ','
----------------------------------------------------------------------------------------------------
--split sport2 field and create temp table for final result           
IF OBJECT_ID('Tempdb..#sprt2') IS NOT NULL 
    DROP TABLE #sprt2
SELECT  ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
        id ,
        userid ,
        CASE WHEN CHARINDEX(',', sport2) = 0 THEN sport2
             ELSE SUBSTRING(sport2, T.n,
                            CHARINDEX(',', SUBSTRING(sport2 + ',', t.n, 100))
                            - 1)
        END AS sport2
INTO    #sprt2
FROM    #data AS d
        JOIN #Tally AS T ON COALESCE(LEN(d.sport2), 1) >= t.n
                            AND SUBSTRING(',' + COALESCE(d.sport2, ''), n, 1) = ','
            
----------------------------------------------------------------------------------------------------
--split sportlevel2 field and create temp table for final result      
IF OBJECT_ID('Tempdb..#lvl2') IS NOT NULL 
    DROP TABLE #lvl2
SELECT  ROW_NUMBER() OVER ( PARTITION  BY Id, userid ORDER BY Id, userid , T.n ) AS RN ,
        id ,
        userid ,
        CASE WHEN CHARINDEX(',', sportlevel2) = 0 THEN sportlevel2
             ELSE SUBSTRING(sportlevel2, T.n,
                            CHARINDEX(',',
                                      SUBSTRING(sportlevel2 + ',', t.n, 100))
                            - 1)
        END AS sportlevel2
INTO    #lvl2
FROM    #data AS d
        JOIN #Tally AS T ON COALESCE(LEN(d.sportlevel2), 1) >= t.n
                            AND SUBSTRING(',' + COALESCE(d.sportlevel2, ''), n,
                                          1) = ','
 
----------------------------------------------------------------------------------------------------
--final data set          
IF OBJECT_ID('Tempdb..#Final') IS NOT NULL 
    DROP TABLE #Final
SELECT  COALESCE(S1.RN, L1.RN, S2.rn, L2.rn) AS RN ,
        COALESCE(S1.id, L1.id, S2.id, L2.id) AS id ,
        COALESCE(S1.userid, L1.userid, S2.userid, L2.userid) AS userid ,
        S1.sport1 ,
        L1.sportlevel1 ,
        S2.sport2 ,
        L2.sportlevel2
INTO    #Final
FROM    #sprt1 AS S1
        FULL JOIN #lvl1 AS L1 ON S1.id = L1.id
                                 AND S1.RN = L1.RN
        FULL JOIN #sprt2 AS S2 ON COALESCE(S1.id, L1.id) = S2.id
                                  AND COALESCE(S1.RN, L1.RN) = S2.RN
        FULL JOIN #lvl2 AS L2 ON COALESCE(S1.id, L1.id, S2.id) = L2.id
                                 AND COALESCE(S1.RN, L1.RN, S2.rn) = L2.RN
----------------------------------------------------------------------------------------------------
--Final output query
SELECT  CONVERT(VARCHAR, id) + '_'
        + CONVERT(VARCHAR, ROW_NUMBER() OVER ( PARTITION BY id ORDER BY userid , part , sport )) AS id ,
        userid ,
        sport ,
        sportlevel1 ,
        sportlevel2
FROM    ( SELECT    1 part ,
                    id ,
                    userid ,
                    sport1 AS sport ,
                    sportlevel1 ,
                    CASE WHEN sport1 = sport2 THEN sportlevel2
                         ELSE NULL
                    END sportlevel2
          FROM      #final
          WHERE     sport1 IS NOT NULL
          UNION ALL
          SELECT    2 ,
                    id ,
                    userid ,
                    sport2 ,
                    NULL ,
                    sportlevel2
          FROM      #final
          WHERE     sport2 IS NOT NULL
                    AND sport2 != COALESCE(sport1, '')
        ) FinalDataset
 

【讨论】:

  • 非常感谢。您提出的两种解决方案都运行良好!我会用我的数据来测试它。
  • 您的解决方案适用于真实数据。但是,我确实看到了一个由于空间不足而没有显示的问题,也没有提到它。我的数据显示,每个运动列都可以有多个运动,而不是最多 2 个。我会参考这个更新我的问题。
  • @RoLYroLLs,看看我更新的答案,还被告知导致您的帖子逻辑错误,用户 ID 为 77。目前,代码允许使用 10 个逗号,但您可以更改此值。如果需要超过 100,则必须在查询结束时使用 OPTION (MAXRECURSION 1000),因为默认情况下递归不能大于 100。
  • 谢谢我更新了逻辑,我会用我的真实数据尝试你更新的解决方案并告诉你。
  • 这非常适合我们的数据。但是,对于 42 行,处理和返回 163 行需要 1 分 40 秒。有什么办法可以优化吗?
【解决方案4】:

以下方法使用 CTE,但它们不是递归的。输出正是您想要的,并且可以处理 CSV 字段中的任意数量的值。我使用了一个基于 SQLCLR 的字符串拆分器,它在 SQL# 库中免费提供(这是我编写的,但免费版本中有 2 个 String_Split 函数)。如果您愿意,可以使用另一个拆分器,但请确保它不是标量 T-SQL UDF。

请注意,对于“新”ID 字段,我没有将有效的 INT 转换为 VARCHAR 以便它可以与 UserID 的独特运动 # 连接,而是将其转换为a DECIMAL 并将独特的运动 # 放在小数点右侧。这应该比字符串字段更快地过滤和排序。如果有人可以拥有超过 9 项运动,那么将 / 10.0 更改为 / 100.0

SET NOCOUNT ON;

DECLARE @SampleData TABLE
(
  Id INT NOT NULL,
  UserID INT NOT NULL,
  Sport1 VARCHAR(500),
  SportLevel1 VARCHAR(500),
  Sport2 VARCHAR(500),
  SportLevel2 VARCHAR(500)
);

INSERT INTO @SampleData VALUES (1, 11, 'Baseball', 'Varsity', 'Baseball', 'Recreational');
INSERT INTO @SampleData VALUES (2, 22, 'Baseball,Basketball', 'Varsity,Junior Varsity', 'Baseball', 'Varsity');
INSERT INTO @SampleData VALUES (3, 33, 'Soccer', 'Varsity', 'Soccer,Track & Field', 'Recreational,Intramural');
INSERT INTO @SampleData VALUES (4, 44, NULL , NULL , 'Tennis', 'Varsity');
INSERT INTO @SampleData VALUES (5, 55, 'Volleyball', 'Varsity', NULL , NULL);
INSERT INTO @SampleData VALUES (6, 66, 'Baseball,Basketball', 'Varsity,Varsity', 'Soccer,Football', 'Varsity,Varsity');
INSERT INTO @SampleData VALUES (7, 77, 'Baseball,Basketball,Rowing', 'Varsity,Varsity,Varsity', 'Soccer,Football,Volleyball', 'Varsity,Varsity,Recreational');
---------------------------------------------------------------

;WITH s1 AS
(
    SELECT d1.Id, d1.UserID, v1.SplitNum, v1.SplitVal AS [Sport]
    FROM   @SampleData d1
    CROSS APPLY SQL#.String_Split4k(d1.Sport1, N',', 1) v1
), sl1 AS
(
    SELECT d2.Id, d2.UserID, v2.SplitNum, v2.SplitVal AS [SportLevel1]
    FROM   @SampleData d2
    CROSS APPLY SQL#.String_Split4k(d2.SportLevel1, N',', 1) v2
), s2 AS
(
    SELECT d3.Id, d3.UserID, v3.SplitNum, v3.SplitVal AS [Sport]
    FROM   @SampleData d3
    CROSS APPLY SQL#.String_Split4k(d3.Sport2, N',', 1) v3
), sl2 AS
(
    SELECT d4.Id, d4.UserID, v4.SplitNum, v4.SplitVal AS [SportLevel2]
    FROM   @SampleData d4
    CROSS APPLY SQL#.String_Split4k(d4.SportLevel2, N',', 1) v4
)
SELECT COALESCE(s1.UserID, s2.UserID) +
          (ROW_NUMBER() OVER (PARTITION BY COALESCE(s1.UserID, s2.UserID)
                              ORDER BY CASE
                                          WHEN sl1.SportLevel1 IS NOT NULL
                                              AND sl2.SportLevel2 IS NOT NULL THEN 1
                                          WHEN sl1.SportLevel1 IS NOT NULL THEN 2
                                          ELSE 3
                                       END ASC,
                                       COALESCE(s1.Sport, s2.Sport) ASC
                             ) / 10.0
          ) AS [UserID],
       COALESCE(s1.Sport, s2.Sport) AS [Sport],
       sl1.SportLevel1,
       sl2.SportLevel2
FROM   s1
INNER JOIN sl1
        ON sl1.Id = s1.Id
        AND sl1.SplitNum = s1.SplitNum
FULL OUTER JOIN (
            s2
    INNER JOIN sl2
            ON sl2.Id = s2.Id
            AND sl2.SplitNum = s2.SplitNum
        )
        ON s2.Id = s1.Id
        AND s2.Sport = s1.Sport
ORDER BY COALESCE(s1.UserID, s2.UserID) ASC,
         CASE WHEN sl1.SportLevel1 IS NOT NULL THEN 1 ELSE 2 END ASC,
         COALESCE(s1.Sport, s2.Sport) ASC;

【讨论】:

  • 感谢您的输入,另一位用户在没有添加其他内容的情况下提供了一个很好的解决方案。非常感谢!
猜你喜欢
  • 2018-11-02
  • 2019-09-11
  • 1970-01-01
  • 1970-01-01
  • 2019-11-10
  • 1970-01-01
  • 1970-01-01
  • 2018-04-13
  • 1970-01-01
相关资源
最近更新 更多