Encuentre el índice de la última aparición de una subcadena usando T-SQL


128

¿Hay una manera sencilla de encontrar el índice de la última aparición de una cadena usando SQL? Estoy usando SQL Server 2000 en este momento. Básicamente necesito la funcionalidad que System.String.LastIndexOfproporciona el método .NET . Una pequeña búsqueda en Google reveló esto: Función para recuperar el último índice , pero eso no funciona si pasa una expresión de columna de "texto". Otras soluciones que se encuentran en otros lugares funcionan solo mientras el texto que está buscando tenga 1 carácter.

Probablemente tendré que preparar una función. Si lo hago, lo publicaré aquí para que lo puedan ver y tal vez usarlo.

Respuestas:


33

Está limitado a una pequeña lista de funciones para el tipo de datos de texto.

Todo lo que puedo sugerir es comenzar PATINDEX, pero trabajar hacia atrás desde DATALENGTH-1, DATALENGTH-2, DATALENGTH-3etc. hasta que obtenga un resultado o termine en cero (DATALENGTH-DATALENGTH)

Esto realmente es algo que SQL Server 2000simplemente no puede manejar.

Editar para otras respuestas : REVERSE no está en la lista de funciones que se pueden usar con datos de texto en SQL Server 2000


1
Sí, es bastante incómodo. Parece que esto debería ser simple, ¡pero no lo es!
Raj

... esta es la razón por la cual SQL 2005 tiene varchar (max) para permitir funciones normales
gbn

1
Ah! por lo que "varchar (max)" es una cosa SQL 2005, lo que explica por qué no funcionó cuando lo probé en SQL 2000.
Raj

DATALENGTH no produce el resultado correcto para mí, aunque LENGTH funciona.
Tequila

@Tequila y otros: DATALENGTHdevuelve el número de bytes, no caracteres. Por lo tanto, DATALENGTHdevuelve 2 veces el número de caracteres en una cadena para NVARCHARcadenas. LEN, sin embargo, devuelve el número de caracteres, menos cualquier espacio en blanco al final . Nunca lo uso DATALENGTHpara el cálculo de la longitud de los caracteres, a menos que el espacio en blanco final sea significativo y estoy seguro de que mis tipos de datos son consistentes, ya sean VARCHARoNVARCHAR
rbsdca

175

¿De manera directa? No, pero he usado lo contrario. Literalmente.

En rutinas anteriores, para encontrar la última ocurrencia de una cadena dada, utilicé la función REVERSE (), seguí CHARINDEX, seguido nuevamente por REVERSE para restaurar el orden original. Por ejemplo:

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

muestra cómo extraer los nombres de archivo de base de datos reales de sus "nombres físicos", sin importar cuán profundamente anidados en las subcarpetas. Esto solo busca un carácter (la barra invertida), pero puede aprovecharlo para cadenas de búsqueda más largas.

El único inconveniente es que no sé qué tan bien funcionará esto en los tipos de datos TEXT. He estado en SQL 2005 durante algunos años y ya no estoy familiarizado con el trabajo con TEXT, pero parece recordar que podría usar IZQUIERDA y DERECHA en él.

Philip


1
Lo siento, estoy bastante seguro de que nunca volví cuando estaba trabajando con 2000, y actualmente no tengo acceso a ninguna instalación de SQL 2000.
Philip Kelley

¡Brillante! ¡Nunca hubiera pensado atacar este problema de esta manera!
Jared

44
¡Buena esa! Modifiqué para mis propias necesidades: email.Substring (0, email.lastIndexOf ('@')) == SELECT LEFT (email, LEN (email) -CHARINDEX ('@', REVERSE (email)))
Fredrik Johansson

1
¡Cosas inteligentes como esta es la razón por la que la programación es tan divertida!
Chris

¿Por qué no usar la derecha en lugar de la izquierda en el original en lugar de un reverso adicional
Phil

108

La forma más simple es ...

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

3
+1 Porque NO se dispara un error como 'Parámetro de longitud no válido pasado a la función IZQUIERDA o SUBSTRING' si no se encuentra ninguna coincidencia
Xilmiki

12
Si tu [expr]es más largo que 1 símbolo, ¡también debes invertirlo!
Andrius Naruševičius

60

