Hashaggregate на postgres таблица отношений медленная - PullRequest
0 голосов
/ 03 апреля 2020

У меня следующий сценарий: у меня есть база данных postgres с несколькими записями карт, а также несколькими записями колод (это информационная система карточных игр). В этом случае у меня есть таблица ассоциации между колодами и картами, которая называется deck_cards, которая имеет приблизительно 6 миллионов строк и продолжает расти. Схема базы данных выглядит следующим образом:

decks(id,name)
cards(id,name,extra) -- extra is a varchar field to store general information
deck_cards(id,id_card,id_deck)

cards indexes:
    "Cards_pkey" PRIMARY KEY, btree (id)

deck indexes:
    "Decks_pkey" PRIMARY KEY, btree (id)

deck_cards indexes:
    "deck_cards_pkey" PRIMARY KEY, btree (id)
    "deck_cards_card_id" btree (card_id)
    "deck_cards_deck_id" btree (deck_id)
    "deck_cards_deck_id_card_id" btree (deck_id, card_id) CLUSTER
    "deck_cards_extra_card_id" btree (extra, card_id)

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

Мои попытки были:

EXPLAIN ANALYZE
WITH d AS (
  SELECT deck_id FROM deck_cards
  WHERE extra IS NULL AND card_id = 'XXX'
)
SELECT COUNT(*) AS count, card_id
FROM deck_cards
WHERE
  card_id <> 'XXX'
  AND deck_id IN (SELECT * FROM d)
GROUP BY card_id
ORDER BY count DESC
LIMIT 200;

Полученный результат был:

Limit  (cost=54567.65..54568.15 rows=200 width=24) (actual time=4951.567..4951.611 rows=200 loops=1)
   CTE d
     ->  HashAggregate  (cost=16666.74..16937.95 rows=27121 width=16) (actual time=381.594..395.473 rows=43256 loops=1)
           Group Key: deck_cards_1.deck_id
           ->  Index Scan using deck_cards_extra_card_id on deck_cards deck_cards_1  (cost=0.56..16550.34 rows=46560 width=16) (actual time=0.038..350.081 rows=43258 loops=1)
                 Index Cond: ((extra IS NULL) AND (card_id = 'dc87938d-6df8-4acc-bfd0-3cbb58066057'::uuid))
   ->  Sort  (cost=37629.70..37649.11 rows=7766 width=24) (actual time=4951.565..4951.586 rows=200 loops=1)
         Sort Key: (count(*)) DESC
         Sort Method: top-N heapsort  Memory: 50kB
         ->  HashAggregate  (cost=37216.40..37294.06 rows=7766 width=24) (actual time=4942.328..4947.457 rows=17035 loops=1)
               Group Key: deck_cards.card_id
               ->  Nested Loop  (cost=610.65..24198.67 rows=2603546 width=16) (actual time=439.553..3568.086 rows=3518996 loops=1)
                     ->  HashAggregate  (cost=610.22..612.22 rows=200 width=16) (actual time=439.466..454.442 rows=43256 loops=1)
                           Group Key: d.deck_id
                           ->  CTE Scan on d  (cost=0.00..542.42 rows=27121 width=16) (actual time=381.598..416.827 rows=43256 loops=1)
                     ->  Index Scan using deck_cards_deck_id on deck_cards  (cost=0.43..116.58 rows=135 width=32) (actual time=0.026..0.061 rows=81 loops=43256)
                           Index Cond: (deck_id = d.deck_id)
                           Filter: (card_id <> 'dc87938d-6df8-4acc-bfd0-3cbb58066057'::uuid)
                           Rows Removed by Filter: 1
 Planning time: 0.484 ms
 Execution time: 4952.303 ms

Я также пытался переписать без использования WITH, но я также не получил хороший результат.

