Por que as tabelas de números são "inestimáveis"?

112

Nosso especialista em banco de dados residente está nos dizendo que as tabelas de números são inestimáveis . Eu não entendo bem o porquê. Aqui está uma tabela de números:

USE Model
GO

CREATE TABLE Numbers
(
    Number INT NOT NULL,
    CONSTRAINT PK_Numbers 
        PRIMARY KEY CLUSTERED (Number)
        WITH FILLFACTOR = 100
)

INSERT INTO Numbers
SELECT
    (a.Number * 256) + b.Number AS Number
FROM 
    (
        SELECT number
        FROM master..spt_values
        WHERE 
            type = 'P'
            AND number <= 255
    ) a (Number),
    (
        SELECT number
        FROM master..spt_values
        WHERE 
            type = 'P'
            AND number <= 255
    ) b (Number)
GO

De acordo com a postagem do blog, a lógica apresentada é

As tabelas de números são realmente inestimáveis. Eu os uso o tempo todo para manipulação de strings, simulando funções de janela, preenchendo tabelas de teste com muitos dados, eliminando a lógica do cursor e muitas outras tarefas que seriam incrivelmente difíceis sem elas.

Mas não entendo exatamente quais são esses usos - você pode fornecer alguns exemplos específicos e convincentes de onde uma "tabela de números" economiza muito trabalho no SQL Server - e por que devemos tê-los?

Jeff Atwood
fonte
3
Muitos casos de uso para uma tabela de números podem ser igualmente satisfeitos por um CTE recursivo que gera os números que você precisa em tempo real. No entanto, há uma penalidade de desempenho, bem como algumas outras limitações à abordagem CTE.
Nick Chammas
4
@ Nick: Eu diria que uma tabela de números baseada em CTE on-the-fly vs. uma tabela física é apenas um detalhe de implementação de como você gera a tabela de números. Batata vs. Batata ...
Remus Rusanu
1
@Remus - Sim. Eu só queria apontar essa alternativa para Jeff.
Nick Chammas
2
Eu tenho uma dúzia de respostas usando uma tabela de números no SO stackoverflow.com/search?q=user%3A27535+%2B%22numbers+table%22 .
gbn 25/01

Respostas:

82

Eu já vi muitos usos quando você precisa projetar 'dados ausentes'. Por exemplo. você tem uma série temporal (um log de acesso, por exemplo) e deseja mostrar o número de ocorrências por dia nos últimos 30 dias (pense no painel de análise). Se você fizer um select count(...) from ... group by day, receberá a contagem de todos os dias, mas o resultado terá apenas uma linha para cada dia em que você realmente teve pelo menos um acesso. Por outro lado, se você primeiro projeta uma tabela de dias a partir da tabela de números ( select dateadd(day, -number, today) as day from numbers) e depois se junta às contagens (ou aplicação externa, o que quiser), obterá um resultado que tem 0 para a contagem nos dias em que você não teve acesso. isso é apenas um exemplo. Obviamente, pode-se argumentar que a camada de apresentação do seu painel poderia lidar com os dias ausentes e apenas mostrar um 0, mas algumas ferramentas (por exemplo, SSRS) simplesmente não serão capazes de lidar com isso.

Outros exemplos que eu já vi usaram truques de séries temporais semelhantes (data / hora +/- número) para fazer todos os tipos de cálculos de janela. Em geral, sempre que em uma linguagem imperativa você usasse um loop for com um número bem conhecido de iterações, a natureza declarativa e definida do SQL pode usar um truque baseado em uma tabela de números.

Aliás, sinto a necessidade de enfatizar o fato de que, embora usar uma tabela de números pareça uma execução processual imperativa, não caia na falácia de supor que ela é imperativa. Deixe-me dar um exemplo:

int x;
for (int i=0;i<1000000;++i)
  x = i;
printf("%d",x);

Este programa produzirá 999999, o que é praticamente garantido.

Vamos tentar o mesmo no SQL Server, usando uma tabela numérica. Primeiro, crie uma tabela de 1.000.000 de números:

create table numbers (number int not null primary key);
go

declare @i int = 0
    , @j int = 0;

set nocount on;
begin transaction
while @i < 1000
begin
    set @j = 0;
    while @j < 1000
    begin
        insert into numbers (number) 
            values (@j*1000+@i);
        set @j += 1;
    end
    commit;
    raiserror (N'Inserted %d*1000', 0, 0, @i)
    begin transaction;
    set @i += 1;
end
commit
go

Agora vamos fazer o 'for loop':

