Как оптимизировать фильтр для больших объемов данных? PostgreSQL - PullRequest
0 голосов
/ 08 мая 2020

Через несколько недель go наша команда столкнулась с трудностями с нашим запросом SQL, потому что объем данных сильно увеличился. Мы были бы признательны за любой совет о том, как обновить схему или оптимизировать запрос, чтобы status logi фильтрации c оставались неизменными.

В двух словах: у нас есть две таблицы a и b. b имеет FK для a как M-1.

a

id | processed

1    TRUE

2    TRUE

b

a_id| status | type_id | l_id 

1     '1'      5          105  

1     '3'      6          105 

2     '2'      7          105 

У нас может быть только один статус для уникальной комбинации of (l_id, type_id, a_id).

Нам нужно подсчитать количество a строк, отфильтрованных по статусам из b, сгруппированных по a_id. В таблице a 5 300 000 строк. В таблице b 750000000 строк.

Итак, нам нужно вычислить статус для каждой a строки по следующим правилам: Для a_id в b:

x строк. 1) Если хотя бы один статус x равен «3», то статус для a_id равен «3».

2) Если все статусы x равны 1, тогда статус 1.

И так далее.

В текущем подходе мы используем функцию array_agg () для фильтрации подвыборки. Итак, наш запрос выглядит так:

SELECT COUNT(*)
FROM (
       SELECT
       FROM (
              SELECT at.id                         as id,
                     BOOL_AND(bt.processed)        AS not_pending,
                     ARRAY_AGG(DISTINCT bt.status) AS status
              FROM a AS at
                     LEFT OUTER JOIN b AS bt
                                     ON (at.id = bt.a_id AND bt.l_id = 105 AND
                                         bt.type_id IN (2,10,18,1,4,5,6))
              WHERE at.processed = True
              GROUP BY at.id) sub
       WHERE not_pending = True
         AND status <@ ARRAY ['1']::"char"[]
     ) counter
;

Наш план выглядит так:

Aggregate  (cost=14665999.33..14665999.34 rows=1 width=8) (actual time=1875987.846..1875987.846 rows=1 loops=1)
  ->  GroupAggregate  (cost=14166691.70..14599096.58 rows=5352220 width=37) (actual time=1875987.844..1875987.844 rows=0 loops=1)
        Group Key: at.id
        Filter: (bool_and(bt.processed) AND (array_agg(DISTINCT bt.status) <@ '{1}'::"char"[]))
        Rows Removed by Filter: 5353930
        ->  Sort  (cost=14166691.70..14258067.23 rows=36550213 width=6) (actual time=1860315.593..1864175.762 rows=37430745 loops=1)
              Sort Key: at.id
              Sort Method: external merge  Disk: 586000kB
              ->  Hash Right Join  (cost=1135654.48..8076230.39 rows=36550213 width=6) (actual time=55665.584..1846965.271 rows=37430745 loops=1)
                    Hash Cond: (bt.a_id = at.id)
                    ->  Bitmap Heap Scan on b bt  (cost=882095.79..7418660.65 rows=36704370 width=6) (actual time=51871.658..1826058.186 rows=37430378 loops=1)
                          Recheck Cond: ((l_id = 105) AND (type_id = ANY ('{2,10,18,1,4,5,6}'::integer[])))
                          Rows Removed by Index Recheck: 574462752
                          Heap Blocks: exact=28898 lossy=5726508
                          ->  Bitmap Index Scan on db_page_index_atableobjects  (cost=0.00..872919.69 rows=36704370 width=0) (actual time=51861.815..51861.815 rows=37586483 loops=1)
                                Index Cond: ((l_id = 105) AND (type_id = ANY ('{2,10,18,1,4,5,6}'::integer[])))
                    ->  Hash  (cost=165747.94..165747.94 rows=5352220 width=4) (actual time=3791.710..3791.710 rows=5353930 loops=1)
                          Buckets: 131072  Batches: 128  Memory Usage: 2507kB
                          ->  Seq Scan on a at  (cost=0.00..165747.94 rows=5352220 width=4) (actual time=0.528..2958.004 rows=5353930 loops=1)
                                Filter: processed
                                Rows Removed by Filter: 18659
Planning time: 0.328 ms
Execution time: 1876066.242 ms

Как видите, время выполнения запроса огромно, и мы хотели бы сделать его как минимум < 30 секунд. Мы уже пробовали некоторые подходы, такие как использование bitor () вместо array_agg () и LATERAL JOIN. Но они не дали нам желаемой производительности, и мы решили пока использовать материализованные представления. Но мы все еще находимся в поиске любого другого решения и будем очень благодарны за любые предложения!

План с включенным track_io_timing:

