解析sql中的不规则分隔符
我有一个不规则的分隔符@uid='1585-1586--1-5417-2347-8865'由7个破折号组成;然而,当SQL遇到“-1”时,我需要它返回“-1” 换句话说,我需要这个结果: 而不是: @uid='1585-1586--1-5417-2347-8865'由7个破折号组成。但是,我需要SQL在看到“-1”时返回“-1”,如下面的屏幕截图所示 这可以通过下面的脚本实现,该脚本依赖于UDF解析sql中的不规则分隔符,sql,sql-server,Sql,Sql Server,我有一个不规则的分隔符@uid='1585-1586--1-5417-2347-8865'由7个破折号组成;然而,当SQL遇到“-1”时,我需要它返回“-1” 换句话说,我需要这个结果: 而不是: @uid='1585-1586--1-5417-2347-8865'由7个破折号组成。但是,我需要SQL在看到“-1”时返回“-1”,如下面的屏幕截图所示 这可以通过下面的脚本实现,该脚本依赖于UDF declare @uid nvarchar(100) set @uid = '1585-1586
declare @uid nvarchar(100)
set @uid = '1585-1586--1-5417-2347-8865'
select
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 1) as [Col_A],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 2) as [Col_B],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 3) as [Col_C],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 4) as [Col_D],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 5) as [Col_E],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (@uid, '-', '|')),'||', '|-'), '|') where id = 6) as [Col_F]
您的答案在这里对您有一点帮助,但是,没有定义
[dbo].[fnSplit]
对其他任何人都没有帮助
如果我们可以假设数据定义良好(有6列),那么我们可以“垃圾邮件”一些CHARINDEX
函数来执行此操作。如答案所示,您需要替换所有分隔符,然后重新插入双分隔符的值-
:
DECLARE @UID varchar(30) = '1585-1586--1-5417-2347-8865';
DECLARE @Delimiter char(1) = '-';
SELECT SUBSTRING(ca.FixedUID,1,C1.I-1) AS Col1,
SUBSTRING(ca.FixedUID,C1.I+1, C2.I-C1.I-1) AS Col2,
SUBSTRING(ca.FixedUID,C2.I+1, C3.I-C2.I-1) AS Col3,
SUBSTRING(ca.FixedUID,C3.I+1, C4.I-C3.I-1) AS Col4,
SUBSTRING(ca.FixedUID,C4.I+1, C5.I-C4.I-1) AS Col5,
SUBSTRING(ca.FixedUID,C5.I+1, LEN(ca.FixedUID)-C5.I) AS Col6
FROM (VALUES(@UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,@Delimiter,'|'),'||','|' + @Delimiter)))ca(FixedUID)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID)))C1(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C1.I+1)))C2(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C2.I+1)))C3(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C3.I+1)))C4(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C4.I+1)))C5(I);
当然,如果您有一个“空”值,那么这将失败:
DECLARE @UID varchar(30) = '1585--71-5417-2347-8865';
DECLARE @Delimiter char(1) = '-';
SELECT SUBSTRING(ca.FixedUID,1,C1.I-1) AS Col1,
SUBSTRING(ca.FixedUID,C1.I+1, C2.I-C1.I-1) AS Col2,
SUBSTRING(ca.FixedUID,C2.I+1, C3.I-C2.I-1) AS Col3,
SUBSTRING(ca.FixedUID,C3.I+1, C4.I-C3.I-1) AS Col4,
SUBSTRING(ca.FixedUID,C4.I+1, C5.I-C4.I-1) AS Col5,
SUBSTRING(ca.FixedUID,C5.I+1, LEN(ca.FixedUID)-C5.I) AS Col6
FROM (VALUES(@UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,@Delimiter,'|'),'||','|' + @Delimiter)))ca(FixedUID)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID)))C1(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C1.I+1)))C2(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C2.I+1)))C3(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C3.I+1)))C4(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C4.I+1)))C5(I);
传递给LEFT或SUBSTRING函数的长度参数无效
因此,不应使用数据中可能出现的分隔符(但是,我们可以希望,由于这些分隔符都是整数值,因此不存在NULL
值,而是存在0
:'1585-0-71-5417-2347-8865'
)
如果使用类似的字符串拆分器,则可以很好地透视(并取消Pivot)数据,但上述示例中的值将位于错误的位置:
SELECT MAX(CASE DS.ItemNumber WHEN 1 THEN DS.Item END) AS Col1,
MAX(CASE DS.ItemNumber WHEN 2 THEN DS.Item END) AS Col2,
MAX(CASE DS.ItemNumber WHEN 3 THEN DS.Item END) AS Col3,
MAX(CASE DS.ItemNumber WHEN 4 THEN DS.Item END) AS Col4,
MAX(CASE DS.ItemNumber WHEN 5 THEN DS.Item END) AS Col5,
MAX(CASE DS.ItemNumber WHEN 6 THEN DS.Item END) AS Col6
FROM (VALUES(@UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,@Delimiter,'|'),'||','|' + @Delimiter)))ca(FixedUID)
CROSS APPLY dbo.DelimitedSplit8K_LEAD(ca.FixedUID,'|') DS;
这将导致以下结果:
Col1 Col2 Col3 Col4 Col5 Col6
---- ---- ---- ---- ---- ----
1585 -71 5417 2347 8865 NULL
基本上我所做的是从6到1的递归cte。每次迭代我都会删除最后一个分隔的数字,并将其移动到
col\u val
列。
我决定使用reverse
,这样我就可以使用patindex
找到连字符,然后找到数字。这样做可以得到负值。相反,字符串看起来像1--6851-5851-0,然后patindex('%-[0-9]]')
返回2,因为我使用了字符串0-1585-1586--1的right
函数,它将返回-1
我将'0-'
添加到delim_列的开头
,因为我想使用patindex
,而不必考虑最后一个分隔的列
列col\u val
重复上述所有内容,但不是使用@uid
而是使用delim\u列
以下是每个迭代的外观:
col_num delim_column col_val loc
6 0-1585-1586--1-5417-2347 8865 4
5 0-1585-1586--1-5417 2347 4
4 0-1585-1586--1 5417 4
3 0-1585-1586 -1 2
2 0-1585 1586 4
1 0 1585 4
然后我使用一个简单的choose函数来旋转这些列。这将使列名变得干净
DECLARE
@uid VARCHAR(MAX) = '1585-1586--156-5417-2347-8865',
@delim_count INT = 0
--First, count the number of delimiters. We do this by temporarily replacing '--' with a single '-'
--and then count the difference in lengths of the two strings (one with '-' and one without)
SELECT @delim_count = LEN(REPLACE(@uid, '--', '-')) - LEN(REPLACE(REPLACE(@uid, '--', '-'), '-','')) - IIF(@uid LIKE '-%', 1, 0)
--next a recursive cte that will lop off the last number each iteration and move the last value to col_val
;WITH fnsplit(col_num, delim_column, col_val, loc)
AS
(
SELECT
@delim_count+1 --start with 6 and then go to 1. remove the +1 and replace col_num > 0 for a zero index
,'0-'+SUBSTRING(@uid,0, LEN(@uid) - LEN(RIGHT(@uid, PATINDEX('%-[0-9]%', reverse(@uid)) - 1)) )
,RIGHT(@uid, PATINDEX('%-[0-9]%', REVERSE(@uid)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(@uid)) - 1
UNION ALL
SELECT
col_num - 1
,SUBSTRING(delim_column,0, LEN(delim_column) - LEN(RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)) )
,RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1
FROM
fnsplit
WHERE
col_num > 1
)
--select * from fnsplit -- uncomment here and comment all below to see the recursion
SELECT
*
FROM
(
SELECT
column_name
,col_val
FROM
fnsplit
CROSS APPLY
(SELECT CHOOSE(col_num, 'Col_A','Col_B','Col_C', 'Col_D', 'Col_E', 'Col_F')) tbl(column_name)
)PVT
PIVOT
(
MAX(col_val)
FOR column_name IN ([Col_A], [Col_B], [Col_C], [Col_D], [Col_E], [Col_F])
) PVT1
什么是
fnSplit
?@Larnu我相信它是来自某个包的UDF,基本上与SQL Server最新版本中的STRING\u SPLIT
做相同的事情。我猜,@TimBiegeleisen,但没有定义,这个答案对未来的读者来说是无用的。我们可以使用R或Python解析脚本来代替自定义的fnspilt
。它可能比这个答案需要更少的行数,并且R在所有当前SQL Server版本(即2016年和更高版本,我认为即使是2014年也不受主流支持)中都可以使用,而,@user716255,这是拆分字符串SQL Server的一种非常好的方式。使用迭代方法的速度要快得多。使用也可以出现在数据中的分隔符通常会导致问题。选择不同的分隔符可以解决此问题。首先不要将非规范化数据存储在数据库中。在将数据插入数据库之前解析并清理数据。在这种情况下,您不能仅仅拆分文本来获取数据,您需要一个正则表达式或一个简单的解析器来处理这两个注释,但我将假设这是一个数据转储,OP请求一个用于ETL目的的查询。;-)@LarryB最好由ETL工具来完成,而不是SQL。这也是首字母缩略词的意思,提取转换,然后加载。即使OP希望先加载,然后转换(ELT),使用其他方法解析也要容易得多languages@PanagiotisKanavos最近我想起了你的评论。同样,我完全同意,但我假设OP意识到,由于时间限制和资源有限,他们无法获得最佳方案。我从未在面试中看到过表格结构和数据示例。我倾向于看到的评论是“照常”的,因为我们应该教育并提供解决方案,这很酷。我从来没有在一家数据没有非规范化或其他愚蠢的公司工作过。清理它需要官僚主义,到那时我将把我的简历提交到别处。
DECLARE
@uid VARCHAR(MAX) = '1585-1586--156-5417-2347-8865',
@delim_count INT = 0
--First, count the number of delimiters. We do this by temporarily replacing '--' with a single '-'
--and then count the difference in lengths of the two strings (one with '-' and one without)
SELECT @delim_count = LEN(REPLACE(@uid, '--', '-')) - LEN(REPLACE(REPLACE(@uid, '--', '-'), '-','')) - IIF(@uid LIKE '-%', 1, 0)
--next a recursive cte that will lop off the last number each iteration and move the last value to col_val
;WITH fnsplit(col_num, delim_column, col_val, loc)
AS
(
SELECT
@delim_count+1 --start with 6 and then go to 1. remove the +1 and replace col_num > 0 for a zero index
,'0-'+SUBSTRING(@uid,0, LEN(@uid) - LEN(RIGHT(@uid, PATINDEX('%-[0-9]%', reverse(@uid)) - 1)) )
,RIGHT(@uid, PATINDEX('%-[0-9]%', REVERSE(@uid)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(@uid)) - 1
UNION ALL
SELECT
col_num - 1
,SUBSTRING(delim_column,0, LEN(delim_column) - LEN(RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)) )
,RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1
FROM
fnsplit
WHERE
col_num > 1
)
--select * from fnsplit -- uncomment here and comment all below to see the recursion
SELECT
*
FROM
(
SELECT
column_name
,col_val
FROM
fnsplit
CROSS APPLY
(SELECT CHOOSE(col_num, 'Col_A','Col_B','Col_C', 'Col_D', 'Col_E', 'Col_F')) tbl(column_name)
)PVT
PIVOT
(
MAX(col_val)
FOR column_name IN ([Col_A], [Col_B], [Col_C], [Col_D], [Col_E], [Col_F])
) PVT1