【问题标题】:How Do I Split a Delimited String in SQL Server Without Creating a Function?如何在不创建函数的情况下在 SQL Server 中拆分分隔字符串?
【发布时间】:2018-08-02 10:58:52
【问题描述】:

我正在使用 SQL Server 数据库。我有一列包含一个分隔列表,我需要编写一个查询,将列表的值拆分为行。通过浏览 StackOverflow 和其他网络,我知道这是一个常见问题。事实上,我在这里找到了一个广泛的分析:

http://www.sommarskog.se/arrays-in-sql.html

不幸的是,我在该网站和其他地方看到的每个解决方案都需要我创建一个函数。这不是我的选择——我缺乏使用 CREATE 命令所需的权限。

没有 CREATE,我知道我可以使用 PARSENAME 函数,类似这样(感谢 How do I split a string so I can access item x? 的 Nathan Bedford。):

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 2) 

但是,PARSENAME 仅适用于包含 4 个或更少项目的列表。因此,我的问题是:如何编写查询来拆分超过 4 个项目的分隔字符串而不在数据库中创建新对象?

编辑:

感谢大家的快速解答。我可能遗漏了一些重要信息——我正在通过 ODBC 连接与数据库进行交互。除了 CREATE 语句之外,似乎还有其他语句不起作用。例如,我似乎无法在一个语句中使用 DECLARE 来定义将在另一个语句中使用的变量。据我所知,我必须将所有内容放入一个 SELECT 语句中(尽管 WITH 似乎也适用于声明公用表)。不幸的是,到目前为止建议的所有解决方案似乎都需要在 SELECT 语句之外声明变量,而这不起作用。请多多包涵——我边走边学。

