Найти индекс последнего вхождения подстроки с помощью T-SQL


128

Есть ли простой способ найти индекс последнего вхождения строки с помощью SQL? Я использую SQL Server 2000 прямо сейчас. Мне в основном нужна функциональность, которую System.String.LastIndexOfпредоставляет метод .NET . Небольшой поиск в Google показал это - функция для получения последнего индекса - но это не работает, если вы передаете "текстовое" выражение столбца. Другие решения, найденные в другом месте, работают только до тех пор, пока текст, который вы ищете, составляет 1 символ.

Мне, наверное, придется приготовить еду. Если я это сделаю, я опубликую его здесь, чтобы вы, ребята, могли его посмотреть и, возможно, использовать.

Ответы:


33

Вы ограничены небольшим списком функций для текстового типа данных.

Все, что я могу предложить, это начать PATINDEX, но работать в обратном направлении от и DATALENGTH-1, DATALENGTH-2, DATALENGTH-3т. Д., Пока вы не получите результат или не закончите до нуля (DATALENGTH-DATALENGTH)

Это действительно то, с чем SQL Server 2000просто не справиться.

Изменить для других ответов : REVERSE отсутствует в списке функций, которые могут использоваться с текстовыми данными в SQL Server 2000


1
Да, это довольно неудобно. Кажется, это должно быть просто, но это не так!
Raj

... вот почему в SQL 2005 есть varchar (max) для нормальных функций
gbn,

1
Ах! так что "varchar (max)" - это вещь SQL 2005, что объясняет, почему она не сработала, когда я попробовал ее на SQL 2000.
Радж

DATALENGTH не дает мне правильного результата, хотя LENGTH работает.
Текила

@Tequila и другие: DATALENGTHвозвращает количество байтов, а не символов. Следовательно, DATALENGTHвозвращает удвоенное количество символов в строке для NVARCHARстрок. LENоднако возвращает количество символов за вычетом любых конечных пробелов . Я никогда не использую DATALENGTHдля вычисления длины символа, если конечный пробел не является значительным, и я точно знаю, что мои типы данных согласованы, независимо от того, являются ли они VARCHARилиNVARCHAR
rbsdca

175

Прямолинейный путь? Нет, но я использовал обратное. Буквально.

В предыдущих подпрограммах, чтобы найти последнее вхождение данной строки, я использовал функцию REVERSE (), затем CHARINDEX, а затем снова REVERSE, чтобы восстановить исходный порядок. Например:

