Banco de dados para consultas agregadas de intervalo eficientes?


11

Como um exemplo simplificado, suponha que eu tenha uma tabela como esta:

seq | value
----+------
102 | 11954
211 | 43292
278 | 19222
499 |  3843

A tabela pode conter centenas de milhões de registros, e eu preciso fazer consultas frequentemente como esta:

SELECT sum(value) WHERE seq > $a and seq < $b

Mesmo se seqestiver indexado, uma implementação típica de banco de dados percorrerá cada linha para calcular a soma no melhor dos casos O(n), onde né o tamanho do intervalo.

Existe algum banco de dados que possa fazer isso com eficiência, como em O(log(n))consulta?

Encontrei uma estrutura de dados chamada Árvore de Segmentos, conforme descrito aqui . Às vezes também chamado de árvore de intervalo ou árvore de intervalo, embora todos esses nomes sejam descritos como uma variação ligeiramente diferente da estrutura de dados.

No entanto, não encontrei nenhum banco de dados que implemente essa estrutura de dados. Implementá-lo do zero é fácil para uma estrutura na memória, mas torna-se complicado se for necessário persistir ou for grande demais para caber na memória. Se houver um padrão eficiente para implementar isso em um banco de dados existente, isso também poderá ajudar.

Nota lateral: Esta não é uma tabela apenas anexada; portanto, uma solução como manter uma soma acumulada não funcionará neste caso.


Este é o caso de uso típico para bancos de dados organizados por colunas, dos quais existem muitos .
23917 mustaccio

Mesmo um banco de dados organizado por coluna ainda precisará de O (n) tempo para verificar n linhas. Dito isto, muitos bancos de dados organizados em colunas são muito bons em paralelizar essas consultas, portanto, eles serão executados muito mais rapidamente nesse banco de dados.
Brian

Respostas:


8

Usando índices do SQL Server ColumnStore

Bem, ok, apenas um - um índice CS agrupado.

Se você quiser ler sobre o hardware em que fiz isso, vá até aqui . Divulgação completa, escrevi essa postagem no site da empresa em que trabalho.

Para o teste!

Aqui está um código genérico para criar uma tabela muito grande. Mesmo aviso que Evan, isso pode demorar um pouco para criar e indexar.

USE tempdb

CREATE TABLE t1 (Id INT NOT NULL, Amount INT NOT NULL)

;WITH T (N)
AS ( SELECT X.N
     FROM ( 
      VALUES (NULL), (NULL), (NULL),
             (NULL), (NULL), (NULL),
             (NULL), (NULL), (NULL), 
             (NULL) ) AS X (N) 
           ), NUMS (N) AS ( 
            SELECT TOP ( 710000000 ) 
                    ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL )) AS N
            FROM   T AS T1, T AS T2, T AS T3, 
                   T AS T4, T AS T5, T AS T6, 
                   T AS T7, T AS T8, T AS T9, 
                   T AS T10 )
INSERT dbo.t1 WITH ( TABLOCK ) (
    Id, Amount )
SELECT NUMS.N % 999 AS Id, NUMS.N % 9999 AS Amount
FROM   NUMS;

--(705032704 row(s) affected) --Aw, close enough

Bem, Evan vence pela simplicidade, mas eu já falei sobre isso antes.

Aqui está a definição do índice. La e dee e dah.

CREATE CLUSTERED COLUMNSTORE INDEX CX_WOAHMAMA ON dbo.t1

Observando uma contagem, todo ID tem uma distribuição bastante uniforme:

SELECT t.Id, COUNT(*) AS [Records]
FROM dbo.t1 AS t
GROUP BY t.Id
ORDER BY t.Id

Resultados:

Id  Records
0   5005005
1   5005006
2   5005006
3   5005006
4   5005006
5   5005006

...

994 5005005
995 5005005
996 5005005
997 5005005
998 5005005

Com cada ID tendo ~ 5.005.005 linhas, podemos observar um intervalo muito pequeno de IDs para obter uma soma de 10 milhões de linhas.

