Como você conta o número de ocorrências de uma certa substring em um varchar SQL?

150

Eu tenho uma coluna que possui valores formatados como a, b, c, d. Existe uma maneira de contar o número de vírgulas nesse valor no T-SQL?

Orion Adrian
fonte

Respostas:

245

A primeira maneira que vem à mente é fazê-lo indiretamente, substituindo a vírgula por uma sequência vazia e comparando os comprimentos

Declare @string varchar(1000)
Set @string = 'a,b,c,d'
select len(@string) - len(replace(@string, ',', ''))
cmsjr
fonte
13
Isso responde à pergunta como está escrito no texto, mas não como está escrito no título. Para fazê-lo funcionar com mais de um caractere, basta adicionar um / len (searchterm) ao redor da coisa. Publicou uma resposta caso seja útil para alguém.
Andrew Barrett
Alguém apontou para mim que isso nem sempre funciona como esperado. Considere o seguinte: SELECT LEN ('a, b, c, d,') - LEN (REPLACE ('a, b, c, d,', ',', '')) Por razões que ainda não entendi , o espaço entre d e a coluna final fará com que isso retorne 5 em vez de 4. Vou postar outra resposta que corrige isso, caso seja útil para qualquer pessoa.
BubbleKing
5
Talvez usar DATALENGTH em vez de LEN seja melhor, porque LEN retorna o tamanho da string aparada.
Rodrigocl #
2
DATALENGTH () / 2 também é complicado devido aos tamanhos de caracteres não óbvios. Veja stackoverflow.com/a/11080074/1094048 para obter uma maneira simples e precisa de obter o comprimento da string.
Pkuderov
@rodrigocl Por que não enrolar um LTRIMao redor da string da seguinte maneira SELECT LEN(RTRIM(@string)) - LEN(REPLACE(RTRIM(@string), ',', '')):?
Alex Bello
67

Extensão rápida da resposta do cmsjr que funciona para cadeias de caracteres com mais de mais caracteres.

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
Andrew Barrett
fonte
16
Uma pequena melhoria seria usar DATALENGTH () / 2 em vez de LEN (). O LEN ignorará qualquer espaço em branco à direita dbo.CountOccurancesOfString( 'blah ,', ','), retornará 2 em vez de 1 e dbo.CountOccurancesOfString( 'hello world', ' ')falhará com a divisão por zero.
Rory
5
O comentário de Rory é útil. Descobri que poderia substituir LEN por DATALENGTH na função de Andrew e obter o resultado desejado. Parece que dividir por 2 não é necessário com a maneira como a matemática funciona.
Garland Pope
@AndrewBarrett: O que acrescenta quando várias strings têm o mesmo comprimento?
User2284570
2
DATALENGTH()/2também é complicado por causa dos tamanhos de caracteres não óbvios. Veja stackoverflow.com/a/11080074/1094048 para obter uma maneira simples e precisa.
Pkuderov 13/07/2016
26

Você pode comparar o comprimento da string com aquele em que as vírgulas são removidas:

len(value) - len(replace(value,',',''))
Guffa
fonte
8

Com base na solução da @ Andrew, você obterá um desempenho muito melhor usando uma função com valor de tabela não processual e o 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
Russell Fox
fonte
Eu uso essa mesma função em muitos dos meus bancos de dados herdados, pois ajuda bastante com muitos bancos de dados antigos e projetados incorretamente. Economiza muito tempo e é muito rápido, mesmo em grandes conjuntos de dados.
Caim #
6

A resposta de @csmjr tem um problema em alguns casos.

Sua resposta foi fazer o seguinte:

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

Isso funciona na maioria dos cenários, no entanto, tente executar o seguinte:

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

Por alguma razão, REPLACE se livra da vírgula final, mas TAMBÉM o espaço logo antes (não sei por que). Isso resulta em um valor retornado de 5 quando você esperaria 4. Aqui está outra maneira de fazer isso que funcionará mesmo neste cenário especial:

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

Observe que você não precisa usar asteriscos. Qualquer substituição de dois caracteres serve. A idéia é que você estenda a string em um caractere para cada instância do caractere que está contando e subtraia a extensão do original. É basicamente o método oposto da resposta original que não vem com o estranho efeito colateral de corte.

bolha
fonte
5
"Por alguma razão, REPLACE se livra da vírgula final, mas TAMBÉM o espaço logo antes (não sei por que)." SUBSTITUIR não está se livrando da última vírgula e do espaço anterior, na verdade, é a função LEN que ignora o espaço em branco resultante no final da cadeia por causa desse espaço.
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)
NIKHIL THAKUR
fonte
isso realmente retorna 1 a menos a contagem real
The Integrator
1

A resposta aceita está correta, estendendo-a para usar 2 ou mais caracteres na substring:

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

Se sabemos que há uma limitação no LEN e no espaço, por que não podemos substituir o espaço primeiro? Então sabemos que não há espaço para confundir o LEN.

