在 SQL 中仅选择特定字符之后的数字字符
Posted
技术标签:
【中文标题】在 SQL 中仅选择特定字符之后的数字字符【英文标题】:Select only numeric characters after a specific character in TSQL 【发布时间】:2022-01-08 09:47:54 【问题描述】:我有一个 VARCHAR 数据库字段,其数据以下列方式存储:
DatabaseField
1185731-2148838B
1185731-2148838S
1185731-2148838W
1185731-2148839B
我想将此字段分成两个单独的列 - 包含“-”两侧的数字。
要获取'-'之前和之后的值,我可以使用以下
SUBSTRING(DatabaseField,0,CHARINDEX('-',DatabaseField))
SUBSTRING(DatabaseField,CHARINDEX('-',DatabaseField)+1,LEN(DatabaseField))
这给了我
NewColumnA NewColumnB
1185731 2148838B
1185731 2148838S
1185731 2148838W
1185731 2148839B
我怎样才能从新列 B 中删除任何非数字字符 - 它可能不是列中的一个字母 - 它可能是 2 或 3,所以我不能只从新列中删除最后一个数字。
【问题讨论】:
如果数字字符的个数相同,则直接使用LEFT
即可。
不一定一样。
非数字字符是否总是在字符串的末尾?
是的,总是在最后,但它可以是一个或多个非数字字符。
【参考方案1】:
填充它们!
declare @test table (DatabaseField varchar(30));
insert into @test values
('1185731-2148838B')
, ('1234567-1234567?')
, ('1185731-214883612WAH')
, ('1185731-2148839BLAH')
, ('AYE-CARAMBA')
, ('Stufffffff')
;
select DatabaseField
, [NewColumnA] = NULLIF(STUFF(DatabaseField,PATINDEX('%[0-9]-%',Databasefield),42,''),'')
, [NewColumnB] = PARSENAME(REPLACE(STUFF(DatabaseField,PATINDEX('%[^0-9-]%',DatabaseField),42,''),'-','.'),1)
from @test;
DatabaseField | NewColumnA | NewColumnB |
---|---|---|
1185731-2148838B | 118573 | 2148838 |
1234567-1234567? | 123456 | 1234567 |
1185731-214883612WAH | 118573 | 214883612 |
1185731-2148839BLAH | 118573 | 2148839 |
AYE-CARAMBA | null | null |
Stufffffff | null | null |
测试 dbfiddle here
【讨论】:
我身边还有一个。出于口味问题,我不喜欢PARSENAME()
拆分字符串,但是 - 好吧 - 它在这种情况下有效:-)
谢谢。好吧,如果使用不当, parsename 可能会带来风险,因为它并不是真正为此目的而设计的。但对于少于 5 个分隔符的小字符串,它相当方便。
顺便说一句:就像艾伦的回答一样,这将与没有任何非数字字符的值打破......请记住:-)
@Shnugo 很好发现。实际上,问题不在于 NewColumB,它应该给 null。但第一个是 CHARINDEX()-1。所以我也塞了那个。【参考方案2】:
就像(现已删除)的答案一样,我会使用 PARSENAME
来拆分 2 个部分。然后您可以使用PATINDEX
查找第一个非数字字符并从中获取LEFT
字符:
SELECT LEFT(PN.Field1,ISNULL(NULLIF(PATINDEX('%[^0-9]%',PN.Field1),0),LEN(Field1))) AS Field1,
LEFT(PN.Field2,ISNULL(NULLIF(PATINDEX('%[^0-9]%',PN.Field2),0)-1,LEN(Field2))) AS Field2
FROM (VALUES('1185731-2148838B'),
('1185731-2148838S'),
('1185731-2148838W'),
('1185731-2148839B'))V(YourString)
CROSS APPLY (VALUES(PARSENAME(REPLACE(V.YourString,'-','.'),2),PARSENAME(REPLACE(V.YourString,'-','.'),1)))PN(Field1,Field2);
【讨论】:
【参考方案3】:我会这样做:
DECLARE @test TABLE (DatabaseField VARCHAR(30));
INSERT @test VALUES('1185731-2148838B'), ('1234567-1234567?'),
('1185731-214883612WAH'), ('1185731-2148839BLAH');
SELECT
t.DatabaseField,
NewA = SUBSTRING(clean.S,1,f1.Pos),
NewB = SUBSTRING(clean.S, f1.Pos+2, 30)
FROM @test AS t
CROSS APPLY (VALUES(CHARINDEX('-', t.DatabaseField)-1)) AS f1(Pos)
CROSS APPLY (VALUES(PATINDEX('%[^0-9-]%', t.DatabaseField)-1)) AS f2(Pos)
CROSS APPLY (VALUES(SUBSTRING(t.DatabaseField,1,f2.Pos))) AS clean(S);
返回:
DatabaseField NewA NewB
------------------------------ ------------------------------ ------------------------------
1185731-2148838B 1185731 2148838
1234567-1234567? 1234567 1234567
1185731-214883612WAH 1185731 214883612
1185731-2148839BLAH 1185731 2148839
【讨论】:
很好的答案,加上我身边的一个。当根本没有非数字字符时,这将中断...不知道,这是否重要,但值得牢记...【参考方案4】:您可以定义一个函数来根据 REGEX 检查列并删除字符串中的任何非数字字符。并在您的查询中使用它。
更新了答案以容纳更多长度并避免第 2 列的字符索引陷阱。拆分基于找到的第一个“-”。也添加了一些示例
CREATE OR ALTER FUNCTION dbo.fnGetNumbersOnly
(
@StringToCheck NVARCHAR(MAX)
)
RETURNS @Results TABLE
(
NumbericValue BIGINT
)
/*****************************************************************************************************************************************************
SELECT dbo.fnGetNumbers('21488-d*39B') AS Result
*****************************************************************************************************************************************************/
AS
BEGIN
DECLARE @Start INT = 1,
@End INT = ISNULL (LEN (@StringToCheck), 0)
DECLARE @Result NVARCHAR(MAX) = N''
WHILE @Start <= @End
BEGIN
DECLARE @Char CHAR(1) = SUBSTRING (@StringToCheck, @Start, 1)
IF (@Char NOT LIKE '[0-9]')
BEGIN
SET @Start += 1;
CONTINUE
END
SET @Result += @Char
SET @Start += 1;
END
INSERT INTO @Results
(
NumbericValue
)
SELECT @Result
RETURN
END
GO
SELECT c.stringtosplit AS GivenData,
a.NumbericValue AS NewColumnA,
b.NumbericValue AS NewColumnB
FROM
(
SELECT '1185731-2148838B' AS stringtosplit
UNION ALL
SELECT '1185731-2148838S'
UNION ALL
SELECT '1185731-2148838W'
UNION ALL
SELECT '1185731-2148839B'
UNION ALL
SELECT '1185-731-21484-#839B'
UNION ALL
SELECT '1185731-2148yt839B'
) AS c
CROSS APPLY dbo.fnGetNumbersOnly (LEFT(c.stringtosplit, CHARINDEX ('-', c.stringtosplit))) AS a
CROSS APPLY dbo.fnGetNumbersOnly (REPLACE (c.stringtosplit, LEFT(c.stringtosplit, CHARINDEX ('-', c.stringtosplit)), '')) AS b
GO
【讨论】:
这会非常慢。循环非常慢,标量 UDF 也是如此。如果性能很重要,则放弃循环并将标量函数更改为内联函数。 我同意,但这取决于我们打算查询的数据库和数据集。我只是给出了与所问内容相关的答案。如果需要,我们可以将其制成一个更快的表值函数,并将其添加为交叉应用并显示结果集。我们可以将数据移动到临时表中并循环遍历它。这完全取决于我们 进行查询以使用表值函数。 @Nav,这并没有让它变得更好......WHILE
-loop 与 multi-statement TVF 的组合是众所周知的性能杀手。 .. inline TVF 要好得多,但这不允许 procedural 方法(这就是它实际上更好的原因)
同意。我从来没有说过它是性能最好的版本。我说这完全取决于我们想要实现的目标以及我们拥有的信息。【参考方案5】:
还有一个建议:
SELECT t.YourField
,B.firstPart
,CASE WHEN posNonNum < 1 THEN B.secondPart ELSE LEFT(B.secondPart,posNonNum) END AS secondPart
FROM YourTable t
CROSS APPLY(VALUES(CHARINDEX('-',t.YourField))) A(posHyphen)
CROSS APPLY(VALUES(LEFT(t.YourField,posHyphen-1),RIGHT(t.YourField,LEN(t.YourField)-posHyphen))) B(firstPart,secondPart)
CROSS APPLY(VALUES(PATINDEX('%[^0-9]%',B.secondPart))) C(posNonNum);
简而言之:
第一个APPLY
将分隔连字符的位置添加到结果集中
第二个APPLY
使用LEFT()
获取第一部分,RIGHT()
返回第二部分。
第三个APPLY
将在第二部分中查找第一个非数字字符。
最后的SELECT
添加CASE
来处理PATINDEX()
找不到任何非数字字符的情况。
【讨论】:
以上是关于在 SQL 中仅选择特定字符之后的数字字符的主要内容,如果未能解决你的问题,请参考以下文章
我想编写一个 sql (Oracle SQL) 查询来从特定字符之后的列中选择字符串的一部分