Повысить производительность COUNT / GROUP-BY в большой таблице PostgresSQL?

24

Я использую PostgresSQL 9.2 и имею отношение в 12 столбцов с 6 700 000 строк. Он содержит узлы в трехмерном пространстве, каждый из которых ссылается на пользователя (который его создал). Чтобы запросить, какой пользователь создал, сколько узлов я делаю следующее (добавлено explain analyzeдля получения дополнительной информации):

EXPLAIN ANALYZE SELECT user_id, count(user_id) FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                    QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253668.70..253669.07 rows=37 width=8) (actual time=1747.620..1747.623 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220278.79 rows=6677983 width=8) (actual time=0.019..886.803 rows=6677983 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1747.653 ms

Как видите, это занимает около 1,7 секунды. Это не так уж плохо, учитывая объем данных, но мне интересно, можно ли это улучшить. Я пытался добавить индекс BTree в пользовательский столбец, но это никак не помогло.

У вас есть альтернативные предложения?


Ради полноты, это полное определение таблицы со всеми ее индексами (без ограничений внешнего ключа, ссылок и триггеров):

    Column     |           Type           |                      Modifiers                    
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 reviewer_id   | integer                  | not null default (-1)
 review_time   | timestamp with time zone |
 editor_id     | integer                  |
 parent_id     | bigint                   |
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
 skeleton_id   | bigint                   |
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)
    "skeleton_id_treenode_index" btree (skeleton_id)
    "treenode_editor_index" btree (editor_id)
    "treenode_location_x_index" btree (((location).x))
    "treenode_location_y_index" btree (((location).y))
    "treenode_location_z_index" btree (((location).z))
    "treenode_parent_id" btree (parent_id)
    "treenode_user_index" btree (user_id)

Изменить: Это результат, когда я использую запрос (и индекс), предложенный @ypercube (запрос занимает около 5,3 секунды без EXPLAIN ANALYZE):

EXPLAIN ANALYZE SELECT u.id, ( SELECT COUNT(*) FROM treenode AS t WHERE t.project_id=1 AND t.user_id = u.id ) AS number_of_nodes FROM auth_user As u;
                                                                        QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on auth_user u  (cost=0.00..6987937.85 rows=46 width=4) (actual time=29.934..5556.147 rows=46 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=151911.65..151911.66 rows=1 width=0) (actual time=120.780..120.780 rows=1 loops=46)
           ->  Bitmap Heap Scan on treenode t  (cost=4634.41..151460.44 rows=180486 width=0) (actual time=13.785..114.021 rows=145174 loops=46)
                 Recheck Cond: ((project_id = 1) AND (user_id = u.id))
                 Rows Removed by Index Recheck: 461076
                 ->  Bitmap Index Scan on treenode_user_index  (cost=0.00..4589.29 rows=180486 width=0) (actual time=13.082..13.082 rows=145174 loops=46)
                       Index Cond: ((project_id = 1) AND (user_id = u.id))
 Total runtime: 5556.190 ms
(9 rows)

Time: 5556.804 ms

Редактировать 2: Это результат, когда я использую indexon project_id, user_id(но пока не оптимизирую схему), как предложил @ erwin-brandstetter (запрос выполняется с 1,5 секундами с той же скоростью, что и мой исходный запрос):

EXPLAIN ANALYZE SELECT user_id, count(user_id) as ct FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                        QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253670.88..253671.24 rows=37 width=8) (actual time=1807.334..1807.339 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220280.62 rows=6678050 width=8) (actual time=0.183..893.491 rows=6678050 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1807.368 ms
(4 rows)
Томка
источник
У вас также есть таблица Usersс user_idпервичным ключом?
ypercubeᵀᴹ
Я только что увидел, что есть стороннее дополнение columnstore для Postgres. Кроме того, я просто хотел отправить сообщение из нового приложения ios
swasheck
2
Спасибо за хороший, ясный, полный вопрос - версии, определения таблиц и т. Д.
Крейг Рингер
@ypercube Да, у меня есть таблица пользователей.
Томка
Сколько разных project_idа user_id? Таблица обновляется постоянно или вы можете работать с материализованным представлением (какое-то время)?
Эрвин Брандштеттер

Ответы:

25

Основная проблема - отсутствующий индекс. Но это еще не все.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • У вас много bigintстолбцов. Вероятно, перебор. Как правило, этого integerболее чем достаточно для столбцов типа project_idи user_id. Это также поможет следующий пункт.
    При оптимизации определения таблицы рассмотрите этот связанный ответ с акцентом на выравнивание и заполнение данных . Но большинство остального тоже применимо:

  • Слон в комнате : нет индексаproject_id . Создай. Это важнее, чем остальная часть этого ответа.
    Находясь в этом, сделайте это многоколоночным индексом:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Если бы вы следовали моему совету, integerбыло бы идеально здесь:

  • user_idопределяется NOT NULL, так count(user_id)что эквивалентно count(*), но последний немного короче и быстрее. (В этом конкретном запросе это применимо даже без user_idопределения NOT NULL.)

  • idэто уже первичный ключ, дополнительным UNIQUEограничением является бесполезный балласт . Брось это:

    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)

    В сторону: я бы не использовал в idкачестве имени столбца. Используйте что-то вроде описания treenode_id.

