Meningkatkan kinerja COUNT / GROUP-BY dalam tabel PostgresSQL besar?

24

Saya menjalankan PostgresSQL 9.2 dan memiliki hubungan 12 kolom dengan sekitar 6.700.000 baris. Ini berisi node dalam ruang 3D, masing-masing referensi pengguna (yang membuatnya). Untuk menanyakan pengguna yang telah membuat berapa node, saya melakukan hal berikut (ditambahkan explain analyzeuntuk informasi lebih lanjut):

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

Seperti yang Anda lihat, ini membutuhkan waktu sekitar 1,7 detik. Ini tidak terlalu buruk mengingat jumlah data, tetapi saya ingin tahu apakah ini dapat ditingkatkan. Saya mencoba menambahkan indeks BTree pada kolom pengguna, tetapi ini tidak membantu sama sekali.

Apakah Anda punya saran alternatif?


Demi kelengkapan, ini adalah definisi tabel lengkap dengan semua indeksnya (tanpa batasan, referensi, dan pemicu kunci asing):

    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)

Sunting: Ini hasilnya, ketika saya menggunakan kueri (dan indeks) yang diajukan oleh @ypercube (kueri membutuhkan waktu sekitar 5,3 detik tanpa 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

Sunting 2: Ini adalah hasilnya, ketika saya menggunakan indexon project_id, user_id(tapi belum ada optimasi skema, belum) seperti yang disarankan oleh @ erwin-brandstetter (kueri berjalan dengan 1,5 detik pada kecepatan yang sama dengan permintaan asli saya):

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)
Tomka
sumber
Apakah Anda juga memiliki tabel Usersdengan user_idsebagai kunci utama?
ypercubeᵀᴹ
Saya baru saja melihat ada addon pihak ketiga untuk Postgres. Juga, saya hanya ingin memposting dari aplikasi ios baru
swasheck
2
Terima kasih atas pertanyaan yang bagus, jelas, lengkap - versi, definisi tabel, dll.
Craig Ringer
@ ypercube Ya, saya punya tabel Pengguna.
tomka
Berapa banyak berbeda project_iddan user_id? Apakah tabel diperbarui terus menerus atau dapatkah Anda bekerja dengan tampilan yang terwujud (untuk beberapa waktu)?
Erwin Brandstetter

Jawaban:

25

Masalah utama adalah indeks yang hilang. Tetapi masih ada lagi.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Anda memiliki banyak bigintkolom. Mungkin berlebihan. Biasanya, integerlebih dari cukup untuk kolom seperti project_iddan user_id. Ini juga akan membantu item berikutnya.
    Saat mengoptimalkan definisi tabel, pertimbangkan jawaban terkait ini, dengan penekanan pada perataan dan padding data . Tetapi sebagian besar sisanya berlaku juga:

  • The gajah di dalam ruangan : tidak ada indeks padaproject_id . Buat satu. Ini lebih penting daripada sisa jawaban ini.
    Sementara berada di sana, buatlah indeks multikolom:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Jika Anda mengikuti saran saya, integerakan sempurna di sini:

  • user_iddidefinisikan NOT NULL, jadi count(user_id)sama dengan count(*), tetapi yang terakhir sedikit lebih pendek dan lebih cepat. (Dalam permintaan khusus ini, ini bahkan akan berlaku tanpa user_idditentukan NOT NULL.)

  • idsudah menjadi kunci utama, UNIQUEkendala tambahan adalah pemberat yang tidak berguna . Jatuhkan:

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

    Selain: Saya tidak akan menggunakan idnama kolom. Gunakan sesuatu yang deskriptif treenode_id.

Informasi tambahan

Q: How many different project_id and user_id?
A: not more than five different project_id.

Itu berarti Postgres harus membaca sekitar 20% dari seluruh tabel untuk memenuhi permintaan Anda. Kecuali itu dapat menggunakan pemindaian hanya indeks , pemindaian berurutan pada tabel akan lebih cepat daripada melibatkan indeks apa pun. Tidak ada lagi kinerja yang diperoleh di sini - kecuali dengan mengoptimalkan pengaturan tabel dan server.

