web-dev-qa-db-ja.com

大きなPostgresQLテーブルでのCOUNT / GROUP-BYのパフォーマンスを改善しますか?

私はPostgresSQL 9.2を実行しており、約6,700,000行の12列の関係があります。 3Dスペースにノードが含まれ、各ノードは(それを作成した)ユーザーを参照します。どのユーザーがノードをいくつ作成したかを照会するには、次のようにします(詳細については_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によって提案されたクエリ(およびインデックス)を使用したときの結果です(クエリには_EXPLAIN ANALYZE_を使用しないと約5.3秒かかります)。

_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:これは、@ erwin-brandstetterが提案したように_project_id, user_id_でindexを使用したときの結果です(ただし、スキーマの最適化はまだ行われていません)(クエリは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)
_
26
tomka

主な問題は、欠落しているインデックスです。しかし、もっとあります。

_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_のような説明的なものを使用します。

追加情報

Q:_How many different project_id and user_id?_
A:_not more than five different project_id_。

つまり、Postgresはクエリを満たすためにテーブル全体の約20%を読み取る必要があります。 index-only scanを使用できない場合を除き、テーブルの順次スキャンは、インデックスを使用するよりも高速です。テーブルとサーバーの設定を最適化する以外は、ここでパフォーマンスを上げる必要はありません。

index-only scanの場合:どれほど効果的かを確認するには、余裕がある場合は_VACUUM ANALYZE_を実行します(テーブルを排他的にロックします)。次に、クエリを再試行してください。インデックスをonly使用すると、適度に高速になるはずです。最初にこの関連する答えを読んでください:

Postgres 9.6で追加されたマニュアルページ および インデックスのみのスキャンに関するPostgres Wiki も同様です。

28

最初に(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)

7
ypercubeᵀᴹ