Mengapa indeks gin pada kolom jsonb memperlambat permintaan saya dan apa yang dapat saya lakukan?

10

Inisialisasi data uji:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE docs (data JSONB NOT NULL DEFAULT '{}');
-- generate 200k documents, ~half with type: "type1" and another half with type: "type2", unique incremented index and random uuid per each row
INSERT INTO docs (data)
SELECT json_build_object('id', gen_random_uuid(), 'type', (CASE WHEN random() > 0.5 THEN 'type1' ELSE 'type2' END) ,'index', n)::JSONB
FROM generate_series(1, 200000) n;
-- inset one more row with explicit uuid to query by it later
INSERT INTO docs (data) VALUES (json_build_object('id', '30e84646-c5c5-492d-b7f7-c884d77d1e0a', 'type', 'type1' ,'index', 200001)::JSONB);

Permintaan pertama - filter berdasarkan data-> jenis dan batas:

-- FAST ~19ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
LIMIT 25;
/* "Limit  (cost=0.00..697.12 rows=25 width=90) (actual time=0.029..0.070 rows=25 loops=1)"
   "  ->  Seq Scan on docs  (cost=0.00..5577.00 rows=200 width=90) (actual time=0.028..0.061 rows=25 loops=1)"
   "        Filter: (data @> '{"type": "type1"}'::jsonb)"
   "        Rows Removed by Filter: 17"
   "Planning time: 0.069 ms"
   "Execution time: 0.098 ms" 
*/

Permintaan kedua - memfilter menurut data-> jenis, memesan berdasarkan data-> indeks dan batas

-- SLOW ~250ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index' -- added ORDER BY
LIMIT 25;

/* "Limit  (cost=5583.14..5583.21 rows=25 width=90) (actual time=236.750..236.754 rows=25 loops=1)"
   "  ->  Sort  (cost=5583.14..5583.64 rows=200 width=90) (actual time=236.750..236.750 rows=25 loops=1)"
   "        Sort Key: ((data -> 'index'::text))"
   "        Sort Method: top-N heapsort  Memory: 28kB"
   "        ->  Seq Scan on docs  (cost=0.00..5577.50 rows=200 width=90) (actual time=0.020..170.797 rows=100158 loops=1)"
   "              Filter: (data @> '{"type": "type1"}'::jsonb)"
   "              Rows Removed by Filter: 99842"
   "Planning time: 0.075 ms"
   "Execution time: 236.785 ms"
*/

Kueri ketiga - sama seperti Kedua (sebelumnya) tetapi dengan indeks btree pada data-> indeks:

CREATE INDEX docs_data_index_idx ON docs ((data->'index'));

-- FAST ~19ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index' -- added BTREE index on this field
LIMIT 25;
/* "Limit  (cost=0.42..2473.98 rows=25 width=90) (actual time=0.040..0.125 rows=25 loops=1)"
   "  ->  Index Scan using docs_data_index_idx on docs  (cost=0.42..19788.92 rows=200 width=90) (actual time=0.038..0.119 rows=25 loops=1)"
   "        Filter: (data @> '{"type": "type1"}'::jsonb)"
   "        Rows Removed by Filter: 17"
   "Planning time: 0.127 ms"
   "Execution time: 0.159 ms"
*/

Kueri keempat - sekarang filter berdasarkan data-> id dan batas = 1:

-- SLOW ~116ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> ('{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}')::JSONB -- querying by "id" field now
LIMIT 1;
/* "Limit  (cost=0.00..27.89 rows=1 width=90) (actual time=97.990..97.990 rows=1 loops=1)"
   "  ->  Seq Scan on docs  (cost=0.00..5577.00 rows=200 width=90) (actual time=97.989..97.989 rows=1 loops=1)"
   "        Filter: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "        Rows Removed by Filter: 189999"
   "Planning time: 0.064 ms"
   "Execution time: 98.012 ms"
*/ 

Kueri kelima - sama dengan Keempat tetapi dengan indeks gin (json_path_ops) pada data:

CREATE INDEX docs_data_idx ON docs USING GIN (data jsonb_path_ops);

