兩台伺服器上的Postgresql查詢計劃不同
我在 prod 和 dev 之間執行相同的查詢並收到一些不同的查詢計劃時遇到了一些奇怪的問題,我不知道為什麼。我會盡我所能,但如果我忘記了一些重要的事情,請告訴我。
PostgreSQL:11.6
GCE 產品規格:
- 中央處理器:6
- 記憶體:64g
- 儲存:固態硬碟
GCE 開發規範:
- 中央處理器:4
- 記憶體:24g
- 儲存:硬碟
產品配置:
shared_buffers = 16GB seq_page_cost = 1.0 random_page_cost = 3.0 effective_cache_size = 48GB work_mem = 512MB maintenance_work_mem = 2GB effective_io_concurrency = 200
開發配置:
shared_buffers = 3840MB seq_page_cost = 1.0 random_page_cost = 4.0 effective_cache_size = 11520MB work_mem = 512MB maintenance_work_mem = 960MB effective_io_concurrency = 4
表定義
Table "api.ms1_peak" Column | Type | Collation | Nullable | Default -----------------------+------------------+-----------+----------+--------------------------------------------------- ms1_peak_id | integer | | not null | nextval('api.ms1_peak_ms1_peak_id_seq'::regclass) mzml_id | integer | | | charge | integer | | | mz | double precision | | | adduct_m | double precision | | | rt_corrected | double precision | | | Indexes: "ms1_peak_pkey" PRIMARY KEY, btree (ms1_peak_id) "ms1_peak_ms1_peak_id_idx" btree (ms1_peak_id) INCLUDE (mz, rt_corrected) "ms1_peak_mz_rt_ms1_peak_id_idx" btree (mz, rt_corrected) INCLUDE (ms1_peak_id, charge) "ms1_peak_mzml_idx" btree (mzml_id) Foreign-key constraints: "ms1_peak_mzml_id_fkey" FOREIGN KEY (mzml_id) REFERENCES api.mzml(mzml_id) ON DELETE CASCADE
Table "api.molecular_structure" Column | Type | Collation | Nullable | Default ------------------------+------------------+-----------+----------+------------------------------------------------------------------------- molecular_structure_id | integer | | not null | nextval('api.molecular_structure_molecular_structure_id_seq'::regclass) monoisotopic_mass | double precision | | | mol | mol | | not null | Indexes: "molecular_structure_pkey" PRIMARY KEY, btree (molecular_structure_id) "molecular_structure_unique_idx" UNIQUE, btree (md5(mol::text)) "molecular_structure_monoisotopic_mass_idx" btree (monoisotopic_mass, molecular_structure_id) Referenced by: TABLE "api.smallmolecule_fragment" CONSTRAINT "smallmolecule_molecular_structure_id_fkey" FOREIGN KEY (molecular_structure_id) REFERENCES api.molecular_structure(molecular_structure_id) ON DELETE CASCADE TABLE "api.smallmolecule" CONSTRAINT "smallmolecule_molecular_structure_id_fkey" FOREIGN KEY (molecular_structure_id) REFERENCES api.molecular_structure(molecular_structure_id) ON DELETE SET NULL
Table "api.smallmolecule" Column | Type | Collation | Nullable | Default ------------------------+--------------------------+-----------+----------+------------------------------------------------------------- smallmolecule_id | integer | | not null | nextval('api.smallmolecule_smallmolecule_id_seq'::regclass) created_date | timestamp with time zone | | | now() original_smiles | text | | | reference | json | | | names | jsonb | | | cas | text | | | names_tsvector | tsvector | | | molecular_structure_id | integer | | | Indexes: "smallmolecule_pkey" PRIMARY KEY, btree (smallmolecule_id) "smallmolecule_molecular_structure_id_idx" btree (molecular_structure_id, smallmolecule_id) "smallmolecule_names_idx" gist (names_tsvector) Foreign-key constraints: "smallmolecule_molecular_structure_id_fkey" FOREIGN KEY (molecular_structure_id) REFERENCES api.molecular_structure(molecular_structure_id) ON DELETE SET NULL "smallmolecule_organization_id_fkey" FOREIGN KEY (organization_id) REFERENCES api.organization(organization_id) ON DELETE SET NULL
有問題的查詢
select p.ms1_peak_id, ( select coalesce(json_agg(sm.smallmolecule_id), '[]') from api.molecular_structure as ms join api.smallmolecule sm using(molecular_structure_id) where ms.monoisotopic_mass BETWEEN p.adduct_m - public.mz_tol(p.adduct_m, 5) AND p.adduct_m + public.mz_tol(p.adduct_m, 5) ) as smallmolecules from api.ms1_peak as p where p.mzml_id = 20634 and adduct_m is not null order by ms1_peak_id asc;
在開發伺服器上,這是查詢計劃
Sort (cost=94236607.88..94236610.32 rows=973 width=36) (actual time=173.075..173.144 rows=1082 loops=1) Output: p.ms1_peak_id, ((SubPlan 1)) Sort Key: p.ms1_peak_id Sort Method: quicksort Memory: 106kB Buffers: shared hit=58597 -> Bitmap Heap Scan on api.ms1_peak p (cost=39.56..94236559.59 rows=973 width=36) (actual time=17.735..172.456 rows=1082 loops=1) Output: p.ms1_peak_id, (SubPlan 1) Recheck Cond: (p.mzml_id = 20634) Filter: (p.adduct_m IS NOT NULL) Rows Removed by Filter: 1318 Heap Blocks: exact=399 Buffers: shared hit=58597 -> Bitmap Index Scan on ms1_peak_mzml_idx (cost=0.00..39.32 rows=1967 width=0) (actual time=0.180..0.180 rows=2400 loops=1) Index Cond: (p.mzml_id = 20634) Buffers: shared hit=12 SubPlan 1 -> Aggregate (cost=96843.52..96843.53 rows=1 width=32) (actual time=0.157..0.157 rows=1 loops=1082) Output: COALESCE(json_agg(sm.smallmolecule_id), '[]'::json) Buffers: shared hit=58186 -> Hash Join (cost=6062.96..96842.90 rows=246 width=4) (actual time=0.136..0.156 rows=1 loops=1082) Output: sm.smallmolecule_id Hash Cond: (ms.molecular_structure_id = sm.molecular_structure_id) Buffers: shared hit=58186 -> Bitmap Heap Scan on api.molecular_structure ms (cost=816.07..91096.68 rows=26500 width=4) (actual time=0.019..0.111 rows=89 loops=1082) Output: ms.molecular_structure_id, ms.molecular_weight, ms.monoisotopic_mass, ms.molecular_formula, ms.flat_smiles, ms.mol, ms.ecfp6, ms.fcfp6 Recheck Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Heap Blocks: exact=54237 Buffers: shared hit=57961 -> Bitmap Index Scan on molecular_structure_monoisotopic_mass_idx (cost=0.00..809.45 rows=26500 width=0) (actual time=0.012..0.012 rows=89 loops=1082) Index Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Buffers: shared hit=3724 -> Hash (cost=4633.03..4633.03 rows=49108 width=8) (actual time=16.781..16.782 rows=49102 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Buckets: 65536 Batches: 1 Memory Usage: 2431kB Buffers: shared hit=225 -> Index Only Scan using smallmolecule_molecular_structure_id_idx on api.smallmolecule sm (cost=0.41..4633.03 rows=49108 width=8) (actual time=0.030..7.495 rows=49108 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Heap Fetches: 0 Buffers: shared hit=225 Planning Time: 0.440 ms Execution Time: 173.291 ms
在生產中
Sort (cost=3158406.45..3158408.86 rows=965 width=36) (actual time=10773.364..10773.433 rows=1082 loops=1) Output: p.ms1_peak_id, ((SubPlan 1)) Sort Key: p.ms1_peak_id Sort Method: quicksort Memory: 106kB Buffers: shared hit=358573 -> Index Scan using ms1_peak_mzml_idx on api.ms1_peak p (cost=0.56..3158358.61 rows=965 width=36) (actual time=14.920..10770.308 rows=1082 loops=1) Output: p.ms1_peak_id, (SubPlan 1) Index Cond: (p.mzml_id = 20634) Filter: (p.adduct_m IS NOT NULL) Rows Removed by Filter: 1318 Buffers: shared hit=358573 SubPlan 1 -> Aggregate (cost=3266.75..3266.76 rows=1 width=32) (actual time=9.924..9.924 rows=1 loops=1082) Output: COALESCE(json_agg(sm.smallmolecule_id), '[]'::json) Buffers: shared hit=357961 -> Hash Join (cost=1164.60..3266.13 rows=246 width=4) (actual time=8.142..9.913 rows=1 loops=1082) Output: sm.smallmolecule_id Inner Unique: true Hash Cond: (sm.molecular_structure_id = ms.molecular_structure_id) Buffers: shared hit=357961 -> Index Only Scan using smallmolecule_molecular_structure_id_idx on api.smallmolecule sm (cost=0.41..1973.03 rows=49108 width=8) (actual time=0.009..5.957 rows=49108 loops=1017) Output: sm.molecular_structure_id, sm.smallmolecule_id Heap Fetches: 0 Buffers: shared hit=322390 -> Hash (cost=834.29..834.29 rows=26392 width=4) (actual time=0.087..0.087 rows=89 loops=1082) Output: ms.molecular_structure_id Buckets: 32768 Batches: 1 Memory Usage: 262kB Buffers: shared hit=35571 -> Index Only Scan using molecular_structure_monoisotopic_mass_idx on api.molecular_structure ms (cost=0.45..834.29 rows=26392 width=4) (actual time=0.032..0.071 rows=89 loops=1082) Output: ms.molecular_structure_id Index Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Heap Fetches: 0 Buffers: shared hit=35571 Planning Time: 2.329 ms Execution Time: 10773.584 ms
有趣的是在 dev 上我得到了一個不同的查詢計劃。它通常使用點陣圖掃描,而在生產中它使用僅索引/索引掃描。
現在,如果我禁用點陣圖掃描強制更多的僅索引路由,我可以讓開發伺服器輸出更接近的查詢計劃到 prod
這是設置後的開發輸出
set enable_bitmapscan = off;
Sort (cost=107687464.30..107687466.74 rows=973 width=36) (actual time=166.059..166.129 rows=1082 loops=1) Output: p.ms1_peak_id, ((SubPlan 1)) Sort Key: p.ms1_peak_id Sort Method: quicksort Memory: 106kB Buffers: shared hit=58988 -> Index Scan using ms1_peak_mzml_idx on api.ms1_peak p (cost=0.56..107687416.01 rows=973 width=36) (actual time=17.591..165.425 rows=1082 loops=1) Output: p.ms1_peak_id, (SubPlan 1) Index Cond: (p.mzml_id = 20634) Filter: (p.adduct_m IS NOT NULL) Rows Removed by Filter: 1318 Buffers: shared hit=58988 SubPlan 1 -> Aggregate (cost=110667.53..110667.54 rows=1 width=32) (actual time=0.150..0.150 rows=1 loops=1082) Output: COALESCE(json_agg(sm.smallmolecule_id), '[]'::json) Buffers: shared hit=58376 -> Hash Join (cost=5247.33..110666.91 rows=246 width=4) (actual time=0.123..0.149 rows=1 loops=1082) Output: sm.smallmolecule_id Hash Cond: (ms.molecular_structure_id = sm.molecular_structure_id) Buffers: shared hit=58376 -> Index Only Scan using molecular_structure_monoisotopic_mass_idx on api.molecular_structure ms (cost=0.45..104920.69 rows=26500 width=4) (actual time=0.010..0.106 rows=89 loops=1082) Output: ms.monoisotopic_mass, ms.molecular_structure_id Index Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Heap Fetches: 96251 Buffers: shared hit=58151 -> Hash (cost=4633.03..4633.03 rows=49108 width=8) (actual time=16.941..16.941 rows=49102 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Buckets: 65536 Batches: 1 Memory Usage: 2431kB Buffers: shared hit=225 -> Index Only Scan using smallmolecule_molecular_structure_id_idx on api.smallmolecule sm (cost=0.41..4633.03 rows=49108 width=8) (actual time=0.027..7.519 rows=49108 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Heap Fetches: 0 Buffers: shared hit=225
這個查詢更接近生產,而且速度更快(dev 上的兩種情況都比 prod 快)但是兩者之間的查詢計劃仍然略有不同。請注意,prod 查詢計劃使用內部唯一性:true while 將索引放在底部的“molecular_structure_monoisotopix_mass_idx”上並循環 1082 次,而在 dev 上,此索引使用率在解釋中較高,而不使用內部唯一性:true
通過 explain.depesz 將這些查詢計劃分解並顯示生產查詢計劃程序在 api.smallmolecule 上僅掃描索引需要 6 秒,而在雜湊連接上需要 4 秒。
在 dev 上,僅掃描 api.smallmolecule 的索引為 8ms,hash join 為 28ms。
開發和生產上的分子結構和小分子數據是相同的。兩者都被抽真空和分析。所以這些表的數據集是相同的。
分子結構上約 520 萬行,小分子上約 49000 行。
通過調整配置,我可以設法讓開發伺服器執行並輸出完全相同的計劃,當我設置
random_page_cost = 2.0
. 這會產生完全相同的計劃。所以我開始認為這絕對是兩者之間的調整問題。在 prod 上使用 random_page_cost 進行調整似乎對此特定查詢沒有影響。這些伺服器位於 Google Cloud Engine 上,prod 使用持久 SSD,因此我知道在開發人員使用 HDD 時,使用專用 SSD 設置調整某些值可能不准確。
關於如何在生產中調整或調試以獲得更好結果的任何想法都會很棒。任何我想念的東西都讓我知道。
編輯:
真空分析已在生產中執行,這是新數據,因此沒有新的插入/更新/刪除,所以我不認為索引碎片是問題所在。
在生產中,如果我禁用索引掃描
set enable_indexscans = 'off'
,我會得到與開發機器性能匹配的點陣圖堆掃描計劃和 180 毫秒。Sort (cost=70758692.10..70758694.51 rows=966 width=36) (actual time=179.621..179.692 rows=1082 loops=1) Output: p.ms1_peak_id, ((SubPlan 1)) Sort Key: p.ms1_peak_id Sort Method: quicksort Memory: 106kB Buffers: shared hit=60536 -> Bitmap Heap Scan on api.ms1_peak p (cost=42.52..70758644.20 rows=966 width=36) (actual time=29.350..179.233 rows=1082 loops=1) Output: p.ms1_peak_id, (SubPlan 1) Recheck Cond: (p.mzml_id = 20634) Filter: (p.adduct_m IS NOT NULL) Rows Removed by Filter: 1318 Heap Blocks: exact=399 Buffers: shared hit=60536 -> Bitmap Index Scan on ms1_peak_mzml_idx (cost=0.00..42.28 rows=1962 width=0) (actual time=0.169..0.169 rows=2400 loops=1) Index Cond: (p.mzml_id = 20634) Buffers: shared hit=12 SubPlan 1 -> Aggregate (cost=73243.01..73243.02 rows=1 width=32) (actual time=0.163..0.163 rows=1 loops=1082) Output: COALESCE(json_agg(sm.smallmolecule_id), '[]'::json) Buffers: shared hit=60125 -> Hash Join (cost=3970.76..73242.40 rows=246 width=4) (actual time=0.145..0.162 rows=1 loops=1082) Output: sm.smallmolecule_id Hash Cond: (ms.molecular_structure_id = sm.molecular_structure_id) Buffers: shared hit=60125 -> Bitmap Heap Scan on api.molecular_structure ms (cost=576.97..69450.26 rows=26392 width=4) (actual time=0.019..0.106 rows=89 loops=1082) Output: ms.molecular_structure_id, ms.molecular_weight, ms.monoisotopic_mass, ms.molecular_formula, ms.flat_smiles, ms.mol, ms.ecfp6, ms.fcfp6 Recheck Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Heap Blocks: exact=54237 Buffers: shared hit=57836 -> Bitmap Index Scan on molecular_structure_monoisotopic_mass_idx (cost=0.00..570.37 rows=26392 width=0) (actual time=0.012..0.012 rows=89 loops=1082) Index Cond: ((ms.monoisotopic_mass >= (p.adduct_m - (('5'::double precision * p.adduct_m) / '1000000'::double precision))) AND (ms.monoisotopic_mass <= (p.adduct_m + (('5'::double precision * p.adduct_m) / '1000000'::double precision)))) Buffers: shared hit=3599 -> Hash (cost=2780.02..2780.02 rows=49102 width=8) (actual time=28.821..28.822 rows=49102 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Buckets: 65536 Batches: 1 Memory Usage: 2431kB Buffers: shared hit=2289 -> Seq Scan on api.smallmolecule sm (cost=0.00..2780.02 rows=49102 width=8) (actual time=0.007..19.592 rows=49108 loops=1) Output: sm.smallmolecule_id, sm.molecular_structure_id Buffers: shared hit=2289 Planning Time: 0.461 ms Execution Time: 179.824 ms (40 rows)
我注意到的另一件事是,如果我刪除子查詢中的連接
smallmolecule table
,這個查詢會變得非常快。編輯2:
我設法做一個 pg_basebackup 並在開發和生產之間完全同步數據。唯一多了幾百萬個插入的表是 ms1_peak 表,在這些結果期間,molecules_structure 和 smallmolecule 表在兩者之間從未改變。
ms1_peak 的結果集在過時的 dev 和 prod 之間仍然返回相同的結果(1082 個峰值)。
但無論如何,我仍然不相信大型峰值表是原因,因為該查詢非常快。但是現在 dev 和 prod 都產生了完全相同的查詢計劃和 10 秒的性能。為什麼加入小分子表超級慢。
問題來自對點陣圖索引掃描
api.molecular_structure
(26392 而不是實際的 89)的錯誤行計數估計,並且它必須是傾斜計劃的數據中的微小差異。估計很難改進。
重寫查詢怎麼樣?
SELECT p.ms1_peak_id, coalesce(json_agg(sm.smallmolecule_id), '[]') FROM api.ms1_peak AS p LEFT JOIN api.molecular_structure AS ms ON ms.monoisotopic_mass BETWEEN p.adduct_m - public.mz_tol(p.adduct_m, 5) AND p.adduct_m + public.mz_tol(p.adduct_m, 5) LEFT JOIN api.smallmolecule sm USING (molecular_structure_id) WHERE p.mzml_id = 20634 AND p.adduct_m IS NOT NULL GROUP BY p.ms1_peak_id ORDER BY p.ms1_peak_id ASC;
不確定這是否會提高速度,但值得一試。
由於您的產品上有 SSD,您是否嘗試過設置 seq_page_cost = random_page_cost = 1.0?有很多文章是這樣建議的。
我們觀察到了一個類似的問題,這是因為我們的開發環境在生產環境中數據量很小。有很多已終止的客戶端數據(未觸及的行)正在污染索引。因此,我們調整了索引以排除終止客戶的數據,然後在 prod/dev 上的性能大致相同。