Thay thế EAV bằng JSONB trong PostgreSQL

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 (Thực thể-Thuộc tính-Giá trị), nó xảy ra khá thường xuyên. Một bảng chứa các thực thể (bản ghi), một bảng khác chứa tên thuộc tính (thuộc tính) và bảng thứ ba liên kết các thực thể với các thuộc tính của chúng và chứa giá trị của các thuộc tính đó cho thực thể hiện tại. Điều này mang lại cho bạn khả năng có các bộ thuộc tính khác nhau cho các đối tượng khác nhau và cũng có thể thêm các thuộc tính một cách nhanh chóng mà không thay đổi cấu trúc cơ sở dữ liệu.

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 DigitalOcean Ubuntu 14.04. Sau khi thiết lập một số tham số trong postgresql.conf tôi đã chạy này tập lệnh sử dụng psql. Các bảng sau được tạo để trình bày dữ liệu ở dạng EAV:

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 jsonb_set()và sẽ chuyển giá trị mới của chúng ta dưới dạng đối tượng JSONB. Tuy nhiên, chúng ta không cần biết trước bất kỳ định danh nào. Nhìn vào ví dụ EAV, chúng ta cần biết cả thực thể_id và thực thể_attribute_id để thực hiện cập nhật. Nếu bạn muốn cập nhật một thuộc tính trong cột JSONB dựa trên tên đối tượng thì tất cả đều được thực hiện trong một dòng đơn giản.

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 Điều này bưu kiện.
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 GIẢI THÍCH PHÂN TÍCH trong các truy vấn, để tính toán thời gian thực hiện. Mỗi truy vấn được thực hiện ít nhất ba lần vì lần đầu tiên trình lập kế hoạch truy vấn mất nhiều thời gian hơn. Đầu tiên tôi chạy các truy vấn mà không có bất kỳ chỉ mục nào. Rõ ràng, đây là một lợi thế của JSONB, vì các phép nối cần thiết cho EAV không thể sử dụng chỉ mục (các trường khóa ngoài không được lập chỉ mục). Sau đó tôi tạo chỉ mục trên 2 cột khóa ngoại của bảng giá trị EAV, cũng như chỉ mục GIN cho một cột JSONB.

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:

Thay thế EAV bằng JSONB trong PostgreSQL

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):

Thay thế EAV bằng JSONB trong PostgreSQL

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+

Thay thế EAV bằng JSONB trong PostgreSQL

Đố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

Thêm một lời nhận xét