EXPLAIN ANALYZE
SELECT COUNT(*) AS count, card_id
FROM deck_cards
WHERE card_id <> 'dc87938d-6df8-4acc-bfd0-3cbb58066057' AND deck_id IN (
  SELECT DISTINCT deck_id FROM deck_cards
  WHERE extra IS NULL AND card_id = 'dc87938d-6df8-4acc-bfd0-3cbb58066057'
)
GROUP BY card_id
ORDER BY count DESC
LIMIT 200;

Полученный результат был похож на предыдущий с точки зрения производительности:

Limit  (cost=127334.18..127334.68 rows=200 width=24) (actual time=5098.815..5098.982 rows=200 loops=1)
   ->  Sort  (cost=127334.18..127353.59 rows=7766 width=24) (actual time=5098.813..5098.834 rows=200 loops=1)
         Sort Key: (count(*)) DESC
         Sort Method: top-N heapsort  Memory: 52kB
         ->  Finalize GroupAggregate  (cost=126804.38..126998.53 rows=7766 width=24) (actual time=5081.173..5095.062 rows=17035 loops=1)
               Group Key: deck_cards.card_id
               ->  Sort  (cost=126804.38..126843.21 rows=15532 width=24) (actual time=5081.164..5086.096 rows=44616 loops=1)
                     Sort Key: deck_cards.card_id
                     Sort Method: external merge  Disk: 1488kB
                     ->  Gather  (cost=124092.27..125723.13 rows=15532 width=24) (actual time=4964.087..5039.956 rows=44616 loops=1)
                           Workers Planned: 2
                           Workers Launched: 2
                           ->  Partial HashAggregate  (cost=123092.27..123169.93 rows=7766 width=24) (actual time=4889.013..4909.163 rows=14872 loops=3)
                                 Group Key: deck_cards.card_id
                                 ->  Hash Join  (cost=17548.17..115477.70 rows=1522913 width=16) (actual time=1058.268..3482.032 rows=1172999 loops=3)
                                       Hash Cond: (deck_cards.deck_id = deck_cards_1.deck_id)
                                       ->  Parallel Seq Scan on deck_cards  (cost=0.00..92233.65 rows=2169622 width=32) (actual time=0.053..1233.727 rows=1736981 loops=3)
                                             Filter: (card_id <> 'dc87938d-6df8-4acc-bfd0-3cbb58066057'::uuid)
                                             Rows Removed by Filter: 14421
                                       ->  Hash  (cost=17209.16..17209.16 rows=27121 width=16) (actual time=1057.194..1057.194 rows=43256 loops=3)
                                             Buckets: 65536 (originally 32768)  Batches: 1 (originally 1)  Memory Usage: 2540kB
                                             ->  HashAggregate  (cost=16666.74..16937.95 rows=27121 width=16) (actual time=942.447..988.024 rows=43256 loops=3)
                                                   Group Key: deck_cards_1.deck_id
                                                   ->  Index Scan using deck_cards_extra_card_id on deck_cards deck_cards_1  (cost=0.56..16550.34 rows=46560 width=16) (actual time=0.077..855.472 rows=43258 loops=3)
                                                         Index Cond: ((extra IS NULL) AND (card_id = 'dc87938d-6df8-4acc-bfd0-3cbb58066057'::uuid))
 Planning time: 0.373 ms
 Execution time: 5099.848 ms

Может кто-нибудь сказать мне, если я делаю что-то не так, если есть лучший способ обратиться к данным этого типа, или если я застрял в этой проблеме, и я должен искать решение для ответа на мои запросы API, используя только кэш?

[EDIT]

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

(card_id, deck_id, extra)
(A, 1, NULL)
(C, 1, NULL)
(A, 2,NULL)
(C, 2,NULL)
(Y, 2,NULL)
(A,3,'foo')
(C,3,NULL)

- The response I want is looking for card = 'A' AND extra IS NULL:
(C, 2)
(Y, 1)

Ответы [ 2 ]

0 голосов
/ 03 апреля 2020
\i tmp.sql

        -- make some usable data
        -- UUID --> bigint