Aggregate  (cost=14665999.33..14665999.34 rows=1 width=8) (actual time=2820945.285..2820945.285 rows=1 loops=1)
  Buffers: shared hit=23 read=5998844, temp read=414465 written=414880
  I/O Timings: read=2655805.505
  ->  GroupAggregate  (cost=14166691.70..14599096.58 rows=5352220 width=930) (actual time=2820945.283..2820945.283 rows=0 loops=1)
        Group Key: at.id
        Filter: (bool_and(bt.processed) AND (array_agg(DISTINCT bt.status) <@ '{1}'::"char"[]))
        Rows Removed by Filter: 5353930
        Buffers: shared hit=23 read=5998844, temp read=414465 written=414880
        I/O Timings: read=2655805.505
        ->  Sort  (cost=14166691.70..14258067.23 rows=36550213 width=6) (actual time=2804900.123..2808826.358 rows=37430745 loops=1)
              Sort Key: at.id
              Sort Method: external merge  Disk: 586000kB
              Buffers: shared hit=18 read=5998840, temp read=414465 written=414880
              I/O Timings: read=2655805.491
              ->  Hash Right Join  (cost=1135654.48..8076230.39 rows=36550213 width=6) (actual time=55370.788..2791441.542 rows=37430745 loops=1)
                    Hash Cond: (bt.a_id = at.id)
                    Buffers: shared hit=15 read=5998840, temp read=142879 written=142625
                    I/O Timings: read=2655805.491
                    ->  Bitmap Heap Scan on b bt  (cost=882095.79..7418660.65 rows=36704370 width=6) (actual time=51059.047..2769127.810 rows=37430378 loops=1)
                          Recheck Cond: ((l_id = 105) AND (type_id = ANY ('{2,10,18,1,4,5,6}'::integer[])))
                          Rows Removed by Index Recheck: 574462752
                          Heap Blocks: exact=28898 lossy=5726508
                          Buffers: shared hit=13 read=5886842
                          I/O Timings: read=2653254.939
                          ->  Bitmap Index Scan on db_page_index_atableobjects  (cost=0.00..872919.69 rows=36704370 width=0) (actual time=51049.365..51049.365 rows=37586483 loops=1)
                                Index Cond: ((l_id = 105) AND (type_id = ANY ('{2,10,18,1,4,5,6}'::integer[])))
                                Buffers: shared hit=12 read=131437
                                I/O Timings: read=49031.671
                    ->  Hash  (cost=165747.94..165747.94 rows=5352220 width=4) (actual time=4309.761..4309.761 rows=5353930 loops=1)
                          Buckets: 131072  Batches: 128  Memory Usage: 2507kB
                          Buffers: shared hit=2 read=111998, temp written=15500
                          I/O Timings: read=2550.551
                          ->  Seq Scan on a at  (cost=0.00..165747.94 rows=5352220 width=4) (actual time=0.515..3457.040 rows=5353930 loops=1)
                                Filter: processed
                                Rows Removed by Filter: 18659
                                Buffers: shared hit=2 read=111998
                                I/O Timings: read=2550.551
Planning time: 0.347 ms
Execution time: 2821022.622 ms

Ответы [ 2 ]

1 голос
/ 11 мая 2020

В текущем плане практически все время уходит на чтение страниц таблицы для сканирования кучи Bitmap. У вас уже должен быть индекс для чего-то вроде (l_id, type_id). Если вы измените его (создайте новый, а затем, при необходимости, отбросьте старый) вместо этого на (ld_id, type_id, processed, a_id, status) или, возможно, на (ld_id, type_id, a_id, status) where processed), тогда он, вероятно, может переключиться на сканирование только по индексу, которое может избежать чтения таблицы как все данные присутствуют в индексе. Вам нужно будет убедиться, что стол хорошо пропылесосен, чтобы эта статика была эффективной. Я бы просто вручную очистил таблицу один раз перед построением индекса, а затем, если это сработает, вы можете в этот момент беспокоиться о том, как сохранить ее в хорошем состоянии. просто установите его на 20. Если это сработает; вы можете поиграть с ним еще, чтобы найти оптимальную настройку), так что более одного запроса чтения ввода-вывода в таблице могут быть выполнены одновременно. Насколько это будет эффективно, будет зависеть от вашей системы ввода-вывода, и я не знаю ответа на этот вопрос для db.r5.xlarge. Однако сканирование только по индексу лучше, поскольку оно использует меньше ресурсов, в то время как этот метод просто использует те же ресурсы быстрее. (Если у вас есть несколько похожих запросов, выполняемых одновременно, это важно. Кроме того, если вы платите за ввод-вывод, вам нужно меньше их, а не столько же быстрее)

Другой вариант - попытаться изменить форму план полностью за счет вложенного l oop из a в b. Чтобы это имело надежду, вам понадобится индекс для b, который содержит a_id и l_id в качестве ведущих столбцов (в любом порядке). Если у вас уже есть такой индекс, и он, естественно, не выбирает такой план, вы можете принудительно set enable_hashjoin=off.. Мне кажется, что вложенный l oop, который должен ударить другую сторону 5 353 930 раз, является не будет лучше того, что у вас есть сейчас, даже если у другой стороны есть эффективный индекс.

0 голосов
/ 08 мая 2020

Вы можете отфильтровать и сгруппировать таблицу B перед объединением ее с A. И упорядочить обе таблицы по идентификатору, потому что это увеличивает скорость сканирования таблицы при обработке операции объединения. Пожалуйста, проверьте этот код:

with at as (
select distinct at.id, at.processed
from a AS at
WHERE at.processed = True
order by at.id
),

bt as (
select bt.a_id, bt.l_id, bt.type_id, --BOOL_AND(bt.processed) AS not_pending, 
ARRAY_AGG(DISTINCT bt.status) as status
from b AS bt
group by bt.a_id, bt.l_id, bt.type_id
having bt.l_id = 105 AND bt.type_id IN (2,10,18,1,4,5,6)
order by bt.a_id
),

counter as (
select at.id, 
case 
when '1' = all(status) then '1' 
when '3' = any(status) then '3' 
else status end as status
from at inner join bt on at.id=bt.a_id
)

select count (*) from counter where status='1'
...