Come si conta il numero di occorrenze di una determinata sottostringa in un varchar SQL?


150

Ho una colonna con valori formattati come a, b, c, d. C'è un modo per contare il numero di virgole in quel valore in T-SQL?

Risposte:


245

Il primo modo che viene in mente è farlo indirettamente sostituendo la virgola con una stringa vuota e confrontando le lunghezze

Declare @string varchar(1000)
Set @string = 'a,b,c,d'
select len(@string) - len(replace(@string, ',', ''))

13
Questo risponde alla domanda come scritto nel testo, ma non come scritto nel titolo. Per farlo funzionare per più di un personaggio, devi solo aggiungere un / len (searchterm) attorno alla cosa. Inserita una risposta nel caso sia utile per qualcuno.
Andrew Barrett,

Qualcuno mi ha fatto notare che questo non funziona sempre come previsto. Considera quanto segue: SELEZIONA LEN ('a, b, c, d,') - LEN (REPLACE ('a, b, c, d,', ',', '')) Per motivi che non ho ancora capito , lo spazio tra la d e la colonna finale fa sì che questo ritorni 5 invece di 4. Invierò un'altra risposta che lo risolve, nel caso sia utile a chiunque.
Bubbleking

5
Forse usare DATALENGTH invece LEN sarebbe meglio, perché LEN restituisce la dimensione della stringa tagliata.
rodrigocl,

2
DATALENGTH () / 2 è anche complicato a causa di dimensioni dei caratteri non ovvie. Guarda stackoverflow.com/a/11080074/1094048 per un modo semplice e preciso per ottenere la lunghezza della stringa.
pkuderov,

@rodrigocl Perché non avvolgere un LTRIMgiro la stringa come segue: SELECT LEN(RTRIM(@string)) - LEN(REPLACE(RTRIM(@string), ',', ''))?
Alex Bello,

67

Rapida estensione della risposta di cmsjr che funziona per stringhe con più di più caratteri.

CREATE FUNCTION dbo.CountOccurrencesOfString
(
    @searchString nvarchar(max),
    @searchTerm nvarchar(max)
)
RETURNS INT
AS
BEGIN
    return (LEN(@searchString)-LEN(REPLACE(@searchString,@searchTerm,'')))/LEN(@searchTerm)
END

Uso:

SELECT * FROM MyTable
where dbo.CountOccurrencesOfString(MyColumn, 'MyString') = 1

16
Un leggero miglioramento sarebbe l'uso di DATALENGTH () / 2 invece di LEN (). LEN ignorerà qualsiasi spazio vuoto finale, quindi dbo.CountOccurancesOfString( 'blah ,', ',')restituirà 2 invece di 1 e dbo.CountOccurancesOfString( 'hello world', ' ')non riuscirà con divisione per zero.
Rory,

5
Il commento di Rory è utile. Ho scoperto che potevo semplicemente sostituire LEN con DATALENGTH nella funzione di Andrew e ottenere il risultato desiderato. Sembra che la divisione per 2 non sia necessaria con il modo in cui la matematica risolve.
Ghirlanda Papa

@AndrewBarrett: quale append quando più stringhe hanno la stessa lunghezza?
user2284570,

2
DATALENGTH()/2è anche complicato a causa di dimensioni dei caratteri non ovvie. Guarda stackoverflow.com/a/11080074/1094048 per un modo semplice e preciso.
pkuderov,

26

È possibile confrontare la lunghezza della stringa con una lunghezza in cui vengono rimosse le virgole:

len(value) - len(replace(value,',',''))

8

Basandoti sulla soluzione di @ Andrew, otterrai prestazioni molto migliori utilizzando una funzione a valori di tabella non procedurale e CROSS APPLY:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/*  Usage:
    SELECT t.[YourColumn], c.StringCount
    FROM YourDatabase.dbo.YourTable t
        CROSS APPLY dbo.CountOccurrencesOfString('your search string',     t.[YourColumn]) c
*/
CREATE FUNCTION [dbo].[CountOccurrencesOfString]
(
    @searchTerm nvarchar(max),
    @searchString nvarchar(max)

)
RETURNS TABLE
AS
    RETURN 
    SELECT (DATALENGTH(@searchString)-DATALENGTH(REPLACE(@searchString,@searchTerm,'')))/NULLIF(DATALENGTH(@searchTerm), 0) AS StringCount

Uso questa stessa funzione in molti dei miei database legacy, mi aiuta molto con molti database vecchi e progettati in modo improprio. Risparmia molto tempo ed è molto veloce anche su grandi set di dati.
Caimen

