Thiết lập
Tôi đang xây dựng trên thiết lập của @ Jack để giúp mọi người theo dõi và so sánh dễ dàng hơn. Đã thử nghiệm với PostgreSQL 9.1.4 .
CREATE TABLE lexikon (
lex_id serial PRIMARY KEY
, word text
, frequency int NOT NULL -- we'd need to do more if NULL was allowed
, lset int
);
INSERT INTO lexikon(word, frequency, lset)
SELECT 'w' || g -- shorter with just 'w'
, (1000000 / row_number() OVER (ORDER BY random()))::int
, g
FROM generate_series(1,1000000) g
Từ đây tôi đi một con đường khác:
ANALYZE lexikon;
Bảng phụ
Giải pháp này không thêm các cột vào bảng gốc, nó chỉ cần một bảng trợ giúp nhỏ. Tôi đặt nó trong lược đồ public
, sử dụng bất kỳ lược đồ nào bạn chọn.
CREATE TABLE public.lex_freq AS
WITH x AS (
SELECT DISTINCT ON (f.row_min)
f.row_min, c.row_ct, c.frequency
FROM (
SELECT frequency, sum(count(*)) OVER (ORDER BY frequency DESC) AS row_ct
FROM lexikon
GROUP BY 1
) c
JOIN ( -- list of steps in recursive search
VALUES (400),(1600),(6400),(25000),(100000),(200000),(400000),(600000),(800000)
) f(row_min) ON c.row_ct >= f.row_min -- match next greater number
ORDER BY f.row_min, c.row_ct, c.frequency DESC
)
, y AS (
SELECT DISTINCT ON (frequency)
row_min, row_ct, frequency AS freq_min
, lag(frequency) OVER (ORDER BY row_min) AS freq_max
FROM x
ORDER BY frequency, row_min
-- if one frequency spans multiple ranges, pick the lowest row_min
)
SELECT row_min, row_ct, freq_min
, CASE freq_min <= freq_max
WHEN TRUE THEN 'frequency >= ' || freq_min || ' AND frequency < ' || freq_max
WHEN FALSE THEN 'frequency = ' || freq_min
ELSE 'frequency >= ' || freq_min
END AS cond
FROM y
ORDER BY row_min;
Bảng trông như thế này:
row_min | row_ct | freq_min | cond
--------+---------+----------+-------------
400 | 400 | 2500 | frequency >= 2500
1600 | 1600 | 625 | frequency >= 625 AND frequency < 2500
6400 | 6410 | 156 | frequency >= 156 AND frequency < 625
25000 | 25000 | 40 | frequency >= 40 AND frequency < 156
100000 | 100000 | 10 | frequency >= 10 AND frequency < 40
200000 | 200000 | 5 | frequency >= 5 AND frequency < 10
400000 | 500000 | 2 | frequency >= 2 AND frequency < 5
600000 | 1000000 | 1 | frequency = 1
Vì cột cond
sẽ được sử dụng trong SQL động hơn nữa, bạn phải làm cho bảng này an toàn . Luôn lập sơ đồ đủ điều kiện cho bảng nếu bạn không thể chắc chắn về dòng điện thích hợp search_path
và thu hồi các đặc quyền ghi từ public
(và bất kỳ vai trò không đáng tin cậy nào khác):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
Bảng lex_freq
phục vụ ba mục đích:
- Tạo các chỉ mục một phần cần thiết tự động.
- Cung cấp các bước cho chức năng lặp.
- Thông tin meta để điều chỉnh.
Chỉ mục
DO
Tuyên bố này tạo ra tất cả các chỉ mục cần thiết:
DO
$$
DECLARE
_cond text;
BEGIN
FOR _cond IN
SELECT cond FROM public.lex_freq
LOOP
IF _cond LIKE 'frequency =%' THEN
EXECUTE 'CREATE INDEX ON lexikon(lset) WHERE ' || _cond;
ELSE
EXECUTE 'CREATE INDEX ON lexikon(lset, frequency DESC) WHERE ' || _cond;
END IF;
END LOOP;
END
$$
Tất cả các chỉ mục một phần này cùng nhau trải rộng bảng một lần. Chúng có cùng kích thước với một chỉ số cơ bản trên toàn bộ bảng:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Chỉ có 21 MB chỉ mục cho bảng 50 MB cho đến nay.
Tôi tạo ra hầu hết các chỉ mục một phần trên (lset, frequency DESC)
. Cột thứ hai chỉ giúp trong trường hợp đặc biệt. Nhưng vì cả hai cột có liên quan đều thuộc loại integer
, do đặc thù của việc căn chỉnh dữ liệu kết hợp với MAXALIGN trong , cột thứ hai không làm cho chỉ mục lớn hơn. Đó là một chiến thắng nhỏ cho hầu như không có chi phí.
Không có điểm nào trong việc làm điều đó đối với các chỉ mục một phần chỉ kéo dài một tần số duy nhất. Đó chỉ là trên (lset)
. Các chỉ mục được tạo trông như thế này:
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2500;
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 625 AND frequency < 2500;
-- ...
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2 AND frequency < 5;
CREATE INDEX ON lexikon(lset) WHERE freqency = 1;
Chức năng
Chức năng này có phần giống phong cách với giải pháp của @ Jack:
CREATE OR REPLACE FUNCTION f_search(_lset_min int, _lset_max int, _limit int)
RETURNS SETOF lexikon
$func$
DECLARE
_n int;
_rest int := _limit; -- init with _limit param
_cond text;
BEGIN
FOR _cond IN
SELECT l.cond FROM public.lex_freq l ORDER BY l.row_min
LOOP
-- RAISE NOTICE '_cond: %, _limit: %', _cond, _rest; -- for debugging
RETURN QUERY EXECUTE '
SELECT *
FROM public.lexikon
WHERE ' || _cond || '
AND lset >= $1
AND lset <= $2
ORDER BY frequency DESC
LIMIT $3'
USING _lset_min, _lset_max, _rest;
GET DIAGNOSTICS _n = ROW_COUNT;
_rest := _rest - _n;
EXIT WHEN _rest < 1;
END LOOP;
END
$func$ LANGUAGE plpgsql STABLE;
Sự khác biệt chính:
SQL động với RETURN QUERY EXECUTE
.
Khi chúng tôi lặp lại các bước, một kế hoạch truy vấn khác nhau có thể được hưởng lợi. Kế hoạch truy vấn cho SQL tĩnh được tạo một lần và sau đó được sử dụng lại - có thể tiết kiệm một số chi phí. Nhưng trong trường hợp này, truy vấn rất đơn giản và các giá trị rất khác nhau. SQL động sẽ là một chiến thắng lớn.
Năng độngLIMIT
cho mọi bước truy vấn.
Điều này giúp theo nhiều cách: Đầu tiên, các hàng chỉ được tìm nạp khi cần thiết. Kết hợp với SQL động, điều này cũng có thể tạo ra các kế hoạch truy vấn khác nhau để bắt đầu. Thứ hai: Không cần bổ sung LIMIT
trong lệnh gọi hàm để cắt giảm thặng dư.
Điểm chuẩn
Thiết lập
Tôi chọn bốn ví dụ và thực hiện ba bài kiểm tra khác nhau với mỗi bài kiểm tra. Tôi lấy thứ tốt nhất trong năm để so sánh với bộ đệm ấm:
Truy vấn SQL thô có dạng:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
Tương tự sau khi tạo chỉ mục này
CREATE INDEX ON lexikon(lset);
Cần về cùng một không gian với tất cả các chỉ mục một phần của tôi với nhau:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
Chức năng
SELECT * FROM f_search(20000, 30000, 5);
Các kết quả
SELECT * FROM f_search(20000, 30000, 5);
1: Tổng thời gian chạy: 315.458 ms
2: Tổng thời gian chạy: 36.458 ms
3: Tổng thời gian chạy: 0.330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Tổng thời gian chạy: 294.819 ms
2: Tổng thời gian chạy: 18.915 ms
3: Tổng thời gian chạy: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Tổng thời gian chạy: 426.831 ms
2: Tổng thời gian chạy: 217.874 ms
3: Tổng thời gian chạy: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Tổng thời gian chạy: 2458.205 ms
2: Tổng thời gian chạy: 2458.205 ms - đối với phạm vi lớn của lset, quét seq nhanh hơn chỉ mục.
3: Tổng thời gian chạy: 0,266 ms
Phần kết luận
Như mong đợi, lợi ích từ chức năng tăng lên với phạm vi lớn hơn lset
và nhỏ hơnLIMIT
.
Với phạm vi rất nhỏlset
, truy vấn thô kết hợp với chỉ mục thực sự nhanh hơn . Bạn sẽ muốn kiểm tra và có thể phân nhánh: truy vấn thô cho các phạm vi nhỏ lset
, gọi hàm khác. Bạn thậm chí có thể xây dựng nó thành chức năng cho một "thế giới tốt nhất" - đó là những gì tôi sẽ làm.
Tùy thuộc vào phân phối dữ liệu của bạn và các truy vấn thông thường, nhiều bước hơn lex_freq
có thể giúp thực hiện. Thử để tìm điểm ngọt. Với các công cụ được trình bày ở đây, nó sẽ dễ dàng để kiểm tra.