Поліпшення продуктивності 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: Це результат, коли Я використовую indexна project_id, user_id(але не оптимізації схеми, поки) , як @ Ервін-Брандштеттер не запропонував (пробіги запиту з 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ᵀᴹ

Щойно я побачив, що для Postgres є стороннє додавання стовпців стовпців. Також я просто хотів публікувати повідомлення з нового додатку ios
swasheck

2
Дякуємо за гарне, чітке, повне запитання - версії, визначення таблиць тощо.
Крейг Рінгер

@ypercube Так, у мене є таблиця користувачів.
tomka

Скільки різних 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взагалі не використовується. Я скоро перевіряю оптимізацію схеми.
tomka

1
Якщо я відключую seqscan, використовується індекс ( Index Only Scan using treenode_project_id_user_id_index on treenode), але запит займає приблизно 2,5 секунди (що приблизно на 1 секунду довше, ніж у seqscan).
tomka

1
Дякуємо за ваше оновлення Ці відсутні шматочки повинні були бути частиною мого питання, це правильно. Я просто не знав їх впливу. Я оптимізую свою схему, як ви запропонували --- давайте подивимось, що я можу отримати від цього. Дякую за ваше пояснення, для мене це має сенс, і тому я позначу вашу відповідь як прийняту.
tomka

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)


Дуже дякую за вашу відповідь. Ви маєте рацію, у мене є таблиця користувачів. Однак, використовуючи ваш запит у 9.2, для отримання результату потрібно приблизно 5 секунд - незалежно від того, створено чи ні індекс. Я створив індекс так:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);але спробував і без USINGпункту. Я щось сумую?
tomka

Скільки рядків у 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.
tomka

Цікаво, як можна SELECT DISTINCT user_id FROM treenode WHERE project_id=1;повернути 38 рядків, тоді як запити повертаються лише 9. Збито.
ypercubeᵀᴹ

Чи можете ви спробувати це ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.