CREATE TABLE decks
        ( id bigserial NOT NULL PRIMARY KEY
        , name text
        );
CREATE TABLE cards
        ( id bigserial NOT NULL PRIMARY KEY
        , name text
        , extra text
        );

CREATE TABLE deck_cards
        ( id_card  bigint NOT NULL REFERENCES cards(id)
        , id_deck bigint NOT NULL REFERENCES decks(id)
        , PRIMARY KEY (id_card,id_deck)
        , UNIQUE (id_deck, id_card)
        );

INSERT INTO cards(name, extra)
SELECT 'name_' || gs::text
        , NULLIF(gs%41,0)::text
FROM generate_series(1,1000) gs
        ;

INSERT INTO decks(name)
SELECT 'deck_' || gs::text
FROM generate_series(1,1000) gs
        ;
INSERT INTO deck_cards(id_card, id_deck)
SELECT c.id, d.id
FROM cards c
JOIN decks d ON random() < 0.1
        ;
VACUUM ANALYZE decks;
VACUUM ANALYZE cards;
VACUUM ANALYZE deck_cards;
        -- do the query
EXPLAIN ANALYZE
SELECT dc.id_card
        , COUNT(*) AS cnt
FROM deck_cards dc
WHERE dc.id_card <> 123
AND EXISTS ( -- select all decks that have an X card
         SELECT *
        FROM deck_cards xdc
        JOIN cards x ON x.id = xdc.id_card
        WHERE xdc.id_card = 123 -- deck must have an X-card
        AND x.extra IS NULL
        AND xdc.id_deck = dc.id_deck  -- same deck as outer query
        )
GROUP BY id_card
        ;

Результирующий план:


                                                                                  QUERY PLAN                                                                              
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=486.80..496.80 rows=1000 width=16) (actual time=7.198..7.428 rows=999 loops=1)
   Group Key: dc.id_card
   ->  Nested Loop  (cost=7.70..430.26 rows=11307 width=8) (actual time=0.227..4.757 rows=11078 loops=1)
         ->  HashAggregate  (cost=7.28..8.22 rows=94 width=8) (actual time=0.199..0.234 rows=111 loops=1)
               Group Key: xdc.id_deck
               ->  Nested Loop  (cost=0.69..7.03 rows=99 width=8) (actual time=0.127..0.165 rows=111 loops=1)
                     ->  Index Scan using cards_pkey on cards x  (cost=0.28..2.69 rows=1 width=8) (actual time=0.068..0.069 rows=1 loops=1)
                           Index Cond: (id = 123)
                           Filter: (extra IS NULL)
                     ->  Index Only Scan using deck_cards_pkey on deck_cards xdc  (cost=0.42..3.35 rows=99 width=16) (actual time=0.057..0.082 rows=111 loops=1)
                           Index Cond: (id_card = 123)
                           Heap Fetches: 0
         ->  Index Only Scan using deck_cards_id_deck_id_card_key on deck_cards dc  (cost=0.42..3.49 rows=100 width=16) (actual time=0.013..0.030 rows=100 loops=111)
               Index Cond: (id_deck = xdc.id_deck)
               Filter: (id_card <> 123)
               Rows Removed by Filter: 1
               Heap Fetches: 0
 Planning Time: 0.988 ms
 Execution Time: 7.621 ms
(19 rows)
0 голосов
/ 03 апреля 2020

Вы можете просто использовать оконную функцию и агрегирование:

SELECT COUNT(*) AS count,
       card_id
FROM (SELECT dc.*, COUNT(*) FILTER (WHERE card_id = 'A' and extra IS NULL) OVER (PARTITION BY deck_id) as cnt
      FROM deck_cards dc
     ) dc
WHERE cnt > 0 AND card_id <> 'A'
GROUP BY card_id
ORDER BY count DESC;

Здесь - это db <> скрипка.

Это может иметь лучшую производительность, чем ваша версия .

...