Encontre o índice da última ocorrência de uma sub-string usando o T-SQL

127

Existe uma maneira direta de encontrar o índice da última ocorrência de uma string usando SQL? Estou usando o SQL Server 2000 agora. Basicamente, preciso da funcionalidade que o System.String.LastIndexOfmétodo .NET fornece. Um pouco de pesquisa revelou isso - Função para recuperar o último índice - mas isso não funciona se você passar uma expressão de coluna "texto". Outras soluções encontradas em outros lugares funcionam apenas desde que o texto que você está procurando tenha 1 caractere.

Provavelmente terei que preparar uma função. Se eu fizer isso, vou postá-lo aqui para que vocês possam ver e talvez fazer uso.

Raj
fonte

Respostas:

33

Você está limitado a uma pequena lista de funções para o tipo de dados de texto.

Tudo o que posso sugerir é começar PATINDEX, mas trabalhar de trás para frente, DATALENGTH-1, DATALENGTH-2, DATALENGTH-3etc, até você obter um resultado ou terminar em zero (DATALENGTH-DATALENGTH)

Isso realmente é algo que SQL Server 2000simplesmente não pode lidar.

Editar para outras respostas : REVERSE não está na lista de funções que podem ser usadas com dados de texto no SQL Server 2000

gbn
fonte
1
Sim, é muito estranho. Parece que deve ser simples, só que não é!
22499 Raj
... é por isso que SQL 2005 tem varchar (max) para permitir que as funções normais
GBN
1
Ah! tão "varchar (max)" é uma coisa SQL 2005, o que explica por que não funcionou quando eu tentei no SQL 2000.
Raj
DATALENGTH falha em produzir o resultado correto para mim, embora LENGTH funcione.
Tequila
@Tequila e outros: DATALENGTHretorna o número de bytes, não caracteres. Portanto, DATALENGTHretorna 2 x o número de caracteres em uma string para NVARCHARstrings. LEN, no entanto, retorna o número de caracteres, menos qualquer espaço em branco à direita . Eu nunca utilizar DATALENGTHpara o cálculo do comprimento de caracteres, a menos espaços em branco é significativo e eu sei com certeza que os meus tipos de dados são consistentes, quer sejam VARCHARouNVARCHAR
rbsdca
174

Maneira simples? Não, mas eu usei o contrário. Literalmente.

Em rotinas anteriores, para encontrar a última ocorrência de uma determinada string, usei a função REVERSE (), seguida de CHARINDEX, seguida novamente por REVERSE para restaurar a ordem original. Por exemplo:

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

mostra como extrair os nomes dos arquivos de banco de dados reais de seus "nomes físicos", não importa quão profundamente aninhados nas subpastas. Isso procura apenas um caractere (a barra invertida), mas você pode criar isso para cadeias de pesquisa mais longas.

A única desvantagem é que não sei como isso funcionará nos tipos de dados TEXT. Estou no SQL 2005 há alguns anos e não estou mais familiarizado com o trabalho com o TEXT - mas me lembro que você poderia usar ESQUERDA e DIREITA nele?

Philip

Philip Kelley
fonte
1
Desculpe - tenho certeza de que nunca voltei quando estava trabalhando com o 2000 e, atualmente, não tenho acesso a nenhuma instalação do SQL 2000.
Philip Kelley
Brilhante! Nunca teria pensado em atacar esse problema dessa maneira!
Jared
4
Agradável! Eu modifiquei para minhas próprias necessidades: email.Substring (0, email.lastIndexOf ('@')) == SELECIONAR ESQUERDO (email, LEN (email) -CHARINDEX ('@', REVERSE (email)))
Fredrik Johansson
1
Coisas inteligentes como essa são por que a programação é tão divertida!
21415 Chris
por que não usar a direita em vez da esquerda no original em vez de um reverso adicional
Phil
108

A maneira mais simples é ....

REVERSE(SUBSTRING(REVERSE([field]),0,CHARINDEX('[expr]',REVERSE([field]))))
Mptje
fonte
3
+1 Porque erro NÃO fogo como 'parâmetro comprimento inválido passado para a função LEFT ou SUBSTRING' se nenhuma correspondência foi encontrada
Xilmiki
12
Se o seu [expr]símbolo for maior que 1, você também precisará revertê-lo!
Andrius Naruševičius
60