len(replace(@string, ' ', '-')) - len(replace(replace(@string, ' ', '-'), ',', ''))
MartinC
fonte
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'
Shiva
fonte
0

Darrel Lee, acho que tem uma resposta muito boa. Substitua CHARINDEX()por PATINDEX()e você também pode fazer uma regexpesquisa fraca ao longo de uma string ...

Como, digamos que você use isso para @pattern:

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

Por que você talvez queira fazer algo louco como esse?

Digamos que você esteja carregando seqüências de texto delimitadas em uma tabela intermediária, em que o campo que contém os dados é algo como um varchar (8000) ou nvarchar (max) ...

Às vezes, é mais fácil / rápido executar o ELT (Extract-Load-Transform) com dados, em vez de ETL (Extract-Transform-Load), e uma maneira de fazer isso é carregar os registros delimitados como estão em uma tabela de preparação, especialmente se você pode querer uma maneira mais simples de ver os registros excepcionais, em vez de lidar com eles como parte de um pacote SSIS ... mas essa é uma guerra santa por um segmento diferente.

user1390375
fonte
0

A seguir, você deve executar o truque para pesquisas de caracteres únicos e vários caracteres:

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;

A função pode ser simplificada um pouco usando uma tabela de números (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
          );
cmfox1970
fonte
0

Use esse código, ele está funcionando perfeitamente. Eu criei uma função sql que aceita dois parâmetros, o primeiro parâmetro é a string longa que queremos pesquisar e pode aceitar o comprimento da string com até 1500 caracteres (é claro que você pode estendê-la ou mesmo alterá-la para tipo de dados de texto ) E o segundo parâmetro é a substring que queremos calcular o número de ocorrências (seu tamanho é de até 200 caracteres, é claro que você pode alterá-lo conforme sua necessidade). e a saída é um número inteiro, representa o número de frequência ..... aproveite.


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
Um dia
fonte
0

Finalmente, escrevo esta função que deve cobrir todas as situações possíveis, adicionando um prefixo e um sufixo de caracteres à entrada. esse caractere é avaliado como diferente de qualquer caractere contido no parâmetro de pesquisa, portanto, não pode afetar o resultado.

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 ,', ',')
Arden Inside
fonte
0
Declare @MainStr nvarchar(200)
Declare @SubStr nvarchar(10)
Set @MainStr = 'nikhildfdfdfuzxsznikhilweszxnikhil'
Set @SubStr = 'nikhil'
Select (Len(@MainStr) - Len(REPLACE(@MainStr,@SubStr,'')))/Len(@SubStr)
NIKHIL THAKUR
fonte
0

No SQL 2017 ou superior, você pode usar este:

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

esse código T-SQL localiza e imprime todas as ocorrências do padrão @p na frase @s. você pode executar qualquer processamento da sentença posteriormente.

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

o resultado é: 1 6 13 20

Hasan Zafari
fonte
0

para SQL Server 2017

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

Você pode usar o seguinte procedimento armazenado para buscar valores.

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
Nilesh
fonte
a linha 4 deste procedimento armazenado define @c1a resposta que ele exige. Qual é a utilidade do restante do código, considerando que ele precisa de uma tabela pré-existente chamada table1para funcionar, possui um delímetro codificado e não pode ser usado em linha como a resposta aceita dos dois meses anteriores?
Nick.McDermaid
-1

O teste Substituir / Len é bonito, mas provavelmente muito ineficiente (especialmente em termos de memória). Uma função simples com um loop fará o trabalho.

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
Darrel Lee
fonte
Do outro lado qualquer tabela de dimensões apreciáveis, usando uma função processual é muito mais ineficiente
Nick.McDermaid
Bom ponto. A chamada Len criada é muito mais rápida que uma função definida pelo uso?
perfil completo de Darrel Lee
Em uma grande escala de registros, sim. No entanto, para ter certeza, você teria que testar em um grande conjunto de registros com grandes seqüências de caracteres. Nunca escreva qualquer coisa processual no SQL se você pode evitá-lo (ou seja, laços)
Nick.McDermaid
-3

Talvez você não deva armazenar dados dessa maneira. É uma prática ruim armazenar sempre uma lista delimitada por vírgulas em um campo. A TI é muito ineficiente para consultas. Essa deve ser uma tabela relacionada.

HLGEM
fonte
+1 por pensar nisso. É o que geralmente começo quando alguém usa dados separados por vírgula em um campo.
Guffa
6
Parte do objetivo desta pergunta era coletar dados existentes como esse e dividi-los adequadamente.
Orion Adrian
7
Alguns de nós recebem bancos de dados legados onde isso foi feito e não podemos fazer nada sobre isso.
eddieroger
@Mulmoth, é claro que é uma resposta. você corrige o problema, não o sintoma. O problema está no design do banco de dados.
HLGEM
1
@HLGEM A questão pode apontar para um problema, mas pode ser entendido de maneira mais geral. A questão é totalmente legítima para bancos de dados muito bem normalizados.
Zeemee