SELECT COUNT(*) AS [Records], SUM(t.Amount) AS [Total]
FROM   dbo.t1 AS t
WHERE  t.Id > 0
       AND t.Id < 3;

Resultado:

Records     Total
10010012    50015062308

Perfil da consulta:

Table 't1'. Scan count 6, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 2560758, lob physical reads 0, lob read-ahead reads 0.
Table 't1'. Segment reads 4773, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 564 ms,  elapsed time = 106 ms.

Por diversão, uma agregação maior:

SELECT COUNT(*) AS [Records], SUM(CONVERT(BIGINT, t.Amount)) AS [Total]
FROM   dbo.t1 AS t
WHERE  t.Id > 0
       AND t.Id < 101;

Resultados:

Records     Total
500500505   2501989114575

Perfil da consulta:

Table 't1'. Scan count 6, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 2560758, lob physical reads 0, lob read-ahead reads 0.
Table 't1'. Segment reads 4773, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 1859 ms,  elapsed time = 321 ms.

Espero que isto ajude!



2

PostgreSQL com um índice BRIN

Mesmo se seq estiver indexado, uma implementação típica de banco de dados percorrerá cada linha para calcular a soma no melhor dos casos O (n), em que n é o tamanho do intervalo.

Isso não é verdade. Pelo menos, nenhum banco de dados decente fará isso. O PostgreSQL suporta a criação de índices BRIN nesses tipos de tabelas. Os índices BRIN são super pequenos e podem caber em memória RAM, mesmo em tabelas tão grandes. Centenas de milhões de linhas não são nada.

Aqui, 300 milhões de linhas definidas como você solicitou. Aviso: pode levar muito tempo para criá-lo (Tempo: 336057.807 ms + 95121.809 ms para o índice).

CREATE TABLE foo
AS
  SELECT seq::int, trunc(random()*100000)::int AS v
  FROM generate_series(1,3e8) AS gs(seq);

CREATE INDEX ON foo USING BRIN (seq);

ANALYZE foo;

E agora...

EXPLAIN ANALYZE SELECT sum(v) FROM foo WHERE seq BETWEEN 424242 AND 6313376;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=1486163.53..1486163.54 rows=1 width=4) (actual time=1493.888..1493.888 rows=1 loops=1)
   ->  Bitmap Heap Scan on foo  (cost=58718.12..1471876.19 rows=5714938 width=4) (actual time=12.565..1035.153 rows=5889135 loops=1)
         Recheck Cond: ((seq >= 424242) AND (seq <= 6313376))
         Rows Removed by Index Recheck: 41105
         Heap Blocks: lossy=26240
         ->  Bitmap Index Scan on foo_seq_idx  (cost=0.00..57289.38 rows=5714938 width=0) (actual time=10.378..10.378 rows=262400 loops=1)
               Index Cond: ((seq >= 424242) AND (seq <= 6313376))
 Planning time: 0.125 ms
 Execution time: 1493.948 ms
(9 rows)

1,4 segundos para agregar / somar 5.889.135 linhas no intervalo especificado.

Apesar de a tabela ter 10 GB, o índice BRIN é de 304 kB.

Ainda mais rápido

Se isso ainda não for rápido o suficiente, você poderá armazenar em cache os agregados em 100 mil linhas.

CREATE MATERIALIZED VIEW cache_foo
AS
  SELECT seq/1e5::int AS grp, sum(v)
  FROM foo GROUP BY seq/1e5::int
  ORDER BY 1;

Agora você só precisará usar as 2(1e5-1)linhas brin e agregada em vez de 300 milhões ou o que quer.

Hardware

Lenovo x230, i5-3230M, 16GB RAM, 1 TB Samsung 840 SSD.


Obrigado, vou ler e experimentar mais com os índices BRIN. Parece a melhor opção até agora.
Ralf

3
Boas sugestões, tanto (índice BRIN e visão materializada). Mas a consulta, mesmo com o índice BRIN, ainda é O (n). Edite e não reivindique o contrário. A visão materializada pode ser melhor do que O(n), talvez O(sqrt(n)). Depende de como você definirá os intervalos a serem utilizados na materialização.
ypercubeᵀᴹ
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.