通过分隔符拆分多个字段

时间:2018-04-09 12:53:39

标签: sql-server tsql

我必须编写一个可以在我们的数据库上执行部分更新的SP,这些更改存储在PU表的记录中。值字段包含由固定分隔符分隔的所有值。表字段引用一个Schemes表,其中包含Colums fiels中类似方式的每个表的列名。

现在对于我的SP,我需要在具有列/值对的临时表中拆分值字段和列字段,这对于PU表中的每个记录都会发生。

一个例子:

我们的PU表看起来像这样:

CREATE TABLE [dbo].[PU](
    [Table] [nvarchar](50) NOT NULL,
    [Values] [nvarchar](max) NOT NULL
)

The PU table

为此示例插入SQL:

INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Person','John Doe;26');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Person','Jane Doe;22');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Person','Mike Johnson;20');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Person','Mary Jane;24');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Course','Mathematics');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Course','English');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Course','Geography');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Campus','Campus A;Schools Road 1;Educationville');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Campus','Campus B;Schools Road 31;Educationville');
INSERT INTO [dbo].[PU]([Table],[Values]) VALUES ('Campus','Campus C;Schools Road 22;Educationville');

我们有一个与此类似的Schemes表:

CREATE TABLE [dbo].[Schemes](
    [Table] [nvarchar](50) NOT NULL,
    [Columns] [nvarchar](max) NOT NULL
)

The Schemes table

为此示例插入SQL:

INSERT INTO [dbo].[Schemes]([Table],[Columns]) VALUES ('Person','[Name];[Age]');
INSERT INTO [dbo].[Schemes]([Table],[Columns]) VALUES ('Course','[Name]');
INSERT INTO [dbo].[Schemes]([Table],[Columns]) VALUES ('Campus','[Name];[Address];[City]');

因此,PU表的第一条记录应该产生如下的临时表:

John

第五届将有:

Mathematics

最后,第8条PU记录应该导致:

Campus A

你明白了。 我尝试使用以下查询来创建临时表,但是当PU记录中有多个值时,它会失败:

DECLARE @Fields TABLE
(
    [Column] INT,
    [Value] VARCHAR(MAX)
)

INSERT INTO @Fields
    SELECT TOP 1
        (SELECT Value FROM STRING_SPLIT([dbo].[Schemes].[Columns], ';')), 
        (SELECT Value FROM STRING_SPLIT([dbo].[PU].[Values], ';'))
    FROM [dbo].[PU] INNER JOIN [dbo].[Schemes] ON [dbo].[PU].[Table] = [dbo].[Schemes].[Table]

TOP 1正确获取第一张PU记录,因为每次PU记录在处理后都会被删除。

错误是:

子查询返回的值超过1。当子查询遵循=,!=,<,< =,>,> =或子查询用作表达式时,不允许这样做。

在Person记录的情况下,拆分确实一次返回2个值/列,我只想将值存储在2个记录中而不是出错。

有关重写上述查询的任何帮助吗?

另请注意,数据只是通用的废话。能够有两个都有分隔值的字段,总是等于金额(例如PU表中的'人'在字段中总是有2个分隔值),并且在几个列/标题行中将它们分解就是重点问题。

更新:工作实施

根据Sean Lange的(接受)答案,我能够制定以下实施方案来克服这个问题:

由于我需要重用它,所以组合列/值功能由一个新函数执行,声明如下:

