Rechercher l'index de la dernière occurrence d'une sous-chaîne à l'aide de T-SQL

128

Existe-t-il un moyen simple de trouver l'index de la dernière occurrence d'une chaîne à l'aide de SQL? J'utilise actuellement SQL Server 2000. J'ai essentiellement besoin des fonctionnalités System.String.LastIndexOffournies par la méthode .NET . Un peu de googler a révélé ceci - Fonction pour récupérer le dernier index - mais cela ne fonctionne pas si vous passez dans une expression de colonne "texte". Les autres solutions trouvées ailleurs ne fonctionnent que tant que le texte que vous recherchez fait 1 caractère.

Je devrai probablement préparer une fonction. Si je le fais, je le posterai ici pour que vous puissiez le regarder et peut-être l'utiliser.

Raj
la source

Réponses:

33

Vous êtes limité à une petite liste de fonctions pour le type de données texte.

Tout ce que je peux suggérer, c'est de commencer par PATINDEX, mais de travailler à rebours, DATALENGTH-1, DATALENGTH-2, DATALENGTH-3etc. jusqu'à ce que vous obteniez un résultat ou que vous finissiez à zéro (DATALENGTH-DATALENGTH)

C'est vraiment quelque chose qui SQL Server 2000ne peut tout simplement pas gérer.

Modifier pour d'autres réponses : REVERSE ne figure pas dans la liste des fonctions pouvant être utilisées avec des données texte dans SQL Server 2000

gbn
la source
1
Ouais, c'est assez gênant. Cela semble devoir être simple, mais ce n'est pas le cas!
Raj
... c'est pourquoi SQL 2005 a varchar (max) pour autoriser les fonctions normales
gbn
1
Ah! donc "varchar (max)" est une chose SQL 2005, ce qui explique pourquoi cela n'a pas fonctionné quand je l'ai essayé sur SQL 2000.
Raj
DATALENGTH ne parvient pas à produire le résultat correct pour moi, bien que LENGTH fonctionne.
Tequila
@Tequila et autres: DATALENGTHrenvoie le nombre d'octets et non de caractères. Par conséquent, DATALENGTHrenvoie 2 x le nombre de caractères dans une chaîne pour les NVARCHARchaînes. LEN, cependant, renvoie le nombre de caractères, moins les espaces de fin . Je ne l'utilise jamais DATALENGTHpour le calcul de la longueur des caractères à moins que l'espace blanc de fin ne soit significatif et je sais avec certitude que mes types de données sont cohérents, qu'ils soient VARCHARouNVARCHAR
rbsdca
175

Une manière simple? Non, mais j'ai utilisé l'inverse. Au sens propre.

Dans les routines précédentes, pour trouver la dernière occurrence d'une chaîne donnée, j'ai utilisé la fonction REVERSE (), suivie de CHARINDEX, suivie à nouveau de REVERSE pour restaurer l'ordre d'origine. Par exemple:

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

montre comment extraire les noms de fichiers réels de la base de données à partir de leurs "noms physiques", quelle que soit la profondeur de leur imbrication dans les sous-dossiers. Cela ne recherche qu'un seul caractère (la barre oblique inverse), mais vous pouvez utiliser cela pour des chaînes de recherche plus longues.

Le seul inconvénient est que je ne sais pas dans quelle mesure cela fonctionnera sur les types de données TEXT. Je suis sur SQL 2005 depuis quelques années maintenant, et je ne suis plus familier avec TEXT - mais il me semble que vous pourriez utiliser GAUCHE et DROITE dessus?

Philippe

Philippe Kelley
la source
1
Désolé - je suis à peu près sûr que je ne l'ai jamais fait lorsque je travaillais avec 2000, et je n'ai actuellement accès à aucune installation SQL 2000.
Philip Kelley
Brillant! Jamais je n'aurais pensé à attaquer ce problème de cette façon!
Jared
4
Joli! J'ai modifié pour mes propres besoins: email.Substring (0, email.lastIndexOf ('@')) == SELECT LEFT (email, LEN (email) -CHARINDEX ('@', REVERSE (email)))
Fredrik Johansson
1
Des trucs intelligents comme celui-ci sont la raison pour laquelle la programmation est si amusante!
Chris
pourquoi ne pas simplement utiliser la droite au lieu de la gauche sur l'original au lieu d'un revers supplémentaire
Phil
108

Le moyen le plus simple est ...

REVERSE(SUBSTRING(REVERSE([field]),0,CHARINDEX('[expr]',REVERSE([field]))))
Mptje
la source
3
+1 Parce que NOT déclenche une erreur comme `` Paramètre de longueur invalide passé à la fonction LEFT ou SUBSTRING '' si aucune correspondance n'a été trouvée
Xilmiki
12
Si votre [expr]symbole dépasse 1 symbole, vous devez également l'inverser!
Andrius Naruševičius
60

