SQL 服务器如何将管道分隔的列字符串更改为行

SQL server how to change pipe separated column string to rows

我在 table 中有 3 列,其中 2 列的字符串由“|”分隔管道。这两个列值相互依赖。

例如:我在 table 中有这样的数据:

ID  product quantity
1   A|B|C   1|2|3
2   X|Y|Z   7|8|9

我想把它改成这样:

ID  product quantity
1   A   1
1   B   2
1   C   3
2   X   7
2   Y   8
2   Z   9

因为我正在使用 SSMS,所以除了 SQL,我别无选择。我尝试使用交叉应用,但我没有得到正确的结果。对于 1 行,我收到 9 行而不是 3 行。 谁能建议我应该使用哪种方法?

提前致谢!! 杰克

这相当棘手,因为您需要使值匹配。以下采用递归 CTE 方法:

with cte as (
      select id,
             left(product, charindex('|', product + '|') - 1) as product,
             left(quantity, charindex('|', quantity + '|') - 1) as quantity,
             substring(product, charindex('|', product + '|') + 1, 1000) as products,
             substring(quantity, charindex('|', quantity + '|') + 1, 1000) as quantities
     from t
     union all
      select id,
             left(products, charindex('|', products + '|') - 1) as product,
             left(quantities, charindex('|', quantities + '|') - 1) as quantity,
             substring(products, charindex('|', products + '|') + 1, 1000) as products,
             substring(quantities, charindex('|', quantities + '|') + 1, 1000) as quantities
     from cte
     where products <> '' and quantities <> ''
    )
select id, product, quantity
from cte;

这里有点Rextester.

测试数据

CREATE TABLE #t (ID INT,  product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO #t VALUES 
(1   ,'A|B|C' ,  '1|2|3'),
(2   ,'X|Y|Z' ,  '7|8|9');

查询

WITH Products AS (
        SELECT    ID
                , Product_Split.a.value('.', 'VARCHAR(100)') Products
                , ROW_NUMBER() OVER (PARTITION BY ID ORDER BY (SELECT NULL)) rn
        FROM (
              SELECT  ID
                     ,Cast ('<X>' 
                           + Replace(product, '|', '</X><X>') 
                           + '</X>' AS XML) AS Product_Data
                FROM #t
            ) AS t 
        CROSS APPLY Product_Data.nodes ('/X') AS Product_Split(a) 
),
 Quantities AS (
        SELECT    ID
                , Quantity_Split.a.value('.', 'VARCHAR(100)') Quantity
                , ROW_NUMBER() OVER (PARTITION BY ID ORDER BY (SELECT NULL)) rn
        FROM (
                SELECT  ID
                        ,Cast ('<X>' 
                            + Replace(quantity, '|', '</X><X>') 
                            + '</X>' AS XML) AS Quantity_Data
                FROM #t
            ) AS t 
        CROSS APPLY Quantity_Data.nodes ('/X') AS Quantity_Split(a)
 )
 SELECT   t.ID
        , P.Products
        , Q.Quantity

 FROM #t t
 LEFT JOIN Products     P   ON t.ID = p.ID
 LEFT JOIN Quantities   Q   ON Q.ID = t.ID 
                            AND Q.rn = p.rn

结果集

╔════╦══════════╦══════════╗
║ ID ║ Products ║ Quantity ║
╠════╬══════════╬══════════╣
║  1 ║ A        ║        1 ║
║  1 ║ B        ║        2 ║
║  1 ║ C        ║        3 ║
║  2 ║ X        ║        7 ║
║  2 ║ Y        ║        8 ║
║  2 ║ Z        ║        9 ║
╚════╩══════════╩══════════╝

拆分字符串很容易,有很多例子。这里棘手的部分是通过它们的位置连接片段。我的建议是使用 XML 的能力来按位置定位元素:

DECLARE @tbl TABLE(ID INT,  product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO @tbl VALUES 
 (1   ,'A|B|C' ,  '1|2|3')
,(2   ,'X|Y|Z' ,  '7|8|9');

--这是查询

WITH CastedToXML AS
(
    SELECT *
         ,CAST('<x>' + REPLACE(product,'|','</x><x>') + '</x>' AS XML) AS ProductXml
         ,CAST('<x>' + REPLACE(quantity,'|','</x><x>') + '</x>' AS XML) AS QuantityXml
    FROM @tbl
)
SELECT *
      ,ProductXml.value('/x[sql:column("Nmbr")][1]','nvarchar(10)') AS ProductAtPosition
      ,QuantityXml.value('/x[sql:column("Nmbr")][1]','int') AS QuantityAtPosition
FROM CastedToXML
--Create a set of running numbers (spt_values is just a pre-filled table with many rows)
CROSS APPLY (SELECT TOP(CastedToXML.ProductXml.value('count(/x)','int')) 
             ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) 
             FROM master..spt_values) AS Tally(Nmbr);

结果

+----+------+-------------------+--------------------+
| ID | Nmbr | ProductAtPosition | QuantityAtPosition |
+----+------+-------------------+--------------------+
| 1  | 1    | A                 | 1                  |
+----+------+-------------------+--------------------+
| 1  | 2    | B                 | 2                  |
+----+------+-------------------+--------------------+
| 1  | 3    | C                 | 3                  |
+----+------+-------------------+--------------------+
| 2  | 1    | X                 | 7                  |
+----+------+-------------------+--------------------+
| 2  | 2    | Y                 | 8                  |
+----+------+-------------------+--------------------+
| 2  | 3    | Z                 | 9                  |
+----+------+-------------------+--------------------+

一些解释:

演员 xml 将您的 A|B|C 转移到

<x>A</x>
<x>B</x>
<x>C</x>

此列表与使用 <x> 的计数作为 TOP 限制 动态创建的数字集相结合

现在可以很容易地从你的 XML 中按位置选出 <x>

试试吧!

更新:非唯一 ID

DECLARE @tbl TABLE(ID INT,  product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO @tbl VALUES 
 (1   ,'A|B|C' ,  '1|2|3')
,(2   ,'X|Y|Z' ,  '7|8|9')
,(3   ,'a|b|c' ,  '7|8|9')
,(2   ,'D|e|f' ,  '7|8|9')
;
--This is the query

WITH CastedToXML AS
(
    SELECT *
         ,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID) AS RowIndex
         ,CAST('<x>' + REPLACE(product,'|','</x><x>') + '</x>' AS XML) AS ProductXml
         ,CAST('<x>' + REPLACE(quantity,'|','</x><x>') + '</x>' AS XML) AS QuantityXml
    FROM @tbl
)
SELECT *
      ,ProductXml.value('/x[sql:column("Nmbr")][1]','nvarchar(10)') AS ProductAtPosition
      ,QuantityXml.value('/x[sql:column("Nmbr")][1]','int') AS QuantityAtPosition
FROM CastedToXML
--Create a set of running numbers (spt_values is just a pre-filled table with many rows)
CROSS APPLY (SELECT TOP(CastedToXML.ProductXml.value('count(/x)','int')) 
             ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) 
             FROM master..spt_values) AS Tally(Nmbr);