JSON adalah singkatan dari JavaScript Object Notation. Ini adalah format standar terbuka yang mengatur data menjadi pasangan kunci/nilai dan array yang dirinci dalam RFC 7159. JSON adalah format paling umum yang digunakan oleh layanan web untuk bertukar data, menyimpan dokumen, data tidak terstruktur, dll. Dalam posting ini, kita akan untuk menunjukkan kepada Anda kiat dan teknik tentang cara menyimpan dan mengindeks data JSON secara efektif di PostgreSQL.
Anda juga dapat melihat webinar Bekerja dengan Data JSON kami di PostgreSQL vs. MongoDB webinar dalam kemitraan dengan PostgresConf untuk mempelajari lebih lanjut tentang topik ini, dan lihat halaman SlideShare kami untuk mengunduh slide.
Mengapa Menyimpan JSON di PostgreSQL?
Mengapa database relasional harus peduli dengan data tidak terstruktur? Ternyata ada beberapa skenario yang berguna.
-
Fleksibilitas skema
Salah satu alasan utama untuk menyimpan data menggunakan format JSON adalah fleksibilitas skema. Menyimpan data Anda di JSON berguna saat skema Anda lancar dan sering berubah. Jika Anda menyimpan setiap kunci sebagai kolom, ini akan mengakibatkan operasi DML yang sering – ini bisa sulit jika kumpulan data Anda besar - misalnya, pelacakan peristiwa, analitik, tag, dll. Catatan:Jika kunci tertentu selalu ada di dokumen Anda, mungkin masuk akal untuk menyimpannya sebagai kolom kelas satu. Kami membahas lebih lanjut tentang pendekatan ini di bagian “Pola &Antipola JSON” di bawah ini.
-
Objek bertingkat
Jika kumpulan data Anda memiliki objek bersarang (tunggal atau multi-level), dalam beberapa kasus, lebih mudah untuk menanganinya di JSON daripada mendenormalisasi data menjadi kolom atau beberapa tabel.
-
Menyinkronkan dengan sumber data eksternal
Sering kali sistem eksternal menyediakan data sebagai JSON, jadi ini mungkin merupakan penyimpanan sementara sebelum data diserap ke bagian lain dari sistem. Misalnya, transaksi Stripe.
Garis Waktu Dukungan JSON di PostgreSQL
Dukungan JSON di PostgreSQL diperkenalkan pada 9.2 dan terus meningkat di setiap rilis ke depannya.
-
Wave 1:PostgreSQL 9.2 (2012) menambahkan dukungan untuk tipe data JSON
Database JSON di 9.2 cukup terbatas (dan mungkin overhyped pada saat itu) – pada dasarnya string yang dimuliakan dengan beberapa validasi JSON dimasukkan. Ini berguna untuk memvalidasi JSON yang masuk dan menyimpannya di database. Rincian lebih lanjut diberikan di bawah ini.
-
Wave 2:PostgreSQL 9.4 (2014) menambahkan dukungan untuk tipe data JSONB
JSONB adalah singkatan dari "JSON Binary" atau "JSON lebih baik" tergantung pada siapa Anda bertanya. Ini adalah format biner yang didekomposisi untuk menyimpan JSON. JSONB mendukung pengindeksan data JSON, dan sangat efisien dalam mengurai dan menanyakan data JSON. Dalam kebanyakan kasus, saat Anda bekerja dengan JSON di PostgreSQL, Anda harus menggunakan JSONB.
-
Wave 3:PostgreSQL 12 (2019) menambahkan dukungan untuk standar SQL/JSON dan kueri JSONPATH
JSONPath menghadirkan mesin kueri JSON yang andal ke PostgreSQL.
Kapan Anda Harus Menggunakan JSON vs. JSONB?
Dalam kebanyakan kasus, JSONB adalah yang harus Anda gunakan. Namun, ada beberapa kasus tertentu di mana JSON bekerja lebih baik:
- JSON mempertahankan pemformatan asli (alias spasi putih) dan urutan kunci.
- JSON mempertahankan kunci duplikat.
- JSON lebih cepat diserap dibandingkan JSONB – namun, jika Anda melakukan pemrosesan lebih lanjut, JSONB akan lebih cepat.
Misalnya, jika Anda hanya mencerna log JSON dan tidak menanyakannya dengan cara apa pun, JSON mungkin merupakan opsi yang lebih baik untuk Anda. Untuk keperluan blog ini, saat kami merujuk ke dukungan JSON di PostgreSQL, kami akan merujuk ke JSONB ke depannya.
Menggunakan JSONB di PostgreSQL:Cara Efektif Menyimpan &Mengindeks Data JSON di PostgreSQLKlik Untuk TweetPola &Antipola JSONB
Jika PostgreSQL sangat mendukung JSONB, mengapa kita membutuhkan kolom lagi? Mengapa tidak membuat tabel dengan gumpalan JSONB saja dan membuang semua kolom seperti skema di bawah ini:
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
Pada akhirnya, kolom masih merupakan teknik yang paling efisien untuk menangani data Anda. Penyimpanan JSONB memiliki beberapa kelemahan dibandingkan kolom tradisional:
-
PostreSQL tidak menyimpan statistik kolom untuk kolom JSONB
PostgreSQL mengelola statistik tentang distribusi nilai di setiap kolom tabel - nilai paling umum (MCV), entri NULL, histogram distribusi. Berdasarkan data ini, perencana kueri PostgreSQL membuat keputusan cerdas tentang rencana yang akan digunakan untuk kueri. Pada titik ini, PostgreSQL tidak menyimpan statistik apa pun untuk kolom atau kunci JSONB. Hal ini terkadang dapat menghasilkan pilihan yang buruk seperti menggunakan gabungan loop bersarang vs. gabungan hash, dll. Contoh yang lebih mendetail tentang hal ini diberikan dalam posting blog ini – Kapan Menghindari JSONB Dalam Skema PostgreSQL.
-
Penyimpanan JSONB menghasilkan jejak penyimpanan yang lebih besar
Penyimpanan JSONB tidak menghapus duplikat nama kunci di JSON. Ini dapat menghasilkan jejak penyimpanan yang jauh lebih besar dibandingkan dengan MongoDB BSON pada WiredTiger atau penyimpanan kolom tradisional. Saya menjalankan tes sederhana dengan model JSONB di bawah ini yang menyimpan sekitar 10 juta baris data, dan inilah hasilnya – Dalam beberapa hal ini mirip dengan model penyimpanan MongoDB MMAPV1 di mana kunci dalam JSONB disimpan apa adanya tanpa kompresi apa pun. Satu perbaikan jangka panjang adalah memindahkan nama kunci ke kamus tingkat tabel dan merujuk kamus ini alih-alih menyimpan nama kunci berulang kali. Sampai saat itu, solusinya mungkin menggunakan nama yang lebih ringkas (gaya unix) daripada nama yang lebih deskriptif. Misalnya, jika Anda menyimpan jutaan instance kunci tertentu, sebaiknya beri nama "pb" daripada "publisherName".
Cara paling efisien untuk memanfaatkan JSONB di PostgreSQL adalah dengan menggabungkan kolom dan JSONB. Jika kunci sangat sering muncul di blob JSONB Anda, mungkin lebih baik disimpan sebagai kolom. Gunakan JSONB sebagai "tangkap semua" untuk menangani bagian variabel skema Anda sambil memanfaatkan kolom tradisional untuk bidang yang lebih stabil.
Struktur Data JSONB
Baik JSONB dan MongoDB BSON pada dasarnya adalah struktur pohon, menggunakan node multi-level untuk menyimpan data JSONB yang diurai. MongoDB BSON memiliki struktur yang sangat mirip.
Sumber gambar
JSONB &TOAST
Pertimbangan penting lainnya untuk penyimpanan adalah bagaimana JSONB berinteraksi dengan TOAST (The Oversize Attribute Storage Technique). Biasanya, ketika ukuran kolom Anda melebihi TOAST_TUPLE_THRESHOLD (default 2kb), PostgreSQL akan mencoba mengompresi data dan memuat 2kb. Jika itu tidak berhasil, data dipindahkan ke penyimpanan out-of-line. Inilah yang mereka sebut “MENGORONG” data. Saat data diambil, proses kebalikan "deTOASTting" perlu terjadi. Anda juga dapat mengontrol strategi penyimpanan TOAST:
- Diperpanjang – Memungkinkan penyimpanan dan kompresi out-of-line (menggunakan pglz). Ini adalah opsi default.
- Eksternal – Memungkinkan penyimpanan out-of-line, tetapi tidak kompresi.
Jika Anda mengalami penundaan karena kompresi atau dekompresi TOAST, salah satu opsinya adalah secara proaktif menyetel penyimpanan kolom ke 'DIPERPANJANG'. Untuk semua detailnya, lihat dokumen PostgreSQL ini.
Operator &Fungsi JSONB
PostgreSQL menyediakan berbagai operator untuk bekerja di JSONB. Dari dokumen:
Operator | Deskripsi |
---|---|
-> | Dapatkan elemen larik JSON (diindeks dari nol, bilangan bulat negatif dihitung dari akhir) |
-> | Dapatkan bidang objek JSON dengan kunci |
->> | Dapatkan elemen larik JSON sebagai teks |
->> | Dapatkan bidang objek JSON sebagai teks |
#> | Dapatkan objek JSON di jalur yang ditentukan |
#>> | Dapatkan objek JSON pada jalur yang ditentukan sebagai teks |
@> | Apakah nilai JSON kiri berisi entri jalur/nilai JSON kanan di tingkat atas? |
<@ | Apakah entri jalur/nilai JSON kiri terdapat di tingkat atas dalam nilai JSON kanan? |
? | Apakah string ada sebagai kunci tingkat atas dalam nilai JSON? |
?| | Lakukan salah satu dari larik ini string ada sebagai kunci tingkat atas? |
?& | Lakukan semua larik ini string ada sebagai kunci tingkat atas? |
|| | Gabungkan dua nilai jsonb menjadi nilai jsonb baru |
- | Hapus pasangan kunci/nilai atau string elemen dari operan kiri. Pasangan kunci/nilai dicocokkan berdasarkan nilai kuncinya. |
- | Hapus beberapa pasangan kunci/nilai atau string elemen dari operan kiri. Pasangan kunci/nilai dicocokkan berdasarkan nilai kuncinya. |
- | Hapus elemen larik dengan indeks tertentu (bilangan bulat negatif dihitung dari akhir). Melempar kesalahan jika penampung tingkat atas bukan larik. |
#- | Hapus bidang atau elemen dengan jalur yang ditentukan (untuk array JSON, bilangan bulat negatif dihitung dari akhir) |
@? | Apakah jalur JSON mengembalikan item apa pun untuk nilai JSON yang ditentukan? |
@@ | Mengembalikan hasil pemeriksaan predikat jalur JSON untuk nilai JSON yang ditentukan. Hanya item pertama dari hasil yang diperhitungkan. Jika hasilnya bukan Boolean, maka null dikembalikan. |
PostgreSQL juga menyediakan berbagai Fungsi Pembuatan dan Fungsi Pemrosesan untuk bekerja dengan data JSONB.
Indeks JSONB
JSONB menyediakan beragam opsi untuk mengindeks data JSON Anda. Pada tingkat tinggi, kita akan menggali 3 jenis indeks yang berbeda – GIN, BTREE dan HASH. Tidak semua jenis indeks mendukung semua kelas operator, jadi diperlukan perencanaan untuk mendesain indeks Anda berdasarkan jenis operator dan kueri yang Anda rencanakan untuk digunakan.
Indeks GIN
GIN adalah singkatan dari "Generalized Inverted indexes". Dari dokumen:
“GIN dirancang untuk menangani kasus di mana item yang akan diindeks adalah nilai komposit, dan kueri yang akan ditangani oleh indeks perlu mencari elemen nilai yang muncul dalam item komposit. Misalnya, item dapat berupa dokumen, dan kueri dapat berupa penelusuran dokumen yang berisi kata-kata tertentu.”
GIN mendukung dua kelas operator:
- jsonb_ops (default) – ?, ?|, ?&, @>, @@, @? [Indeks setiap kunci dan nilai dalam elemen JSONB]
- jsonb_pathops – @>, @@, @? [Hanya indeks nilai dalam elemen JSONB]
CREATE INDEX datagin ON books USING gin (data);
Operator Keberadaan (?, ?|, ?&)
Operator ini dapat digunakan untuk memeriksa keberadaan kunci tingkat atas di JSONB. Mari kita buat indeks GIN pada kolom data JSONB. Misalnya, temukan semua buku yang tersedia dalam huruf braille. JSON terlihat seperti ini:
"{"tags": {"nk594127": {"ik71786": "iv678771"}}, "braille": false, "keywords": ["abc", "kef", "keh"], "hardcover": true, "publisher": "EfgdxUdvB0", "criticrating": 1}
demo=# select * from books where data ? 'braille'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} ..... demo=# explain analyze select * from books where data ? 'braille'; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) (actual time=0.033..0.039 rows=15 loops=1) Recheck Cond: (data ? 'braille'::text) Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.022..0.022 rows=15 loops=1) Index Cond: (data ? 'braille'::text) Planning Time: 0.102 ms Execution Time: 0.067 ms (7 rows)
Seperti yang Anda lihat dari output explain, indeks GIN yang kita buat sedang digunakan untuk pencarian. Bagaimana jika kita ingin mencari buku yang braille atau hardcover?
demo=# explain analyze select * from books where data ?| array['braille','hardcover']; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.029..0.035 rows=15 loops=1) Recheck Cond: (data ?| '{braille,hardcover}'::text[]) Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.023..0.023 rows=15 loops=1) Index Cond: (data ?| '{braille,hardcover}'::text[]) Planning Time: 0.138 ms Execution Time: 0.057 ms (7 rows)
Indeks GIN mendukung operator "keberadaan" hanya pada kunci "tingkat atas". Jika kunci tidak berada di level atas, maka indeks tidak akan digunakan. Ini akan menghasilkan pemindaian berurutan:
demo=# select * from books where data->'tags' ? 'nk455671'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} 685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0} (2 rows) demo=# explain analyze select * from books where data->'tags' ? 'nk455671'; QUERY PLAN ---------------------------------------------------------------------------------------------------------- Seq Scan on books (cost=0.00..38807.29 rows=1000 width=158) (actual time=0.018..270.641 rows=2 loops=1) Filter: ((data -> 'tags'::text) ? 'nk455671'::text) Rows Removed by Filter: 1000017 Planning Time: 0.078 ms Execution Time: 270.728 ms (5 rows)
Cara memeriksa keberadaan di dokumen bersarang adalah dengan menggunakan "indeks ekspresi". Mari kita buat indeks pada data->tags:
CREATE INDEX datatagsgin ON books USING gin (data->'tags');
demo=# select * from books where data->'tags' ? 'nk455671'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} 685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0} (2 rows) demo=# explain analyze select * from books where data->'tags' ? 'nk455671'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------ Bitmap Heap Scan on books (cost=12.75..1007.75 rows=1000 width=158) (actual time=0.031..0.035 rows=2 loops=1) Recheck Cond: ((data ->'tags'::text) ? 'nk455671'::text) Heap Blocks: exact=2 -> Bitmap Index Scan on datatagsgin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.021..0.021 rows=2 loops=1) Index Cond: ((data ->'tags'::text) ? 'nk455671'::text) Planning Time: 0.098 ms Execution Time: 0.061 ms (7 rows)
Catatan:Alternatif di sini adalah menggunakan operator @>:
select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
Namun, ini hanya berfungsi jika nilainya adalah objek. Jadi, jika Anda tidak yakin apakah nilainya adalah objek atau nilai primitif, ini dapat menyebabkan hasil yang salah.
Operator Jalur @>, <@
Operator “path” dapat digunakan untuk kueri multi-level data JSONB Anda. Mari kita gunakan mirip dengan ? operator di atas:
select * from books where data @> '{"braille":true}'::jsonb; demo=# explain analyze select * from books where data @> '{"braille":true}'::jsonb; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.040..0.048 rows=6 loops=1) Recheck Cond: (data @> '{"braille": true}'::jsonb) Rows Removed by Index Recheck: 9 Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.030..0.030 rows=15 loops=1) Index Cond: (data @> '{"braille": true}'::jsonb) Planning Time: 0.100 ms Execution Time: 0.076 ms (8 rows)
Operator jalur mendukung kueri objek bertingkat atau objek tingkat atas:
demo=# select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb; id | author | isbn | rating | data -----+-----------------+------------+--------+------------------------------------------------------------------------------------- 346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3} (1 row) demo=# explain analyze select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.491..0.492 rows=1 loops=1) Recheck Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb) Heap Blocks: exact=1 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.092..0.092 rows=1 loops=1) Index Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb) Planning Time: 0.090 ms Execution Time: 0.523 ms
Kueri juga bisa multi-level:
demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} (1 row)
GIN Indeks Kelas Operator “pathops”
GIN juga mendukung opsi "pathops" untuk mengurangi ukuran indeks GIN. Saat Anda menggunakan opsi pathops, satu-satunya dukungan operator adalah “@>” jadi Anda harus berhati-hati dengan pertanyaan Anda. Dari dokumen:
“Perbedaan teknis antara jsonb_ops dan jsonb_path_ops indeks GIN adalah bahwa yang pertama membuat item indeks independen untuk setiap kunci dan nilai dalam data, sedangkan yang terakhir membuat item indeks hanya untuk setiap nilai dalam data”
Anda dapat membuat indeks pathops GIN sebagai berikut:
CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
Pada kumpulan data kecil saya yang terdiri dari 1 juta buku, Anda dapat melihat bahwa indeks pathops GIN lebih kecil – Anda harus menguji dengan kumpulan data Anda untuk memahami penghematannya:
public | dataginpathops | index | sgpostgres | books | 67 MB | public | datatagsgin | index | sgpostgres | books | 84 MB |
Mari kita jalankan kembali kueri kita sebelumnya dengan indeks pathops:
demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} (1 row) demo=# explain select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; QUERY PLAN ----------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) Recheck Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb) -> Bitmap Index Scan on dataginpathops (cost=0.00..12.50 rows=1000 width=0) Index Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb) (4 rows)
Namun, seperti yang disebutkan di atas, opsi “pathops” tidak mendukung semua skenario yang didukung oleh kelas operator default. Dengan indeks GIN "pathops", semua kueri ini tidak dapat memanfaatkan indeks GIN. Singkatnya, Anda memiliki indeks yang lebih kecil tetapi mendukung kasus penggunaan yang lebih terbatas.
select * from books where data ? 'tags'; => Sequential scan select * from books where data @> '{"tags" :{}}'; => Sequential scan select * from books where data @> '{"tags" :{"k7888":{}}}' => Sequential scan
Indeks B-Tree
Indeks B-tree adalah jenis indeks yang paling umum dalam database relasional. Namun, jika Anda mengindeks seluruh kolom JSONB dengan indeks B-tree, satu-satunya operator yang berguna adalah “=”, <, <=,>,>=. Pada dasarnya, ini hanya dapat digunakan untuk perbandingan seluruh objek, yang memiliki kasus penggunaan yang sangat terbatas.
Skenario yang lebih umum adalah menggunakan "indeks ekspresi" pohon-B. Untuk primer, lihat di sini – Indeks pada Ekspresi. Indeks ekspresi B-tree dapat mendukung operator perbandingan umum '=', '<', '>', '>=', '<='. Seperti yang mungkin Anda ingat, indeks GIN tidak mendukung operator ini. Mari kita pertimbangkan kasus ketika kita ingin mengambil semua buku dengan data->kritik> 4. Jadi, Anda akan membuat kueri seperti ini:
demo=# select * from books where data->'criticrating' > 4; ERROR: operator does not exist: jsonb >= integer LINE 1: select * from books where data->'criticrating' >= 4; ^ HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
Ya, itu tidak berfungsi karena operator ‘->’ mengembalikan jenis JSONB. Jadi kita perlu menggunakan sesuatu seperti ini:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Jika Anda menggunakan versi sebelum PostgreSQL 11, itu akan menjadi lebih buruk. Anda harus terlebih dahulu melakukan kueri sebagai teks dan kemudian mengirimkannya ke bilangan bulat:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Untuk indeks ekspresi, indeks harus sama persis dengan ekspresi kueri. Jadi, indeks kita akan terlihat seperti ini:
demo=# CREATE INDEX criticrating ON books USING BTREE (((data->'criticrating')::int4)); CREATE INDEX demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1) Index Cond: (((data -> 'criticrating'::text))::integer = 3) Planning Time: 0.103 ms Execution Time: 79.019 ms (4 rows) demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1) Index Cond: (((data -> 'criticrating'::text))::integer = 3) Planning Time: 0.103 ms Execution Time: 79.019 ms (4 rows) 1 From above we can see that the BTREE index is being used as expected.
Indeks Hash
Jika Anda hanya tertarik pada operator "=", maka indeks Hash menjadi menarik. Misalnya, pertimbangkan kasus ketika kita mencari tag tertentu pada sebuah buku. Elemen yang akan diindeks dapat berupa elemen tingkat atas atau sangat bersarang.
Misalnya. tag->penerbit =XlekfkLOtL
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Indeks hash juga cenderung berukuran lebih kecil daripada indeks B-tree atau GIN. Tentu saja, ini pada akhirnya tergantung pada kumpulan data Anda.
demo=# select * from books where data->'publisher' = 'XlekfkLOtL' demo-# ; id | author | isbn | rating | data -----+-----------------+------------+--------+------------------------------------------------------------------------------------- 346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3} (1 row) demo=# explain analyze select * from books where data->'publisher' = 'XlekfkLOtL'; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------- Index Scan using publisherhash on books (cost=0.00..2.02 rows=1 width=158) (actual time=0.016..0.017 rows=1 loops=1) Index Cond: ((data -> 'publisher'::text) = 'XlekfkLOtL'::text) Planning Time: 0.080 ms Execution Time: 0.035 ms (4 rows)
Special Mention:GIN Trigram Indexes
PostgreSQL supports string matching using trigram indexes. Trigram indexes work by breaking up text into trigrams. Trigrams are basically words broken up into sequences of 3 letters. More information can be found in the documentation. GIN indexes support the “gin_trgm_ops” class that can be used to index the data in JSONB. You can choose to use expression indexes to build the trigram index on a particular column.
CREATE EXTENSION pg_trgm; CREATE INDEX publisher ON books USING GIN ((data->'publisher') gin_trgm_ops); demo=# select * from books where data->'publisher' LIKE '%I0UB%'; id | author | isbn | rating | data ----+-----------------+------------+--------+--------------------------------------------------------------------------------- 4 | KiEk3xjqvTpmZeS | EYqXO9Nwmm | 0 | {"tags": {"nk3": {"ik1": "iv1"}}, "publisher": "MI0UBqZJDt", "criticrating": 1} (1 row)
As you can see in the query above, we can search for any arbitrary string occurring at any potion. Unlike the B-tree indexes, we are not restricted to left anchored expressions.
demo=# explain analyze select * from books where data->'publisher' LIKE '%I0UB%'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=9.78..111.28 rows=100 width=158) (actual time=0.033..0.033 rows=1 loops=1) Recheck Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text) Heap Blocks: exact=1 -> Bitmap Index Scan on publisher (cost=0.00..9.75 rows=100 width=0) (actual time=0.025..0.025 rows=1 loops=1) Index Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text) Planning Time: 0.213 ms Execution Time: 0.058 ms (7 rows)
Special Mention:GIN Array Indexes
JSONB has great built-in support for indexing arrays. Let's consider an example of indexing an array of strings using a GIN index in the case when our JSONB data contains a "keyword" element and we would like to find rows with particular keywords:
{"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2} CREATE INDEX keywords ON books USING GIN ((data->'keywords') jsonb_path_ops); demo=# select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------- 1000003 | zEG406sLKQ2IU8O | viPdlu3DZm | 4 | {"tags": {"nk263020": {"ik203820": "iv817928"}}, "keywords": ["abc", "kef", "keh"], "publisher": "7NClevxuTM", "criticrating": 2} 1000004 | GCe9NypHYKDH4rD | so6TQDYzZ3 | 4 | {"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2} (2 rows) demo=# explain analyze select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=54.75..1049.75 rows=1000 width=158) (actual time=0.026..0.028 rows=2 loops=1) Recheck Cond: ((data -> 'keywords'::text) @> '["abc", "keh"]'::jsonb) Heap Blocks: exact=1 -> Bitmap Index Scan on keywords (cost=0.00..54.50 rows=1000 width=0) (actual time=0.014..0.014 rows=2 loops=1) Index Cond: ((data -> 'keywords'::text) @&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; '["abc", "keh"]'::jsonb) Planning Time: 0.131 ms Execution Time: 0.063 ms (7 rows)
The order of the items in the array on the right does not matter. For example, the following query would return the same result as the previous:
demo=# explain analyze select * from books where data->'keywords' @> '["keh","abc"]'::jsonb;
All elements in the right side array of the containment operator need to be present - basically like an "AND" operator. If you want "OR" behavior, you can construct it in the WHERE clause:
demo=# explain analyze select * from books where (data->'keywords' @> '["abc"]'::jsonb OR data->'keywords' @> '["keh"]'::jsonb);
More details on the behavior of the containment operators with arrays can be found in the documentation.
SQL/JSON &JSONPath
SQL standard added support for JSON in SQL - SQL/JSON Standard-2016. With the PostgreSQL 12/13 releases, PostgreSQL has one of the best implementations of the SQL/JSON standard. For more details refer to the PostgreSQL 12 announcement.
One of the core features of SQL/JSON is support for the JSONPath language to query JSONB data. JSONPath allows you to specify an expression (using a syntax similar to the property access notation in Javascript) to query your JSONB data. This makes it simple and intuitive, but is also very powerful to query your JSONB data. Think of JSONPath as the logical equivalent of XPath for XML.
.key | Returns an object member with the specified key. |
[*] | Wildcard array element accessor that returns all array elements. |
.* | Wildcard member accessor that returns the values of all members located at the top level of the current object. |
.** | Recursive wildcard member accessor that processes all levels of the JSON hierarchy of the current object and returns all the member values, regardless of their nesting level. |
Refer to JSONPath documentation for the full list of operators. JSONPath also supports a variety of filter expressions.
JSONPath Functions
PostgreSQL 12 provides several functions to use JSONPath to query your JSONB data. From the docs:
- jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON value.
- jsonb_path_match - Returns the result of JSONB path predicate check for the specified JSONB value. Only the first item of the result is taken into account. If the result is not Boolean, then null is returned.
- jsonb_path_query - Gets all JSONB items returned by JSONB path for the specified JSONB value. There are also a couple of other variants of this function that handle arrays of objects.
Let's start with a simple query - finding books by publisher:
demo=# select * from books where data @@ '$.publisher == "ktjKEZ1tvq"'; id | author | isbn | rating | data ---------+-----------------+------------+--------+---------------------------------------------------------------------------------------------------------------------------------- 1000001 | 4RNsovI2haTgU7l | GwSoX67gLS | 2 | {"tags": {"nk542369": {"ik55240": "iv305393"}}, "keywords": ["abc", "def", "geh"], "publisher": "ktjKEZ1tvq", "criticrating": 0} (1 row) demo=# explain analyze select * from books where data @@ '$.publisher == "ktjKEZ1tvq"'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=21.75..1014.25 rows=1000 width=158) (actual time=0.123..0.124 rows=1 loops=1) Recheck Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath) Heap Blocks: exact=1 -> Bitmap Index Scan on datagin (cost=0.00..21.50 rows=1000 width=0) (actual time=0.110..0.110 rows=1 loops=1) Index Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath) Planning Time: 0.137 ms Execution Time: 0.194 ms (7 rows)
You can rewrite this expression as a JSONPath filter:
demo=# select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');
You can also use very complex query expressions. For example, let's select books where print style =hardcover and price =100:
select * from books where jsonb_path_exists(data, '$.prints[*] ?(@.style=="hc" &amp;amp;amp;amp;&amp;amp;amp;amp; @.price == 100)');
However, index support for JSONPath is very limited at this point - this makes it dangerous to use JSONPath in the where clause. JSONPath support for indexes will be improved in subsequent releases.
demo=# explain analyze select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")'); QUERY PLAN ------------------------------------------------------------------------------------------------------------ Seq Scan on books (cost=0.00..36307.24 rows=333340 width=158) (actual time=0.019..480.268 rows=1 loops=1) Filter: jsonb_path_exists(data, '$."publisher"?(@ == "ktjKEZ1tvq")'::jsonpath, '{}'::jsonb, false) Rows Removed by Filter: 1000028 Planning Time: 0.095 ms Execution Time: 480.348 ms (5 rows)
Projecting Partial JSON
Another great use case for JSONPath is projecting partial JSONB from the row that matches. Consider the following sample JSONB:
demo=# select jsonb_pretty(data) from books where id = 1000029; jsonb_pretty ----------------------------------- { "tags": { "nk678947": { "ik159670": "iv32358 } }, "prints": [ { "price": 100, "style": "hc" }, { "price": 50, "style": "pb" } ], "braille": false, "keywords": [ "abc", "kef", "keh" ], "hardcover": true, "publisher": "ppc3YXL8kK", "criticrating": 3 }
Select only the publisher field:
demo=# select jsonb_path_query(data, '$.publisher') from books where id = 1000029; jsonb_path_query ------------------ "ppc3YXL8kK" (1 row)
Select the prints field (which is an array of objects):
demo=# select jsonb_path_query(data, '$.prints') from books where id = 1000029; jsonb_path_query --------------------------------------------------------------- [{"price": 100, "style": "hc"}, {"price": 50, "style": "pb"}] (1 row)
Select the first element in the array prints:
demo=# select jsonb_path_query(data, '$.prints[0]') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
Select the last element in the array prints:
demo=# select jsonb_path_query(data, '$.prints[$.size()]') from books where id = 1000029; jsonb_path_query ------------------------------ {"price": 50, "style": "pb"} (1 row)
Select only the hardcover prints from the array:
demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc")') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
We can also chain the filters:
demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc") ?(@.price ==100)') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
In summary, PostgreSQL provides a powerful and versatile platform to store and process JSON data. There are several gotcha's that you need to be aware of, but we are optimistic that it will be fixed in future releases.
|