Si vous utilisez Sqlserver 2005 ou supérieur, l'utilisation de la REVERSEfonction plusieurs fois est préjudiciable aux performances, le code ci-dessous est plus efficace.

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
la source
1
Cela peut sembler évident avec le recul, mais si vous recherchez une chaîne au lieu d'un seul caractère, vous devez faire: 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
la source
8

Question ancienne mais toujours valable, voici donc ce que j'ai créé sur la base des informations fournies par d'autres ici.

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

Cela a très bien fonctionné pour moi.

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

a mieux fonctionné pour moi

marque brito
la source
4

Hmm, je sais que c'est un ancien thread, mais une table de pointage pourrait le faire dans SQL2000 (ou toute autre base de données):

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

Un tableau de pointage est juste un tableau de nombres incrémentiels.

Le substring(@str, _Tally.n, 1) = @delimobtient la position de chaque délimiteur, puis vous obtenez simplement la position maximale dans cet ensemble.

Les tableaux de pointage sont géniaux. Si vous ne les avez pas encore utilisés, il existe un bon article sur SQL Server Central (Free reg, ou utilisez simplement Bug Me Not ( http://www.bugmenot.com/view/sqlservercentral.com )).

* EDIT: Supprimé n <= LEN(TEXT_FIELD), car vous ne pouvez pas utiliser LEN () sur le type TEXT. Tant que le substring(...) = @delimreste, le résultat est toujours correct.

Chris
la source
Agréable. Je pense que c'est effectivement la même solution que la réponse acceptée par gbn; vous utilisez simplement une table pour stocker les entiers 1, 2, 3, etc. qui sont soustraits de DATALENGTH et que vous lisez du premier caractère en avant au lieu du dernier caractère en arrière.
Michael Petito
2

Inversez à la fois votre chaîne et votre sous-chaîne, puis recherchez la première occurrence.

AK
la source
Bon point. Je n'ai pas 2000 maintenant, et je ne me souviens pas si je pouvais le faire quand je l'ai fait.
AK
2

Certaines des autres réponses renvoient une chaîne réelle alors que j'avais plus besoin de connaître l'index réel int. Et les réponses qui font cela semblent trop compliquer les choses. En utilisant certaines des autres réponses comme source d'inspiration, j'ai fait ce qui suit ...

Tout d'abord, j'ai créé une fonction:

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

Ensuite, dans votre requête, vous pouvez simplement faire ceci:

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

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

Ce qui précède doit renvoyer 23 (le dernier index de ':')

J'espère que cela a rendu les choses un peu plus faciles pour quelqu'un!

Matt Goodwin
la source
2

Je me rends compte que c'est une question vieille de plusieurs années, mais ...

Sur Access 2010, vous pouvez utiliser InStrRev()pour le faire. J'espère que cela t'aides.

Dan
la source
2

Cette réponse utilise MS SQL Server 2008 (je n'ai pas accès à MS SQL Server 2000), mais la façon dont je la vois selon l'OP sont 3 situations à prendre en compte. D'après ce que j'ai essayé, aucune réponse ne couvre les 3:

  1. Renvoie le dernier index d'un caractère recherché dans une chaîne donnée.
  2. Renvoie le dernier index d'une sous-chaîne de recherche (plus qu'un simple caractère) dans une chaîne donnée.
  3. Si le caractère ou la sous-chaîne de recherche n'est pas dans la chaîne donnée, retournez 0

La fonction que j'ai créée prend 2 paramètres:

@String NVARCHAR(MAX) : La chaîne à rechercher

@FindString NVARCHAR(MAX) : Soit un seul caractère, soit une sous-chaîne pour obtenir le dernier index de @String

Il renvoie un INTqui est soit l'indice positif de @FindStringin, @Stringsoit la 0signification qui @FindStringn'est pas dans@String

Voici une explication de ce que fait la fonction:

  1. S'initialise @ReturnValpour 0indiquer que ce @FindStringn'est pas@String
  2. Vérifie l'index du @FindStringin @Stringen utilisantCHARINDEX()
  3. Si l'index de @FindStringin @Stringest 0, @ReturnValest laissé comme0
  4. Si l'index de @FindStringin @Stringest > 0, @FindStringis in @String, il calcule le dernier index de @FindStringin @Stringen utilisantREVERSE()
  5. Renvoie @ReturnValun nombre positif qui est le dernier index de @FindStringin @Stringou 0indiquant qu'il @FindStringn'est pas dans@String

Voici le script de création de la fonction (prêt pour le copier-coller):

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

Voici un petit peu qui teste commodément la fonction:

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)

Je n'ai exécuté cela que sur MS SQL Server 2008 parce que je n'ai accès à aucune autre version, mais d'après ce que j'ai examiné, cela devrait être bon pour 2008+ au moins.

Prendre plaisir.

Gharbad le faible
la source
1

Je sais que ce sera inefficace mais avez-vous envisagé de convertir le textchamp pour varcharpouvoir utiliser la solution fournie par le site Web que vous avez trouvé? Je sais que cette solution créerait des problèmes car vous pourriez potentiellement tronquer l'enregistrement si la longueur du textchamp dépassait la longueur de votre varchar(pour ne pas mentionner qu'il ne serait pas très performant).