6

La risposta di @csmjr ha dei problemi in alcuni casi.

La sua risposta era di fare questo:

Declare @string varchar(1000)
Set @string = 'a,b,c,d'
select len(@string) - len(replace(@string, ',', ''))

Funziona nella maggior parte degli scenari, tuttavia, prova a eseguire questo:

DECLARE @string VARCHAR(1000)
SET @string = 'a,b,c,d ,'
SELECT LEN(@string) - LEN(REPLACE(@string, ',', ''))

Per qualche motivo, REPLACE elimina la virgola finale ma ANCHE lo spazio appena prima di esso (non so perché). Ciò si traduce in un valore restituito di 5 quando ti aspetteresti 4. Ecco un altro modo per farlo che funzionerà anche in questo scenario speciale:

DECLARE @string VARCHAR(1000)
SET @string = 'a,b,c,d ,'
SELECT LEN(REPLACE(@string, ',', '**')) - LEN(@string)

Nota che non è necessario utilizzare gli asterischi. Qualsiasi sostituzione a due caratteri farà. L'idea è di allungare la stringa di un carattere per ogni istanza del personaggio che stai contando, quindi sottrarre la lunghezza dell'originale. È sostanzialmente il metodo opposto della risposta originale che non ha lo strano effetto collaterale di rifilatura.


5
"Per qualche motivo, REPLACE elimina la virgola finale ma ANCHE lo spazio appena prima di esso (non so perché)." SOSTITUIRE non si sta liberando dell'ultima virgola e dello spazio precedente, in realtà è la funzione LEN che ignora lo spazio bianco risultante alla fine della stringa a causa di quello spazio.
Imranullah Khan,

2
Declare @string varchar(1000)

DECLARE @SearchString varchar(100)

Set @string = 'as as df df as as as'

SET @SearchString = 'as'

select ((len(@string) - len(replace(@string, @SearchString, ''))) -(len(@string) - 
        len(replace(@string, @SearchString, ''))) % 2)  / len(@SearchString)

questo in realtà restituisce 1 in meno del conteggio effettivo
The Integrator

1

La risposta accettata è corretta, estendendola per utilizzare 2 o più caratteri nella sottostringa:

Declare @string varchar(1000)
Set @string = 'aa,bb,cc,dd'
Set @substring = 'aa'
select (len(@string) - len(replace(@string, @substring, '')))/len(@substring)

1

Se sappiamo che esiste una limitazione per LEN e spazio, perché non possiamo sostituire prima lo spazio? Quindi sappiamo che non c'è spazio per confondere LEN.

len(replace(@string, ' ', '-')) - len(replace(replace(@string, ' ', '-'), ',', ''))

0
DECLARE @records varchar(400)
SELECT @records = 'a,b,c,d'
select  LEN(@records) as 'Before removing Commas' , LEN(@records) - LEN(REPLACE(@records, ',', '')) 'After Removing Commans'

0

Darrel Lee Penso che abbia una risposta abbastanza buona. Sostituiscilo CHARINDEX()con PATINDEX()e puoi fare anche qualche regexricerca debole lungo una stringa ...

Ad esempio, supponi di usarlo per @pattern:

set @pattern='%[-.|!,'+char(9)+']%'

Perché vorresti forse fare qualcosa di folle come questo?

Supponi di caricare stringhe di testo delimitate in una tabella di gestione temporanea, in cui il campo contenente i dati è simile a varchar (8000) o nvarchar (max) ...

A volte è più facile / veloce eseguire ELT (Estrai-Carica-Trasforma) con dati anziché ETL (Estrai-Trasforma-Carica), e un modo per farlo è caricare i record delimitati così com'è in una tabella di gestione temporanea, specialmente se potresti voler un modo più semplice di vedere i record eccezionali piuttosto che gestirli come parte di un pacchetto SSIS ... ma questa è una guerra santa per un thread diverso.


0

Quanto segue dovrebbe fare il trucco sia per le ricerche di caratteri singoli che multipli:

CREATE FUNCTION dbo.CountOccurrences
(
   @SearchString VARCHAR(1000),
   @SearchFor    VARCHAR(1000)
)
RETURNS TABLE
AS
   RETURN (
             SELECT COUNT(*) AS Occurrences
             FROM   (
                       SELECT ROW_NUMBER() OVER (ORDER BY O.object_id) AS n
                       FROM   sys.objects AS O
                    ) AS N
                    JOIN (
                            VALUES (@SearchString)
                         ) AS S (SearchString)
                         ON
                         SUBSTRING(S.SearchString, N.n, LEN(@SearchFor)) = @SearchFor
          );