Si está utilizando Sqlserver 2005 o superior, el uso de la REVERSEfunción muchas veces es perjudicial para el rendimiento, el siguiente código es más 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

1
Puede parecer obvio en retrospectiva, pero si está buscando una cadena en lugar de un solo carácter, debe hacer: 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

Pregunta antigua pero aún válida, así que aquí está lo que creé basado en la información proporcionada por otros aquí.

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

7

Esto funcionó muy bien para mí.

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

funcionó mejor para mí


4

Hmm, sé que este es un hilo antiguo, pero una tabla de conteo podría hacer esto en SQL2000 (o cualquier otra base de datos):

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

Una tabla de conteo es solo una tabla de números incrementales.

El substring(@str, _Tally.n, 1) = @delimobtiene la posición de cada delimitador, a continuación, que acaba de obtener la máxima posición en ese conjunto.

Las tablas de conteo son increíbles. Si no los ha usado antes, hay un buen artículo sobre SQL Server Central (Free reg, o simplemente use Bug Me Not ( http://www.bugmenot.com/view/sqlservercentral.com )).

* EDITAR: eliminado n <= LEN(TEXT_FIELD), ya que no puede usar LEN () en el tipo de TEXTO. Mientras substring(...) = @delimpermanezca, el resultado sigue siendo correcto.


Agradable. Sin embargo, creo que esta es efectivamente la misma solución que la respuesta aceptada por gbn; solo está usando una tabla para almacenar los enteros 1, 2, 3, etc., que se restan de DATALENGTH y se leen desde el primer carácter hacia adelante en lugar del último carácter hacia atrás.
Michael Petito

2

Invierta tanto su cadena como su subcadena, luego busque la primera aparición.


Buen punto. No tengo 2000 ahora, y no puedo recordar si podría hacerlo cuando lo hice.
AK

2

Algunas de las otras respuestas devuelven una cadena real, mientras que tenía más necesidad de conocer el índice real int. Y las respuestas que hacen eso parecen complicar demasiado las cosas. Usando algunas de las otras respuestas como inspiración, hice lo siguiente ...

Primero, creé una función:

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

Luego, en su consulta, simplemente puede hacer esto:

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

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

Lo anterior debería devolver 23 (el último índice de ':')

¡Espero que esto lo haya hecho un poco más fácil para alguien!


2

Me doy cuenta de que esta es una pregunta de varios años, pero ...

Encendido Access 2010, puede usar InStrRev()para hacer esto. Espero que esto ayude.


2

Esta respuesta usa MS SQL Server 2008 (no tengo acceso a MS SQL Server 2000), pero la forma en que lo veo de acuerdo con el OP son 3 situaciones a tener en cuenta. Por lo que he intentado, ninguna respuesta aquí cubre los 3 de ellos:

  1. Devuelve el último índice de un carácter de búsqueda en una cadena dada.
  2. Devuelve el último índice de una subcadena de búsqueda (más que un simple carácter) en una cadena dada.
  3. Si el carácter de búsqueda o la subcadena no está en la cadena dada, devuelva 0

La función que se me ocurrió toma 2 parámetros:

@String NVARCHAR(MAX) : La cadena a buscar

@FindString NVARCHAR(MAX) : Un solo carácter o una subcadena para obtener el último índice de en @String

Devuelve un INTque es el índice positivo de @FindStringin @Stringo un 0significado que @FindStringno está en@String

Aquí hay una explicación de lo que hace la función:

  1. Inicializa @ReturnValpara 0indicar que @FindStringno está en@String
  2. Comprueba el índice de la @FindStringen @Stringmediante el uso deCHARINDEX()
  3. Si el índice de @FindStringin @Stringes 0, @ReturnValse deja como0
  4. Si el índice de @FindStringen @Stringes > 0, @FindStringes en @Stringlo que calcula el último índice de @FindStringen @Stringmediante el uso deREVERSE()
  5. Devuelve @ReturnValque es un número positivo que es el último índice de @FindStringin @Stringo 0que indica que @FindStringno está en@String

Aquí está el script de función crear (copiar y pegar listo):

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

Aquí hay un poco que prueba convenientemente la función:

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)

Solo ejecuté esto en MS SQL Server 2008 porque no tengo acceso a ninguna otra versión, pero por lo que he investigado, esto debería ser bueno para 2008+ al menos.

Disfrutar.


1

Sé que será ineficiente, pero ¿ha considerado enviar el textcampo para varcharque pueda usar la solución proporcionada por el sitio web que encontró? Sé que esta solución crearía problemas, ya que potencialmente podría truncar el registro si la longitud en el textcampo desbordara la longitud de su varchar(sin mencionar que no sería muy eficiente).

Como sus datos están dentro de un textcampo (y está utilizando SQL Server 2000), sus opciones son limitadas.


Sí, la conversión a "varchar" no es una opción, ya que los datos que se procesan con frecuencia exceden el máximo que se puede contener en un "varchar". ¡Gracias por tu respuesta!
Raj

1

Si desea obtener el índice del último espacio en una cadena de palabras, puede usar esta expresión DERECHA (nombre, (CHARINDEX ('', REVERSE (name), 0)) para devolver la última palabra en la cadena. es útil si desea analizar el apellido de un nombre completo que incluye las iniciales del primer y / o segundo nombre.


1

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

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

No lo he probado, puede estar desactivado por uno debido al índice cero, pero funciona en SUBSTRINGfunción cuando se corta desde los @indexOfcaracteres hasta el final de la cadena

SUBSTRING([MyField], 0, @LastIndexOf)


1

Este código funciona incluso si la subcadena contiene más de 1 carácter.

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

Necesitaba encontrar la enésima última posición de una barra diagonal inversa en una ruta de carpeta. Aquí está mi solució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

Aquí están mis casos de prueba que pasan

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

Para obtener la parte antes de la última aparición del delimitador (solo funciona NVARCHARdebido al DATALENGTHuso):

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

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

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

0

Esta respuesta cumple con los requisitos del OP. específicamente, permite que la aguja tenga más de un solo carácter y no genera un error cuando no se encuentra la aguja en el pajar. Me pareció que la mayoría (¿todas?) De las otras respuestas no manejaban esos casos extremos. Más allá de eso, agregué el argumento "Posición inicial" proporcionado por la función CharIndex del servidor MS SQL nativo. Traté de reflejar exactamente la especificación para CharIndex, excepto para procesar de derecha a izquierda en lugar de izquierda a derecha. Por ejemplo, devuelvo nulo si la aguja o el pajar son nulos y devuelvo cero si la aguja no se encuentra en el pajar. Una cosa que no pude evitar es que con la función incorporada el tercer parámetro es opcional. Con las funciones definidas por el usuario de SQL Server, todos los parámetros deben proporcionarse en la llamada a menos que la función se llame usando "EXEC" . Si bien el tercer parámetro debe incluirse en la lista de parámetros, puede proporcionar la palabra clave "default" como marcador de posición sin tener que asignarle un valor (consulte los ejemplos a continuación). Dado que es más fácil eliminar el tercer parámetro de esta función si no se desea que agregarlo si es necesario, lo he incluido aquí como punto 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

0

Encontré este hilo mientras buscaba una solución a mi problema similar que tenía exactamente el mismo requisito pero era para un tipo diferente de base de datos que también carecía de la REVERSEfunción.

En mi caso, esto fue para una base de datos OpenEdge (Progress) , que tiene una sintaxis ligeramente diferente. Esto puso INSTRa mi disposición la función que ofrecen la mayoría de las bases de datos escritas de Oracle .

Entonces se me ocurrió el siguiente código:

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

Sin embargo, para mi situación específica (que es la base de datos OpenEdge (Progress) ) esto no resultó en el comportamiento deseado porque reemplazar el carácter con un carácter vacío dio la misma longitud que la cadena original. Esto no tiene mucho sentido para mí, pero pude evitar el problema con el siguiente código:

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

Ahora entiendo que este código no resolverá el problema de T-SQL porque no hay alternativa a la INSTRfunción que ofrece la Occurencepropiedad.

Solo para ser exhaustivo, agregaré el código necesario para crear esta función escalar para que pueda usarse de la misma manera que lo hice en los ejemplos anteriores.

  -- 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 lo obvio, cuando la REVERSEfunción está disponible no necesita crear esta función escalar y simplemente puede obtener el resultado requerido de esta manera:

SELECT
  LEN(foo.filepath) - CHARINDEX('/', REVERSE(foo.filepath))+1 AS LastIndexOfSlash 
FROM foo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.