Étant donné que vos données se texttrouvent dans un champ (et que vous utilisez SQL Server 2000), vos options sont limitées.

Andrew Hare
la source
Oui, la conversion en "varchar" n'est pas une option car les données en cours de traitement dépassent fréquemment le maximum pouvant être conservé dans un "varchar". Merci pour votre réponse!
Raj
1

Si vous souhaitez obtenir l'index du dernier espace dans une chaîne de mots, vous pouvez utiliser cette expression RIGHT (nom, (CHARINDEX ('', REVERSE (nom), 0)) pour renvoyer le dernier mot de la chaîne. est utile si vous souhaitez analyser le nom de famille d'un nom complet qui inclut les initiales du prénom et / ou du deuxième prénom.

Justin Stephens
la source
1

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

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

Je n'ai pas testé, il peut être désactivé de un à cause de l'index zéro, mais fonctionne en SUBSTRINGfonction lors du découpage des @indexOfcaractères à la fin de votre chaîne

SUBSTRING([MyField], 0, @LastIndexOf)

Rouan
la source
1

Ce code fonctionne même si la sous-chaîne contient plus d'un caractère.

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
la source
0

J'avais besoin de trouver la nième dernière position d'une barre oblique inverse dans le chemin d'un dossier. Voici ma solution.

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

Voici mes cas de test qui passent

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
décret
la source
0

Pour obtenir la partie avant la dernière occurrence du délimiteur (fonctionne uniquement en NVARCHARraison de l' DATALENGTHutilisation):

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
la source
0

Cette réponse répond aux exigences du PO. spécifiquement, il permet à l'aiguille d'être plus d'un seul caractère et il ne génère pas d'erreur lorsque l'aiguille n'est pas trouvée dans la botte de foin. Il me semblait que la plupart (toutes?) Des autres réponses ne traitaient pas ces cas extrêmes. Au-delà, j'ai ajouté l'argument "Position de départ" fourni par la fonction CharIndex native du serveur MS SQL. J'ai essayé de refléter exactement la spécification de CharIndex, sauf pour traiter de droite à gauche au lieu de gauche à droite. par exemple, je renvoie null si aiguille ou meule de foin est nulle et je renvoie zéro si l'aiguille n'est pas trouvée dans la meule de foin. Une chose que je ne pouvais pas contourner est qu'avec la fonction intégrée, le troisième paramètre est facultatif. Avec les fonctions définies par l'utilisateur SQL Server, tous les paramètres doivent être fournis dans l'appel sauf si la fonction est appelée à l'aide de "EXEC" . Alors que le troisième paramètre doit être inclus dans la liste des paramètres, vous pouvez lui fournir le mot-clé "default" comme espace réservé sans avoir à lui donner une valeur (voir les exemples ci-dessous). Puisqu'il est plus facile de supprimer le troisième paramètre de cette fonction s'il n'est pas souhaité que de l'ajouter si nécessaire, je l'ai inclus ici comme point de départ.

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
la source
0

Je suis tombé sur ce fil en cherchant une solution à mon problème similaire qui avait exactement la même exigence mais qui concernait un type de base de données différent qui manquait également de REVERSEfonction.

Dans mon cas, c'était pour une base de données OpenEdge (Progress) , qui a une syntaxe légèrement différente. Cela INSTRm'a rendu disponible la fonction que la plupart des bases de données typées Oracle offrent .

Je suis donc venu avec le code suivant:

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

Cependant, pour ma situation spécifique (étant la base de données OpenEdge (Progress) ), cela n'a pas abouti au comportement souhaité car le remplacement du caractère par un caractère vide a donné la même longueur que la chaîne d'origine. Cela n'a pas beaucoup de sens pour moi, mais j'ai pu contourner le problème avec le code ci-dessous:

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

Maintenant, je comprends que ce code ne résoudra pas le problème pour T-SQL car il n'y a pas d'alternative à la INSTRfonction qui offre la Occurencepropriété.

Juste pour être complet, j'ajouterai le code nécessaire pour créer cette fonction scalaire afin qu'elle puisse être utilisée de la même manière que je l'ai fait dans les exemples ci-dessus.

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

Pour éviter l'évidence, lorsque la REVERSEfonction est disponible, vous n'avez pas besoin de créer cette fonction scalaire et vous pouvez simplement obtenir le résultat requis comme ceci:

SELECT
  LEN(foo.filepath) - CHARINDEX('/', REVERSE(foo.filepath))+1 AS LastIndexOfSlash 
FROM foo
Océans
la source