Znajdź indeks ostatniego wystąpienia podłańcucha za pomocą T-SQL

127

Czy istnieje prosty sposób na znalezienie indeksu ostatniego wystąpienia łańcucha za pomocą SQL? Obecnie używam SQL Server 2000. Zasadniczo potrzebuję funkcjonalności, którą zapewnia .NETSystem.String.LastIndexOf zapewnia metoda . Trochę googlowanie ujawniło to - Funkcja pobierania ostatniego indeksu - ale to nie zadziała, jeśli przekażesz wyrażenie kolumnowe „tekst”. Inne rozwiązania znalezione gdzie indziej działają tylko wtedy, gdy wyszukiwany tekst ma długość 1 znaku.

Prawdopodobnie będę musiał ugotować funkcję. Jeśli to zrobię, opublikuję go tutaj, abyście mogli go obejrzeć i być może z niego skorzystać.

Raj
źródło

Odpowiedzi:

33

Jesteś ograniczony do małej listy funkcji dla danych tekstowych.

Wszystko, co mogę zasugerować, to zacznij od PATINDEX, ale pracuj wstecz od DATALENGTH-1, DATALENGTH-2, DATALENGTH-3itd., Aż uzyskasz wynik lub skończysz na zera (DATALENGTH-DATALENGTH)

To naprawdę jest coś, z czym SQL Server 2000po prostu nie może sobie poradzić.

Edytuj dla innych odpowiedzi : REVERSE nie znajduje się na liście funkcji, których można używać z danymi tekstowymi w SQL Server 2000

gbn
źródło
1
Tak, to dość niezręczne. Wydaje się, że powinno to być proste, ale tak nie jest!
Raj
... dlatego SQL 2005 ma varchar (max), aby umożliwić normalne funkcje
gbn
1
Ach! więc „varchar (max)” to kwestia SQL 2005, co wyjaśnia, dlaczego nie zadziałało, gdy wypróbowałem go na SQL 2000.
Raj
DATALENGTH nie daje mi poprawnego wyniku, chociaż LENGTH działa.
Tequila,
@Tequila i inni: DATALENGTHzwraca liczbę bajtów, a nie znaków. Dlatego DATALENGTHzwraca 2 x liczbę znaków w ciągu dla NVARCHARciągów. LENjednak zwraca liczbę znaków minus wszelkie końcowe białe znaki . Nigdy nie używać DATALENGTHdo obliczania długości postać chyba końcowe spacje są istotne i wiem na pewno, że moje typy danych są spójne, czy są VARCHARlubNVARCHAR
rbsdca
174

Prosty sposób? Nie, ale użyłem odwrotności. Dosłownie.

We wcześniejszych procedurach, aby znaleźć ostatnie wystąpienie danego ciągu, użyłem funkcji REVERSE (), a następnie CHARINDEX, a następnie ponownie REVERSE, aby przywrócić pierwotną kolejność. Na przykład:

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

pokazuje, jak wyodrębnić rzeczywiste nazwy plików bazy danych z ich „nazw fizycznych”, bez względu na to, jak głęboko są one zagnieżdżone w podfolderach. To wyszukuje tylko jeden znak (ukośnik odwrotny), ale możesz na tej podstawie zbudować dłuższe ciągi wyszukiwania.

Jedynym minusem jest to, że nie wiem, jak dobrze to zadziała na typach danych TEXT. Korzystam z SQL 2005 od kilku lat i nie jestem już zaznajomiony z pracą z TEKSTEM - ale wydaje mi się, że mógłbyś użyć na nim LEWEGO i PRAWEGO?

Philip

Philip Kelley
źródło
1
Przepraszam - jestem prawie pewien, że nigdy nie wróciłem do pracy z 2000 i obecnie nie mam dostępu do żadnych instalacji SQL 2000.
Philip Kelley
Znakomity! Nigdy nie przyszło mi do głowy, żeby rozwiązać ten problem w ten sposób!
Jared
4
Niezłe! Zmodyfikowałem na własne potrzeby: email.Substring (0, email.lastIndexOf ('@')) == SELECT LEFT (email, LEN (email) -CHARINDEX ('@', REVERSE (email)))
Fredrik Johansson
1
Takie sprytne rzeczy sprawiają, że programowanie jest tak zabawne!
Chris,
dlaczego nie po prostu użyć prawej zamiast lewej na oryginale zamiast dodatkowego rewersu
Phil
108