-- FAST ~17ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::JSONB -- added gin index with json_path_ops
LIMIT 1;
/* "Limit  (cost=17.55..20.71 rows=1 width=90) (actual time=0.027..0.027 rows=1 loops=1)"
   "  ->  Bitmap Heap Scan on docs  (cost=17.55..649.91 rows=200 width=90) (actual time=0.026..0.026 rows=1 loops=1)"
   "        Recheck Cond: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "        Heap Blocks: exact=1"
   "        ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=0.016..0.016 rows=1 loops=1)"
   "              Index Cond: (data @> '{"id": "30e84646-c5c5-492d-b7f7-c884d77d1e0a"}'::jsonb)"
   "Planning time: 0.095 ms"
   "Execution time: 0.055 ms"
*/

Kueri keenam (dan terakhir) - sama dengan Kueri ketiga (kueri menurut data-> jenis, urutan menurut data-> indeks, batas):

-- SLOW AGAIN! ~224ms
EXPLAIN ANALYZE
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
/* "Limit  (cost=656.06..656.12 rows=25 width=90) (actual time=215.927..215.932 rows=25 loops=1)"
   "  ->  Sort  (cost=656.06..656.56 rows=200 width=90) (actual time=215.925..215.925 rows=25 loops=1)"
   "        Sort Key: ((data -> 'index'::text))"
   "        Sort Method: top-N heapsort  Memory: 28kB"
   "        ->  Bitmap Heap Scan on docs  (cost=17.55..650.41 rows=200 width=90) (actual time=33.134..152.618 rows=100158 loops=1)"
   "              Recheck Cond: (data @> '{"type": "type1"}'::jsonb)"
   "              Heap Blocks: exact=3077"
   "              ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=32.468..32.468 rows=100158 loops=1)"
   "                    Index Cond: (data @> '{"type": "type1"}'::jsonb)"
   "Planning time: 0.157 ms"
   "Execution time: 215.992 ms"
*/

Jadi sepertinya permintaan Keenam (sama seperti Ketiga) jauh lebih lambat ketika ada indeks gin pada kolom data. Mungkin karena tidak ada banyak nilai berbeda untuk bidang tipe data> (hanya "type1" atau "type2")? Apa yang bisa saya lakukan? Saya perlu indeks gin untuk membuat pertanyaan lain yang mengambil manfaat dari itu ...

pengguna606521
sumber

Jawaban:

5

Sepertinya Anda mengalami masalah bahwa jsonbkolom memiliki angka statistik rata-rata 1%, seperti yang dilaporkan di sini Mengatasi kekurangan statistik jsonb? . Melihat rencana permintaan Anda, perbedaan antara taksiran dan eksekusi aktual sangat besar. Perkiraan mengatakan mungkin ada 200 baris, dan mengembalikan 100158 baris, yang menyebabkan perencana lebih menyukai strategi tertentu daripada yang lain.

Karena pilihan dalam kueri keenam tampaknya mendukung pemindaian indeks bitmap daripada pemindaian indeks, Anda dapat mendorong perencana bersamaan dengan SET enable_bitmapscan=offmencoba dan membuatnya untuk kembali ke perilaku yang Anda miliki dalam contoh ketiga Anda.

Begini cara kerjanya untuk saya:

postgres@[local]:5432:postgres:=# EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=656.06..656.12 rows=25 width=90) (actual time=117.338..117.343 rows=25 loops=1)
   Buffers: shared hit=3096
   ->  Sort  (cost=656.06..656.56 rows=200 width=90) (actual time=117.336..117.338 rows=25 loops=1)
         Sort Key: ((data -> 'index'::text))
         Sort Method: top-N heapsort  Memory: 28kB
         Buffers: shared hit=3096
         ->  Bitmap Heap Scan on docs  (cost=17.55..650.41 rows=200 width=90) (actual time=12.838..80.584 rows=99973 loops=1)
               Recheck Cond: (data @> '{"type": "type1"}'::jsonb)
               Heap Blocks: exact=3077
               Buffers: shared hit=3096
               ->  Bitmap Index Scan on docs_data_idx  (cost=0.00..17.50 rows=200 width=0) (actual time=12.469..12.469 rows=99973 loops=1)
                     Index Cond: (data @> '{"type": "type1"}'::jsonb)
                     Buffers: shared hit=19
 Planning time: 0.088 ms
 Execution time: 117.405 ms