SELECT
   mf.name
  ,mf.physical_name
  ,reverse(left(reverse(physical_name), charindex('\', reverse(physical_name)) -1))
 from sys.master_files mf

показывает, как извлечь фактические имена файлов базы данных из их «физических имен», независимо от того, насколько глубоко они вложены во вложенные папки. При этом выполняется поиск только одного символа (обратная косая черта), но вы можете использовать это для более длинных строк поиска.

Единственный недостаток в том, что я не знаю, насколько хорошо это будет работать с типами данных TEXT. Я использую SQL 2005 уже несколько лет и больше не знаком с работой с ТЕКСТОМ - но я, кажется, припоминаю, что вы могли использовать на нем ВЛЕВО и ВПРАВО?

Филипп


1
Извините - я почти уверен, что никогда не возвращался, когда работал с 2000, и в настоящее время у меня нет доступа к каким-либо установкам SQL 2000.
Филип Келли

Brilliant! Никогда бы не подумал так решить эту проблему!
Джаред

4
Хороший! Я изменил для своих нужд: email.Substring (0, email.lastIndexOf ('@')) == SELECT LEFT (email, LEN (email) -CHARINDEX ('@', REVERSE (email)))
Фредрик Йоханссон,

1
Вот почему программирование так весело!
Крис

почему бы просто не использовать правую, а не левую в оригинале вместо лишнего реверса
Фил

108

Самый простой способ ...

REVERSE(SUBSTRING(REVERSE([field]),0,CHARINDEX('[expr]',REVERSE([field]))))

3
+1 Потому что НЕ запускает ошибку, такую ​​как «Недопустимый параметр длины, переданный в функцию LEFT или SUBSTRING», если совпадение не найдено
Xilmiki

12
Если ваш [expr]длиннее 1 символа, вам тоже нужно перевернуть его!
Андрюс Нарушявичюс

60

Если вы используете Sqlserver 2005 или более поздней версии, REVERSEмногократное использование функции снижает производительность, приведенный ниже код более эффективен.

DECLARE @FilePath VARCHAR(50) = 'My\Super\Long\String\With\Long\Words'
DECLARE @FindChar VARCHAR(1) = '\'

-- Shows text before last slash
SELECT LEFT(@FilePath, LEN(@FilePath) - CHARINDEX(@FindChar,REVERSE(@FilePath))) AS Before
-- Shows text after last slash
SELECT RIGHT(@FilePath, CHARINDEX(@FindChar,REVERSE(@FilePath))-1) AS After
-- Shows the position of the last slash
SELECT LEN(@FilePath) - CHARINDEX(@FindChar,REVERSE(@FilePath)) AS LastOccuredAt

1
Задним числом это может показаться очевидным, но если вы ищете строку вместо одного символа, вам нужно сделать: LEN (@FilePath) - CHARINDEX (REVERSE (@FindString), REVERSE (@FilePath))
pkExec

14
DECLARE @FilePath VARCHAR(50) = 'My\Super\Long\String\With\Long\Words'
DECLARE @FindChar VARCHAR(1) = '\'

SELECT LEN(@FilePath) - CHARINDEX(@FindChar,REVERSE(@FilePath)) AS LastOccuredAt

8

Старый, но все еще актуальный вопрос, вот что я создал на основе информации, предоставленной другими здесь.

create function fnLastIndexOf(@text varChar(max),@char varchar(1))
returns int
as
begin
return len(@text) - charindex(@char, reverse(@text)) -1
end

7

У меня это сработало очень хорошо.

REVERSE(SUBSTRING(REVERSE([field]), CHARINDEX(REVERSE('[expr]'), REVERSE([field])) + DATALENGTH('[expr]'), DATALENGTH([field])))

6
REVERSE(SUBSTRING(REVERSE(ap_description),CHARINDEX('.',REVERSE(ap_description)),len(ap_description)))  

работал лучше для меня


4

Хм, я знаю, что это старый поток, но таблица подсчета может сделать это в SQL2000 (или любой другой базе данных):

DECLARE @str CHAR(21),
        @delim CHAR(1)
 SELECT @str = 'Your-delimited-string',
        @delim = '-'

SELECT
    MAX(n) As 'position'
FROM
    dbo._Tally
WHERE
    substring(@str, _Tally.n, 1) = @delim

Таблица подсчета - это просто таблица возрастающих чисел.

substring(@str, _Tally.n, 1) = @delimПолучает положение каждого разделителя, то вы просто получить позицию максимума в этом наборе.

Таблицы подсчета потрясающие. Если вы не использовали их раньше, есть хорошая статья на SQL Server Central (Бесплатная регистрация или просто используйте Bug Me Not ( http://www.bugmenot.com/view/sqlservercentral.com )).

* EDIT: удалено n <= LEN(TEXT_FIELD), так как вы не можете использовать LEN () для типа TEXT. Пока substring(...) = @delimостается хоть результат все равно правильный.


Ницца. Я думаю, что это фактически то же самое решение, что и принятый ответ gbn; вы просто используете таблицу для хранения целых чисел 1, 2, 3 и т.д., которые вычитаются из DATALENGTH и читаются с первого символа вперед вместо последнего символа назад.
Майкл Петито,

2

Поменяйте местами вашу строку и вашу подстроку, затем найдите первое вхождение.


Хорошая точка зрения. У меня сейчас нет 2000, и я не могу вспомнить, смогу ли я сделать это, когда у меня было.
AK

2

Некоторые другие ответы возвращают фактическую строку, тогда как мне нужно было больше знать фактический индекс int. И ответы, которые делают это, кажется, слишком усложняют ситуацию. Вдохновляясь некоторыми другими ответами, я сделал следующее ...

Сначала я создал функцию:

CREATE FUNCTION [dbo].[LastIndexOf] (@stringToFind varchar(max), @stringToSearch varchar(max))
RETURNS INT
AS
BEGIN
    RETURN (LEN(@stringToSearch) - CHARINDEX(@stringToFind,REVERSE(@stringToSearch))) + 1
END
GO

Затем в своем запросе вы можете просто сделать это:

declare @stringToSearch varchar(max) = 'SomeText: SomeMoreText: SomeLastText'

select dbo.LastIndexOf(':', @stringToSearch)

Вышеупомянутое должно вернуть 23 (последний индекс ':')

Надеюсь, это кому-то помогло!


2

Я понимаю, что этому вопросу несколько лет, но ...

На Access 2010, вы можете использовать , InStrRev()чтобы сделать это. Надеюсь это поможет.


2

В этом ответе используется MS SQL Server 2008 (у меня нет доступа к MS SQL Server 2000), но я вижу это в соответствии с OP, это 3 ситуации, которые следует учитывать. Из того, что я пробовал, ответ здесь не охватывает всех трех из них:

  1. Возвращает последний индекс искомого символа в заданной строке.
  2. Возвращает последний индекс подстроки поиска (более одного символа) в заданной строке.
  3. Если искомого символа или подстроки нет в данной строке, верните 0

Функция, которую я придумал, принимает 2 параметра:

@String NVARCHAR(MAX) : Строка для поиска

@FindString NVARCHAR(MAX) : Либо одиночный символ, либо подстрока, для получения последнего индекса в @String

Она возвращает INTчто либо положительный индекс @FindStringв @Stringили 0что означает , что @FindStringне в@String

Вот объяснение того, что делает функция:

  1. Инициализация @ReturnValна 0что указывает @FindStringне в@String
  2. Проверяет индекс @FindStringin @Stringс помощьюCHARINDEX()
  3. Если индекс @FindStringin @Stringравен 0, @ReturnValостается как0
  4. Если индекс @FindStringin @Stringравен > 0, @FindStringis in, @Stringпоэтому последний индекс @FindStringin вычисляется @Stringс помощьюREVERSE()
  5. Возвращает @ReturnValлибо положительное число, которое является последним индексом @FindStringв, @Stringлибо 0указывает, что @FindStringне входит@String

Вот сценарий создания функции (готово для копирования и вставки):

CREATE FUNCTION [dbo].[fn_LastIndexOf] 
(@String NVARCHAR(MAX)
, @FindString NVARCHAR(MAX))
RETURNS INT
AS 
BEGIN
    DECLARE @ReturnVal INT = 0
    IF CHARINDEX(@FindString,@String) > 0
        SET @ReturnVal = (SELECT LEN(@String) - 
        (CHARINDEX(REVERSE(@FindString),REVERSE(@String)) + 
        LEN(@FindString)) + 2)  
    RETURN @ReturnVal
END

Вот небольшой фрагмент, который удобно проверяет функцию:

DECLARE @TestString NVARCHAR(MAX) = 'My_sub2_Super_sub_Long_sub1_String_sub_With_sub_Long_sub_Words_sub2_'
, @TestFindString NVARCHAR(MAX) = 'sub'

SELECT dbo.fn_LastIndexOf(@TestString,@TestFindString)

Я запускал это только на MS SQL Server 2008, потому что у меня нет доступа к какой-либо другой версии, но из того, что я изучил, это должно быть хорошо, по крайней мере, для 2008+.

Наслаждаться.


1

Я знаю, что это будет неэффективно, но рассматривали ли вы возможность преобразования textполя, varcharчтобы можно было использовать решение, предоставленное веб-сайтом, который вы нашли? Я знаю, что это решение вызовет проблемы, поскольку вы потенциально можете обрезать запись, если длина в textполе превышает длину вашего varchar(не говоря уже о том, что это будет не очень производительно).

Поскольку ваши данные находятся внутри textполя (и вы используете SQL Server 2000), ваши возможности ограничены.


Да, приведение к «varchar» не является вариантом, поскольку обрабатываемые данные часто превышают максимум, который может храниться в «varchar». Спасибо за ответ!
Raj

1

Если вы хотите получить индекс последнего пробела в строке слов, вы можете использовать это выражение RIGHT (name, (CHARINDEX ('', REVERSE (name), 0)), чтобы вернуть последнее слово в строке. полезен, если вы хотите разобрать фамилию из полного имени, которое включает инициалы для имени и / или отчества.


1

@indexOf = <whatever characters you are searching for in your string>

@LastIndexOf = LEN([MyField]) - CHARINDEX(@indexOf, REVERSE([MyField]))

Не тестировал, он может быть отключен на единицу из-за нулевого индекса, но работает в SUBSTRINGфункции при отключении от @indexOfсимволов до конца вашей строки

SUBSTRING([MyField], 0, @LastIndexOf)


1

Этот код работает, даже если подстрока содержит более 1 символа.

DECLARE @FilePath VARCHAR(100) = 'My_sub_Super_sub_Long_sub_String_sub_With_sub_Long_sub_Words'
DECLARE @FindSubstring VARCHAR(5) = '_sub_'

-- Shows text before last substing
SELECT LEFT(@FilePath, LEN(@FilePath) - CHARINDEX(REVERSE(@FindSubstring), REVERSE(@FilePath)) - LEN(@FindSubstring) + 1) AS Before
-- Shows text after last substing
SELECT RIGHT(@FilePath, CHARINDEX(REVERSE(@FindSubstring), REVERSE(@FilePath)) -1) AS After
-- Shows the position of the last substing
SELECT LEN(@FilePath) - CHARINDEX(REVERSE(@FindSubstring), REVERSE(@FilePath)) AS LastOccuredAt

0

Мне нужно было найти n-ю последнюю позицию обратной косой черты в пути к папке. Вот мое решение.

/*
http://stackoverflow.com/questions/1024978/find-index-of-last-occurrence-of-a-sub-string-using-t-sql/30904809#30904809
DROP FUNCTION dbo.GetLastIndexOf
*/
CREATE FUNCTION dbo.GetLastIndexOf
(
  @expressionToFind         VARCHAR(MAX)
  ,@expressionToSearch      VARCHAR(8000)
  ,@Occurrence              INT =  1        -- Find the nth last 
)
RETURNS INT
AS
BEGIN

    SELECT  @expressionToSearch = REVERSE(@expressionToSearch)

    DECLARE @LastIndexOf        INT = 0
            ,@IndexOfPartial    INT = -1
            ,@OriginalLength    INT = LEN(@expressionToSearch)
            ,@Iteration         INT = 0

    WHILE (1 = 1)   -- Poor man's do-while
    BEGIN
        SELECT @IndexOfPartial  = CHARINDEX(@expressionToFind, @expressionToSearch)

        IF (@IndexOfPartial = 0) 
        BEGIN
            IF (@Iteration = 0) -- Need to compensate for dropping out early
            BEGIN
                SELECT @LastIndexOf = @OriginalLength  + 1
            END
            BREAK;
        END

        IF (@Occurrence > 0)
        BEGIN
            SELECT @expressionToSearch = SUBSTRING(@expressionToSearch, @IndexOfPartial + 1, LEN(@expressionToSearch) - @IndexOfPartial - 1)
        END

        SELECT  @LastIndexOf = @LastIndexOf + @IndexOfPartial
                ,@Occurrence = @Occurrence - 1
                ,@Iteration = @Iteration + 1

        IF (@Occurrence = 0) BREAK;
    END

    SELECT @LastIndexOf = @OriginalLength - @LastIndexOf + 1 -- Invert due to reverse
    RETURN @LastIndexOf 
END
GO

GRANT EXECUTE ON GetLastIndexOf TO public
GO

Вот мои тестовые примеры, которые проходят

SELECT dbo.GetLastIndexOf('f','123456789\123456789\', 1) as indexOf -- expect 0 (no instances)
SELECT dbo.GetLastIndexOf('\','123456789\123456789\', 1) as indexOf -- expect 20
SELECT dbo.GetLastIndexOf('\','123456789\123456789\', 2) as indexOf -- expect 10
SELECT dbo.GetLastIndexOf('\','1234\6789\123456789\', 3) as indexOf -- expect 5

0

Для того, чтобы получить часть до последнего совпадения разделителем (работает только NVARCHARза счет DATALENGTHиспользования):

DECLARE @Fullstring NVARCHAR(30) = '12.345.67890.ABC';

DECLARE @Delimiter CHAR(1) = '.';

SELECT SUBSTRING(@Fullstring, 1, DATALENGTH(@Fullstring)/2 - CHARINDEX(@Delimiter, REVERSE(@Fullstring)));

0

Этот ответ соответствует требованиям ОП. в частности, он позволяет игле быть более чем одним символом и не генерирует ошибку, если игла не найдена в стоге сена. Мне казалось, что большинство (все?) Других ответов не обрабатывали эти крайние случаи. Кроме того, я добавил аргумент «Начальная позиция», предоставляемый встроенной функцией CharIndex сервера MS SQL. Я попытался точно отразить спецификацию CharIndex, за исключением обработки справа налево, а не слева направо. например, я возвращаю ноль, если игла или стог сена равны нулю, и я возвращаю ноль, если игла не найдена в стоге сена. Одна вещь, которую я не мог обойти, - это то, что со встроенной функцией третий параметр является необязательным. В функциях, определяемых пользователем SQL Server, в вызове должны быть указаны все параметры, кроме случаев, когда функция вызывается с использованием «EXEC». , Хотя третий параметр должен быть включен в список параметров, вы можете указать ключевое слово «default» в качестве заполнителя для него, не задавая ему значение (см. Примеры ниже). Так как при нежелании третий параметр легче удалить из этой функции, чем добавить его при необходимости, я включил его здесь в качестве отправной точки.

create function dbo.lastCharIndex(
 @needle as varchar(max),
 @haystack as varchar(max),
 @offset as bigint=1
) returns bigint as begin
 declare @position as bigint
 if @needle is null or @haystack is null return null
 set @position=charindex(reverse(@needle),reverse(@haystack),@offset)
 if @position=0 return 0
 return (len(@haystack)-(@position+len(@needle)-1))+1
end
go

select dbo.lastCharIndex('xyz','SQL SERVER 2000 USES ANSI SQL',default) -- returns 0
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',default) -- returns 27
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',1) -- returns 27
select dbo.lastCharIndex('SQL','SQL SERVER 2000 USES ANSI SQL',11) -- returns 1

0

Я наткнулся на эту ветку, когда искал решение моей аналогичной проблемы, которая имела те же требования, но была для другого типа базы данных, в которой также не хватало REVERSE функции.

В моем случае это было для базы данных OpenEdge (Progress) , которая имеет немного другой синтаксис. Это сделало INSTRдоступной для меня функцию, которую предлагает большинство типизированных баз данных Oracle. .

Итак, я придумал следующий код:

SELECT 
  INSTR(foo.filepath, '/',1, LENGTH(foo.filepath) - LENGTH( REPLACE( foo.filepath, '/',  ''))) AS IndexOfLastSlash 
FROM foo

Однако для моей конкретной ситуации (являющейся базой данных OpenEdge (Progress) ) это не привело к желаемому поведению, поскольку замена символа пустым символом дала ту же длину, что и исходная строка. Для меня это не имеет особого смысла, но я смог обойти проблему с помощью следующего кода:

SELECT 
  INSTR(foo.filepath, '/',1, LENGTH( REPLACE( foo.filepath, '/',  'XX')) - LENGTH(foo.filepath))  AS IndexOfLastSlash 
FROM foo

Теперь я понимаю, что этот код не решит проблему для T-SQL, потому что нет альтернативы INSTRфункции, которая предлагает Occurenceсвойство.

Чтобы быть внимательным, я добавлю код, необходимый для создания этой скалярной функции, чтобы ее можно было использовать так же, как и в приведенных выше примерах.

  -- Drop the function if it already exists
  IF OBJECT_ID('INSTR', 'FN') IS NOT NULL
    DROP FUNCTION INSTR
  GO

  -- User-defined function to implement Oracle INSTR in SQL Server
  CREATE FUNCTION INSTR (@str VARCHAR(8000), @substr VARCHAR(255), @start INT, @occurrence INT)
  RETURNS INT
  AS
  BEGIN
    DECLARE @found INT = @occurrence,
            @pos INT = @start;

    WHILE 1=1 
    BEGIN
        -- Find the next occurrence
        SET @pos = CHARINDEX(@substr, @str, @pos);

        -- Nothing found
        IF @pos IS NULL OR @pos = 0
            RETURN @pos;

        -- The required occurrence found
        IF @found = 1
            BREAK;

        -- Prepare to find another one occurrence
        SET @found = @found - 1;
        SET @pos = @pos + 1;
    END

    RETURN @pos;
  END
  GO

Чтобы избежать очевидного, когда REVERSEфункция доступна, вам не нужно создавать эту скалярную функцию, и вы можете просто получить требуемый результат следующим образом:

SELECT
  LEN(foo.filepath) - CHARINDEX('/', REVERSE(foo.filepath))+1 AS LastIndexOfSlash 
FROM foo

0

обрабатывает поиск чего-либо длиной более 1 символа. не стесняйтесь увеличивать размеры parm, если хотите.

не мог сопротивляться публикации

drop function if exists lastIndexOf
go 
create function lastIndexOf(@searchFor varchar(100),@searchIn varchar(500))
returns int
as
begin 

if LEN(@searchfor) > LEN(@searchin) return 0 
declare @r varchar(500), @rsp varchar(100)
select @r = REVERSE(@searchin)
select @rsp = REVERSE(@searchfor)
return len(@searchin) - charindex(@rsp, @r) - len(@searchfor)+1
end 

и тесты

select dbo.lastIndexof('greg','greg greg asdflk; greg sadf' )  -- 18
select dbo.lastIndexof('greg','greg greg asdflk; grewg sadf' )  --5
select dbo.lastIndexof(' ','greg greg asdflk; grewg sadf' ) --24
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.