Mengapa PostgreSQL memilih pesanan bergabung yang lebih mahal?


13

PostgreSQL menggunakan default, plus

default_statistics_target=1000
random_page_cost=1.5

Versi: kapan

PostgreSQL 10.4 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit

Saya sudah menyedot debu dan dianalisis. Permintaannya sangat mudah:

SELECT r.price
FROM account_payer ap
  JOIN account_contract ac ON ap.id = ac.account_payer_id
  JOIN account_schedule "as" ON ac.id = "as".account_contract_id
  JOIN schedule s ON "as".id = s.account_schedule_id
  JOIN rate r ON s.id = r.schedule_id
WHERE ap.account_id = 8

Setiap idkolom adalah kunci utama, dan semua yang bergabung adalah hubungan kunci asing, dan setiap kunci asing memiliki indeks. Ditambah indeks untuk account_payer.account_id.

Dibutuhkan 3,93 untuk mengembalikan baris 76rb.

Merge Join  (cost=8.06..83114.08 rows=3458267 width=6) (actual time=0.228..3920.472 rows=75548 loops=1)
  Merge Cond: (s.account_schedule_id = "as".id)
  ->  Nested Loop  (cost=0.57..280520.54 rows=6602146 width=14) (actual time=0.163..3756.082 rows=448173 loops=1)
        ->  Index Scan using schedule_account_schedule_id_idx on schedule s  (cost=0.14..10.67 rows=441 width=16) (actual time=0.035..0.211 rows=89 loops=1)
        ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.025..39.903 rows=5036 loops=89)
              Index Cond: (schedule_id = s.id)
  ->  Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)
        ->  Nested Loop  (cost=0.43..49.32 rows=55 width=8) (actual time=0.048..1.110 rows=66 loops=1)
              ->  Nested Loop  (cost=0.29..27.46 rows=105 width=16) (actual time=0.030..0.616 rows=105 loops=1)
                    ->  Index Scan using account_schedule_pkey on account_schedule "as"  (cost=0.14..6.22 rows=105 width=16) (actual time=0.014..0.098 rows=105 loops=1)
                    ->  Index Scan using account_contract_pkey on account_contract ac  (cost=0.14..0.20 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=105)
                          Index Cond: (id = "as".account_contract_id)
              ->  Index Scan using account_payer_pkey on account_payer ap  (cost=0.14..0.21 rows=1 width=8) (actual time=0.003..0.003 rows=1 loops=105)
                    Index Cond: (id = ac.account_payer_id)
                    Filter: (account_id = 8)
                    Rows Removed by Filter: 0
Planning time: 5.843 ms
Execution time: 3929.317 ms

Jika saya atur join_collapse_limit=1, dibutuhkan 0,16 detik, kecepatan 25x.

Nested Loop  (cost=6.32..147323.97 rows=3458267 width=6) (actual time=8.908..151.860 rows=75548 loops=1)
  ->  Nested Loop  (cost=5.89..390.23 rows=231 width=8) (actual time=8.730..11.655 rows=66 loops=1)
        Join Filter: ("as".id = s.account_schedule_id)
        Rows Removed by Join Filter: 29040
        ->  Index Scan using schedule_pkey on schedule s  (cost=0.27..17.65 rows=441 width=16) (actual time=0.014..0.314 rows=441 loops=1)
        ->  Materialize  (cost=5.62..8.88 rows=55 width=8) (actual time=0.001..0.011 rows=66 loops=441)
              ->  Hash Join  (cost=5.62..8.61 rows=55 width=8) (actual time=0.240..0.309 rows=66 loops=1)
                    Hash Cond: ("as".account_contract_id = ac.id)
                    ->  Seq Scan on account_schedule "as"  (cost=0.00..2.05 rows=105 width=16) (actual time=0.010..0.028 rows=105 loops=1)
                    ->  Hash  (cost=5.02..5.02 rows=48 width=8) (actual time=0.178..0.178 rows=61 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 11kB
                          ->  Hash Join  (cost=1.98..5.02 rows=48 width=8) (actual time=0.082..0.143 rows=61 loops=1)
                                Hash Cond: (ac.account_payer_id = ap.id)
                                ->  Seq Scan on account_contract ac  (cost=0.00..1.91 rows=91 width=16) (actual time=0.007..0.023 rows=91 loops=1)
                                ->  Hash  (cost=1.64..1.64 rows=27 width=8) (actual time=0.048..0.048 rows=27 loops=1)
                                      Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                      ->  Seq Scan on account_payer ap  (cost=0.00..1.64 rows=27 width=8) (actual time=0.009..0.023 rows=27 loops=1)
                                            Filter: (account_id = 8)
                                            Rows Removed by Filter: 24
  ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.018..1.685 rows=1145 loops=66)
        Index Cond: (schedule_id = s.id)