GO

---------------------------------------------------------------------------------------
-- Test the function for single and multiple character searches
---------------------------------------------------------------------------------------
DECLARE @SearchForComma      VARCHAR(10) = ',',
        @SearchForCharacters VARCHAR(10) = 'de';

DECLARE @TestTable TABLE
(
   TestData VARCHAR(30) NOT NULL
);

INSERT INTO @TestTable
     (
        TestData
     )
VALUES
     ('a,b,c,de,de ,d e'),
     ('abc,de,hijk,,'),
     (',,a,b,cde,,');

SELECT TT.TestData,
       CO.Occurrences AS CommaOccurrences,
       CO2.Occurrences AS CharacterOccurrences
FROM   @TestTable AS TT
       OUTER APPLY dbo.CountOccurrences(TT.TestData, @SearchForComma) AS CO
       OUTER APPLY dbo.CountOccurrences(TT.TestData, @SearchForCharacters) AS CO2;

La funzione può essere semplificata un po 'usando una tabella di numeri (dbo.Nums):

   RETURN (
             SELECT COUNT(*) AS Occurrences
             FROM   dbo.Nums AS N
                    JOIN (
                            VALUES (@SearchString)
                         ) AS S (SearchString)
                         ON
                         SUBSTRING(S.SearchString, N.n, LEN(@SearchFor)) = @SearchFor
          );

0

Usa questo codice, funziona perfettamente. Ho creato una funzione sql che accetta due parametri, il primo parametro è la stringa lunga che vogliamo cercare in essa e può accettare una lunghezza della stringa fino a 1500 caratteri (ovviamente puoi estenderla o persino cambiarla in tipo di dati di testo ). E il secondo parametro è la sottostringa che vogliamo calcolare il numero della sua occorrenza (la sua lunghezza è fino a 200 caratteri, ovviamente puoi cambiarla in base alle tue esigenze). e l'output è un numero intero, rappresenta il numero di frequenza ..... divertitevi.


CREATE FUNCTION [dbo].[GetSubstringCount]
(
  @InputString nvarchar(1500),
  @SubString NVARCHAR(200)
)
RETURNS int
AS
BEGIN 
        declare @K int , @StrLen int , @Count int , @SubStrLen int 
        set @SubStrLen = (select len(@SubString))
        set @Count = 0
        Set @k = 1
        set @StrLen =(select len(@InputString))
    While @K <= @StrLen
        Begin
            if ((select substring(@InputString, @K, @SubStrLen)) = @SubString)
                begin
                    if ((select CHARINDEX(@SubString ,@InputString)) > 0)
                        begin
                        set @Count = @Count +1
                        end
                end
                                Set @K=@k+1
        end
        return @Count
end

0

Finalmente scrivo questa funzione che dovrebbe coprire tutte le possibili situazioni, aggiungendo un prefisso char e un suffisso all'input. questo carattere viene valutato come diverso da qualsiasi carattere inserito nel parametro di ricerca, quindi non può influenzare il risultato.

CREATE FUNCTION [dbo].[CountOccurrency]
(
@Input nvarchar(max),
@Search nvarchar(max)
)
RETURNS int AS
BEGIN
    declare @SearhLength as int = len('-' + @Search + '-') -2;
    declare @conteinerIndex as int = 255;
    declare @conteiner as char(1) = char(@conteinerIndex);
    WHILE ((CHARINDEX(@conteiner, @Search)>0) and (@conteinerIndex>0))
    BEGIN
        set @conteinerIndex = @conteinerIndex-1;
        set @conteiner = char(@conteinerIndex);
    END;
    set @Input = @conteiner + @Input + @conteiner
    RETURN (len(@Input) - len(replace(@Input, @Search, ''))) / @SearhLength
END 

uso

select dbo.CountOccurrency('a,b,c,d ,', ',')

0
Declare @MainStr nvarchar(200)
Declare @SubStr nvarchar(10)
Set @MainStr = 'nikhildfdfdfuzxsznikhilweszxnikhil'
Set @SubStr = 'nikhil'
Select (Len(@MainStr) - Len(REPLACE(@MainStr,@SubStr,'')))/Len(@SubStr)

0

In SQL 2017 o versioni successive, puoi utilizzare questo:

declare @hits int = 0
set @hits = (select value from STRING_SPLIT('F609,4DFA,8499',','));
select count(@hits)

