Chỉ mục PostgreSQL cho ứng dụng SaaS: chọn giữa btree, GIN và GiST dựa trên các dạng truy vấn thực tế như lọc, tìm kiếm, JSONB và mảng.

Một chỉ mục thay đổi cách PostgreSQL tìm các hàng. Nếu không có chỉ mục, cơ sở dữ liệu thường phải đọc nhiều phần của bảng (quét tuần tự) rồi loại bỏ phần lớn. Với chỉ mục phù hợp, nó có thể nhảy trực tiếp tới các hàng khớp (index lookup), sau đó chỉ lấy những gì cần.
Bạn nhận thấy điều này sớm ở ứng dụng SaaS vì các màn hình hàng ngày thường nặng truy vấn. Một cú nhấp có thể kích hoạt vài lần đọc: trang danh sách, tổng số phần tử, vài card dashboard và một ô tìm kiếm. Khi một bảng tăng từ vài nghìn lên tới vài triệu hàng, cùng một truy vấn từng rất nhanh bắt đầu chậm.
Ví dụ điển hình là trang Orders lọc theo trạng thái và ngày, sắp thứ tự theo mới nhất, có phân trang. Nếu PostgreSQL phải quét toàn bộ bảng orders để tìm các đơn đã thanh toán trong 30 ngày gần nhất, mỗi lần tải trang sẽ làm nhiều việc thừa. Một chỉ mục tốt biến việc đó thành một bước nhảy nhanh tới vùng dữ liệu đúng.
Chỉ mục không miễn phí. Mỗi chỉ mục mua lại tốc độ đọc nhanh hơn cho những truy vấn cụ thể, nhưng cũng làm chậm ghi (INSERT/UPDATE/DELETE phải cập nhật chỉ mục) và tốn thêm lưu trữ (và làm tăng áp lực cache). Chính vì vậy bạn nên bắt đầu từ mẫu truy vấn thực tế, chứ không phải từ loại chỉ mục.
Một quy tắc đơn giản tránh làm việc thừa: chỉ thêm chỉ mục khi bạn có thể chỉ ra một truy vấn cụ thể, thường xuyên mà nó sẽ tăng tốc. Nếu bạn xây màn hình bằng bộ dựng chat như Koder.ai, tốt khi lưu lại SQL phía sau các trang danh sách và dashboard và dùng đó làm danh sách mong muốn cho chỉ mục.
Hầu hết nhầm lẫn về chỉ mục sẽ tan biến khi bạn ngừng nghĩ theo tính năng (JSON, tìm kiếm, mảng) và bắt đầu nghĩ theo dạng truy vấn: WHERE làm gì, và bạn mong đợi kết quả được sắp theo cách nào?
Dùng B-tree khi truy vấn của bạn trông giống so sánh thông thường và bạn quan tâm đến thứ tự sắp xếp. Nó là con ngựa kéo chính cho equality, range và join.
Ví dụ: lọc theo tenant_id = ?, status = 'active', created_at \u003e= ?, join users.id = orders.user_id, hoặc hiển thị “mới nhất trước” với ORDER BY created_at DESC.
GIN (Generalized Inverted Index) phù hợp khi một cột chứa nhiều phần tử và bạn hỏi “nó có chứa X không?” Điều này thường gặp với khóa JSONB, phần tử mảng và vector tìm kiếm toàn văn.
Ví dụ: metadata @\u003e {'plan':'pro'} trên JSONB, tags @\u003e ARRAY['urgent'], hoặc to_tsvector(body) @@ plainto_tsquery('reset password').
GiST (Generalized Search Tree) phù hợp cho các câu hỏi về khoảng cách hoặc chồng lấp, nơi giá trị hành xử như range hoặc hình dạng. Nó thường được dùng cho kiểu range, dữ liệu hình học và một số truy vấn “khớp gần nhất”.
Ví dụ: các cửa sổ thời gian chồng lấp với cột range, một số tìm kiếm theo độ tương đồng (ví dụ với toán tử trigram), hoặc truy vấn không gian (nếu bạn dùng PostGIS).
Cách thực tế để chọn:
Chỉ mục tăng tốc đọc, nhưng bù lại làm chậm ghi và tốn đĩa. Trong SaaS, sự đánh đổi này quan trọng nhất với các bảng nóng như events, sessions và activity logs.
Hầu hết các màn hình danh sách SaaS chia sẻ dạng giống nhau: ranh giới tenant, vài bộ lọc và một thứ tự sắp xếp dự đoán được. Chỉ mục B-tree là lựa chọn mặc định ở đây, và thường là rẻ nhất để duy trì.
Mẫu phổ biến là WHERE tenant_id = ? cộng với các bộ lọc như status = ?, user_id = ?, và một khoảng thời gian như created_at \u003e= ?. Với chỉ mục B-tree ghép, đặt các bộ lọc bằng equality trước (các cột bạn so sánh bằng =), rồi thêm cột bạn sắp xếp.
Các quy tắc hoạt động tốt trong hầu hết app:
tenant_id nếu mọi truy vấn có phạm vi tenant.= tiếp theo (thường là status, user_id).ORDER BY ở cuối (thường created_at hoặc id).INCLUDE để che phủ trang danh sách mà không làm khóa chỉ mục rộng hơn.Một ví dụ thực tế: trang Tickets hiển thị mục mới nhất trước, lọc theo trạng thái.
-- Query
SELECT id, status, created_at, title
FROM tickets
WHERE tenant_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;
-- Index
CREATE INDEX tickets_tenant_status_created_at_idx
ON tickets (tenant_id, status, created_at DESC)
INCLUDE (title);
Chỉ mục này hỗ trợ cả lọc lẫn sắp xếp, nên Postgres có thể tránh sắp xếp một tập kết quả lớn. Phần INCLUDE (title) giúp trang danh sách chạm ít trang bảng hơn, trong khi giữ khóa chỉ mục tập trung vào lọc và sắp xếp.
Với các khoảng thời gian, cùng ý tưởng vẫn áp dụng:
SELECT id, created_at
FROM events
WHERE tenant_id = $1
AND created_at \u003e= $2
AND created_at \u003c $3
ORDER BY created_at DESC
LIMIT 100;
CREATE INDEX events_tenant_created_at_idx
ON events (tenant_id, created_at DESC);
Phân trang là nơi nhiều ứng dụng SaaS chậm lại. Offset pagination (OFFSET 50000) buộc cơ sở dữ liệu phải đi qua nhiều hàng. Seek pagination giữ tốc độ bằng cách dùng khóa sắp xếp cuối cùng đã thấy:
SELECT id, created_at
FROM tickets
WHERE tenant_id = $1
AND created_at \u003c $2
ORDER BY created_at DESC
LIMIT 50;
Với chỉ mục B-tree phù hợp, điều này vẫn nhanh ngay cả khi bảng lớn.
Hầu hết ứng dụng SaaS là đa-tenant: mọi truy vấn phải ở trong một tenant. Nếu chỉ mục không bao gồm tenant_id, Postgres vẫn có thể tìm hàng nhanh, nhưng thường sẽ quét nhiều entry chỉ mục hơn cần thiết. Chỉ mục nhận biết tenant giữ dữ liệu mỗi tenant gom lại trong chỉ mục nên các màn hình phổ biến vẫn nhanh và dự đoán được.
Quy tắc đơn giản: đặt tenant_id đầu khi truy vấn luôn lọc theo tenant. Sau đó thêm cột bạn lọc hoặc sắp xếp nhiều nhất.
Các chỉ mục nhàm nhưng tác động lớn thường trông như:
(tenant_id, created_at) cho danh sách mục mới và phân trang cursor(tenant_id, status) cho bộ lọc trạng thái (Open, Paid, Failed)(tenant_id, user_id) cho màn hình “mục do người dùng này sở hữu”(tenant_id, updated_at) cho views admin “thay đổi gần đây”(tenant_id, external_id) cho tra cứu từ webhook hoặc importOver-indexing xảy ra khi bạn thêm chỉ mục mới cho mỗi màn hình hơi khác nhau. Trước khi tạo thêm, kiểm tra xem chỉ mục ghép hiện có đã che phủ các cột bên trái bạn cần chưa. Ví dụ, nếu bạn có (tenant_id, created_at), thường bạn không cần thêm (tenant_id, created_at, id) trừ khi bạn thật sự lọc trên id sau những cột đó.
Partial index có thể giảm kích thước và chi phí ghi khi hầu hết hàng không liên quan. Chúng làm việc tốt với soft deletes và “chỉ active”, ví dụ: chỉ lập chỉ mục khi deleted_at IS NULL, hoặc chỉ khi status = 'active'.
Mỗi chỉ mục thêm làm việc ghi nặng hơn. Insert phải cập nhật mọi chỉ mục, và update có thể chạm nhiều chỉ mục ngay cả khi bạn thay đổi một cột. Nếu app của bạn ingest nhiều events (kể cả các app xây nhanh với Koder.ai), giữ chỉ mục tập trung vào vài dạng truy vấn người dùng thực sự gặp mỗi ngày.
JSONB tiện khi app cần trường mở như feature flags, thuộc tính người dùng, hoặc cài đặt theo tenant. Điểm khó là các toán tử JSONB khác nhau hành xử khác nhau, nên chỉ mục tốt nhất phụ thuộc cách bạn truy vấn.
Hai dạng quan trọng nhất:
@\u003e.-\u003e / -\u003e\u003e (thường so sánh với =).Nếu bạn thường xuyên lọc với @\u003e, một chỉ mục GIN trên cột JSONB thường có lợi.
-- Query shape: containment
SELECT id
FROM accounts
WHERE tenant_id = $1
AND metadata @\u003e '{"region":"eu","plan":"pro"}';
-- Index
CREATE INDEX accounts_metadata_gin
ON accounts
USING GIN (metadata);
Nếu cấu trúc JSON của bạn dự đoán được và bạn chủ yếu dùng @\u003e trên các khóa top-level, jsonb_path_ops có thể nhỏ hơn và nhanh hơn, nhưng nó hỗ trợ ít loại toán tử hơn.
Nếu UI thường xuyên lọc theo một trường cụ thể (như plan), trích xuất trường đó và lập chỉ mục nó thường nhanh và rẻ hơn một GIN rộng.
SELECT id
FROM accounts
WHERE tenant_id = $1
AND metadata-\u003e\u003e'plan' = 'pro';
CREATE INDEX accounts_plan_expr
ON accounts ((metadata-\u003e\u003e'plan'));
Quy tắc thực tế: giữ JSONB cho các thuộc tính linh hoạt, ít khi lọc, nhưng nâng các trường ổn định, dùng nhiều (plan, status, created_at) thành cột thật. Nếu bạn lặp nhanh trên một app sinh tự động, thường là chỉnh schema đơn giản khi thấy bộ lọc nào xuất hiện trên mọi trang.
Ví dụ: nếu bạn lưu {\"tags\":[\"beta\",\"finance\"],\"region\":\"us\"} trong JSONB, dùng GIN khi bạn lọc theo các gói thuộc tính (@\u003e), và thêm chỉ mục biểu thức cho một vài khóa chi phối hầu hết danh sách (plan, region).
Mảng hấp dẫn vì dễ lưu và dễ đọc. Một cột users.roles text[] hoặc projects.labels text[] hoạt động tốt khi bạn chủ yếu hỏi một câu: hàng này có chứa giá trị đó không? Đó chính là nơi GIN hữu ích.
GIN là lựa chọn chính cho truy vấn membership trên mảng. Nó phá mảng thành từng phần tử và xây dựng tra cứu nhanh tới các hàng chứa chúng.
Các dạng truy vấn mảng thường hưởng lợi:
@\u003e (array contains)\u0026\u0026 (array shares any items)= ANY(...), nhưng @\u003e thường dự đoán được hơnVí dụ điển hình lọc người dùng theo vai trò:
-- Find users who have the "admin" role
SELECT id, email
FROM users
WHERE roles @\u003e ARRAY['admin'];
CREATE INDEX users_roles_gin ON users USING GIN (roles);
Và lọc dự án theo bộ nhãn (phải bao gồm cả hai nhãn):
SELECT id, name
FROM projects
WHERE labels @\u003e ARRAY['billing', 'urgent'];
CREATE INDEX projects_labels_gin ON projects USING GIN (labels);
Nơi mọi người bất ngờ: một số mẫu không dùng chỉ mục như bạn tưởng. Nếu bạn biến mảng thành chuỗi (array_to_string(labels, ',')) rồi chạy LIKE, chỉ mục GIN sẽ không giúp. Cũng vậy, nếu bạn cần “bắt đầu bằng” hay khớp mơ hồ trong nhãn, đó là lĩnh vực tìm kiếm văn bản, chứ không phải membership mảng.
Mảng cũng trở nên khó duy trì khi chúng thành một cơ sở dữ liệu con: cập nhật thường xuyên, cần metadata theo phần tử (ai thêm nhãn, khi nào, vì sao), hoặc cần phân tích theo nhãn. Lúc đó, bảng join như project_labels(project_id, label) thường dễ kiểm soát, truy vấn và phát triển hơn.
Với ô tìm kiếm, hai mẫu thường xuất hiện: tìm kiếm toàn văn (tìm bản ghi về một chủ đề) và khớp mờ (xử lý lỗi đánh máy, tên không đầy đủ và mẫu ILIKE). Chỉ mục đúng quyết định giữa “nhanh tức thì” và “timeout khi 10k user”.
Dùng full-text khi người dùng gõ từ thực và bạn muốn kết quả xếp theo độ liên quan, như tìm ticket theo tiêu đề và mô tả. Thiết lập thường là lưu tsvector (thường là cột sinh) và lập chỉ mục bằng GIN. Bạn tìm với @@ và một tsquery.
-- Tickets: full-text search on subject + body
ALTER TABLE tickets
ADD COLUMN search_vec tsvector
GENERATED ALWAYS AS (
to_tsvector('simple', coalesce(subject,'') || ' ' || coalesce(body,''))
) STORED;
CREATE INDEX tickets_search_vec_gin
ON tickets USING GIN (search_vec);
-- Query
SELECT id, subject
FROM tickets
WHERE search_vec @@ plainto_tsquery('simple', 'invoice failed');
-- Customers: fuzzy name search using trigrams
CREATE INDEX customers_name_trgm
ON customers USING GIN (name gin_trgm_ops);
SELECT id, name
FROM customers
WHERE name ILIKE '%jon smth%';
Nên lưu gì trong vector: chỉ những trường bạn thực sự tìm kiếm. Nếu bạn bao gồm mọi thứ (notes, logs nội bộ), bạn phải trả tiền bằng kích thước chỉ mục và chi phí ghi.
Dùng trigram similarity khi người dùng tìm tên, email hoặc cụm ngắn và bạn cần khớp phần, dung sai lỗi chính tả. Trigram giúp với ILIKE '%term%' và toán tử similarity. GIN thường nhanh hơn cho “có khớp không?”; GiST có khi phù hợp hơn nếu bạn cũng quan tâm đến sắp xếp theo độ tương đồng.
Các quy tắc ngón tay cái:
tsvector cho tìm kiếm theo độ liên quan.Cảnh báo:
ILIKE '%abc') bắt buộc quét.Nếu bạn phát hành màn hình tìm kiếm nhanh, coi chỉ mục là một phần của tính năng: UX tìm kiếm và lựa chọn chỉ mục cần được thiết kế cùng nhau.
Bắt đầu với chính xác truy vấn ứng dụng bạn chạy, không phỏng đoán. Một “màn hình chậm” thường là một câu SQL cụ thể với WHERE và ORDER BY rõ ràng. Sao chép nó từ logs, output debug ORM, hoặc bất cứ công cụ ghi truy vấn bạn dùng.
Một quy trình giữ được trong ứng dụng thực tế:
EXPLAIN (ANALYZE, BUFFERS) trên cùng truy vấn.=, \u003e=, LIKE, @\u003e, @@), chứ không chỉ tên cột.EXPLAIN (ANALYZE, BUFFERS) với dữ liệu thực tế.Ví dụ cụ thể. Một trang Customers lọc theo tenant và status, sắp theo mới nhất và phân trang:
SELECT id, created_at, email
FROM customers
WHERE tenant_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Nếu EXPLAIN cho thấy quét tuần tự và sắp xếp, một chỉ mục B-tree khớp lọc và sắp xếp thường khắc phục:
CREATE INDEX ON customers (tenant_id, status, created_at DESC);
Nếu phần chậm là lọc JSONB như metadata @\u003e '{"plan":"pro"}', đó chỉ thẳng tới GIN. Nếu là tìm kiếm toàn văn như to_tsvector(...) @@ plainto_tsquery(...), đó cũng chỉ thẳng tới chỉ mục GIN. Nếu là các toán tử style “closest match” hoặc overlap, GiST thường phù hợp.
Sau khi thêm chỉ mục, đo lại sự đánh đổi. Kiểm tra kích thước chỉ mục, thời gian insert và update, và liệu nó giúp top vài truy vấn chậm hay chỉ một trường hợp lẻ. Trong các dự án thay đổi nhanh (kể cả ones built on Koder.ai), việc kiểm tra lại này giúp bạn tránh tích tụ chỉ mục không dùng.
Phần lớn vấn đề không phải là chọn B-tree vs GIN vs GiST. Mà là xây chỉ mục trông đúng mà không khớp cách app truy vấn bảng.
Những lỗi hay gặp nhất:
tenant_id và created_at, nhưng chỉ mục bắt đầu bằng created_at, planner có thể bỏ qua.status, is_active hoặc boolean thường ít giúp vì chúng khớp quá nhiều hàng. Nên ghép với cột có chọn lọc (như tenant_id hoặc created_at) hoặc bỏ qua.ANALYZE chưa chạy gần đây, planner có thể chọn kế hoạch tệ dù chỉ mục đúng tồn tại.Ví dụ cụ thể: màn hình Invoices lọc theo tenant_id và status, rồi sắp theo created_at DESC. Chỉ mục chỉ trên status hầu như không giúp. Một giải pháp tốt hơn là chỉ mục ghép bắt đầu với tenant_id, sau đó status, rồi created_at (lọc trước, sắp xếp sau). Thay đổi này thường đánh bại việc thêm ba chỉ mục riêng lẻ.
Hãy coi mỗi chỉ mục là một chi phí. Nó phải chứng minh giá trị trong truy vấn thực tế, không chỉ trên lý thuyết.
Thay đổi chỉ mục dễ deploy và phiền phức để hoàn tác nếu chúng tăng chi phí ghi hoặc khóa bảng bận rộn. Trước khi merge, coi đó như một bản release nhỏ.
Bắt đầu bằng việc quyết định bạn tối ưu gì. Lấy hai bảng xếp hạng ngắn từ logs hoặc monitoring: truy vấn chạy nhiều nhất, và truy vấn có độ trễ cao nhất. Với mỗi truy vấn, ghi lại chính xác hình dạng: cột lọc, thứ tự sắp xếp, join, và các toán tử dùng (equals, range, IN, ILIKE, toán tử JSONB, array contains). Điều này tránh phỏng đoán và giúp bạn chọn loại chỉ mục đúng.
Checklist trước khi phát hành:
Sau khi thêm chỉ mục, xác nhận nó giúp trong kế hoạch thực tế. Chạy EXPLAIN (ANALYZE, BUFFERS) trên truy vấn chính xác và so sánh trước/sau. Rồi theo dõi production trong một ngày:
Nếu bạn xây với Koder.ai, đáng để giữ SQL sinh ra cho một hai màn hình chậm cạnh thay đổi, để chỉ mục khớp chính xác với những gì app chạy.
Hình dung một màn hình admin phổ biến: danh sách Users với scope tenant, vài bộ lọc, sắp theo last active, và một ô tìm kiếm. Đây là nơi chỉ mục ngừng ở lý thuyết và bắt đầu tiết kiệm thời gian thực.
Ba dạng truy vấn bạn sẽ thường thấy:
-- 1) List page with tenant + status filter + sort
SELECT id, email, last_active_at
FROM users
WHERE tenant_id = $1 AND status = $2
ORDER BY last_active_at DESC
LIMIT 50;
-- 2) Search box (full-text)
SELECT id, email
FROM users
WHERE tenant_id = $1
AND to_tsvector('simple', coalesce(name,'') || ' ' || coalesce(email,'')) @@ plainto_tsquery($2)
ORDER BY last_active_at DESC
LIMIT 50;
-- 3) Filter on JSON metadata (plan, flags)
SELECT id
FROM users
WHERE tenant_id = $1
AND metadata @\u003e '{"plan":"pro"}'::jsonb;
Một bộ chỉ mục nhỏ nhưng có mục đích cho màn hình này:
(tenant_id, status, last_active_at DESC).tsvector sinh ra với chỉ mục GIN.GIN (metadata) khi bạn dùng @\u003e thường xuyên, hoặc chỉ mục B-tree biểu thức như ((metadata-\u003e\u003e'plan')) khi bạn chủ yếu lọc một khóa.Nhu cầu hỗn hợp là bình thường. Nếu một trang làm cả lọc + tìm kiếm + JSON, tránh nhồi nhét mọi thứ vào một chỉ mục khổng lồ. Giữ B-tree cho sắp xếp/phân trang, rồi thêm một chỉ mục chuyên biệt (thường là GIN) cho phần tốn kém.
Bước tiếp theo: chọn một màn hình chậm, ghi lại 2-3 dạng truy vấn hàng đầu của nó, và xem lại mỗi chỉ mục theo mục đích (lọc, sắp xếp, tìm kiếm, JSON). Nếu chỉ mục không rõ ràng khớp với truy vấn thực tế, bỏ nó khỏi kế hoạch. Nếu bạn lặp nhanh trên Koder.ai, làm việc này khi thêm màn hình mới có thể ngăn chặn phình chỉ mục khi schema vẫn thay đổi.
Một chỉ mục cho phép PostgreSQL tìm các hàng phù hợp mà không phải đọc hầu hết bảng. Với các màn hình SaaS thông dụng như danh sách, dashboard và tìm kiếm, chỉ mục phù hợp có thể biến một quét tuần tự chậm thành một truy vấn tra cứu nhanh, có khả năng mở rộng khi bảng lớn lên.
Bắt đầu với B-tree cho hầu hết truy vấn ứng dụng vì nó tốt cho các bộ lọc =, bộ lọc phạm vi, join và ORDER BY. Nếu truy vấn chủ yếu là về tính bao chứa (JSONB, mảng) hoặc tìm kiếm văn bản, thì GIN thường là lựa chọn tiếp theo; GiST phù hợp hơn với các trường hợp overlap hoặc truy vấn kiểu “gần nhất”.
Đặt các cột bạn lọc bằng = ở đầu, sau đó đặt cột bạn sắp xếp ở cuối. Thứ tự này khớp cách planner có thể duyệt chỉ mục hiệu quả, nên vừa lọc vừa trả về hàng theo thứ tự mà không cần sắp xếp thêm.
Nếu mọi truy vấn đều được giới hạn theo tenant_id, đưa tenant_id lên đầu giữ cho dữ liệu mỗi tenant gom lại trong chỉ mục. Điều này thường giảm lượng mục chỉ mục và dữ liệu bảng PostgreSQL phải truy cập cho các trang danh sách hàng ngày.
INCLUDE cho phép thêm các cột bổ sung để hỗ trợ index-only reads cho các trang danh sách mà không làm rộng khóa chỉ mục. Nó hữu ích khi bạn lọc và sắp xếp bằng vài cột nhưng cũng hiển thị vài trường phụ trên màn hình.
Dùng partial index khi bạn chỉ quan tâm một tập con hàng, ví dụ “chưa xóa” hoặc “chỉ active”. Nó giữ chỉ mục nhỏ hơn và rẻ hơn để bảo trì, điều này quan trọng với các bảng nóng có nhiều insert và update.
Dùng GIN trên cột JSONB khi bạn thường xuyên truy vấn theo phép bao chứa như metadata @\u003e '{"plan":"pro"}'. Nếu bạn chủ yếu lọc theo một vài khóa JSON cụ thể, chỉ mục biểu thức B-tree trên (metadata-\u003e\u003e'plan') thường nhỏ hơn và nhanh hơn.
GIN phù hợp khi câu hỏi chính của bạn là “mảng này có chứa X không?” dùng các toán tử như @\u003e hoặc \u0026\u0026. Nếu bạn cần metadata cho từng phần tử, cập nhật thường xuyên hoặc phân tích theo nhãn, bảng liên kết (join table) thường dễ duy trì và lập chỉ mục tốt hơn.
Với tìm kiếm toàn văn, lưu một tsvector (thường là cột sinh) và lập chỉ mục nó bằng GIN, sau đó truy vấn với @@ để có kết quả theo độ liên quan. Với khớp mờ như ILIKE '%name%' và dung sai lỗi chính tả, chỉ mục trigram (thường GIN) thường là công cụ phù hợp.
Sao chép chính xác SQL ứng dụng bạn chạy và chạy EXPLAIN (ANALYZE, BUFFERS) để thấy nơi tốn thời gian và liệu bạn đang quét, sắp xếp, hay lọc đắt tiền. Thêm chỉ mục nhỏ nhất khớp với toán tử và thứ tự sắp xếp của truy vấn, rồi chạy lại EXPLAIN để xác nhận nó được dùng và cải thiện kế hoạch.