CREATE FUNCTION [dbo].[JoinDelimitedColumnValue]
        (@splitValues VARCHAR(8000), @splitColumns VARCHAR(8000),@pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
  WITH MyValues AS
(
    SELECT ColumnPosition = x.ItemNumber,
        ColumnValue = x.Item
    FROM  dbo.DelimitedSplit8K(@splitValues, @pDelimiter) x
)

, ColumnData AS
(
    SELECT ColumnPosition = x.ItemNumber,
        ColumnName = x.Item
    FROM  dbo.DelimitedSplit8K(@splitColumns, @pDelimiter) x
)

SELECT cd.ColumnName,
    v.ColumnValue
FROM MyValues v
JOIN ColumnData cd ON cd.ColumnPosition = v.ColumnPosition
;

如果是上述示例数据,我将使用以下SQL调用此函数:

DECLARE @FieldValues VARCHAR(8000), @FieldColumns VARCHAR(8000)
SELECT TOP 1 @FieldValues=[dbo].[PU].[Values], @FieldColumns=[dbo].[Schemes].[Columns] FROM [dbo].[PU] INNER JOIN [dbo].[Schemes] ON [dbo].[PU].[Table] = [dbo].[Schemes].[Table]

INSERT INTO @Fields
SELECT [Column] = x.[ColumnName],[Value] = x.[ColumnValue] FROM [dbo].[JoinDelimitedColumnValue](@FieldValues, @FieldColumns, @Delimiter) x

2 个答案:

答案 0 :(得分:2)

这种数据结构使得这种方式更加复杂。你可以在这里利用Jeff Moden的分配器。 http://www.sqlservercentral.com/articles/Tally+Table/72993/分裂者和所有其他分子的主要区别在于他返回每个元素的序数位置。为什么所有其他分离者不这样做是超出我的。对于这样的事情,这是必要的。您有两组分隔数据,您必须确保它们以正确的顺序重新组装。

我看到的最大问题是您的主表中没有任何内容可用作正确排序结果的锚点。你需要一些东西,甚至是一个标识来确保输出行保持“在一起”。为了实现我只是在PU表中添加了一个标识。

alter table PU add RowOrder int identity not null

既然我们有一个锚,这对于简单的查询来说仍然有点麻烦,但它是可以实现的。

这样的东西现在可以使用了。

with MyValues as
(
    select p.[Table]
        , ColumnPosition = x.ItemNumber
        , ColumnValue = x.Item
        , RowOrder
    from PU p
    cross apply dbo.DelimitedSplit8K(p.[Values], ';') x
)

, ColumnData as
(
    select ColumnName = replace(replace(x.Item, ']', ''), '[', '') 
        , ColumnPosition = x.ItemNumber
        , s.[Table]
    from Schemes s
    cross apply dbo.DelimitedSplit8K(s.Columns, ';') x
)

select cd.[Table]
    , v.ColumnValue
    , cd.ColumnName
from MyValues v
join ColumnData cd on cd.[Table] = v.[Table] 
    and cd.ColumnPosition = v.ColumnPosition
order by v.RowOrder
    , v.ColumnPosition

答案 1 :(得分:1)

我建议不要首先存储这样的值。我建议在表中使用键值,最好不要使用表和列作为复合键。我建议避免使用保留字。我也不知道你正在使用什么版本的SQL。我将假设您使用的是最新版本的Microsoft SQL Server,它将支持我提供的存储过程。

以下是解决方案的概述: 1)您需要将PU和Schema表转换为一个表,在该表中,您将在其自己的行中隔离的列列表中包含每个“列”值。如果您可以使用这种格式而不是提供的格式存储数据,那么您的状况会更好。

我的意思是

Table|Columns
Person|Jane Doe;22

需要转换为

Table|Column|OrderInList
Person|Jane Doe|1
Person|22|2

有多种方法可以做到这一点,但我更喜欢我选择的xml技巧。您可以在线找到多个拆分字符串示例,因此我不会专注于此。使用任何可以提供最佳性能的东西。不幸的是,您可能无法摆脱这个表值函数。

<强>更新 感谢Shnugo的性能增强评论,我已经更新了我的xml分割器,为你提供了行号,这减少了我的一些代码。我对Schema列表做了同样的事情。

2)由于新的Schema表和新的PU表现在都有每列出现的顺序,PU表和模式表可以在“Table”和OrderInList

上连接
CREATE FUNCTION [dbo].[fnSplitStrings_XML]
(
   @List       NVARCHAR(MAX),
   @Delimiter  VARCHAR(255)
)
RETURNS TABLE
AS
   RETURN 
   (
      SELECT y.i.value('(./text())[1]', 'nvarchar(4000)') AS Item,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as RowNumber
      FROM 
      ( 
        SELECT CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.') AS x
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO
CREATE Procedure uspGetColumnValues
 as
 Begin

--Split each value in PU
select p.[Table],p.[Values],a.[Item],CHARINDEX(a.Item,p.[Values]) as LocationInStringForSorting,a.RowNumber
into #PuWithOrder
from PU p
cross apply [fnSplitStrings_XML](p.[Values],';') a  --use whatever string split function is working best for you (performance wise)

--Split each value in Schema
select s.[Table],s.[Columns],a.[Item],CHARINDEX(a.Item,s.[Columns]) as LocationInStringForSorting,a.RowNumber
into #SchemaWithOrder
from Schemes s
cross apply [fnSplitStrings_XML](s.[Columns],';') a  --use whatever string split function is working best for you (performance wise)



DECLARE @Fields TABLE  --If this is an ETL process, maybe make this a permanent table with an auto incrementing Id and reference this table in all steps after this.
(
[Table] NVARCHAR(50),
[Columns] NVARCHAR(MAX),
    [Column] VARCHAR(MAX),
    [Value] VARCHAR(MAX),
    OrderInList int
)
INSERT INTO @Fields([Table],[Columns],[Column],[Value],OrderInList)
Select pu.[Table],pu.[Values] as [Columns],s.Item as [Column],pu.Item as [Value],pu.RowNumber
from #PuWithOrder pu
join #SchemaWithOrder s on pu.[Table]=s.[Table] and pu.RowNumber=s.RowNumber

Select [Table],[Columns],[Column],[Value],OrderInList
from @Fields
order by [Table],[Columns],OrderInList

   END
   GO

   EXEC uspGetColumnValues

   GO

<强>更新 由于您的工作实现是一个表值函数,我还有另一个建议。我看到的问题是你使用一个表值函数,它最终一次处理一条记录。您将根据需要使用基于集合的操作和批处理获得更好的性能。使用tabled值函数,您可能会循环遍历每一行。如果这是某种ETL过程,如果您有一个批量处理行的存储过程,那么您的团队会更好。将结果分成一个更好的表可能是有意义的,您的团队可以使用下游工作,而不是让它们使用可能很慢的表值函数。