0

questo codice T-SQL trova e stampa tutte le occorrenze del pattern @p nella frase @s. successivamente è possibile eseguire qualsiasi elaborazione sulla frase.

declare @old_hit int = 0
declare @hit int = 0
declare @i int = 0
declare @s varchar(max)='alibcalirezaalivisualization'
declare @p varchar(max)='ali'
 while @i<len(@s)
  begin
   set @hit=charindex(@p,@s,@i)
   if @hit>@old_hit 
    begin
    set @old_hit =@hit
    set @i=@hit+1
    print @hit
   end
  else
    break
 end

il risultato è: 1 6 13 20


0

per SQL Server 2017

declare @hits int = 0;
set @hits = (select count(*) from (select value from STRING_SPLIT('F609,4DFA,8499',',')) a);
select @hits;

-1

È possibile utilizzare la seguente procedura memorizzata per recuperare valori.

IF  EXISTS (SELECT * FROM sys.objects 
WHERE object_id = OBJECT_ID(N'[dbo].[sp_parsedata]') AND type in (N'P', N'PC'))
    DROP PROCEDURE [dbo].[sp_parsedata]
GO
create procedure sp_parsedata
(@cid integer,@st varchar(1000))
as
  declare @coid integer
  declare @c integer
  declare @c1 integer
  select @c1=len(@st) - len(replace(@st, ',', ''))
  set @c=0
  delete from table1 where complainid=@cid;
  while (@c<=@c1)
    begin
      if (@c<@c1) 
        begin
          select @coid=cast(replace(left(@st,CHARINDEX(',',@st,1)),',','') as integer)
          select @st=SUBSTRING(@st,CHARINDEX(',',@st,1)+1,LEN(@st))
        end
      else
        begin
          select @coid=cast(@st as integer)
        end
      insert into table1(complainid,courtid) values(@cid,@coid)
      set @c=@c+1
    end

la riga 4 di questa procedura memorizzata imposta @c1la risposta richiesta. A che cosa serve il resto del codice, considerando che ha bisogno di una tabella preesistente chiamata table1per funzionare, ha un delimitatore codificato e non può essere utilizzato in linea come la risposta accettata di due mesi prima?
Nick.McDermaid

-1

Il test Sostituisci / Len è carino, ma probabilmente molto inefficiente (soprattutto in termini di memoria). Una semplice funzione con un ciclo farà il lavoro.

CREATE FUNCTION [dbo].[fn_Occurences] 
(
    @pattern varchar(255),
    @expression varchar(max)
)
RETURNS int
AS
BEGIN

    DECLARE @Result int = 0;

    DECLARE @index BigInt = 0
    DECLARE @patLen int = len(@pattern)

    SET @index = CHARINDEX(@pattern, @expression, @index)
    While @index > 0
    BEGIN
        SET @Result = @Result + 1;
        SET @index = CHARINDEX(@pattern, @expression, @index + @patLen)
    END

    RETURN @Result

END

Attraverso qualsiasi tabella di dimensioni apprezzabili, l'utilizzo di una funzione procedurale è molto più inefficiente
Nick.McDermaid

Buon punto. Le chiamate Len costruite sono molto più veloci di una funzione definita dall'utente?
Darrel Lee,

Su larga scala di record, sì. Anche se per essere sicuri, dovresti provare su un recordset di grandi dimensioni con stringhe di grandi dimensioni. Non scrivere mai nulla di procedurale in SQL se puoi evitarlo (es. Loop)
Nick.McDermaid

-3

Forse non dovresti archiviare i dati in questo modo. È una cattiva pratica archiviare mai un elenco delimitato da virgole in un campo. L'IT è molto inefficiente per le query. Questa dovrebbe essere una tabella correlata.


+1 per pensarci. È quello che di solito inizio quando qualcuno usa i dati separati da virgola in un campo.
Guffa,

6
Parte dello scopo di questa domanda era quello di prendere i dati esistenti in quel modo e dividerli in modo appropriato.
Orion Adrian,

7
Ad alcuni di noi vengono dati database legacy in cui è stato fatto e non possiamo farci nulla.
eddieroger,

@Mulmoth, ovviamente è una risposta. risolvi il problema, non il sintomo. Il problema riguarda la progettazione del database.
HLGEM

1
@HLGEM La domanda può indicare un problema, ma può essere compresa in modo più generale. La domanda è totalmente legittima per database molto ben normalizzati.
Zeemee,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.