База данных для эффективного диапазона совокупных запросов?

11

В качестве упрощенного примера, предположим, у меня есть такая таблица:

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

Таблица может содержать сотни миллионов записей, и мне нужно часто делать такие запросы:

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

Даже если seqон проиндексирован, типичная реализация базы данных будет проходить по каждой строке для вычисления суммы в лучшем случае O(n), где nразмер диапазона.

Есть ли база данных, которая может сделать это эффективно, как в O(log(n))запросе?

Я наткнулся на структуру данных, которая называется деревом сегментов, как описано здесь . Также иногда упоминается как дерево диапазонов или дерево интервалов, хотя все эти имена часто описываются как слегка отличающийся вариант структуры данных.

Однако я не сталкивался ни с одной базой данных, которая реализует такую ​​структуру данных. Реализовать его с нуля легко для структуры в памяти, но он становится сложным, если его необходимо сохранить или он слишком велик, чтобы поместиться в памяти. Если есть эффективный шаблон для реализации этого поверх существующей базы данных, это также может помочь.

Примечание: это таблица не только для добавления, поэтому такое решение, как сохранение накопленной суммы, в этом случае не будет работать.

Ralf
источник
Это типичный вариант использования баз данных, организованных по столбцам, которых много .
mustaccio
Даже организованная по столбцам база данных все равно потребует O (n) времени для сканирования n строк. Тем не менее, многие организованные по столбцам базы данных очень хорошо распараллеливают такие запросы, поэтому в такой базе данных они будут работать намного быстрее.
Брайан

Ответы:

8

Использование SQL Server ColumnStore indexes

Ну, ладно, только один - кластерный индекс CS.

Если вы хотите прочитать об оборудовании, на котором я это сделал, зайдите сюда . Полное раскрытие, я написал этот пост в блоге на сайте компании, в которой я работаю.

На тесте!

Вот некоторый общий код для создания довольно большой таблицы. То же предупреждение, что и у Эвана, для сборки и индексации может потребоваться некоторое время.

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

Ну, Эван побеждает за простоту, но я говорил об этом раньше.

Вот определение индекса. Ла и Ди и Дах.

CREATE CLUSTERED COLUMNSTORE INDEX CX_WOAHMAMA ON dbo.t1

Глядя на количество, каждый Id имеет довольно равномерное распределение:

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

Результаты:

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

...

994 5005005
995 5005005
996 5005005
997 5005005
998 5005005

С каждым идентификатором, имеющим ~ 5,005,005 строк, мы можем рассмотреть довольно маленький диапазон идентификаторов, чтобы получить сумму в 10 миллионов строк.

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

Результат:

Records     Total
10010012    50015062308

Профиль запроса:

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.

Для удовольствия, большая агрегация:

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

Результаты:

Records     Total
500500505   2501989114575

Профиль запроса:

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.

Надеюсь это поможет!

Эрик Дарлинг
источник
sp_BlitzErik chat.stackexchange.com/rooms/59048/no-mods-allowed
Эван Кэрролл,
2

PostgreSQL с индексом BRIN

Даже если seq проиндексирован, типичная реализация базы данных будет проходить по каждой строке для вычисления суммы в лучшем случае O (n), где n - размер диапазона.

Это не правда. По крайней мере, ни одна приличная база данных не сделает этого. PostgreSQL поддерживает создание индексов BRIN для таблиц такого типа. Индексы BRIN очень малы и могут поместиться в оперативной памяти даже на таких больших столах. Сотни миллионов строк - это не ничто.

Здесь 300 миллионов строк определены так же, как вы их заказали. Предупреждение: на его создание может уйти много времени (время: 336057,880 мс + 95121,809 мс для индекса).

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;

И сейчас...

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 секунды для агрегирования / суммирования 5 889 135 строк в заданном диапазоне.

Несмотря на то, что таблица составляет 10 ГБ, индекс BRIN составляет 304 КБ.

Даже быстрее

Если это все еще недостаточно быстро, вы можете кэшировать агрегаты по 100 тыс. Строк.

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

Теперь вам нужно будет только использовать 2(1e5-1)строки brin и aggregate, а не 300 миллионов или что-то еще.

аппаратные средства

Lenovo x230, i5-3230M, 16 ГБ оперативной памяти, 1 ТБ Samsung 840 SSD.

Эван Кэрролл
источник
Спасибо, я прочитаю и поэкспериментирую с индексами BRIN. Пока это выглядит как лучший вариант.
Ральф
3
Хорошие предложения, оба (индекс BRIN и материализованное представление). Но запрос, даже с индексом BRIN, по-прежнему равен O (n). Пожалуйста, измените и не утверждайте иначе. Материализованный взгляд может быть лучше, чем O(n), возможно O(sqrt(n)). Зависит от того, как вы будете определять интервалы, которые будут использоваться при материализации.
ypercubeᵀᴹ