Se você estiver usando o Sqlserver 2005 ou superior, o uso da REVERSEfunção muitas vezes é prejudicial ao desempenho, o código abaixo é mais eficiente.

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
Binoj Antony
fonte
1
Pode parecer óbvio em retrospectiva, mas se você estiver procurando por uma sequência em vez de um único caractere, precisará fazer: 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
Shivendra
fonte
8

Pergunta antiga, mas ainda válida, então aqui está o que eu criei com base nas informações fornecidas por outras pessoas aqui.

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

Isso funcionou muito bem para mim.

REVERSE(SUBSTRING(REVERSE([field]), CHARINDEX(REVERSE('[expr]'), REVERSE([field])) + DATALENGTH('[expr]'), DATALENGTH([field])))
Karthik DV
fonte
6
REVERSE(SUBSTRING(REVERSE(ap_description),CHARINDEX('.',REVERSE(ap_description)),len(ap_description)))  

funcionou melhor para mim

Mark Brito
fonte
4

Hmm, eu sei que esse é um thread antigo, mas uma tabela de registro poderia fazer isso no SQL2000 (ou em qualquer outro banco de dados):

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

Uma tabela de registro é apenas uma tabela de números incrementados.

O substring(@str, _Tally.n, 1) = @delimobtém a posição de cada delimitador, então você apenas obtém a posição máxima nesse conjunto.