Добавлена ​​информация

В: How many different project_id and user_id?
А: not more than five different project_id.

Это означает, что Postgres должен прочитать около 20% всей таблицы, чтобы удовлетворить ваш запрос. Если он не может использовать сканирование только по индексу , последовательное сканирование таблицы будет быстрее, чем использование каких-либо индексов. Здесь больше не нужно повышать производительность, кроме как за счет оптимизации настроек таблицы и сервера.

Что касается сканирования только по индексу : чтобы увидеть, насколько эффективным это может быть, запустите, VACUUM ANALYZEесли вы можете себе это позволить (блокирует исключительно таблицу). Затем попробуйте еще раз. Теперь он должен быть немного быстрее, используя только индекс. Сначала прочтите этот связанный ответ:

А также страница справочника, добавленная с Postgres 9.6 и Postgres Wiki по сканированию только по индексу .

Эрвин Брандштеттер
источник
1
Эрвин, спасибо за ваши предложения. Вы правы, ибо user_idи project_id integerдолжно быть более чем достаточно. Использование count(*)вместо count(user_id)сохранения около 70 мс, это хорошо знать. Я добавил EXPLAIN ANALYZEзапрос после того, как добавил предложенное indexв первый пост. Это не улучшает производительность, хотя (но и не повредит). Кажется, indexне используется вообще. Я скоро опробую оптимизацию схемы.
Томка
1
Если я отключу seqscan, используется индекс ( Index Only Scan using treenode_project_id_user_id_index on treenode), но тогда запрос занимает около 2,5 секунд (что примерно на 1 секунду дольше, чем с помощью seqscan).
Tomka
1
Спасибо за ваше обновление. Эти недостающие биты должны были быть частью моего вопроса, это правильно. Я просто не знал об их влиянии. Я оптимизирую свою схему, как вы предложили - давайте посмотрим, что я могу извлечь из этого. Спасибо за ваше объяснение, оно имеет смысл для меня, и поэтому я отмечу ваш ответ как принятый.
Томка
7

Сначала я добавлю индекс, (project_id, user_id)а затем в версии 9.3 попробую этот запрос:

SELECT u.user_id, c.number_of_nodes 
FROM users AS u
   , LATERAL
     ( SELECT COUNT(*) AS number_of_nodes 
       FROM treenode AS t
       WHERE t.project_id = 1 
         AND t.user_id = u.user_id
     ) c 
-- WHERE c.number_of_nodes > 0 ;   -- you probably want this as well
                                   -- to show only relevant users

В 9.2 попробуйте это:

SELECT u.user_id, 
       ( SELECT COUNT(*) 
         FROM treenode AS t
         WHERE t.project_id = 1 
           AND t.user_id = u.user_id
       ) AS number_of_nodes  
FROM users AS u ;

Я полагаю, у вас есть usersстол. Если нет, замените usersна:
(SELECT DISTINCT user_id FROM treenode)

ypercubeᵀᴹ
источник
Большое спасибо за ответ. Вы правы, у меня есть таблица пользователей. Однако при использовании вашего запроса в 9.2 для получения результата требуется около 5 секунд - независимо от того, создан индекс или нет. Я создал индекс следующим образом:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);но я попытался и без USINGпредложения. Я что-то пропустил?
Томка
Сколько строк в usersтаблице и сколько строк возвращает запрос (сколько у них пользователей project_id=1)? Можете ли вы показать объяснение этого запроса после добавления индекса?
ypercubeᵀᴹ
1
Во-первых, я ошибся в своем первом комментарии. Без предложенного вами индекса потребуется около 40 секунд (!) Для получения результата. Это займет около 5 секунд с на indexместе. Извините за путаницу. В моей usersтаблице 46 записей. Запрос возвращает только 9 строк. Удивительно, но SELECT DISTINCT user_id FROM treenode WHERE project_id=1;возвращает 38 строк. Я добавил explainсвой первый пост. И чтобы избежать путаницы: мой usersстол на самом деле называется auth_user.
Томка
Интересно, как можно SELECT DISTINCT user_id FROM treenode WHERE project_id=1;вернуть 38 строк, тогда как запросы возвращают только 9. Buffled.
ypercubeᵀᴹ
Можете ли вы попробовать это ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