使用來自許多表的列提高 order by 的性能
使用 PostgreSQL 8.4,我正在嘗試使用 order by 和兩個表的索引列來查閱具有 100 萬條記錄的兩個表,並且我正在失去性能(1 列需要 30 毫秒,兩列需要 5 分鐘)。例如:
select r.code, r.test_code, r.sample_code, s.barcode, s.registry_date from requests r inner join samples s on (s.code = r.sample_code) order by s.barcode asc , r.code asc limit 21;
表資訊:
CREATE TABLE public.samples ( code BIGINT NOT NULL, barcode VARCHAR(40) NOT NULL, registry_date TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT samples_pkey PRIMARY KEY(code) ); CREATE INDEX idx_samp_barcode ON public.samples (barcode); CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code); CREATE INDEX idx_samp_barcode_code_desc ON public.samples (barcode DESC, code DESC); CREATE INDEX idx_test_identifier_desc ON public.samples (barcode DESC); CREATE TABLE public.requests ( code BIGINT NOT NULL, test_code INTEGER NOT NULL, sample_code BIGINT NOT NULL, CONSTRAINT requests_pkey PRIMARY KEY(code), CONSTRAINT "Requests_S_fk" FOREIGN KEY (sample_code) REFERENCES public.samples(code) ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE, CONSTRAINT "Requests_T_fk" FOREIGN KEY (test_code) REFERENCES public.tests(code) ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE ); CREATE INDEX idx_req_test_code ON public.requests (test_code); CREATE INDEX idx_req_test_code_desc ON public.requests (test_code DESC); CREATE INDEX request_sample_code_index ON public.requests (sample_code); CREATE INDEX requests_sample_code_desc_idx ON public.requests (sample_code DESC, code DESC); CREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);
- 每個表有 100 萬行。
- 兩個表中的所有行都是不同的。
- 如果我單獨按任一列排序,則執行時間約為 30 毫秒。
- 沒有要求就沒有樣品。
- 一個樣本可以有很多
s.code
個 smaes.barcode
。我怎樣才能提高性能?
問題
這是一個比一眼就能看出的更複雜的問題。您正在按兩列排序,每列來自不同的表,同時您連接其他兩列。這使得 Postgres 無法使用提供的索引,並且必須預設(非常)昂貴的順序掃描。這是 dba.SE 上的一個相關案例:
索引
您需要兩個索引以獲得最佳性能,這兩個索引都已經存在:
CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code); CREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);
但是您擁有的普通查詢無法使用它們,即使在 pg 9.4 中也是如此。
詢問
Postgres 8.4 是一個相當大的障礙。但我想我找到了一種遞歸 CTE 的方法:
WITH RECURSIVE cte AS ( ( -- all parentheses are required SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date FROM ( SELECT s.barcode FROM samples s -- WHERE EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code) ORDER BY s.barcode LIMIT 1 -- get smallest barcode to start with ) s0 JOIN samples s USING (barcode) -- join all samples with same barcode JOIN requests r ON r.sample_code = s.code ORDER BY r.code -- start with ordered list ) UNION ALL ( SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date FROM ( SELECT s.barcode FROM cte c JOIN samples s ON s.barcode > c.barcode -- WHERE EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code) ORDER BY s.barcode LIMIT 1 -- get next higher barcode ) s0 JOIN samples s USING (barcode) -- join all samples with same barcode JOIN requests r ON r.sample_code = s.code ORDER BY r.code -- again, ordered list ) ) TABLE cte LIMIT 21;
在 pg 9.4 中測試並為我工作。它使用索引(部分用於 pg 9.2+ 中的僅索引掃描)。
第 8.4 頁已經有遞歸 CTE。剩下的是基本的 SQL(甚至
TABLE
在 pg 8.4 中可用(並且可以替換為SELECT * FROM
)。我希望沒有限制破壞聚會,我不再安裝 pg 8.4。解釋
評論部分:
-- WHERE EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)
如果可能有根本沒有請求的樣本,則需要,您在評論中排除了這一點。
該查詢依賴於遞歸 CTE 的這種記錄行為:
提示:遞歸查詢評估算法以 廣度優先搜尋順序生成其輸出。
大膽強調我的。在這方面,也是:
這是有效的,因為 PostgreSQL 的實現只評估查詢的行數**,與父查詢實際獲取的****行數一樣多
WITH
**。不建議在生產中使用此技巧,因為其他系統可能會以不同的方式工作。此外,如果您使外部查詢對遞歸查詢的結果進行排序或將它們連接到其他表,它通常不起作用。大膽強調我的。因此,只有在您不在
ORDER BY
外部查詢中添加另一個時,才能保證在 PostgreSQL 中工作。
LIMIT
這對於外部查詢中的(相對)較小的查詢來說很快。對於手頭的情況,應該很快。該查詢對於大型LIMIT
.PL/pgSQL 函式
我能想到的另一個選擇是使用 plpgsql 的程序解決方案。應該會快一點,尤其是對於重複呼叫:
CREATE OR REPLACE FUNCTION f_demo(_limit int) RETURNS TABLE (code int8, test_code int, sample_code int8 , barcode varchar, registry_date timestamptz) AS $func$ DECLARE _barcode text; _n int; _rest int := _limit; -- init with _limit parameter BEGIN FOR _barcode IN SELECT DISTINCT s.barcode FROM samples s -- WHERE EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code) ORDER BY s.barcode LOOP RETURN QUERY SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date FROM samples s JOIN requests r ON r.sample_code = s.code WHERE s.barcode = _barcode ORDER BY r.code LIMIT _limit; GET DIAGNOSTICS _n = ROW_COUNT; _rest := _rest - _n; EXIT WHEN _rest < 1; END LOOP; END $func$ LANGUAGE plpgsql;
稱呼:
SELECT * FROM f_demo(21);
MATERIALIZED VIEW
large
OFFSET
具有與 large 類似的效果LIMIT
。雖然所有跳過的行都不會添加到 I/O,但仍需要計算它們以確定偏移*後的第一行。*如果需要,請使用MATERIALIZED VIEW
帶有添加行號和索引的 a。Postgres 9.3+ 有一個內置功能,使用過時的軟體,您必須手動編寫解決方案。不過,這並不復雜。基本上是從預定義
SELECT
語句或VIEW
類似的填充的快照表。基本上,將表創建為:CREATE TABLE req_sample AS SELECT row_number() OVER (ORDER BY s.barcode, r.code) AS rn , r.code, r.test_code, r.sample_code, s.barcode, s.registry_date FROM requests r JOIN samples s on (s.code = r.sample_code) ORDER by s.barcode, r.code; ALTER TABLE req_sample ADD CONSTRAINT req_sample_rk PRIMARY KEY (code); CREATE INDEX foo ON req_sample (rn);
將該 SELECT 保存為函式、視圖或純查詢文本。在一筆交易中刷新(昂貴):
BEGIN; -- drop PK & index TRUNCATE req_sample; INSERT INTO req_sample SELECT ... ; -- add PK & index COMMIT;
冗餘
barcode
列如果基礎表發生很大變化並且您需要“目前”結果,則 MV 會變得更加昂貴。還有一個更***便宜的選擇:*將 儲存
barcode
在requests
表中冗餘。您可以將其作為 FK 約束中的第二列包含在samples
("Requests_S_fk"
) 中,並ON UPDATE CASCADE
避免數據過時/衝突。然後您可以使用 in 上的(barcode, code)
索引requests
。對於最後兩個選項,您可以使用直接適用的索引簡化分頁,並使用
WHERE
上一頁中的值的子句替換OFFSET
. 考慮一下use-the-index-luke.com 上關於“以 PostgreSQL 方式完成分頁”的展示文稿。