Rez*_*eza 6 sql-server sql-server-2014 csv string-manipulation
我正在使用 SQL Server 2014 并且我有一个包含一列包含CSV字符串的表:
110,200,310,130,null
Run Code Online (Sandbox Code Playgroud)
该表的输出如下所示:
我想选择第二列作为多列,将 CSV 字符串的每个项目放在一个单独的列中,如下所示:
所以我创建了一个用于拆分字符串的函数:
create FUNCTION [dbo].[fn_splitstring]
(
@List nvarchar(2000),
@SplitOn nvarchar(5)
)
RETURNS @RtnValue table
(
Id int identity(1,1),
Value nvarchar(100)
)
AS
BEGIN
while (Charindex(@SplitOn,@List)>0)
begin
insert into @RtnValue (value)
select
Value = ltrim(rtrim(Substring(@List,1,Charindex(@SplitOn,@List)-1)))
set @List = Substring(@List,Charindex(@SplitOn,@List)+len(@SplitOn),len(@List))
end
insert Into @RtnValue (Value)
select Value = ltrim(rtrim(@List))
return
END
Run Code Online (Sandbox Code Playgroud)
我想像这样使用它:
select Val , (select value from tvf_split_string(cchar1,',')) from table1
Run Code Online (Sandbox Code Playgroud)
但是上面的代码显然行不通,因为该函数将返回多于一行,导致子查询返回多于一个值并破坏代码。
我可以使用类似的东西:
select Val ,
(select value from tvf_split_string(cchar1,',') order by id offset 0 rows fetch next 1 rows only ) as col1,
(select value from tvf_split_string(cchar1,',') order by id offset 1 rows fetch next 1 rows only ) as col2,
................
from table1
Run Code Online (Sandbox Code Playgroud)
但我认为这不是一个好方法。
正确的做法是什么?
一般来说,标量函数和 mTVF(多语句表值函数)有点内置性能问题。如果可以,最好使用 iTVF(内联表值函数),因为它们的代码实际上包含在执行计划中(很像 VIEW 但参数化)而不是单独执行。即使将 iTVF 用作 iSF(内联标量函数),也不要在此处复制整篇文章,请参阅以下内容以获取一些证明。在许多情况下,即使代码相同,标量函数也可能比 iTVF 慢 7 倍。这是链接,您现在无需注册即可访问该链接。总而言之,如果您的函数中包含“BEGIN”一词,则它不会是高性能 iTVF。
转向@Reza 发布的原始问题,并大量借用@stefan 提供的代码来生成一些示例数据行,这是我们将使用的测试数据。我在临时表中执行此操作,以便我们可以轻松删除测试表而不必担心意外删除真实表。
--===== If the test table exists, drop it to make reruns in SSMS eaiser
IF OBJECT_ID('tempdb..#CSV','U') IS NOT NULL
DROP TABLE #CSV
;
--===== Create the test table.
CREATE TABLE #CSV --Using a Temp Table just for demo purposes
(
ID INT IDENTITY(1,1) --or whatever your PK is
,AString VARCHAR(8000)
)
;
--===== Insert some test data
INSERT INTO #CSV
(AString)
SELECT AString
FROM (
VALUES ('123,456,88789,null,null')
,('123,456,99789,1234,null')
,('123,456,00789,1234,null')
,('1,2222,77789,null,null')
,('11,222,88789,null,')
,('111,22,99789,,')
,('1111,2,00789,oooo,null')
) v (AString)
;
Run Code Online (Sandbox Code Playgroud)
如果您仍在使用 SQL Server 2005,请不要绝望。所有代码仍然可以在那里工作。我们只需要改变我们生成测试数据的方式,因为 VALUES 子句直到 2008 年才能处理我们上面生成数据的方式。相反,您需要使用一系列 SELECT/UNION ALL 语句。如果您需要更大量的数据并且 SELECT/UNION ALL 方法在 2016 年仍然可以正常工作,则两者之间几乎没有性能差异。
这是修改后适用于 2005 的测试表生成代码。
--===== If the test table exists, drop it to make reruns in SSMS eaiser
IF OBJECT_ID('tempdb..#CSV','U') IS NOT NULL
DROP TABLE #CSV
;
--===== Create the test table.
CREATE TABLE #CSV --Using a Temp Table just for demo purposes
(
ID INT IDENTITY(1,1) --or whatever your PK is
,AString VARCHAR(8000)
)
;
--===== Insert some test data
INSERT INTO #CSV
(AString)
SELECT AString
FROM (
SELECT '123,456,88789,null,null' UNION ALL
SELECT '123,456,99789,1234,null' UNION ALL
SELECT '123,456,00789,1234,null' UNION ALL
SELECT '1,2222,77789,null,null' UNION ALL
SELECT '11,222,88789,null,' UNION ALL
SELECT '111,22,99789,,' UNION ALL
SELECT '1111,2,00789,oooo,null'
) v (AString)
;
Run Code Online (Sandbox Code Playgroud)
首先,SQL Server 不是用于字符串操作的最佳工具。已经多次证明,与纯 T-SQL 解决方案相比,正确编写的 SQLCLR 几乎可以消除任何字符串拆分尝试。与 SQLCLR 解决方案相关的问题是 1) 找到正确编写的解决方案(正确返回预期的结果集)和 2) 有时很难说服常驻 DBA 允许使用 SQLCLR。如果您确实找到了正确编写以返回预期结果的方法,那么花时间尝试说服 DBA 绝对值得,因为它通常会处理 MAX 数据类型,并且很少关心您是否传递 VARCHAR 或 NVARCHAR 数据,更不用说更快了比我见过的任何 T-SQL 解决方案都要好。
如果您不必担心使用 NVARCHAR 并且不必担心多字符分隔符(或者可以在“传入”的过程中更改分隔符)并且不必担心 MAX 数据类型(有限到 8K 字符串),那么 DelimitedSplit8K 函数可能适合您。您可以在以下 URL 中找到有关它的文章以及针对它的正确“高基数”测试。
我还将告诉您,当 SQL Server 2012 出现时,那篇文章中的高基数测试数据生成代码因使用 XML 连接方式的更改而中断。Wayne Sheffield 做了一些单独的测试,将其与 2016 年新的拆分器功能进行了比较,并对下一篇文章中的测试数据生成代码进行了修复。请注意,文章中还有其他 3 个可用的拆分器,其中 2 个仅在 2016 年可用。他还针对 SQLCLR 进行了测试,并列出了每个拆分器的一些功能差异(某些情况下的缺点)。
Eirikur Eiriksson 还发表了一篇文章,该文章不仅探讨了严重的性能改进,还探讨了拆分“真正的 CSV”,这些“真正的 CSV”可以在“文本限定符”之间包含分隔符(尽管会以相当大的性能为代价)。这是那篇文章的链接。
尽管如此,即使是他也证明了 SQLCLR 比仅使用 T-SQL 所能做的任何事情都要快得多。我推荐该方法而不是任何 T-SQL 方法,但要进行广泛的功能测试,以确保它在某些“奇怪”情况下会返回您期望的结果。
好的。很抱歉关于分路器的冗长转移。让我们回到最初的问题。
我们需要做两件事:
以这样一种方式拆分 CSV,我们知道每个拆分出的元素来自哪一行。
对于我们拆分的每一行,将拆分出来的元素重新组合到同一行中的单独列中。
首先,我们需要创建一个拆分器函数。这是我正在处理的 DelimitedSplit8K 的修改(好吧,不是最近 ;-) ),它仍将支持 SQL Server 2005。最大的区别是使用二进制排序规则以获得额外的性能。此版本尚未经过全面测试(我个人自 2013 年 9 月以来一直在使用它)但是,考虑到更改的简单性以及它们都没有真正更改功能的事实,我相信您会发现它对您的需求。
CREATE FUNCTION [dbo].[DelimitedSplit8K]
/**********************************************************************************************************************
Purpose:
Given a string containing multiple elements separated by a single character delimiter and that single character
delimiter, this function will split the string and return a table of the single elements (Item) and the element
position within the string (ItemNumber).
Notes:
1. Performance of this function approaches that of a CLR.
2. Note that this code implicitly converts NVARCHAR to VARCHAR and that conversion may NOT be faithful.
Revision History:
Note that this code is a modification of a well proven function created as a community effort and initially documented
at the following URL (http://www.sqlservercentral.com/articles/Tally+Table/72993/). This code is still undergoing
tests. Although every care has certainly been taken to ensure its accuracy, you are reminded to do your own tests to
ensure that this function is suitable for whatever application you might use it for.
--Jeff Moden, 01 Sep 2013
**********************************************************************************************************************/
--===== Define I/O parameters
(@pString VARCHAR(8000) , @pDelimiter CHAR(1)) --DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000).
WITH E1(N) AS (--==== Itzik Ben-Gan style of a cCTE (Cascading CTE) and
-- should not be confused with a much slower rCTE (Recursive CTE).
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
), --10E+1 or 10 rows
E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d), --10E+4 or 10,000 rows max
cteTally(N) AS ( --=== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS ( --=== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT CASE WHEN SUBSTRING(@pString,t.N,1) = @pDelimiter COLLATE Latin1_General_BIN THEN t.N+1 END --added short circuit for casting
FROM cteTally t
WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter COLLATE Latin1_General_BIN
),
cteLen(N1,L1)AS ( --=== Return start position and length (for use in substring).
-- The ISNULL/NULLIF combo handles the length for the final of only element.
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(@pDelimiter ,@pString COLLATE Latin1_General_BIN,s.N1) ,0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(@pString, l.N1, l.L1)
FROM cteLen l
;
GO
Run Code Online (Sandbox Code Playgroud)
一旦那个坏男孩就位,那么使用 PIVOT 或 CROSS TAB 解决原始帖子中的问题就变得微不足道了。我更喜欢相当古老的 CROSS TAB 方法,因为它通常比实际的 PIVOT 更快(有时显着)。请参阅以下关于在那里完成的测试的文章(很旧,可能应该更新以进行性能测量,尽管它确实解释了如何使用这两种方法)。
这是使用上述函数和 CROSS TAB 解决此线程上发布的原始问题的代码。
--===== Do the split for each row and repivot to columns using a
-- high performance CROSS TAB. It uses the function only once
-- for each row, which is another advantage iTVFs have over
-- Scalare Functions.
SELECT csv.ID
,Col1 = MAX(CASE WHEN ca.ItemNumber = 1 THEN Item ELSE '' END)
,Col2 = MAX(CASE WHEN ca.ItemNumber = 2 THEN Item ELSE '' END)
,Col3 = MAX(CASE WHEN ca.ItemNumber = 3 THEN Item ELSE '' END)
,Col4 = MAX(CASE WHEN ca.ItemNumber = 4 THEN Item ELSE '' END)
,Col5 = MAX(CASE WHEN ca.ItemNumber = 5 THEN Item ELSE '' END)
FROM #CSV csv
CROSS APPLY dbo.DelimitedSplit8K(csv.AString,',') ca
GROUP BY csv.ID
;
GO
Run Code Online (Sandbox Code Playgroud)
这是使用给定测试数据的上述代码的结果集。
如果您对此有任何疑问或需要澄清,请随时提出。
为了解决这个问题,您可能需要一些更多的过程代码。不同的数据库有不同的内置字符串函数集(如您所知)。因此,为了找到解决方案,我编写了相当通用的“概念验证”代码,并且仅使用 SUBSTR() 函数(相当于 MS SQL 中的 SUBSTRING())。
当查看 LOAD DATA ... (MySQL) 时,您可以看到我们需要一个 .csv 文件和一个现有表。使用我的函数,您将能够通过传递列名加 2 个整数来选择 .csv 文件的部分:一个用于“左侧分隔符”的编号,另一个用于“右侧分隔符”的编号。手边分隔符”。(听起来很可怕......)。
示例:假设我们有一个像这样的逗号分隔值,它存储在名为 csv 的列中:
aaa,111,zzz
Run Code Online (Sandbox Code Playgroud)
如果我们想从中“提取”111,我们可以这样调用该函数:
select split(csv, 1, 2) ... ;
-- 1: start at the first comma
-- 2: end at the second comma
Run Code Online (Sandbox Code Playgroud)
字符串的“开始”和“结束”可以这样选择:
select split(csv, 0, 1) ... ; -- from the start of the string (no comma) up to the first comma
select split(csv, 2, 0) ... ; -- from the second comma right up to the end of the string
Run Code Online (Sandbox Code Playgroud)
我知道函数代码并不完美,并且可以在某些地方进行简化(例如,在Oracle中我们应该使用INSTR(),它可以找到字符串的一部分的特定出现位置)。另外,现在没有异常处理。这只是初稿。开始 ...
create or replace function split(
csvstring varchar2
, lcpos number
, rcpos number )
return varchar2
is
slen pls_integer := 0 ; -- string length
comma constant varchar2(1) := ',' ;
currentchar varchar2(1) := '' ;
commacount pls_integer := 0 ;
firstcommapos pls_integer := 0 ;
secondcommapos pls_integer := 0 ;
begin
slen := length(csvstring);
-- special case: leftmost value
if lcpos = 0 then
firstcommapos := 0 ;
for i in 1 .. slen
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
secondcommapos := i - 1 ;
exit ;
end if ;
end loop ;
return substr(csvstring, 1, secondcommapos) ;
end if ;
-- 2 commas somewhere in the middle of the string
if lcpos > 0 and rcpos > 0 then
for i in 1 .. slen
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
commacount := commacount + 1;
if commacount = lcpos then
firstcommapos := i ;
end if ;
if commacount = rcpos then
secondcommapos := i ;
end if ;
end if ;
end loop ;
return substr(csvstring, firstcommapos + 1, (secondcommapos-1) - firstcommapos ) ;
end if ;
-- special case: rightmost value
if rcpos = 0 then
secondcommapos := slen ;
for i in reverse 1 .. slen -- caution: count DOWN!
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
firstcommapos := i + 1 ;
exit ;
end if ;
end loop ;
return substr(csvstring, firstcommapos, secondcommapos-(firstcommapos-1)) ;
end if ;
end split;
Run Code Online (Sandbox Code Playgroud)
测试:
-- test table, test data
create table csv (
id number generated always as identity primary key
, astring varchar2(256)
);
-- insert some test data
begin
insert into csv (astring) values ('123,456,88789,null,null');
insert into csv (astring) values ('123,456,99789,1234,null');
insert into csv (astring) values ('123,456,00789,1234,null');
insert into csv (astring) values ('1,2222,77789,null,null');
insert into csv (astring) values ('11,222,88789,null,');
insert into csv (astring) values ('111,22,99789,,');
insert into csv (astring) values ('1111,2,00789,oooo,null');
end;
-- testing:
select
split(astring,0,1) col1
, split(astring,1,2) col2
, split(astring,2,3) col3
, split(astring,3,4) col4
, split(astring,4,0) col5
from csv
-- output
COL1 COL2 COL3 COL4 COL5
123 456 88789 null null
123 456 99789 1234 null
123 456 00789 1234 null
1 2222 77789 null null
11 222 88789 null -
111 22 99789 - -
1111 2 00789 oooo null
Run Code Online (Sandbox Code Playgroud)
……这个功能好像有点过分了。然而,如果我们编写更多的过程性代码,依赖它的SQL就会变得相当“优雅”。祝您处理 csv 顺利!
归档时间: |
|
查看次数: |
8695 次 |
最近记录: |