JSON là viết tắt của JavaScript Object Notation. Nó là một định dạng tiêu chuẩn mở tổ chức dữ liệu thành các cặp khóa / giá trị và các mảng được nêu chi tiết trong RFC 7159. JSON là định dạng phổ biến nhất được các dịch vụ web sử dụng để trao đổi dữ liệu, lưu trữ tài liệu, dữ liệu phi cấu trúc, v.v. Trong bài đăng này, chúng ta sẽ để chỉ cho bạn các mẹo và kỹ thuật về cách lưu trữ và lập chỉ mục dữ liệu JSON trong PostgreSQL một cách hiệu quả.
Bạn cũng có thể xem Hội thảo trên web Làm việc với JSON trong PostgreSQL và MongoDB của chúng tôi với sự hợp tác của PostgresConf để tìm hiểu thêm về chủ đề và xem trang SlideShare của chúng tôi để tải xuống các trang trình bày.
Tại sao nên lưu trữ JSON trong PostgreSQL?
Tại sao cơ sở dữ liệu quan hệ thậm chí phải quan tâm đến dữ liệu phi cấu trúc? Nó chỉ ra rằng có một số tình huống hữu ích.
-
Tính linh hoạt của giản đồ
Một trong những lý do chính để lưu trữ dữ liệu bằng định dạng JSON là tính linh hoạt của lược đồ. Lưu trữ dữ liệu của bạn trong JSON rất hữu ích khi giản đồ của bạn linh hoạt và thường xuyên thay đổi. Nếu bạn lưu trữ từng khóa dưới dạng cột, điều này sẽ dẫn đến các hoạt động DML thường xuyên - điều này có thể khó khăn khi tập dữ liệu của bạn lớn - ví dụ:theo dõi sự kiện, phân tích, thẻ, v.v. Lưu ý:Nếu một khóa cụ thể luôn hiện diện trong tài liệu của bạn, nên lưu trữ nó dưới dạng cột hạng nhất. Chúng ta sẽ thảo luận thêm về cách tiếp cận này trong phần “Mẫu JSON và phản vật chất” bên dưới.
-
Đối tượng lồng nhau
Nếu tập dữ liệu của bạn có các đối tượng lồng nhau (đơn hoặc nhiều cấp), trong một số trường hợp, việc xử lý chúng trong JSON sẽ dễ dàng hơn thay vì bất chuẩn hóa dữ liệu thành các cột hoặc nhiều bảng.
-
Đồng bộ hóa với các nguồn dữ liệu bên ngoài
Thông thường, hệ thống bên ngoài đang cung cấp dữ liệu dưới dạng JSON, vì vậy, nó có thể là nơi lưu trữ tạm thời trước khi dữ liệu được nhập vào các phần khác của hệ thống. Ví dụ:giao dịch sọc.
Dòng thời gian Hỗ trợ JSON trong PostgreSQL
Hỗ trợ JSON trong PostgreSQL đã được giới thiệu vào 9.2 và đã được cải thiện đều đặn trong mọi bản phát hành về sau.
-
Wave 1:PostgreSQL 9.2 (2012) đã thêm hỗ trợ cho kiểu dữ liệu JSON
Cơ sở dữ liệu JSON trong 9.2 khá hạn chế (và có lẽ là quá nhiều vào thời điểm đó) - về cơ bản là một chuỗi được tôn vinh với một số xác thực JSON được đưa vào. Sẽ rất hữu ích khi xác thực JSON đến và lưu trữ trong cơ sở dữ liệu. Thông tin chi tiết được cung cấp bên dưới.
-
Wave 2:PostgreSQL 9.4 (2014) đã thêm hỗ trợ cho kiểu dữ liệu JSONB
JSONB là viết tắt của “JSON Binary” hoặc “JSON better” tùy thuộc vào người bạn yêu cầu. Nó là một định dạng nhị phân phân rã để lưu trữ JSON. JSONB hỗ trợ lập chỉ mục dữ liệu JSON và rất hiệu quả trong việc phân tích cú pháp và truy vấn dữ liệu JSON. Trong hầu hết các trường hợp, khi bạn làm việc với JSON trong PostgreSQL, bạn nên sử dụng JSONB.
-
Wave 3:PostgreSQL 12 (2019) đã thêm hỗ trợ cho các truy vấn chuẩn SQL / JSON và JSONPATH
JSONPath mang đến một công cụ truy vấn JSON mạnh mẽ cho PostgreSQL.
Khi nào bạn nên sử dụng JSON so với JSONB?
Trong hầu hết các trường hợp, JSONB là thứ bạn nên sử dụng. Tuy nhiên, có một số trường hợp cụ thể mà JSON hoạt động tốt hơn:
- JSON giữ nguyên định dạng ban đầu (còn gọi là khoảng trắng) và thứ tự của các phím.
- JSON giữ nguyên các khóa trùng lặp.
- JSON nhập nhanh hơn JSONB - tuy nhiên, nếu bạn xử lý thêm, JSONB sẽ nhanh hơn.
Ví dụ:nếu bạn chỉ nhập nhật ký JSON và không truy vấn chúng theo bất kỳ cách nào, thì JSON có thể là một lựa chọn tốt hơn cho bạn. Vì mục đích của blog này, khi chúng tôi đề cập đến hỗ trợ JSON trong PostgreSQL, chúng tôi sẽ đề cập đến JSONB trong tương lai.
Sử dụng JSONB trong PostgreSQL:Cách lưu trữ &lập chỉ mục hiệu quả Dữ liệu JSON trong PostgreSQLClick To TweetMẫu JSONB &Phản vật chất
Nếu PostgreSQL hỗ trợ tuyệt vời cho JSONB, tại sao chúng ta lại cần đến các cột? Tại sao không chỉ tạo một bảng với JSONB blob và loại bỏ tất cả các cột như lược đồ bên dưới:
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
Vào cuối ngày, cột vẫn là kỹ thuật hiệu quả nhất để xử lý dữ liệu của bạn. Lưu trữ JSONB có một số nhược điểm so với các cột truyền thống:
-
PostreSQL không lưu trữ thống kê cột cho các cột JSONB
PostgreSQL duy trì thống kê về sự phân bố của các giá trị trong mỗi cột của bảng - các giá trị phổ biến nhất (MCV), mục nhập NULL, biểu đồ phân phối. Dựa trên dữ liệu này, người lập kế hoạch truy vấn PostgreSQL đưa ra quyết định thông minh về kế hoạch sử dụng cho truy vấn. Tại thời điểm này, PostgreSQL không lưu trữ bất kỳ số liệu thống kê nào cho các cột hoặc khóa JSONB. Điều này đôi khi có thể dẫn đến các lựa chọn kém như sử dụng các phép nối vòng lặp lồng nhau so với các phép nối băm, v.v. Một ví dụ chi tiết hơn về điều này được cung cấp trong bài đăng trên blog này - Khi nào cần tránh JSONB trong lược đồ PostgreSQL.
-
Bộ nhớ JSONB dẫn đến dung lượng bộ nhớ lớn hơn
Bộ nhớ JSONB không trùng lặp các tên khóa trong JSON. Điều này có thể dẫn đến dấu vết lưu trữ lớn hơn đáng kể so với MongoDB BSON trên WiredTiger hoặc lưu trữ cột truyền thống. Tôi đã chạy một bài kiểm tra đơn giản với mô hình JSONB bên dưới lưu trữ khoảng 10 triệu hàng dữ liệu và đây là kết quả - Theo một số cách, điều này tương tự như mô hình lưu trữ MongoDB MMAPV1 nơi các khóa trong JSONB được lưu trữ nguyên bản mà không cần nén. Một cách khắc phục lâu dài là chuyển các tên khóa sang từ điển cấp bảng và tham khảo từ điển này thay vì lưu trữ các tên khóa lặp đi lặp lại. Cho đến lúc đó, cách giải quyết có thể là sử dụng các tên nhỏ gọn hơn (kiểu unix) thay vì các tên mô tả nhiều hơn. Ví dụ:nếu bạn đang lưu trữ hàng triệu bản sao của một khóa cụ thể, thì tốt hơn hết bạn nên đặt tên cho nó là “pb” thay vì “publisherName”.
Cách hiệu quả nhất để tận dụng JSONB trong PostgreSQL là kết hợp các cột và JSONB. Nếu một khóa xuất hiện rất thường xuyên trong các đốm màu JSONB của bạn, thì có lẽ tốt hơn hết bạn nên lưu trữ khóa đó dưới dạng một cột. Sử dụng JSONB như một "nắm bắt tất cả" để xử lý các phần biến trong lược đồ của bạn trong khi tận dụng các cột truyền thống cho các trường ổn định hơn.
Cấu trúc dữ liệu JSONB
Cả JSONB và MongoDB BSON về cơ bản đều là cấu trúc cây, sử dụng các nút nhiều cấp để lưu trữ dữ liệu JSONB đã được phân tích cú pháp. MongoDB BSON có cấu trúc rất giống nhau.
Nguồn hình ảnh
JSONB &TOAST
Một cân nhắc quan trọng khác về lưu trữ là cách JSONB tương tác với TOAST (Kỹ thuật lưu trữ thuộc tính quá khổ). Thông thường, khi kích thước cột của bạn vượt quá TOAST_TUPLE_THRESHOLD (2kb mặc định), PostgreSQL sẽ cố nén dữ liệu và vừa với 2kb. Nếu cách đó không hiệu quả, dữ liệu sẽ được chuyển sang bộ nhớ ngoại tuyến. Đây là những gì họ gọi là "TOASTing" dữ liệu. Khi dữ liệu được tìm nạp, quy trình ngược lại “deTOASTting” cần phải xảy ra. Bạn cũng có thể kiểm soát chiến lược lưu trữ TOAST:
- Mở rộng - Cho phép lưu trữ và nén ngoài luồng (sử dụng pglz). Đây là tùy chọn mặc định.
- Bên ngoài - Cho phép lưu trữ ngoài luồng, nhưng không cho phép nén.
Nếu bạn đang gặp phải sự chậm trễ do nén hoặc giải nén TOAST, một tùy chọn là chủ động đặt bộ nhớ cột thành 'EXTENDED'. Để biết tất cả các chi tiết, vui lòng tham khảo tài liệu PostgreSQL này.
Các hàm &Toán tử JSONB
PostgreSQL cung cấp nhiều toán tử khác nhau để hoạt động trên JSONB. Từ tài liệu:
Toán tử | Mô tả |
---|---|
-> | Nhận phần tử mảng JSON (được lập chỉ mục từ 0, số nguyên âm tính từ cuối) |
-> | Lấy trường đối tượng JSON bằng khóa |
->> | Nhận phần tử mảng JSON dưới dạng văn bản |
->> | Nhận trường đối tượng JSON dưới dạng văn bản |
#> | Nhận đối tượng JSON tại đường dẫn được chỉ định |
#>> | Nhận đối tượng JSON tại đường dẫn được chỉ định dưới dạng văn bản |
@> | Giá trị JSON bên trái có chứa các mục nhập giá trị / đường dẫn JSON bên phải ở cấp cao nhất không? |
<@ | Các mục nhập giá trị / đường dẫn JSON bên trái có nằm ở cấp cao nhất trong giá trị JSON bên phải không? |
? | string tồn tại dưới dạng khóa cấp cao nhất trong giá trị JSON? |
? | | Thực hiện bất kỳ mảng nào trong số chuỗi này tồn tại dưới dạng khóa cấp cao nhất? |
? & | Thực hiện tất cả những chuỗi này tồn tại dưới dạng khóa cấp cao nhất? |
|| | Ghép hai giá trị jsonb thành một giá trị jsonb mới |
- | Xóa cặp khóa / giá trị hoặc chuỗi phần tử từ toán hạng bên trái. Các cặp khóa / giá trị được so khớp dựa trên giá trị khóa của chúng. |
- | Xóa nhiều cặp khóa / giá trị hoặc chuỗi các phần tử từ toán hạng bên trái. Các cặp khóa / giá trị được so khớp dựa trên giá trị khóa của chúng. |
- | Xóa phần tử mảng có chỉ mục được chỉ định (Tính từ cuối số nguyên âm). Ném lỗi nếu vùng chứa cấp cao nhất không phải là một mảng. |
# - | Xóa trường hoặc phần tử có đường dẫn được chỉ định (đối với mảng JSON, tính từ cuối là số nguyên âm) |
@? | Đường dẫn JSON có trả về bất kỳ mục nào cho giá trị JSON được chỉ định không? |
@@ | Trả về kết quả kiểm tra vị từ đường dẫn JSON cho giá trị JSON được chỉ định. Chỉ mục đầu tiên của kết quả được tính đến. Nếu kết quả không phải là Boolean thì trả về null. |
PostgreSQL cũng cung cấp nhiều hàm tạo và hàm xử lý để hoạt động với dữ liệu JSONB.
Chỉ mục JSONB
JSONB cung cấp nhiều tùy chọn để lập chỉ mục dữ liệu JSON của bạn. Ở cấp độ cao, chúng ta sẽ đi sâu vào 3 loại chỉ số khác nhau - GIN, BTREE và HASH. Không phải tất cả các loại chỉ mục đều hỗ trợ tất cả các lớp toán tử, vì vậy cần lập kế hoạch để thiết kế các chỉ mục của bạn dựa trên loại toán tử và truy vấn mà bạn định sử dụng.
Chỉ mục GIN
GIN là viết tắt của "Chỉ mục đảo ngược tổng quát". Từ tài liệu:
“GIN được thiết kế để xử lý các trường hợp trong đó các mục được lập chỉ mục là các giá trị tổng hợp và các truy vấn được chỉ mục xử lý cần tìm kiếm phần tử giá trị xuất hiện trong các mục tổng hợp. Ví dụ:các mục có thể là tài liệu và các truy vấn có thể là tìm kiếm tài liệu có chứa các từ cụ thể. ”
GIN hỗ trợ hai lớp toán tử:
- jsonb_ops (mặc định) -?,? |,? &, @>, @@, @? [Lập chỉ mục từng khóa và giá trị trong phần tử JSONB]
- jsonb_pathops - @>, @@, @? [Chỉ lập chỉ mục các giá trị trong phần tử JSONB]
CREATE INDEX datagin ON books USING gin (data);
Toán tử tồn tại (?,? |,? &)
Các toán tử này có thể được sử dụng để kiểm tra sự tồn tại của các khóa cấp cao nhất trong JSONB. Hãy tạo chỉ mục GIN trên cột JSONB dữ liệu. Ví dụ:tìm tất cả các sách có sẵn bằng chữ nổi. JSON trông giống như sau:
"{"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)
Như bạn có thể thấy từ kết quả giải thích, chỉ mục GIN mà chúng tôi đã tạo đang được sử dụng cho tìm kiếm. Điều gì sẽ xảy ra nếu chúng tôi muốn tìm những cuốn sách bằng chữ nổi hoặc bằng bìa cứng?
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)
Chỉ mục GIN chỉ hỗ trợ toán tử “sự tồn tại” trên các khóa “cấp cao nhất”. Nếu khóa không ở cấp cao nhất, thì chỉ mục sẽ không được sử dụng. Nó sẽ dẫn đến một quá trình quét tuần tự:
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)
Cách để kiểm tra sự tồn tại trong các tài liệu lồng nhau là sử dụng “chỉ mục biểu thức”. Hãy tạo một chỉ mục trên các thẻ data->:
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)
Lưu ý:Một giải pháp thay thế ở đây là sử dụng toán tử @>:
select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
Tuy nhiên, điều này chỉ hoạt động nếu giá trị là một đối tượng. Vì vậy, nếu bạn không chắc giá trị là một đối tượng hay một giá trị nguyên thủy, điều đó có thể dẫn đến kết quả không chính xác.
Toán tử đường dẫn @>, <@
Toán tử “đường dẫn” có thể được sử dụng cho các truy vấn nhiều cấp về dữ liệu JSONB của bạn. Hãy sử dụng nó tương tự như? toán tử trên:
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)
Toán tử đường dẫn hỗ trợ truy vấn các đối tượng lồng nhau hoặc các đối tượng cấp cao nhất:
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
Các truy vấn cũng có thể là nhiều cấp:
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)
Chỉ số GIN Lớp toán tử “disease”
GIN cũng hỗ trợ tùy chọn "bệnh lý" để giảm kích thước của chỉ mục GIN. Khi bạn sử dụng tùy chọn bệnh lý, hỗ trợ nhà điều hành duy nhất là “@>”, vì vậy bạn cần phải cẩn thận với các truy vấn của mình. Từ tài liệu:
“Sự khác biệt về mặt kỹ thuật giữa chỉ mục jsonb_ops và jsonb_path_ops GIN là chỉ mục trước tạo các mục chỉ mục độc lập cho từng khóa và giá trị trong dữ liệu, trong khi chỉ mục sau chỉ tạo các mục chỉ mục cho mỗi giá trị trong dữ liệu ”
Bạn có thể tạo chỉ mục bệnh lý GIN như sau:
CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
Trên tập dữ liệu nhỏ gồm 1 triệu cuốn sách của tôi, bạn có thể thấy rằng chỉ số GIN bệnh tật nhỏ hơn - bạn nên kiểm tra với tập dữ liệu của mình để hiểu được khoản tiết kiệm:
public | dataginpathops | index | sgpostgres | books | 67 MB | public | datatagsgin | index | sgpostgres | books | 84 MB |
Hãy chạy lại truy vấn của chúng ta từ trước với chỉ mục bệnh lý:
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)
Tuy nhiên, như đã đề cập ở trên, tùy chọn “disease” không hỗ trợ tất cả các trường hợp mà lớp toán tử mặc định hỗ trợ. Với chỉ số GIN “bệnh lý”, tất cả các truy vấn này không thể tận dụng chỉ số GIN. Tóm lại, bạn có một chỉ mục nhỏ hơn nhưng nó hỗ trợ một trường hợp sử dụng hạn chế hơn.
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
Chỉ mục B-Tree
Chỉ mục B-tree là loại chỉ mục phổ biến nhất trong cơ sở dữ liệu quan hệ. Tuy nhiên, nếu bạn lập chỉ mục toàn bộ cột JSONB với chỉ mục B-tree, thì các toán tử hữu ích duy nhất là “=”, <, <=,>,> =. Về cơ bản, điều này chỉ có thể được sử dụng để so sánh toàn bộ đối tượng, điều này có một trường hợp sử dụng rất hạn chế.
Một tình huống phổ biến hơn là sử dụng "chỉ mục biểu thức" B-tree. Để biết sơ bộ, hãy tham khảo tại đây - Chỉ mục về Biểu thức. Các chỉ mục biểu thức cây B có thể hỗ trợ các toán tử so sánh phổ biến ‘=’, ‘<’, ‘>’, ‘> =’, ‘<=’. Như bạn có thể nhớ lại, chỉ mục GIN không hỗ trợ các toán tử này. Hãy xem xét trường hợp chúng ta muốn truy xuất tất cả sách có dữ liệu-> phê bình> 4. Vì vậy, bạn sẽ tạo một truy vấn như sau:
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.
Chà, điều đó không hoạt động vì toán tử ‘->’ trả về loại JSONB. Vì vậy, chúng ta cần sử dụng một cái gì đó như thế này:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Nếu bạn đang sử dụng phiên bản trước PostgreSQL 11, phiên bản này trở nên xấu hơn. Trước tiên, bạn cần truy vấn dưới dạng văn bản và sau đó truyền nó thành số nguyên:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Đối với chỉ mục biểu thức, chỉ mục cần phải khớp chính xác với biểu thức truy vấn. Vì vậy, chỉ mục của chúng tôi sẽ trông giống như sau:
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.
Chỉ mục băm
Nếu bạn chỉ quan tâm đến toán tử "=", thì các chỉ mục băm trở nên thú vị. Ví dụ:hãy xem xét trường hợp khi chúng ta đang tìm kiếm một thẻ cụ thể trên một cuốn sách. Phần tử được lập chỉ mục có thể là phần tử cấp cao nhất hoặc được lồng sâu vào nhau.
Ví dụ:tags-> nhà xuất bản =XlekfkLOtL
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Chỉ mục băm cũng có xu hướng có kích thước nhỏ hơn so với chỉ mục B-tree hoặc GIN. Tất nhiên, điều này cuối cùng phụ thuộc vào tập dữ liệu của bạn.
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)
Lưu ý Đặc biệt:Chỉ số GIN Trigram
PostgreSQL hỗ trợ so khớp chuỗi bằng cách sử dụng các chỉ mục trigram. Chỉ mục bát quái hoạt động bằng cách chia nhỏ văn bản thành bát quái. Bát quái về cơ bản là các từ được chia thành chuỗi 3 chữ cái. 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.
|