As tabelas de registro são incríveis. Se você não os tiver usado antes, há um bom artigo no SQL Server Central (registro gratuito ou use Bug Me Not ( http://www.bugmenot.com/view/sqlservercentral.com )).

* EDIT: Removido n <= LEN(TEXT_FIELD), pois você não pode usar LEN () no tipo TEXT. Enquanto os substring(...) = @delimrestos que o resultado ainda está correto.

Chris
fonte
Agradável. Penso que esta é efetivamente a mesma solução que a resposta aceita por gbn; você está apenas usando uma tabela para armazenar os números inteiros 1, 2, 3 etc. que são subtraídos de DATALENGTH e lendo do primeiro caractere para a frente, em vez do último caractere.
Michael Petito
2

Inverta sua string e sua substring e procure a primeira ocorrência.

AK
fonte
Bom ponto. Não tenho 2000 agora e não me lembro se poderia fazê-lo quando o fiz.
AK
2

Algumas das outras respostas retornam uma string real, enquanto eu precisava mais saber o índice real int. E as respostas que fazem isso parecem complicar demais as coisas. Usando algumas das outras respostas como inspiração, fiz o seguinte ...

Primeiro, criei uma função:

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

Em seguida, na sua consulta, você pode simplesmente fazer o seguinte:

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

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

O acima deve retornar 23 (o último índice de ':')

Espero que isso tenha tornado um pouco mais fácil para alguém!

Matt Goodwin
fonte
2

Sei que essa é uma pergunta de vários anos, mas ...

Ativado Access 2010, você pode usar InStrRev()para fazer isso. Espero que isto ajude.

Dan
fonte
2

Esta resposta usa o MS SQL Server 2008 (não tenho acesso ao MS SQL Server 2000), mas a maneira como a vejo de acordo com o OP são três situações a serem consideradas. Pelo que tentei, nenhuma resposta aqui abrange todos os três:

  1. Retorna o último índice de um caractere de pesquisa em uma determinada sequência.
  2. Retorne o último índice de uma sub-string de pesquisa (mais do que apenas um caractere) em uma determinada string.
  3. Se o caractere ou sub-string de pesquisa não estiver na string especificada, retorne 0

A função que criei tem 2 parâmetros:

@String NVARCHAR(MAX) : A sequência a ser pesquisada

@FindString NVARCHAR(MAX) : Um único caractere ou uma sub-string para obter o último índice de @String

Ele retorna um INTque seja o índice de positividade de @FindStringem @Stringou 0o que significa que@FindString não está no@String

Aqui está uma explicação do que a função faz:

  1. Inicializa @ReturnValpara 0indicar que @FindStringnão está em@String
  2. Verifica o índice do @FindStringno @StringusandoCHARINDEX()
  3. Se o índice de @FindStringin @Stringfor 0, @ReturnValé deixado como0
  4. Se o índice de @FindStringin @Stringfor > 0, @FindStringfor in @String, ele calculará o último índice de @FindStringin @StringusandoREVERSE()
  5. Retorna @ReturnValque é um número positivo que é o último índice de @FindStringin @Stringou 0indica que @FindStringnão está em@String

Aqui está o script da função create (pronto para copiar e colar):

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

Aqui está um pouco que testa convenientemente a função:

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)

Eu executei isso apenas no MS SQL Server 2008 porque não tenho acesso a nenhuma outra versão, mas pelo que examinei isso deve ser bom para 2008 ou mais.

Aproveitar.

Gharbad, o fraco
fonte
1

Sei que será ineficiente, mas você já pensou em lançar o textcampo para varcharpoder usar a solução fornecida pelo site encontrado? Sei que essa solução criaria problemas, pois você poderia truncar o registro se o comprimento notext campo exceder o comprimento do seuvarchar (para não mencionar que não teria muito desempenho).

Como seus dados estão dentro de um textcampo (e você está usando o SQL Server 2000), suas opções são limitadas.

Andrew Hare
fonte
Sim, converter para "varchar" não é uma opção, pois os dados sendo processados ​​frequentemente excedem o máximo que pode ser mantido em um "varchar". Obrigado pela sua resposta!
21499 Raj
1

Se você deseja obter o índice do último espaço em uma sequência de palavras, pode usar esta expressão RIGHT (nome, (CHARINDEX ('', REVERSE (nome), 0)) para retornar a última palavra na sequência. é útil se você deseja analisar o sobrenome de um nome completo que inclua iniciais para o nome e / ou o nome do meio.

Justin Stephens
fonte
1

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

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

Não foi testado, pode ser desativado em um por causa do índice zero, mas funciona em SUBSTRINGfunção ao cortar os @indexOfcaracteres até o final da sua string

SUBSTRING([MyField], 0, @LastIndexOf)

Roan
fonte
1

Esse código funciona mesmo se a substring contiver mais de 1 caractere.

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
Dmitry Kovganov
fonte
0

Eu precisava encontrar a enésima última posição de uma barra invertida em um caminho de pasta. Aqui está a minha solução.

/*
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

Aqui estão meus casos de teste que passam

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
decreto
fonte
0

Para obter a peça antes da última ocorrência do delimitador (funciona apenas NVARCHARdevido ao DATALENGTHuso):

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

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

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

Esta resposta atende aos requisitos do OP. especificamente, permite que a agulha tenha mais do que um único caractere e não gera um erro quando a agulha não é encontrada no palheiro. Pareceu-me que a maioria (todas?) Das outras respostas não lidou com esses casos extremos. Além disso, adicionei o argumento "Posição inicial" fornecido pela função CharIndex do servidor MS SQL nativo. Tentei espelhar exatamente a especificação do CharIndex, exceto para processar da direita para a esquerda, em vez da esquerda para a direita. por exemplo, retornarei nulo se a agulha ou o palheiro for nulo e retornarei zero se a agulha não for encontrada no palheiro. Uma coisa que eu não conseguia entender é que, com a função incorporada, o terceiro parâmetro é opcional. Com as funções definidas pelo usuário do SQL Server, todos os parâmetros devem ser fornecidos na chamada, a menos que a função seja chamada usando "EXEC" . Embora o terceiro parâmetro deva ser incluído na lista de parâmetros, você pode fornecer a palavra-chave "padrão" como um espaço reservado para ele sem precisar atribuir um valor (consulte os exemplos abaixo). Como é mais fácil remover o terceiro parâmetro dessa função, se não for desejado, do que seria adicioná-lo, se necessário, eu o incluí aqui como ponto de partida.

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
Ted Cohen
fonte
0

Me deparei com esse segmento enquanto procurava uma solução para meu problema semelhante, que tinha exatamente o mesmo requisito, mas era para um tipo diferente de banco de dados que também não possuía o REVERSE função.

No meu caso, isso era para um banco de dados OpenEdge (Progress) , que possui uma sintaxe um pouco diferente. Isso INSTRdisponibilizou para mim a função que a maioria dos bancos de dados digitados Oracle oferece .

Então, eu vim com o seguinte código:

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

No entanto, para minha situação específica (sendo o banco de dados OpenEdge (Progress) ), isso não resultou no comportamento desejado, pois a substituição do caractere por um caractere vazio deu o mesmo comprimento que a string original. Isso não faz muito sentido para mim, mas eu pude ignorar o problema com o código abaixo:

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

Agora entendo que esse código não resolverá o problema do T-SQL porque não há alternativa à INSTRfunção que oferece oOccurence propriedade.

Apenas para ser aprofundado, adicionarei o código necessário para criar esta função escalar, para que possa ser usada da mesma maneira que fiz nos exemplos acima.

  -- 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

Para evitar o óbvio, quando a REVERSEfunção está disponível, você não precisa criar essa função escalar e pode obter o resultado necessário da seguinte maneira:

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