Planning time: 4.692 ms
Execution time: 160.585 ms

Keluaran ini tidak masuk akal bagi saya. Yang pertama memiliki (sangat tinggi) biaya 280.500 untuk loop bersarang bergabung untuk jadwal dan indeks nilai. Mengapa PostgreSQL sengaja memilih yang sangat mahal untuk bergabung dulu?

Informasi tambahan diminta melalui komentar

Apakah rate_schedule_id_code_modifier_facility_idxindeks gabungan?

Yaitu, dengan schedule_idmenjadi kolom pertama. Saya telah membuatnya menjadi indeks khusus, dan dipilih oleh perencana kueri, tetapi tidak memengaruhi kinerja atau memengaruhi rencana.


Bisakah Anda mengubah pengaturan default_statistics_targetdan random_page_costkembali ke standarnya? Apa yang terjadi ketika Anda meningkatkan default_statistics_targetlebih jauh? Bisakah Anda membuat DB Fiddle (di dbfiddle.uk) dan mencoba mereproduksi masalah di sana?
Colin 't Hart

3
Bisakah Anda memeriksa statistik aktual untuk melihat apakah ada sesuatu yang miring / aneh tentang data Anda? postgresql.org/docs/10/static/planner-stats.html
Colin 't Hart

Berapa nilai saat ini untuk parameter work_mem? Mengubahnya memberikan timing yang berbeda?
eppesuig

Jawaban:


1

Sepertinya statistik Anda tidak akurat (jalankan analisis vakum untuk menyegarkannya) baik Anda memiliki kolom yang berkorelasi dalam model Anda (dan karenanya Anda harus melakukan create statisticsuntuk memberi tahu perencana fakta itu).

The join_collapseparameter memungkinkan perencana untuk mengatur ulang bergabung sehingga melakukan pertama yang menjemput sedikit data. Namun, untuk kinerja, kami tidak dapat membiarkan perencana melakukannya pada kueri dengan banyak gabungan. Secara default, ini diatur ke 8 joins max. Dengan mengaturnya ke 1, Anda cukup menonaktifkan kemampuan itu.

Jadi, bagaimana postgres memperkirakan berapa banyak baris yang harus dijawab oleh kueri ini? Ini menggunakan statistik untuk memperkirakan jumlah baris.

Apa yang dapat kita lihat dalam rencana penjelasan Anda adalah bahwa ada beberapa estimasi jumlah baris yang tidak akurat (nilai pertama adalah estimasi, kedua adalah aktual).

Sebagai contoh, di sini:

Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)

Perencana itu diperkirakan mendapat 55 baris ketika dia benar-benar mendapat 74.697.

Apa yang akan saya lakukan (jika saya berada di posisi Anda) adalah:

  • analyze lima tabel yang terlibat untuk menyegarkan statistik
  • Ulangan explain analyze
  • Lihat perbedaan antara taksiran jumlah baris dan jumlah baris aktual
  • Jika perkiraan jumlah baris benar, mungkin rencananya berubah dan lebih efisien. Jika semuanya baik-baik saja, Anda dapat mempertimbangkan untuk mengubah pengaturan autovacuum Anda sehingga menganalisis (dan vakum) melakukan lebih sering
  • Jika estimasi jumlah baris masih salah, sepertinya Anda memiliki data yang berkorelasi dalam tabel Anda (pelanggaran bentuk normal ketiga). Anda dapat mempertimbangkan untuk mendeklarasikannya dengan CREATE STATISTICS(dokumentasi di sini )

Jika Anda memerlukan informasi lebih lanjut tentang taksiran baris dan kalkulasinya, Anda akan menemukan semua yang Anda butuhkan dalam diskusi conf Tomas Vondra "Buat statistik - Untuk apa ini?" (slide di sini )

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.