Hướng dẫn
Lưu trữ vector trong Postgres: So sánh hiệu năng giữa bảng nội dòng và bảng tách biệt trên AlloyDB Omni
(giờ Việt Nam)
Tóm tắt AI
Gleb Otochkin thực nghiệm trên AlloyDB Omni với 30.000 bản ghi để so sánh hiệu năng giữa việc lưu vector cùng bảng dữ liệu và dùng bảng tách biệt kết hợp JOIN trong các tác vụ tìm kiếm ngữ nghĩa và lọc dữ liệu.
Chính văn · Bản dịch AI



Kiểm thử tìm kiếm ngữ nghĩa, truy vấn có lọc và join nhiều bảng trong AlloyDB để đo lường chi phí thực tế của việc tách rời các vector của bạn.

Giới thiệu
Nếu bạn đang làm việc với vector embeddings, có lẽ bạn đã biết rằng các mô hình embedding đang không ngừng phát triển và bạn cần làm mới các embedding của mình bằng phiên bản mới theo thời gian. Trong bài viết trước, chúng ta đã thảo luận về vấn đề phình to dữ liệu trong các bảng, các phân đoạn TOAST và chỉ mục liên quan đến việc làm mới embedding. Như một giải pháp thay thế cho việc lưu trữ embedding trong cùng bảng với dữ liệu, tôi đề xuất một cấu trúc khác, trong đó các embedding sẽ được đặt trong một bảng chuyên dụng. Trong bài viết này, tôi sẽ cho thấy tác động hiệu năng của nó so với việc lưu trữ embedding trong cùng một bảng.
Cách tôi thực hiện kiểm thử
Để kiểm thử các truy vấn, tôi cần dữ liệu mẫu với các embedding thực tế. Lý do đã được giải thích trong một trong các bài viết trước của tôi — nó có thể gây ra tác động rõ rệt đến kết quả khi sử dụng chỉ mục ANN. Tôi đã chuẩn bị một tập dữ liệu gồm 30 nghìn dòng sản phẩm mẫu, xây dựng các embedding dựa trên mô tả sản phẩm và sử dụng chúng để điền vào một bảng embedding chuyên dụng. Đối với tất cả các bài kiểm thử, tôi đã sử dụng cơ sở dữ liệu AlloyDB Omni. Nó được tích hợp sẵn AI, giúp tôi xây dựng các embedding, các chỉ mục vector và tạo các embedding tìm kiếm bằng cách chuyển đổi các cụm từ tìm kiếm thành các biến vector.
Dưới đây là các bảng được sử dụng trong các bài kiểm thử của tôi:
-- ecomm.products table
demodb=# \d ecomm.products
Table "ecomm.products"
Column | Type | Collation | Nullable | Default
------------------------+------------------------+-----------+----------+---------
id | bigint | | not null |
cost | numeric | | |
category | character varying(255) | | |
name | character varying(255) | | |
brand | character varying(255) | | |
retail_price | numeric | | |
department | character varying(255) | | |
sku | character varying(255) | | |
distribution_center_id | bigint | | |
product_description | text | | |
product_image_uri | text | | |
embedding | vector(768) | | |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
"fk_products_distribution_center_23" btree (distribution_center_id)
"idx_products_brand" btree (brand)
"idx_products_category" btree (category)
"idx_products_retail_price" btree (retail_price)
"idx_products_sku" btree (sku)
"idx_products_vector" hnsw (embedding vector_cosine_ops)
Foreign-key constraints:
"fk_products_distribution_center" FOREIGN KEY (distribution_center_id) REFERENCES ecomm.distribution_centers(id)
Referenced by:
TABLE "ecomm.inventory_items" CONSTRAINT "fk_inventory_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
TABLE "ecomm.order_items" CONSTRAINT "fk_order_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
TABLE "ecomm.product_reviews" CONSTRAINT "fk_reviews_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id) ON DELETE CASCADE
-- ecomm.product_embeddings table
demodb=# \d ecomm.product_embeddings
Table "ecomm.product_embeddings"
Column | Type | Collation | Nullable | Default
------------+-------------+-----------+----------+---------
product_id | bigint | | not null |
embedding | vector(768) | | not null |
Indexes:
"product_embeddings_pkey" PRIMARY KEY, btree (product_id)
"idx_product_embeddings_vector" hnsw (embedding vector_cosine_ops)
-- ecomm.inventory_items table
demodb=# \d ecomm.inventory_items
Table "ecomm.inventory_items"
Column | Type | Collation | Nullable | Default
--------------------------------+-----------------------------+-----------+----------+---------
id | bigint | | not null |
product_id | bigint | | |
created_at | timestamp without time zone | | |
sold_at | timestamp without time zone | | |
cost | numeric | | |
product_category | character varying(255) | | |
product_name | character varying(255) | | |
product_brand | character varying(255) | | |
product_retail_price | numeric | | |
product_department | character varying(255) | | |
product_sku | character varying(255) | | |
product_distribution_center_id | bigint | | |
Indexes:
"inventory_items_pkey" PRIMARY KEY, btree (id)
"fk_inventory_items_distribution_center_8" btree (product_distribution_center_id)
"fk_inventory_items_product_7" btree (product_id)
Foreign-key constraints:
"fk_inventory_items_distribution_center" FOREIGN KEY (product_distribution_center_id) REFERENCES ecomm.distribution_centers(id)
"fk_inventory_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
Referenced by:
TABLE "ecomm.order_items" CONSTRAINT "fk_order_items_inventory_item" FOREIGN KEY (inventory_item_id) REFERENCES ecomm.inventory_items(id)
-- ecomm.product_reviews table
demodb=# \d ecomm.product_reviews
Table "ecomm.product_reviews"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
user_id | bigint | | not null |
product_id | bigint | | not null |
rating | integer | | not null |
review_text | text | | not null |
created_at | timestamp with time zone | | not null | CURRENT_TIMESTAMP
Indexes:
"product_reviews_pkey" PRIMARY KEY, btree (id)
"idx_product_reviews_created_at" btree (created_at DESC)
"idx_product_reviews_product_id" btree (product_id)
"idx_product_reviews_user_id" btree (user_id)
Check constraints:
"product_reviews_rating_check" CHECK (rating >= 1 AND rating <= 5)
Foreign-key constraints:
"fk_reviews_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id) ON DELETE CASCADE
"fk_reviews_user" FOREIGN KEY (user_id) REFERENCES ecomm.users(id) ON DELETE CASCADECác bảng ecomm.products và ecomm.product_embeddings được kết nối bằng khóa chính và cả hai bảng đều có chỉ mục HNSW được xây dựng trên các vector embedding.
Kiểm thử tìm kiếm ngữ nghĩa sản phẩm
Tôi bắt đầu các bài kiểm thử từ một trường hợp đơn giản với tìm kiếm ngữ nghĩa cosine chỉ sử dụng bảng sản phẩm, sau đó so sánh nó với việc join bảng products và bảng product_embeddings. Đối với tìm kiếm, tôi đã chuyển đổi cụm từ ‘lightweight waterproof high quality jacket’ thành một vector embedding và truyền nó dưới dạng biến test_vec. Điều này giúp loại bỏ thời gian phản hồi không thể dự đoán trước của mô hình và cho phép tôi so sánh thời gian thực thi truy vấn một cách chính xác hơn.
-- Set the test_vec variable in psql
SELECT (google_ml.embedding(
model_id => 'text-embedding-005',
content => 'lightweight waterproof high quality jacket'
)::public.vector(768))::text AS test_vec \gsetTrong cùng một phiên psql, tôi đã chạy tìm kiếm với EXPLAIN ANALYZE để xem kế hoạch thực thi và thời gian. Mỗi bài kiểm thử đều được lặp lại nhiều lần.
-- Embeddings in ecomm.products
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.id,
p.name,
p.brand,
p.retail_price
FROM ecomm.products p
ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;Đây là kế hoạch thực thi cho tìm kiếm vector với các embedding "nội dòng" (inline):
Limit (cost=1213.90..1240.56 rows=10 width=86) (actual time=1.112..1.200 rows=10.00 loops=1)
Buffers: shared hit=792
-> Index Scan using idx_products_vector on products p (cost=1213.90..78826.40 rows=29120 width=86) (actual time=1.110..1.197 rows=10.00 loops=1)
Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
Index Searches: 1
Buffers: shared hit=792
Planning:
Buffers: shared hit=1
Planning Time: 0.107 ms
Execution Time: 1.224 msTừ kế hoạch này, có thể thấy truy vấn đang sử dụng chỉ mục HNSW để tìm kiếm qua các embedding và sắp xếp chúng theo độ tương đồng. Tổng thời gian phản hồi để trả về kết quả là khoảng 2,1 ms. Thời gian phản hồi lớn hơn thời gian thực thi thuần túy từ kế hoạch thực thi vì nó bao gồm các chi phí bổ sung như lập kế hoạch, thời gian mạng và thời gian của phần mềm client để hiển thị kết quả.
Sau đó, tôi đưa các embedding vào bảng ecomm.product_embeddings và thực hiện tìm kiếm tương tự:
-- Embeddings in ecomm.product_embeddings
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.id,
p.name,
p.brand,
p.retail_price
FROM ecomm.product_embeddings pe
JOIN ecomm.products p ON p.id = pe.product_id
ORDER BY (pe.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;Đây là kế hoạch thực thi với việc join bảng products và product_embeddings sử dụng khóa chính:
Limit (cost=219.82..250.39 rows=10 width=86) (actual time=1.188..1.351 rows=10.00 loops=1)
Buffers: shared hit=852
-> Nested Loop (cost=219.82..89223.27 rows=29120 width=86) (actual time=1.187..1.348 rows=10.00 loops=1)
Buffers: shared hit=852
-> Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.126..1.141 rows=10.00 loops=1)
Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
Index Searches: 1
Buffers: shared hit=752
-> Index Scan using products_pkey on products p (cost=0.29..1.01 rows=1 width=78) (actual time=0.007..0.007 rows=1.00 loops=10)
Index Cond: (id = pe.product_id)
Index Searches: 10
Buffers: shared hit=50
Planning:
Buffers: shared hit=21
Planning Time: 0.274 ms
Execution Time: 1.381 msTôi đã lấy 10 vector hàng đầu bằng cách sử dụng chỉ mục HNSW trên ecomm.product_embeddings và join kết quả đó với bảng ecomm.products bằng khóa chính. Thời gian thực thi luôn ổn định ở mức khoảng 2,2 ms. Đúng là kế hoạch thực thi có phức tạp hơn một chút nhưng bản thân việc thực thi chỉ mất thêm 0,1 ms. Khi tôi tăng giới hạn từ 10 lên 1000, thời gian phản hồi chỉ tăng thêm khoảng 0,3 ms. Các phép join trong PostgreSQL với khóa chính rất nhanh.
Tìm kiếm ngữ nghĩa có lọc
Trong thực tế, chúng ta hiếm khi thấy các truy vấn chỉ sử dụng một điều kiện tìm kiếm. Trong hầu hết các trường hợp, nó bao gồm các tham số bổ sung. Tôi đã kiểm thử bằng cách thêm bộ lọc vào các cột category và retail_price:
-- With two filters and embeddings in ecomm.products
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.id,
p.name,
p.brand,
p.retail_price,
p.category
FROM ecomm.products p
WHERE p.category = 'Outerwear & Coats'
AND p.retail_price BETWEEN 30 AND 150
ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;Kế hoạch thực thi đã thay đổi khi thêm bộ lọc vào kết quả:
Limit (cost=1213.90..1760.50 rows=10 width=97) (actual time=6.353..6.522 rows=10.00 loops=1)
Buffers: shared hit=817
-> Index Scan using idx_products_vector on products p (cost=1213.90..78829.95 rows=1420 width=97) (actual time=6.342..6.509 rows=10.00 loops=1)
Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
Filter: ((retail_price >= '30'::numeric) AND (retail_price <= '150'::numeric) AND ((category)::text = 'Outerwear & Coats'::text))
Rows Removed by Filter: 7
Index Searches: 1
Buffers: shared hit=799
Planning:
Buffers: shared hit=1
Planning Time: 0.157 ms
Execution Time: 1.308 msThời gian phản hồi luôn ổn định ở mức khoảng 2,2 ms — gần như tương đương với khi không có bộ lọc.
Sau đó, tôi thực hiện truy vấn với các embedding nằm trong một bảng riêng biệt:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.id,
p.name,
p.brand,
p.retail_price,
p.category
FROM ecomm.product_embeddings pe
JOIN ecomm.products p ON p.id = pe.product_id
WHERE p.category = 'Outerwear & Coats'
AND p.retail_price BETWEEN 30 AND 150
ORDER BY (pe.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;Tôi nhận được thêm một bước với bộ lọc trong kế hoạch thực thi:
Limit (cost=219.82..1346.89 rows=10 width=97) (actual time=1.167..1.370 rows=10.00 loops=1)
Buffers: shared hit=894
-> Nested Loop (cost=219.82..89370.85 rows=791 width=97) (actual time=1.166..1.367 rows=10.00 loops=1)
Buffers: shared hit=894
-> Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.110..1.153 rows=17.00 loops=1)
Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
Index Searches: 1
Buffers: shared hit=759
-> Index Scan using products_pkey on products p (cost=0.29..1.02 rows=1 width=89) (actual time=0.006..0.006 rows=0.59 loops=17)
Index Cond: (id = pe.product_id)
Filter: ((retail_price >= '30'::numeric) AND (retail_price <= '150'::numeric) AND ((category)::text = 'Outerwear & Coats'::text))
Rows Removed by Filter: 0
Index Searches: 17
Buffers: shared hit=85
Planning:
Buffers: shared hit=21
Planning Time: 0.347 ms
Execution Time: 1.406 msThời gian phản hồi cho truy vấn luôn nằm trong khoảng từ 2,3 đến 2,4 ms. Một lần nữa, nó chậm hơn so với truy vấn có embedding trong cùng một bảng nhưng sự khác biệt là không đáng kể.
Join nhiều bảng
Nếu bạn có một lược đồ dữ liệu nơi các phần và thuộc tính khác nhau của thông tin doanh nghiệp được lưu trữ trong các bảng khác nhau, bạn sẽ sử dụng các phép join nhiều bảng để lấy dữ liệu cần thiết. Đối với các bài kiểm thử, tôi đã sử dụng phép join với ecomm.product_reviews cùng các phép tổng hợp (aggregation) cho đánh giá sản phẩm.
EXPLAIN (ANALYZE, BUFFERS)
SELECT
top.id,
top.name,
top.brand,
top.retail_price,
top.category,
top.dist,
COUNT(pr.id) AS review_count,
COALESCE(ROUND(AVG(pr.rating), 2), 0) AS avg_rating
FROM (
SELECT
p.id,
p.name,
p.brand,
p.retail_price,
p.category,
(p.embedding <=> :'test_vec'::public.vector(768)) AS dist
FROM ecomm.products p
WHERE p.category = 'Outerwear & Coats'
AND p.retail_price BETWEEN 30 AND 150
ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10
) top
LEFT JOIN ecomm.product_reviews pr ON pr.product_id = top.id
GROUP BY top.id, top.name, top.brand, top.retail_price, top.category, top.dist
ORDER BY top.dist;Truy vấn luôn trả về kết quả ổn định trong khoảng 2,6–2,7 ms. Sau đó, tôi đã sửa đổi truy vấn để sử dụng các embedding được lưu trữ trong bảng ecomm.product_embeddings:
Bài gốc còn tiếp — xem tiếp tại bài gốc ↗
Bài viết được AI dịch và tổng hợp tự động từ Google AI: DEV tác giả. Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.