(15 rows)

Time: 117.813 ms
postgres@[local]:5432:postgres:=# SET enable_bitmapscan = off;
SET
Time: 0.130 ms
postgres@[local]:5432:postgres:=# EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
                                                               QUERY PLAN                                                               
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..1320.48 rows=25 width=90) (actual time=0.017..0.050 rows=25 loops=1)
   Buffers: shared hit=4
   ->  Index Scan using docs_data_index_idx on docs  (cost=0.42..10560.94 rows=200 width=90) (actual time=0.015..0.045 rows=25 loops=1)
         Filter: (data @> '{"type": "type1"}'::jsonb)
         Rows Removed by Filter: 27
         Buffers: shared hit=4
 Planning time: 0.083 ms
 Execution time: 0.071 ms
(8 rows)

Time: 0.402 ms
postgres@[local]:5432:postgres:=#

Jika Anda mencari rute ini, pastikan untuk menonaktifkan pemindaian itu hanya untuk kueri yang menunjukkan perilaku seperti ini, jika tidak, Anda akan mendapatkan perilaku buruk pada paket kueri lainnya juga. Melakukan sesuatu seperti ini seharusnya bekerja dengan baik:

BEGIN;
SET enable_bitmapscan=off;
SELECT * FROM docs
WHERE data @> '{"type": "type1"}'::JSONB
ORDER BY data->'index'
LIMIT 25;
SET enable_bitmapscan=on;
COMMIT;

Harapan yang membantu =)

Kassandry
sumber
Saya tidak yakin apakah saya memahami Anda dengan benar (saya tidak akrab dengan internal PG) - perilaku ini disebabkan oleh kardinalitas rendah pada bidang "tipe" di kolom jsonb (dan secara internal disebabkan oleh angka statistik datar), bukan? Dan itu juga berarti bahwa, jika saya ingin kueri saya dioptimalkan, saya harus tahu perkiraan kardinalitas bidang jsonb. Saya meminta dengan memutuskan apakah saya harus mengaktifkan_bitmapscan atau tidak, kan?
user606521
1
Ya, Anda tampaknya memahami ini dalam kedua hal. Selektivitas dasar 1% lebih suka melihat bidang dalam WHEREklausa dalam indeks gin karena percaya akan mengembalikan lebih sedikit baris, yang tidak benar. Karena Anda dapat memperkirakan jumlah baris yang lebih baik, Anda dapat melihat bahwa, karena Anda melakukannya ORDER BY data->'index' LIMIT 25, pemindaian beberapa entri pertama dari indeks lainnya (50 atau lebih, dengan baris yang dibuang) akan menghasilkan baris yang lebih sedikit, sehingga memberi tahu perencana itu benar-benar tidak boleh mencoba untuk menggunakan hasil bitmap dalam rencana permintaan yang lebih cepat digunakan. Harapan yang jelas semuanya. =)
Kassandry
1
Ada informasi klarifikasi tambahan di sini: databasesoup.com/2015/01/tag-all-things-part-3.html dan dalam presentasi ini thebuild.com/presentations/json2015-pgconfus.pdf untuk membantu juga.
Kassandry
1
Satu-satunya pekerjaan yang saya tahu adalah dari Oleg Bartunov, Tedor Sigaev, dan Alexander Kotorov pada ekstensi JsQuery , dan peningkatan selektivitasnya. Dengan sedikit keberuntungan, itu membuatnya menjadi inti PostgreSQL di 9.6 atau lebih baru.
Kassandry
1
Saya mengutip angka 1% dari email dalam jawaban saya dari Josh Berkus, anggota Tim Inti PostgreSQL. Dari mana asalnya membutuhkan pemahaman yang jauh lebih dalam tentang internal daripada yang saya miliki saat ini, maaf. = (Anda dapat mencoba membalas [email protected]atau memeriksa freenode IRC #postgresqldari mana tepatnya angka itu berasal.
Kassandry