TL; DR: JSONB có thể đơn giản hóa rất nhiều việc phát triển lược đồ cơ sở dữ liệu mà không làm giảm hiệu suất truy vấn.
Giới thiệu
Hãy đưa ra một ví dụ cổ điển về một trong những trường hợp sử dụng lâu đời nhất trong thế giới của cơ sở dữ liệu quan hệ (cơ sở dữ liệu): chúng ta có một thực thể và chúng ta cần lưu một số thuộc tính (thuộc tính) nhất định của thực thể này. Nhưng không phải tất cả các phiên bản đều có thể có cùng một bộ thuộc tính và nhiều thuộc tính hơn có thể được thêm vào trong tương lai.
Cách dễ nhất để giải quyết vấn đề này là tạo một cột trong bảng cơ sở dữ liệu cho từng giá trị thuộc tính và chỉ cần điền những giá trị cần thiết cho một phiên bản thực thể cụ thể. Tuyệt vời! Vấn đề đã được giải quyết... cho đến khi bảng của bạn chứa hàng triệu bản ghi và bạn cần thêm bản ghi mới.
Hãy xem xét mẫu EAV (
Tuy nhiên, tôi sẽ không viết bài này nếu phương pháp EVA không có một số nhược điểm. Vì vậy, ví dụ, để có được một hoặc nhiều thực thể, mỗi thực thể có 1 thuộc tính, cần có 2 phép nối trong truy vấn: thứ nhất là nối với bảng thuộc tính, thứ hai là nối với bảng giá trị. Nếu một thực thể có 2 thuộc tính thì cần có 4 phép nối! Ngoài ra, tất cả các thuộc tính thường được lưu trữ dưới dạng chuỗi, dẫn đến việc truyền kiểu cho cả kết quả và mệnh đề WHERE. Nếu bạn viết nhiều truy vấn thì điều này khá lãng phí về mặt sử dụng tài nguyên.
Bất chấp những thiếu sót rõ ràng này, EAV từ lâu đã được sử dụng để giải quyết các loại vấn đề này. Đây là những thiếu sót không thể tránh khỏi và đơn giản là không có giải pháp thay thế nào tốt hơn.
Nhưng sau đó một “công nghệ” mới xuất hiện trong PostgreSQL...
Bắt đầu với PostgreSQL 9.4, kiểu dữ liệu JSONB đã được thêm vào để lưu trữ dữ liệu nhị phân JSON. Mặc dù việc lưu trữ JSON ở định dạng này thường tốn nhiều không gian và thời gian hơn JSON văn bản thuần túy nhưng việc thực hiện các thao tác trên nó sẽ nhanh hơn nhiều. JSONB cũng hỗ trợ lập chỉ mục, giúp truy vấn nhanh hơn.
Kiểu dữ liệu JSONB cho phép chúng ta thay thế mẫu EAV rườm rà bằng cách chỉ thêm một cột JSONB vào bảng thực thể, giúp đơn giản hóa đáng kể việc thiết kế cơ sở dữ liệu. Nhưng nhiều người cho rằng điều này sẽ đi kèm với việc giảm năng suất... Đó là lý do tại sao tôi viết bài này.
Thiết lập cơ sở dữ liệu thử nghiệm
Để so sánh, tôi đã tạo cơ sở dữ liệu trên bản cài đặt mới của PostgreSQL 9.5 trên bản dựng $80
CREATE TABLE entity (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT
);
CREATE TABLE entity_attribute (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE entity_attribute_value (
id SERIAL PRIMARY KEY,
entity_id INT REFERENCES entity(id),
entity_attribute_id INT REFERENCES entity_attribute(id),
value TEXT
);
Dưới đây là bảng nơi cùng một dữ liệu sẽ được lưu trữ, nhưng với các thuộc tính trong cột loại JSONB – tài sản.
CREATE TABLE entity_jsonb (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
properties JSONB
);
Trông đơn giản hơn rất nhiều phải không? Sau đó, nó được thêm vào các bảng thực thể (thực thể & thực thể_jsonb) 10 triệu bản ghi và theo đó, bảng chứa cùng một dữ liệu bằng cách sử dụng mẫu EAV và cách tiếp cận với cột JSONB - thực thể_jsonb.properties. Do đó, chúng tôi đã nhận được một số loại dữ liệu khác nhau trong toàn bộ tập hợp thuộc tính. Dữ liệu ví dụ:
{
id: 1
name: "Entity1"
description: "Test entity no. 1"
properties: {
color: "red"
lenght: 120
width: 3.1882420
hassomething: true
country: "Belgium"
}
}
Vì vậy, bây giờ chúng tôi có cùng một dữ liệu cho cả hai tùy chọn. Hãy bắt đầu so sánh việc triển khai tại nơi làm việc!
Đơn giản hóa thiết kế của bạn
Trước đây người ta đã tuyên bố rằng thiết kế cơ sở dữ liệu đã được đơn giản hóa rất nhiều: một bảng, bằng cách sử dụng cột JSONB cho các thuộc tính, thay vì sử dụng ba bảng cho EAV. Nhưng điều này được phản ánh như thế nào trong các yêu cầu? Cập nhật một thuộc tính thực thể trông như thế này:
-- EAV
UPDATE entity_attribute_value
SET value = 'blue'
WHERE entity_attribute_id = 1
AND entity_id = 120;
-- JSONB
UPDATE entity_jsonb
SET properties = jsonb_set(properties, '{"color"}', '"blue"')
WHERE id = 120;
Như bạn có thể thấy, yêu cầu cuối cùng có vẻ không đơn giản hơn. Để cập nhật giá trị của một thuộc tính trong đối tượng JSONB, chúng ta phải sử dụng hàm
Bây giờ hãy chọn thực thể mà chúng ta vừa cập nhật dựa trên màu mới của nó:
-- EAV
SELECT e.name
FROM entity e
INNER JOIN entity_attribute_value eav ON e.id = eav.entity_id
INNER JOIN entity_attribute ea ON eav.entity_attribute_id = ea.id
WHERE ea.name = 'color' AND eav.value = 'blue';
-- JSONB
SELECT name
FROM entity_jsonb
WHERE properties ->> 'color' = 'blue';
Tôi nghĩ chúng ta có thể đồng ý rằng cái thứ hai ngắn hơn (không tham gia!), và do đó dễ đọc hơn. JSONB thắng ở đây! Chúng tôi sử dụng toán tử JSON ->> để lấy màu dưới dạng giá trị văn bản từ đối tượng JSONB. Ngoài ra còn có cách thứ hai để đạt được kết quả tương tự trong mô hình JSONB bằng toán tử @>:
-- JSONB
SELECT name
FROM entity_jsonb
WHERE properties @> '{"color": "blue"}';
Điều này phức tạp hơn một chút: chúng tôi kiểm tra xem liệu đối tượng JSON trong cột thuộc tính của nó có chứa đối tượng ở bên phải toán tử @> hay không. Ít đọc hơn, hiệu quả hơn (xem bên dưới).
Hãy làm cho việc sử dụng JSONB trở nên dễ dàng hơn nữa khi bạn cần chọn nhiều thuộc tính cùng một lúc. Đây là lúc cách tiếp cận JSONB thực sự phát huy tác dụng: chúng ta chỉ cần chọn các thuộc tính làm cột bổ sung trong tập kết quả của mình mà không cần nối:
-- JSONB
SELECT name
, properties ->> 'color'
, properties ->> 'country'
FROM entity_jsonb
WHERE id = 120;
Với EAV bạn sẽ cần 2 phép nối cho mỗi thuộc tính bạn muốn truy vấn. Theo tôi, các truy vấn trên cho thấy sự đơn giản hóa rất lớn trong thiết kế cơ sở dữ liệu. Xem thêm ví dụ về cách viết truy vấn JSONB, cũng trong
Bây giờ là lúc nói về hiệu suất.
Năng suất
Để so sánh hiệu suất tôi đã sử dụng
Cập nhật dữ liệu cho thấy các kết quả sau về mặt thời gian (tính bằng mili giây). Lưu ý rằng thang đo là logarit:
Chúng tôi thấy rằng JSONB nhanh hơn nhiều (> 50000-x) so với EAV nếu bạn không sử dụng chỉ mục, vì lý do đã nêu ở trên. Khi chúng tôi lập chỉ mục các cột bằng khóa chính, sự khác biệt gần như biến mất, nhưng JSONB vẫn nhanh hơn EAV 1,3 lần. Lưu ý rằng chỉ mục trên cột JSONB không có tác dụng ở đây vì chúng tôi không sử dụng cột thuộc tính trong tiêu chí đánh giá.
Để chọn dữ liệu dựa trên giá trị thuộc tính, chúng tôi nhận được kết quả sau (tỷ lệ bình thường):
Bạn có thể nhận thấy rằng JSONB lại hoạt động nhanh hơn EAV khi không có chỉ mục, nhưng khi EAV có chỉ mục thì nó vẫn hoạt động nhanh hơn JSONB. Nhưng sau đó tôi thấy rằng thời gian của các truy vấn JSONB là như nhau, điều này khiến tôi nhận ra rằng các chỉ mục GIN không hoạt động. Rõ ràng khi bạn sử dụng chỉ mục GIN trên một cột có thuộc tính được điền, nó chỉ có hiệu lực khi sử dụng toán tử bao gồm @>. Tôi đã sử dụng điều này trong một thử nghiệm mới và nó có tác động rất lớn đến thời gian: chỉ 0,153 mili giây! Tốc độ này nhanh hơn 15000 lần so với EAV và nhanh hơn 25000 lần so với toán tử ->>.
Tôi nghĩ nó đã đủ nhanh!
Kích thước bảng cơ sở dữ liệu
Hãy so sánh kích thước bảng cho cả hai cách tiếp cận. Trong psql, chúng ta có thể hiển thị kích thước của tất cả các bảng và chỉ mục bằng lệnh dti+
Đối với phương pháp EAV, kích thước bảng là khoảng 3068 MB và lập chỉ mục lên tới 3427 MB với tổng số 6,43 GB. Cách tiếp cận JSONB sử dụng 1817 MB cho bảng và 318 MB cho các chỉ mục, tức là 2,08 GB. Hóa ra ít hơn 3 lần! Thực tế này làm tôi hơi ngạc nhiên vì chúng tôi lưu trữ tên thuộc tính trong mọi đối tượng JSONB.
Tuy nhiên, các con số vẫn tự nói lên điều đó: trong EAV, chúng tôi lưu trữ 2 khóa ngoại số nguyên cho mỗi giá trị thuộc tính, dẫn đến 8 byte dữ liệu bổ sung. Ngoài ra, EAV lưu trữ tất cả các giá trị thuộc tính dưới dạng văn bản, trong khi JSONB sẽ sử dụng các giá trị số và boolean bên trong nếu có thể, dẫn đến dấu chân nhỏ hơn.
Kết quả
Nhìn chung, tôi nghĩ việc lưu thuộc tính thực thể ở định dạng JSONB có thể giúp việc thiết kế và duy trì cơ sở dữ liệu của bạn dễ dàng hơn nhiều. Nếu bạn đang chạy nhiều truy vấn thì việc giữ mọi thứ trong cùng một bảng vì thực thể sẽ thực sự hoạt động hiệu quả hơn. Và thực tế là điều này giúp đơn giản hóa sự tương tác giữa các dữ liệu đã là một điểm cộng, nhưng cơ sở dữ liệu thu được có dung lượng nhỏ hơn 3 lần.
Ngoài ra, dựa trên các thử nghiệm đã thực hiện, chúng tôi có thể kết luận rằng tổn thất về hiệu suất là rất không đáng kể. Trong một số trường hợp, JSONB thậm chí còn nhanh hơn EAV, khiến nó thậm chí còn tốt hơn. Tuy nhiên, điểm chuẩn này tất nhiên không bao gồm tất cả các khía cạnh (ví dụ: các thực thể có số lượng thuộc tính rất lớn, sự gia tăng đáng kể về số lượng thuộc tính của dữ liệu hiện có,...), vì vậy nếu bạn có bất kỳ đề xuất nào về cách cải thiện chúng , xin vui lòng để lại trong phần bình luận!
Nguồn: www.habr.com