#tsql #sql-server-2016
#tsql #sql-server-2016
Вопрос:
У меня есть поле базы данных, которое является 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, поэтому я не могу просто удалить последнюю цифру из нового столбца.
Комментарии:
1. Если количество числовых символов одинаковое, вы можете просто использовать
LEFT
.2. Это не обязательно одно и то же.
3. Будут ли нечисловые символы всегда находиться в конце строки (строк)?
4. Да, всегда в конце, но это может быть один или несколько нечисловых символов в конце.
Ответ №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 |
ДА-CARAMBA | null | null |
Чушь собачья | null | null |
Тест на db<>fiddle здесь
Комментарии:
1. Плюс один с моей стороны. По своему вкусу я не люблю
PARSENAME()
разделять строки, но — что ж — в данном случае это работает 🙂2. Спасибо. Что ж, parsename может представлять опасность при неправильном использовании, потому что на самом деле он не был разработан для этой цели. Но для небольших строк с менее чем 5 разделителями это довольно удобно.
3. Кстати: как и в ответе Алана, это приведет к разрыву значений без каких-либо нечисловых символов… Просто имейте в виду 🙂
4. @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
Комментарии:
1. Хороший ответ, плюс один с моей стороны. Это приведет к прерыванию, когда нечисловой символ вообще отсутствует… Не знаю, имеет ли это значение, но стоит иметь в виду…
Ответ №4:
Вы можете определить функцию для проверки столбца на соответствие РЕГУЛЯРНОМУ выражению и удаления любых нечисловых символов в строке. и используйте это в своем запросе.
Обновленный ответ, чтобы учесть большую длину и избежать ошибок с индексом символов для столбца 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
Комментарии:
1. Это было бы ужасно медленно. Циклы очень медленные, как и скалярные UDF. Если важна производительность, тогда прекратите цикл и измените скалярную функцию на встроенную функцию.
2. Я согласен, но это зависит от базы данных и набора данных, которые мы намерены запросить. Я только что дал ответ, связанный с заданным вопросом. При необходимости мы можем превратить это в функцию с табличным значением, которая намного быстрее, и добавить ее в качестве перекрестного применения и показать результирующие наборы. Мы можем перемещать данные во временную таблицу и перебирать их. Это полностью зависит от нас
3. Выполнен запрос для использования функции с табличным значением.
4. @Nav, это не сделало его лучше… Комбинация a
WHILE
-loop вместе с TVF с несколькими операторами является известным убийцей производительности… Гораздо лучше был встроенный TVF, но это не позволяет использовать процедурные подходы (вот почему это лучше на самом деле)5. Согласовано. и я никогда не говорил, что это самая производительная версия. Я сказал, что это полностью зависит от того, чего мы пытаемся достичь, и от того, какая информация у нас есть.
Ответ №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
добавляется aCASE
для решения ситуаций, в которыхPATINDEX()
не найдено ни одного нечислового символа.