Adapun pemindaian hanya indeks : Untuk melihat seberapa efektif itu, jalankan VACUUM ANALYZEjika Anda mampu (mengunci tabel secara eksklusif). Kemudian coba kueri Anda lagi. Seharusnya sekarang lebih cepat dengan hanya menggunakan indeks. Baca jawaban terkait ini terlebih dahulu:

Serta halaman buku panduan ditambahkan dengan Postgres 9.6 dan Postgres Wiki pada pemindaian indeks saja .

Erwin Brandstetter
sumber
1
Erwin, terima kasih atas saran Anda. Anda benar, karena user_iddan project_id integerharus lebih dari cukup. Menggunakan count(*)alih-alih count(user_id)menghemat sekitar 70 ms di sini, itu bagus untuk diketahui. Saya telah menambahkan EXPLAIN ANALYZEkueri setelah saya menambahkan saran Anda indexke posting pertama. Itu tidak meningkatkan kinerja, meskipun (tetapi juga tidak ada salahnya). Sepertinya indextidak digunakan sama sekali. Saya akan segera menguji optimisasi skema.
Tomka
1
Jika saya menonaktifkan seqscan, indeks digunakan ( Index Only Scan using treenode_project_id_user_id_index on treenode), tetapi permintaan memakan waktu sekitar 2,5 detik kemudian (yaitu sekitar 1 detik lebih lama daripada dengan seqscan).
Tomka
1
Terima kasih atas pembaruan Anda. Bit-bit yang hilang ini seharusnya menjadi bagian dari pertanyaan saya, itu benar. Saya hanya tidak menyadari dampaknya. Saya akan mengoptimalkan skema saya seperti yang Anda sarankan --- mari kita lihat apa yang bisa saya dapatkan dari itu. Terima kasih atas penjelasan Anda, masuk akal bagi saya dan karena itu saya akan menandai jawaban Anda sebagai yang diterima.
Tomka
7

Saya pertama-tama akan menambahkan indeks (project_id, user_id)dan kemudian dalam versi 9.3, coba kueri ini:

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

Di 9.2, coba yang ini:

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 ;

Saya menganggap Anda punya usersmeja. Jika tidak, ganti usersdengan:
(SELECT DISTINCT user_id FROM treenode)

ypercubeᵀᴹ
sumber
Terimakasih banyak atas jawaban Anda. Anda benar, saya punya tabel pengguna. Namun, menggunakan kueri Anda dalam 9.2 dibutuhkan sekitar 5 detik untuk mendapatkan hasilnya - terlepas dari apakah indeks dibuat atau tidak. Saya membuat indeks seperti ini:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);tetapi saya mencoba juga tanpa USINGklausa. Apakah saya melewatkan sesuatu?
tomka
Berapa banyak baris yang ada di userstabel dan berapa banyak baris yang dikembalikan oleh kueri (jadi, berapa banyak pengguna yang memilikinya project_id=1)? Bisakah Anda menunjukkan penjelasan dari permintaan ini, setelah Anda menambahkan indeks?
ypercubeᵀᴹ
1
Pertama, saya salah dalam komentar pertama saya. Tanpa indeks yang Anda sarankan, dibutuhkan sekitar 40-an (!) Untuk mengambil hasilnya. Dibutuhkan sekitar 5s dengan indexdi tempat. Maaf bila membingungkan. Di usersmeja saya, saya punya 46 entri. Permintaan hanya mengembalikan 9 baris. Anehnya, SELECT DISTINCT user_id FROM treenode WHERE project_id=1;mengembalikan 38 baris. Saya telah menambahkannya explainke posting pertama saya. Dan untuk mencegah kebingungan: mejaku userssebenarnya dipanggil auth_user.
Tomka
Saya bertanya-tanya bagaimana bisa SELECT DISTINCT user_id FROM treenode WHERE project_id=1;mengembalikan 38 baris sedangkan permintaan hanya mengembalikan 9. Digosok.
ypercubeᵀᴹ
Bisakah Anda mencoba ini ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