Najprostszy sposób to ...

REVERSE(SUBSTRING(REVERSE([field]),0,CHARINDEX('[expr]',REVERSE([field]))))
Mptje
źródło
3
+1 Ponieważ NIE uruchamia się błąd, taki jak „Nieprawidłowy parametr długości przekazany do funkcji LEFT lub SUBSTRING”, jeśli nie znaleziono dopasowania
Xilmiki
12
Jeśli Twój [expr]symbol jest dłuższy niż 1, również musisz go odwrócić!
Andrius Naruševičius
60

Jeśli używasz Sqlserver 2005 lub nowszego, REVERSEwielokrotne używanie funkcji jest szkodliwe dla wydajności, poniższy kod jest bardziej wydajny.

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
źródło
1
Z perspektywy czasu może się to wydawać oczywiste, ale jeśli szukasz ciągu zamiast pojedynczego znaku, musisz zrobić: 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
źródło
8

Stare, ale wciąż aktualne pytanie, więc oto, co stworzyłem na podstawie informacji dostarczonych przez innych tutaj.

create function fnLastIndexOf(@text varChar(max),@char varchar(1))
returns int
as
begin
return len(@text) - charindex(@char, reverse(@text)) -1
end
Jan
źródło
7

To działało bardzo dobrze dla mnie.

REVERSE(SUBSTRING(REVERSE([field]), CHARINDEX(REVERSE('[expr]'), REVERSE([field])) + DATALENGTH('[expr]'), DATALENGTH([field])))
Karthik DV
źródło
6
REVERSE(SUBSTRING(REVERSE(ap_description),CHARINDEX('.',REVERSE(ap_description)),len(ap_description)))  

działało lepiej dla mnie

mark brito
źródło
4

Hmm, wiem, że to stary wątek, ale tabela podsumowująca mogłaby to zrobić w SQL2000 (lub dowolnej innej bazie danych):

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

Tabela podsumowująca to tylko tabela liczb rosnących.

Plik substring(@str, _Tally.n, 1) = @delimDostaje położenie każdego separatora, a następnie po prostu uzyskać maksymalną pozycję w tym zestawie.