declare @x int;
select @x = number 
from numbers with(nolock);
select @x as [@x];

O resultado é:

@x
-----------
88698

Se você está tendo um momento WTF (afinal, number é a chave primária em cluster!), O truque é chamado de varredura da ordem de alocação e eu não a inseri @j*1000+@ipor acidente ... Você também pode arriscar um palpite e dizer que o resultado é porque paralelismo e que às vezes pode ser a resposta correta.

Existem muitas trolls sob essa ponte e mencionei algumas delas em curto-circuito no operador booleano do SQL Server e as funções T-SQL não implicam uma determinada ordem de execução

Remus Rusanu
fonte
55

Encontrei uma tabela de números bastante útil em várias situações.

Em Por que devo considerar o uso de uma tabela de números auxiliares? , escrito em 2004, mostro alguns exemplos:

  • Analisando uma Cadeia de Caracteres
  • Localizando lacunas de identidade
  • Gerando períodos (por exemplo, preenchendo uma tabela de calendário, que também pode ser inestimável)
  • Gerando fatias de tempo
  • Gerando intervalos de IP

Em Maus hábitos para chutar: usando loops para preencher tabelas grandes , mostro como uma tabela de números pode ser usada para simplificar a inserção de muitas linhas (em oposição à abordagem instintiva do uso de um loop while).

Em Processando uma lista de números inteiros: minha abordagem e Mais sobre listas de divisão: delimitadores personalizados, evitando duplicatas e mantendo a ordem , mostro como usar uma tabela de números para dividir uma sequência (por exemplo, um conjunto de valores separados por vírgula) e fornecer desempenho comparações entre este e outros métodos. Mais informações sobre divisão e manipulação de outras cordas:

E na Tabela de números do SQL Server, explicada - Parte 1 , forneço algumas informações sobre o conceito e tenho postagens futuras armazenadas para detalhar aplicativos específicos.

Existem muitos outros usos, esses são apenas alguns que se destacaram o suficiente para escrever sobre eles.

E, como @gbn, tenho algumas respostas sobre o estouro de pilha e neste site que também usam uma tabela de números.

Finalmente, tenho uma série de postagens no blog sobre a geração de conjuntos sem loop, que em parte mostram a vantagem de desempenho do uso de uma tabela de números em comparação com a maioria dos outros métodos (à parte o peculiar extravagante de Remus):

Aaron Bertrand
fonte
26

Aqui está um ótimo exemplo que usei recentemente de Adam Machanic:

CREATE FUNCTION dbo.GetSubstringCount
(
    @InputString TEXT, 
    @SubString VARCHAR(200),
    @NoisePattern VARCHAR(20)
)
RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    RETURN 
    (
        SELECT COUNT(*)
        FROM dbo.Numbers N
        WHERE
            SUBSTRING(@InputString, N.Number, LEN(@SubString)) = @SubString
            AND PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number + LEN(@SubString), 1)) = 0
            AND 0 = 
                CASE 
                    WHEN @NoisePattern = '' THEN 0
                    ELSE PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number - 1, 1))
                END
    )
END

Eu usei outra coisa semelhante a CTEpara encontrar uma instância específica de substring (ou seja, "Encontre o terceiro canal nesta string") para trabalhar com dados delimitados correlacionados:

declare @TargetStr varchar(8000), 
@SearchedStr varchar(8000), 
@Occurrence int
set @TargetStr='a'
set @SearchedStr='abbabba'
set @Occurrence=3;

WITH Occurrences AS (
SELECT Number,
       ROW_NUMBER() OVER(ORDER BY Number) AS Occurrence
FROM master.dbo.spt_values
WHERE Number BETWEEN 1 AND LEN(@SearchedStr) AND type='P'
  AND SUBSTRING(@SearchedStr,Number,LEN(@TargetStr))=@TargetStr)
SELECT Number
FROM Occurrences
WHERE Occurrence=@Occurrence

Se você não possui uma tabela de números, a alternativa é usar algum tipo de loop. Basicamente, uma tabela de números permite fazer iterações baseadas em conjuntos, sem cursores ou loops.

JNK
fonte
5
E o aviso obrigatório sobre o perigo à espreita de fazer a manipulação de cadeia no TVFs em linha: funções T-SQL que não implica uma certa ordem de execução
Remus Rusanu
12

Eu usaria uma tabela de números sempre que precisar de um equivalente SQL de Enumerable.Range. Por exemplo, eu apenas o usei em uma resposta neste site: calculando o número de permutações

AK
fonte