Nghịch thử Postgres Gin index

Gần đây, mình đang tìm hiểu và sử dụng thêm 1 loại index mới mà các RDBMS hỗ trợ: Gin Index (tương tự MySQL có Fulltext). Vậy thì chính xác nó dùng cho mục đích gì ?

Recap Invert Index

GIN (Generalized Inverted Index), như cái tên gợi mở của nó, là một Inverted Index. Nghe tới đây chắc hẳn bạn đọc sẽ thấy nó giống với cách đánh index của ElasticSearch, đây cũng là cách đánh index tối ưu nhất cho use-case searching.

Mục đích của một inverted index, đó là từ 1 elem bất kỳ loại dữ liệu(là một phần tử trong array, một keyword trong 1 documents,…) truy ngược lại nó thuộc về document nào.

Ví dụ 3 đoạn văn bản sau:

This is first document

this is second document

this is third document

Inverted index:

Term DocID
This 1
this 2,3
is 1,2,3
first 1
second 2
third 3
document 1,2,3

Ngược lại, ta cũng có forward index:

DocID Terms
1 This,is,first,document
2 this,is,second,document
3 this,is,third,document

Tất nhiên trong DB, biểu diễn của inverted index còn phức tạp hơn nhiều(tf-id,…) Có thể thấy rằng, với Inverted index, việc tìm kiếm 1 keyword trong 1 docs rất dễ dàng.

Vấn đề

Ok, Use-case fulltext search thì khá phổ biến, hãy thử 1 use-case khác: Post & tags

Funtional Requirement:

  • Một post sẽ có rất nhiều tags, assume nhiều nhất 1000 tags
  • Từ tags, tìm kiếm posts

Không dùng GIN

Không có GIN, ta có thể thiết kế normalized database như thông thường:

posts

Column Type Note
id serial, primary key
title varchar(255)
content text
author_id integer foreign key(user)

tags

Column Type Note
id serial, primary key
name varchar(255)
description text

posts_tags

Column Type Note
post_id integer pkey: (post_id, tag_id)
tag_id integer

Dùng GIN:

posts

Column Type Note
id serial, primary key
title varchar(255)
content text
author_id integer foreign key(user)
tags varchar(255)[]

Như có thể thấy table này đã được denormalized ở cột tags

Tạo GIN index:

CREATE INDEX idx_posts_tags on posts USING GIN (tags);

Query posts có tag postgres:

EXPLAIN ANALYZE SELECT * FROM `posts` 
WHERE "tags" @> ARRAY['postgres'::varchar] ORDER BY ID LIMIT 10;

Kết quả

Limit  (cost=12.03..12.04 rows=1 width=64) (actual time=0.022..0.025 rows=1 loops=1)
  ->  Sort  (cost=12.03..12.04 rows=1 width=64) (actual time=0.021..0.022 rows=1 loops=1)
        Sort Key: id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on posts3  (cost=8.01..12.02 rows=1 width=64) (actual time=0.015..0.017 rows=1 loops=1)
              Recheck Cond: (tags @> '{postgres}'::character varying[])
              Heap Blocks: exact=1
              ->  Bitmap Index Scan on posts3_user_ids_idx  (cost=0.00..8.01 rows=1 width=0) (actual time=0.009..0.010 rows=1 loops=1)
                    Index Cond: (tags @> '{postgres}'::character varying[])
Planning Time: 0.112 ms
Execution Time: 0.049 ms

Như có thể thấy, database được denormalized và đơn giản hơn rất nhiều so với many-to-many relations. Lợi ích có thể thấy được đó là việc migrations, scale ngang sẽ dễ dàng hơn nếu có scale về data

Hạn chế

  • Khi mình test với lượng data lớn lên tới vài triệu rows, việc dùng GIN index và ORDER BY đang có vấn đề - pagination do đó cũng sẽ không work được, Postgres stats đang collect không đủ data, postgres planner do đó đang không sử dụng Gin index (thay vào đó dùng một index khác) làm cho câu query rất chậm, trong tương lai postgres sẽ tích hợp Gin sorted index, fix performance của Order BY khi dùng với GIN. Hiện tại, chỉ có thể có vài cách cheating để force postgres dùng GIN index thay vì một index khác
  • Tất cả ví dụ bên dưới mình đều đã chạy VACUUM FULL và ANALYZE trước khi thực hiện query Ví dụ:
EXPLAIN ANALYZE 
  SELECT * FROM `posts` 
  WHERE 
    "tags" @> ARRAY['postgres'::varchar] ORDER BY id LIMIT 10;

Result:

Limit  (cost=0.43..212.05 rows=10 width=578) (actual time=26417.740..26417.741 rows=0 loops=1)
  ->  Index Scan using posts_pkey on posts  (cost=0.43..219748.75 rows=10384 width=578) (actual time=26417.736..26417.737 rows=0 loops=1)
        Filter: (tags @> '{postgres}'::character varying[])
        Rows Removed by Filter: 2100000
Planning Time: 0.953 ms
Execution Time: 26417.917 ms

Khá là lạ khi mà Postgres Execution planner chọn pkey để thực hiện việc filter

Tuy nhiên, nếu thử order trên một column khác, không có index(như content):

Limit  (cost=32865.58..32865.59 rows=1 width=578) (actual time=0.203..0.204 rows=0 loops=1)
  ->  Sort  (cost=32865.58..32891.54 rows=10384 width=578) (actual time=0.201..0.202 rows=0 loops=1)
        Sort Key: author_id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on posts  (cost=116.47..32813.66 rows=10384 width=578) (actual time=0.192..0.193 rows=0 loops=1)
              Recheck Cond: (tags && '{151818c6-d8ee-40d2-9985-6f6ba8046f51}'::character varying[])
              ->  Bitmap Index Scan on idx_posts_user_ids  (cost=0.00..113.88 rows=10384 width=0) (actual time=0.031..0.031 rows=0 loops=1)
                    Index Cond: (tags && '{151818c6-d8ee-40d2-9985-6f6ba8046f51}'::character varying[])
Planning Time: 0.382 ms
Execution Time: 0.416 ms

Kết luận

  • GIN index là một inverted index hỗ trợ cho RDBMS. Với nó, nhu cầu design denormalized database sẽ có thể được thực hiện dễ dàng
  • Hạn chế cần cân nhắc đó là nó còn khá là immature. Postgres Query Planner còn khá “dummy”, nên có lẽ vẫn còn cần thời gian để cộng đồng postgres cải thiện, lời khuyên là đừng nên dùng nó vào trong Production nếu bạn không biết bạn đang làm gì và well-tested

References

comments powered by Disqus