Database per query aggregate di intervallo efficienti?


11

Come esempio semplificato, supponiamo di avere una tabella come questa:

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

La tabella può contenere centinaia di milioni di record e devo fare frequentemente query come questa:

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

Anche se seqindicizzato, un'implementazione tipica del database eseguirà il ciclo attraverso ogni riga per calcolare la somma nel migliore dei casi O(n), dove si ntrova la dimensione dell'intervallo.

Esiste un database in grado di farlo in modo efficiente, come O(log(n))per query?

Mi sono imbattuto in una struttura di dati chiamata albero di segmenti come descritto qui . A volte indicato anche come albero degli intervalli o albero degli intervalli, sebbene tutti questi nomi siano spesso descritti come una variazione leggermente diversa della struttura dei dati.

Tuttavia, non ho trovato alcun database che implementa tale struttura di dati. L'implementazione da zero è facile per una struttura in memoria, ma diventa difficile se deve essere persistente o è troppo grande per adattarsi alla memoria. Se esiste un modello efficiente per implementarlo su un database esistente, ciò potrebbe anche aiutare.

Nota a margine: questa non è una tabella di sola aggiunta, quindi una soluzione come mantenere una somma cumulativa non funzionerà in questo caso.


Questo è il tipico caso d'uso per database organizzati in colonne, di cui ce ne sono molti .
Mustaccio,

Anche un database organizzato per colonna richiederà comunque il tempo O (n) per scansionare n righe. Detto questo, molti database organizzati per colonne sono molto bravi a parallelizzare tali query, quindi funzioneranno molto più velocemente su un tale database.
Brian,

Risposte:


8

Utilizzo degli indici ColumnStore di SQL Server

Bene, ok, solo uno: un indice CS cluster.

Se vuoi leggere dell'hardware su cui l'ho fatto, vai qui . Informativa completa, ho scritto quel post sul blog sul sito Web dell'azienda per cui lavoro.

Alla prova!

Ecco del codice generico per creare una tabella abbastanza grande. Stesso avvertimento di Evan, questo può richiedere del tempo per essere compilato e indicizzato.

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

Bene, Evan vince per semplicità, ma ho parlato di quella prima.

Ecco la definizione dell'indice. La e dee e dah.

CREATE CLUSTERED COLUMNSTORE INDEX CX_WOAHMAMA ON dbo.t1

Guardando un conteggio, ogni ID ha una distribuzione abbastanza uniforme:

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

risultati:

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

...

994 5005005
995 5005005
996 5005005
997 5005005
998 5005005

Con ogni ID con ~ 5.005.005 righe, possiamo esaminare una gamma piuttosto piccola di ID per ottenere una somma di 10 milioni di righe.

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

Risultato:

Records     Total
10010012    50015062308

Profilo della query:

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.

Per divertimento, una più grande aggregazione:

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

risultati:

Records     Total
500500505   2501989114575

Profilo della query:

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.

Spero che questo ti aiuti!



2

PostgreSQL con un indice BRIN

Anche se seq è indicizzato, un'implementazione tipica del database eseguirà il ciclo attraverso ogni riga per calcolare la somma nel migliore dei casi O (n), dove n è la dimensione dell'intervallo.

Non è vero. Almeno, nessun database decente lo farà. PostgreSQL supporta la creazione di indici BRIN su questo tipo di tabelle. Gli indici BRIN sono super piccoli e possono adattarsi a ram anche su tavoli così grandi. Centinaia di milioni di file non sono niente.

Qui, 300 milioni di righe definite proprio come le hai ordinate. Avviso: la creazione potrebbe richiedere molto tempo (tempo: 336057.807 ms + 95121.809 ms per l'indice).

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

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 secondi per aggregare / sommare 5.889.135 righe nell'intervallo indicato.

Nonostante la tabella sia di 10 GB, l'indice BRIN è di 304 kB.

Ancora più veloce

Se ciò non è ancora abbastanza veloce, è possibile memorizzare nella cache gli aggregati di 100k righe.

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

Ora avrai solo bisogno di usare la brina e le 2(1e5-1)righe aggregate anziché 300 milioni o altro.

Hardware

Lenovo x230, i5-3230M, 16 GB di RAM, SSD Samsung 840 da 1 tb.


Grazie, leggerò e sperimenterò di più con gli indici BRIN. Questa sembra la migliore opzione finora.
Ralf,

3
Simpatici suggerimenti, entrambi (indice BRIN e vista materializzata). Ma la query, anche con l'indice BRIN, è ancora O (n). Modifica e non richiedere diversamente. La visione materializzata potrebbe essere migliore di O(n), forse O(sqrt(n)). Dipende da come definirai gli intervalli da utilizzare nella materializzazione.
ypercubeᵀᴹ
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.