【问题讨论】:

    标签: sql-server


    【解决方案1】:

    使用 XML 的版本。

    declare @S varchar(100) = 'Hello John Smith'
    
    select 
      n.r.value('.', 'varchar(50)')
    from (select cast('<r>'+replace(@S, ' ', '</r><r>')+'</r>' as xml)) as s(XMLCol)
      cross apply s.XMLCol.nodes('r') as n(r)
    

    改用表格 将 @T 替换为您正在使用的表。

    -- Test table
    declare @T table (ID int, Col varchar(100))
    insert into @T values (1, 'Hello John Smith')
    insert into @T values (2, 'xxx yyy zzz')
    
    select 
      T.ID,
      n.r.value('.', 'varchar(50)')
    from @T as T
      cross apply (select cast('<r>'+replace(replace(Col,'&','&amp;'), ' ', '</r><r>')+'</r>' as xml)) as S(XMLCol)
      cross apply S.XMLCol.nodes('r') as n(r)
    

    不使用变量分割字符串'Hello John Smith'

    select 
      n.r.value('.', 'varchar(50)')
    from (select cast('<r>'+replace('Hello John Smith', ' ', '</r><r>')+'</r>' as xml)) as s(XMLCol)
      cross apply s.XMLCol.nodes('r') as n(r)
    

    【讨论】:

    • 顺便说一句,第一个使用 XML 的数组在相当大的数组中性能很差
    • @Johnny_D 可以通过不在查询中进行强制转换而改用 xml 变量来改进。不会像使用数字表一样快。 SQL Fiddle
    • 我不擅长,如果您能展示如何在不进行投射的情况下做到这一点,将不胜感激?
    • @Johnny_D 只需点击我上一条评论中指向 SQL Fiddle 的链接即可。
    • @MikaelEriksson 能否请您更新 SQL 小提琴链接(如果有)。如果没有,无论如何都不要介意感谢您的回答
    【解决方案2】:

    使用内置 master..spt_values 表的示例

    DECLARE @String VARCHAR(1000)
        SELECT @String ='1,4,77,88,4546,234,2,3,54,87,9,6,4,36,6,9,9,6,4,4,68,9,0,5'
    
        SELECT SUBSTRING(',' + @String + ',', Number + 1,
        CHARINDEX(',', ',' + @String + ',', Number + 1) - Number -1)AS VALUE
        FROM master..spt_values
        WHERE Type = 'P'
        AND Number <= LEN(',' + @String + ',') - 1
        AND SUBSTRING(',' + @String + ',', Number, 1) = ','
        GO
    

    更多信息请看这里:Split A String By Using A Number Table

    【讨论】:

      【解决方案3】:

      您可以使用递归 CTE 逐步提取一项

      示例表

      create table aTable(a int identity primary key, b int, c varchar(100))
      insert aTable values (1, 'this is a test string')
      insert aTable values (1, 'this is another test string')
      insert aTable values (2, 'here is a test string to put the others to shame')
      insert aTable values (4, '')
      insert aTable values (5, null)
      insert aTable values (5, '-the end- ')
      

      查询

      ;with tmp(a, b, c, position, single) as (
      select a, b,
          STUFF(c, 1, CHARINDEX(' ', c + ' .'), ''),
          1,
          convert(nvarchar(max),left(c, CHARINDEX(' ', c + ' .') -1))
      from aTable
      union all
      select a, b,
          STUFF(c, 1, CHARINDEX(' ', c + ' .'), ''),
          position+1,
          convert(nvarchar(max),left(c, CHARINDEX(' ', c + ' .') -1))
      from tmp
      where c > '')
      select a, b, single, position
      from tmp
      order by a, position
      

      注意事项:

      • 这里的分隔符是一个空格,是CHARINDEX搜索的。 ' .' 中的点是必需的,因为 SQL Server 通常不认为尾随空格很重要。
      • 原始表的所有列都可以保留在 CTE 中,只需添加它们即可。这里我展示了在输出中保留 2 列 ab 的示例,其中 c 列被拆分为 single 和一个额外的列来指示位置。

      【讨论】:

      • 我发誓这是黑魔法,但它确实有效。 :) (与许多其他答案不同,实际上满足了 OP 的要求,即不使用函数或任何其他需要我们许多人没有的数据库权限的东西。)
      【解决方案4】:

      对于这个问题的迟到者,http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings 上的文章对各种选项的性能进行了出色的分析。考虑的一些选项包括(从网站复制以供参考):

      CLR

      .Net 代码http://dataeducation.com/sqlclr-string-splitting-part-2-even-faster-even-more-scalable/

      CREATE ASSEMBLY CLRUtilities FROM 'c:\DLLs\CLRUtilities.dll' 
        WITH PERMISSION_SET = SAFE;
      GO
      
      CREATE FUNCTION dbo.SplitStrings_CLR
      (
         @List      NVARCHAR(MAX),
         @Delimiter NVARCHAR(255)
      )
      RETURNS TABLE ( Item NVARCHAR(4000) )
      EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi;
      GO
      

      XML

      CREATE FUNCTION dbo.SplitStrings_XML
      (
         @List       NVARCHAR(MAX),
         @Delimiter  NVARCHAR(255)
      )
      RETURNS TABLE
      WITH SCHEMABINDING
      AS
         RETURN 
         (  
            SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
            FROM 
            ( 
              SELECT x = CONVERT(XML, '<i>' 
                + REPLACE(@List, @Delimiter, '</i><i>') 
                + '</i>').query('.')
            ) AS a CROSS APPLY x.nodes('i') AS y(i)
         );
      GO
      

      CTE

      CREATE FUNCTION dbo.SplitStrings_CTE
      (
         @List       NVARCHAR(MAX),
         @Delimiter  NVARCHAR(255)
      )
      RETURNS @Items TABLE (Item NVARCHAR(4000))
      WITH SCHEMABINDING
      AS
      BEGIN
         DECLARE @ll INT = LEN(@List) + 1, @ld INT = LEN(@Delimiter);
      
         WITH a AS
         (
             SELECT
                 [start] = 1,
                 [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                             @List, @ld), 0), @ll),
                 [value] = SUBSTRING(@List, 1, 
                           COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                             @List, @ld), 0), @ll) - 1)
             UNION ALL
             SELECT
                 [start] = CONVERT(INT, [end]) + @ld,
                 [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                             @List, [end] + @ld), 0), @ll),
                 [value] = SUBSTRING(@List, [end] + @ld, 
                           COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                             @List, [end] + @ld), 0), @ll)-[end]-@ld)
             FROM a
             WHERE [end] < @ll
         )
         INSERT @Items SELECT [value]
         FROM a
         WHERE LEN([value]) > 0
         OPTION (MAXRECURSION 0);
      
         RETURN;
      END
      GO
      

      功能

      CREATE FUNCTION dbo.SplitStrings_Moden
      (
         @List NVARCHAR(MAX),
         @Delimiter NVARCHAR(255)
      )
      RETURNS TABLE
      WITH SCHEMABINDING AS
      RETURN
        WITH E1(N)        AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                               UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                               UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
             E2(N)        AS (SELECT 1 FROM E1 a, E1 b),
             E4(N)        AS (SELECT 1 FROM E2 a, E2 b),
             E42(N)       AS (SELECT 1 FROM E4 a, E2 b),
             cteTally(N)  AS (SELECT 0 UNION ALL SELECT TOP (DATALENGTH(ISNULL(@List,1))) 
                               ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E42),
             cteStart(N1) AS (SELECT t.N+1 FROM cteTally t
                               WHERE (SUBSTRING(@List,t.N,1) = @Delimiter OR t.N = 0))
        SELECT Item = SUBSTRING(@List, s.N1, ISNULL(NULLIF(CHARINDEX(@Delimiter,@List,s.N1),0)-s.N1,8000))
          FROM cteStart s;
      

      事实证明,最好的性能来自使用 CLR 函数,而 XML 解决方案也做得很好。几乎在所有情况下,使用数字表(上面没有重复该方法)都会导致最差的性能。

      【讨论】:

      • 小心。当我试图警告那篇文章的作者时,他对所有 10,000 行测试数据使用了重复的重复数据。这在现实生活中通常不会发生,它使测试结果偏向于实际上非常懒惰的 XML 拆分器,顺便说一句,它不处理 XML 特殊字符。
      【解决方案5】:
      【解决方案6】:

      我只会使用创建表的众多函数之一,而不是让它返回值,而是将其放入表变量中。然后使用表变量。这是一个返回表格的示例。

      http://www.codeproject.com/KB/database/SQL_UDF_to_Parse_a_String.aspx

      【讨论】:

        【解决方案7】:

        使用 UDF 最有意义,它对所有项目都很灵活,我通常在我的博客中使用它,

        http://sqlthis.blogspot.com/2005/02/list-to-table.html

        因为它是为获取输入字符串和分隔符而编写的,所以它可以是任何单个字符作为分隔符。我在上面的链接中写了一个,但后来我发现许多网站都发布了类似的解决方案,所以部分内容可能是从我所在的论坛中获得灵感的......

        它有效,希望它对你也有效。

        edit 2017-08-09 按照建议,我已经克隆了下面的代码块。谢谢!

        CREATE FUNCTION udfListToTable (@HList VarChar(2000), @Delimiter CHAR(1))
        RETURNS @ListTable TABLE (Field1 VARCHAR(6))
          AS
          BEGIN
          --By: Francisco Tapia
          --Date: 2/1/2005
          --Purpose: To convert a Comma delimited text to a Temp Variable table To help avoid dynamic sql
          -- Instead you can join the temp table or use it in your where clause if a field is IN the subquery
               DECLARE @FieldText as VarChar(6)
        
               IF RIGHT(RTRIM(@HLIST),1) <>@Delimiter
               SET @HList = @HList + @Delimiter
        
               WHILE CHARINDEX(@Delimiter, @HList) > 0
               BEGIN
                    IF CHARINDEX(@Delimiter, @HList) > 0
                    BEGIN
                         SELECT @FieldText =LEFT(@HList, CHARINDEX(@Delimiter, @HList)-1)
                    END
               ELSE 
               BEGIN
                    SELECT @FieldText = RTRIM(LTRIM(@HList))
               END
               --Insert into Variable Table
               INSERT INTO @ListTable(Field1)
               SELECT RTRIM(LTRIM(@FieldText))
               --Remove Item from list
               SELECT @HList = RIGHT(RTRIM(@HList), LEN(RTRIM(@HList)) -
                    CHARINDEX(@Delimiter, @HList))
               END 
               RETURN
          END
        

        【讨论】:

          【解决方案8】:
           DECLARE @cols  AS NVARCHAR(max), 
                  @Val   VARCHAR(100)='Hi- Hello break this-Wall', 
                  @Deli  VARCHAR(50)='-', 
                  @query AS NVARCHAR(max) 
          
          SELECT @cols = Stuff((SELECT ',' + Quotename(id) 
                                FROM   (SELECT stringpieceid AS ID, 
                                               stringpiece 
                                        FROM 
                               [Utility].[dbo].[Splitstringtotable](@Val, @Deli)) 
                                       X 
                                FOR xml path(''), type).value('.', 'NVARCHAR(MAX)'), 1, 1, 
                         '') 
          
          SELECT @query = 
          'SELECT * FROM (SELECT StringPieceID as ID,StringPiece from [Utility].[dbo].[SplitStringToTable](''' 
                   + @Val + ''',''' + @Deli + '''))X PIVOT  (     MAX(StringPiece)     for [ID] in (' + @cols 
                   + ') ) P' 
          
          PRINT @query 
          
          EXEC Sp_executesql 
            @query 
          

          【讨论】:

          • 如果有分隔符,则将其分为 3 部分,如果没有,则仅在单个部分中
          【解决方案9】:
          USE TRIAL
          GO
          
          CREATE TABLE DETAILS
          (
          ID INT,
          NAME VARCHAR(50),
          ADDRESS VARCHAR(50)
          )
          
          INSERT INTO DETAILS
          VALUES (100, 'POPE-JOHN-PAUL','VATICAN CIT|ROME|ITALY')
          ,(240, 'SIR-PAUL-McARTNEY','NEWYORK CITY|NEWYORK|USA')
          ,(460,'BARRACK-HUSSEIN-OBAMA','WHITE HOUSE|WASHINGTON|USA')
          ,(700, 'PRESIDENT-VLADAMIR-PUTIN','RED SQUARE|MOSCOW|RUSSIA')
          ,(950, 'NARENDRA-DAMODARDAS-MODI','10 JANPATH|NEW DELHI|INDIA')
          
          select [ID]
          ,[NAME]
          ,[ADDRESS]
          ,REPLACE(LEFT(NAME, CHARINDEX('-', NAME)),'-',' ') as First_Name
          ,CASE 
          WHEN CHARINDEX('-',REVERSE(NAME))+ CHARINDEX('-',NAME) < LEN(NAME)
          THEN  SUBSTRING(NAME, CHARINDEX('-', (NAME)) + 1, LEN(NAME) - CHARINDEX('-' 
          , REVERSE(NAME)) - CHARINDEX('-', NAME))
            ELSE 'NULL
            END AS Middle_Name
          ,REPLACE(REVERSE( SUBSTRING( REVERSE(NAME), 1, CHARINDEX('- 
          ',REVERSE(NAME)))), '-','') AS Last_Name 
          ,REPLACE(LEFT(ADDRESS, CHARINDEX('|', ADDRESS)),'|',' ') AS Locality
          ,CASE 
              WHEN CHARINDEX('|',REVERSE(ADDRESS))+ CHARINDEX('|',ADDRESS) < 
            LEN(ADDRESS) 
           THEN SUBSTRING(ADDRESS, CHARINDEX('|', (ADDRESS))+1, LEN(ADDRESS)- 
          CHARINDEX('|', REVERSE(ADDRESS))-CHARINDEX('|',ADDRESS))
            ELSE 'Null' 
          END AS STATE
          ,REPLACE(REVERSE(SUBSTRING(REVERSE(ADDRESS),1 
          ,CHARINDEX('|',REVERSE(ADDRESS)))),'|','') AS Country
           FROM DETAILS
            SELECT CHARINDEX('-', REVERSE(NAME)) AS LAST,CHARINDEX('-',NAME)AS FIRST, 
           LEN(NAME) AS LENGTH
           FROM DETAILS
          

          -- 如果您对代码理解有疑问,请告诉我

          【讨论】:

            【解决方案10】:

            这是一个用户定义的解析函数,它使 SQL Server 的执行也与 VB 的“Split”函数类似。专为交互式杠杆而设计;例如,解析从外部 API 调用的存储过程中的数据。

            https://gallery.technet.microsoft.com/scriptcenter/User-def-function-enabling-98561cce

            【讨论】:

            • OP 明确表示他/她想在没有用户功能的情况下拆分文本。
            【解决方案11】:

            对于 sql Server >= 2016,您可以使用 string_split,如下所示:

            SELECT * FROM string_split('Hello John Smith', ' ')
            

            输出

            +-------+
            | value |
            +-------+
            | Hello |
            | John  |
            | Smith |
            +-------+
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-01-21
              • 2013-05-10
              • 2017-05-29
              • 1970-01-01
              • 2020-04-09
              • 2018-01-03
              相关资源
              最近更新 更多