Tally Tally są niesamowite. Jeśli nie korzystałeś z nich wcześniej, jest dobry artykuł na temat SQL Server Central (bezpłatna rejestracja lub po prostu użyj Bug Me Not ( http://www.bugmenot.com/view/sqlservercentral.com )).

* EDYCJA: Usunięto n <= LEN(TEXT_FIELD), ponieważ nie można użyć LEN () na typie TEKST. Dopóki substring(...) = @delimpozostaje, choć wynik jest nadal prawidłowy.

Chris
źródło
Miły. Myślę, że jest to w rzeczywistości to samo rozwiązanie, co zaakceptowana przez gbn odpowiedź; po prostu używasz tabeli do przechowywania liczb całkowitych 1, 2, 3 itd., które są odejmowane od DATALENGTH i czytasz od pierwszego znaku do przodu zamiast od ostatniego znaku do tyłu.
Michael Petito,
2

Odwróć zarówno ciąg, jak i podciąg, a następnie wyszukaj pierwsze wystąpienie.

AK
źródło
Słuszna uwaga. Nie mam teraz 2000 i nie pamiętam, czy mogłem to zrobić, kiedy to zrobiłem.
AK
2

Niektóre inne odpowiedzi zwracają rzeczywisty ciąg, podczas gdy ja musiałem znać rzeczywisty indeks int. A odpowiedzi, które to robią, wydają się nadmiernie komplikować sprawę. Korzystając z innych odpowiedzi jako inspiracji, zrobiłem następujące ...

Najpierw utworzyłem funkcję:

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

Następnie w swoim zapytaniu możesz po prostu zrobić to:

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

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

Powyższe powinno zwrócić 23 (ostatni indeks „:”)

Mam nadzieję, że komuś to trochę ułatwiło!

Matt Goodwin
źródło
2

Zdaję sobie sprawę, że to pytanie sprzed kilku lat, ale ...

On Access 2010, można użyć InStrRev()do tego celu. Mam nadzieję że to pomoże.

Dan
źródło
2

Ta odpowiedź używa MS SQL Server 2008 (nie mam dostępu do MS SQL Server 2000), ale widzę to według OP to 3 sytuacje, które należy wziąć pod uwagę. Z tego, czego próbowałem, żadna odpowiedź nie obejmuje wszystkich 3 z nich:

  1. Zwraca ostatni indeks szukanego znaku w podanym ciągu.
  2. Zwraca ostatni indeks szukanego podłańcucha (więcej niż pojedynczy znak) w podanym ciągu.
  3. Jeśli szukany znak lub podłańcuch nie znajduje się w podanym ciągu, powróć 0

Funkcja, którą wymyśliłem, przyjmuje 2 parametry:

@String NVARCHAR(MAX) : Ciąg do wyszukiwania

@FindString NVARCHAR(MAX) : Pojedynczy znak lub podłańcuch do pobrania ostatniego indeksu w @String

Zwraca wartość będącą INTdodatnim indeksem @FindStringw @Stringlub 0znaczeniem, którego @FindStringnie ma@String

Oto wyjaśnienie, co robi ta funkcja:

  1. Inicjuje się, @ReturnValaby 0wskazać, że @FindStringnie ma tego w programie@String
  2. Sprawdza indeks @FindStringw @Stringprzy użyciuCHARINDEX()
  3. Jeśli indeks @FindStringw @Stringto 0, @ReturnValjest pozostawiony jako0
  4. Jeśli indeks @FindStringw @Stringjest > 0, @FindStringjest w, @Stringwięc oblicza ostatni indeks @FindStringw @Stringprzy użyciuREVERSE()
  5. Zwraca @ReturnValliczbę dodatnią będącą ostatnim indeksem @FindStringw @Stringlub 0wskazującą, że @FindStringnie ma tego elementu @String

Oto skrypt funkcji tworzenia (gotowy do kopiowania i wklejania):

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

Oto fragment, który wygodnie testuje funkcję:

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)

Uruchomiłem to tylko na MS SQL Server 2008, ponieważ nie mam dostępu do żadnej innej wersji, ale z tego, co sprawdziłem, powinno to być dobre przynajmniej dla 2008+.

Cieszyć się.

Gharbad The Weak
źródło
1

Wiem, że będzie to nieefektywne, ale czy zastanawiałeś się nad rzutowaniem textpola na varcharto, abyś mógł skorzystać z rozwiązania dostarczonego przez znalezioną stronę internetową? Wiem, że to rozwiązanie stworzyłoby problemy, ponieważ możesz potencjalnie skrócić rekord, jeśli długość w textpolu przekroczyłaby długość twojego varchar(nie wspominając o tym, że nie byłby zbyt wydajny).

Ponieważ dane znajdują się wewnątrz textpola (i używasz programu SQL Server 2000), opcje są ograniczone.

Andrew Hare
źródło
Tak, rzutowanie na „varchar” nie jest opcją, ponieważ przetwarzane dane często przekraczają maksimum, które można przechowywać w „varchar”. Dzięki za odpowiedź!
Raj
1

Jeśli chcesz uzyskać indeks ostatniej spacji w ciągu słów, możesz użyć tego wyrażenia RIGHT (nazwa, (CHARINDEX ('', REVERSE (nazwa), 0)), aby zwrócić ostatnie słowo w ciągu. jest przydatne, jeśli chcesz przeanalizować nazwisko pełnego imienia, które zawiera inicjały pierwszego i / lub drugiego imienia.

Justin Stephens
źródło
1

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

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

Nie testowane, może być wyłączone o jeden z powodu zerowego indeksu, ale działa w SUBSTRINGfunkcji podczas odcinania od @indexOfznaków do końca ciągu

SUBSTRING([MyField], 0, @LastIndexOf)

Deresz
źródło
1

Ten kod działa nawet wtedy, gdy podciąg zawiera więcej niż 1 znak.

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
źródło
0

Musiałem znaleźć n-tą ostatnią pozycję ukośnika odwrotnego w ścieżce folderu. Oto moje rozwiązanie.

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

Oto moje przypadki testowe, które pomyślnie przeszły

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
placet
źródło
0

Aby uzyskać część przed ostatnim wystąpieniem ogranicznika (działa tylko ze NVARCHARwzględu na DATALENGTHużycie):

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.
źródło
0

Ta odpowiedź spełnia wymagania PO. w szczególności pozwala na to, aby igła miała więcej niż jeden znak i nie generuje błędu, gdy igła nie zostanie znaleziona w stogu siana. Wydawało mi się, że większość (wszystkie?) Z pozostałych odpowiedzi nie dotyczyła tych skrajnych przypadków. Poza tym dodałem argument "Pozycja początkowa" dostarczany przez natywną funkcję CharIndex serwera MS SQL. Próbowałem dokładnie odzwierciedlić specyfikację CharIndex, z wyjątkiem przetwarzania od prawej do lewej zamiast od lewej do prawej. np. zwracam wartość null, jeśli igła lub stóg siana jest zerowa, a zwracam zero, jeśli igła nie została znaleziona w stogu siana. Jedną rzeczą, której nie mogłem obejść, jest to, że przy wbudowanej funkcji trzeci parametr jest opcjonalny. W przypadku funkcji zdefiniowanych przez użytkownika programu SQL Server wszystkie parametry muszą zostać podane w wywołaniu, chyba że funkcja jest wywoływana przy użyciu polecenia „EXEC” . Podczas gdy trzeci parametr musi znajdować się na liście parametrów, możesz podać słowo kluczowe „default” jako symbol zastępczy dla niego bez konieczności nadawania mu wartości (patrz przykłady poniżej). Ponieważ łatwiej jest usunąć trzeci parametr z tej funkcji, jeśli nie jest to pożądane, niż dodanie go w razie potrzeby, umieściłem go tutaj jako punkt wyjścia.

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
źródło
0

Natknąłem się na ten wątek, szukając rozwiązania mojego podobnego problemu, które miało dokładnie takie same wymagania, ale dotyczyło innego rodzaju bazy danych, która również nie zawierała REVERSE funkcji.

W moim przypadku był to plik bazy danych OpenEdge (Progress) , która ma nieco inną składnię. Dzięki temu INSTRudostępniono mi funkcję, którą oferuje większość baz danych typu Oracle .

Więc wymyśliłem następujący kod:

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

Jednak w mojej konkretnej sytuacji (będącej OpenEdge (Progress) danych ) nie spowodowało to pożądanego zachowania, ponieważ zastąpienie znaku pustym znakiem dało taką samą długość jak oryginalny ciąg. Nie ma to dla mnie większego sensu, ale udało mi się ominąć problem z poniższym kodem:

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

Teraz rozumiem, że ten kod nie rozwiąże problemu T-SQL ponieważ nie ma alternatywy dla INSTRfunkcji, która oferujeOccurence właściwość.

Dla dokładności dodam kod potrzebny do utworzenia tej funkcji skalarnej, aby można było jej używać w taki sam sposób, jak w powyższych przykładach.

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

Aby uniknąć oczywistego, gdy REVERSEfunkcja jest dostępna, nie musisz tworzyć tej funkcji skalarnej i możesz po prostu uzyskać wymagany wynik w następujący sposób:

SELECT
  LEN(foo.filepath) - CHARINDEX('/', REVERSE(foo.filepath))+1 AS LastIndexOfSlash 
FROM foo
Oceany
źródło