選取文章內文可能會排除僅索引掃描:PostgreSQL 必須擷取索引中未包含的欄位。本指南說明如何在 Mac 上診斷這項變化,並在 Node.js 中測量其影響。僅索引掃描的需求。
為什麼變更選取欄位會改變計畫?
假設有一個文章資料表,包含 id、category 和 body,並在 (category, id) 上建立了 B-tree 索引。依分類選取 ID 的查詢,可以從該索引取得篩選值和結果。加入 body 後,該索引便不足以執行僅索引掃描。PostgreSQL 索引與資料表的主要儲存區分開,後者稱為堆積(heap);一般索引掃描也會擷取資料表資料。僅索引掃描與涵蓋索引。
此時,規劃器會比較擷取所需資料表資料列的不同方式。它可能保留 Index Scan,將 Bitmap Index Scan 與 Bitmap Heap Scan 結合,或選擇 Seq Scan。點陣圖掃描會依實體位置分組存取堆積。若要擷取許多分散的資料列,掃描整個資料表可能比逐筆擷取更便宜。即使最上層的掃描節點提到堆積,點陣圖計畫仍會使用索引。使用 EXPLAIN。
也請仔細檢查原始計畫。Index Only Scan 可能會顯示非零的 Heap Fetches:PostgreSQL 有時需要存取資料表資料列來確認可見性。能否避免這些存取,除了索引是否包含所需欄位,也取決於資料表的可見性映射。僅索引掃描的可見性檢查。
在 Mac 上重現比較
請使用正在執行的本機 PostgreSQL 資料庫,以及已安裝且目前仍受支援的 Node.js 版本與 npm。基準範例使用 PostgreSQL 14–18 都提供的功能;目前取得的 PostgreSQL 手冊標示為第 18 版。下方額外的序列化診斷需要 PostgreSQL 17 或更新版本。node-postgres 文件說明了其 Node.js 相容性與安裝指令。PostgreSQL 文件、node-postgres 安裝與相容性。
開啟 Terminal,並在新資料夾中操作。以下指令會建立專案並安裝資料庫驅動程式:
mkdir article-plan-lab
cd article-plan-lab
npm init -y
npm install pg
程式會透過 PGHOST、PGPORT、PGDATABASE 和 PGUSER 使用你現有的本機連線設定。請為已能連線的資料庫設定這些變數。建立未指定明確連線選項的 Client 時,node-postgres 會讀取這些環境變數。使用環境變數連線。
將以下完整程式儲存為 article-plan.cjs。它會在暫存資料表中建立合成資料列、建立索引、列印兩種執行計畫,接著測量兩個一般查詢。兩項比較使用相同的連線與分類值。查詢設定透過 text 提供 SQL,並透過 values 提供參數,符合 node-postgres 文件所述的 API。查詢設定。
const { Client } = require('pg');
const { performance } = require('node:perf_hooks');
async function main() {
const client = new Client();
try {
await client.connect();
const version = await client.query('SHOW server_version');
console.log(version.rows[0]);
await client.query(`CREATE TEMP TABLE article_plan_demo (
id integer PRIMARY KEY,
category integer NOT NULL,
body text NOT NULL
)`);
await client.query(`INSERT INTO article_plan_demo
SELECT g, g % 10, repeat('Synthetic article paragraph. ', 40)
FROM generate_series(1, 50000) AS s(g)`);
await client.query(`CREATE INDEX article_plan_demo_category_id
ON article_plan_demo (category, id)`);
await client.query('VACUUM (ANALYZE) article_plan_demo');
const queries = [
['IDs', 'SELECT id FROM article_plan_demo WHERE category = $1'],
['Bodies', 'SELECT id, body FROM article_plan_demo WHERE category = $1']
];
for (const [label, text] of queries) {
const plan = await client.query({
text: `EXPLAIN (ANALYZE, BUFFERS, VERBOSE) ${text}`,
values: [3]
});
console.log(label);
console.log(plan.rows.map(row => row['QUERY PLAN']).join('\n'));
const start = performance.now();
const result = await client.query({ text, values: [3] });
const elapsed = performance.now() - start;
const bodyBytes = result.rows.reduce((sum, row) =>
sum + (row.body === undefined ? 0 : Buffer.byteLength(row.body)), 0);
console.log({
rows: result.rows.length,
elapsedMs: elapsed.toFixed(2),
bodyBytes
});
}
} finally {
await client.end();
}
}
main().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
在同一個資料夾中執行:
node article-plan.cjs
初始化程序會將 VACUUM (ANALYZE) 作為獨立指令送出。該指令會先執行清理,再收集統計資料,而且不能在交易區塊內執行。請勿將它放進你為此範例新增的任何交易包裝中。VACUUM。
JavaScript 已通過語法檢查,另外也以獨立的確定性計算確認了測試資料的資料列數與內文位元組總數。研究環境無法使用 PostgreSQL,因此這些資料庫查詢僅經過檢查,未在此執行。本文未將任何特定計畫或執行時間呈現為實測結果。
按照有效的順序閱讀計畫
先看掃描名稱,再比較預估與實際資料列數。接著檢查資料表存取與緩衝區活動。這個順序能分辨存取方法的變化、估算不準確,以及結果擷取成本高昂等情況。VERBOSE 也會顯示每個節點的輸出欄位。使用 EXPLAIN、EXPLAIN 選項。
| 觀察結果 | 解讀 | 下一步檢查 |
|---|---|---|
ID 使用 Index Only Scan;內文使用 Index Scan |
內文查詢需要資料表資料 | 比較緩衝區與一般查詢所需時間 |
內文使用 Bitmap Heap Scan |
檢查其子節點是否使用索引 | 比較堆積區塊數與傳回的資料列數 |
內文使用 Seq Scan |
規劃器偏好掃描資料表 | 比較符合條件的預估資料列數與實際資料列數 |
ID 顯示大量 Heap Fetches |
僅索引執行仍會存取堆積 | 檢查這是否反映近期的資料表變更 |
| 預估值與實際資料列數差異很大 | 統計資料可能無法充分描述資料 | 收集統計資料後再比較 |
ANALYZE 會收集規劃器用來選擇計畫的統計資料。如果你變更合成分類的分布,請重新收集統計資料,再解讀下一次比較。由於採用抽樣,估算值在不同次執行間可能略有差異。ANALYZE。
請將 width 視為每列輸出位元組數的估算值,而非傳輸位元組數的實測值。同樣地,規劃器成本採用相對單位,並非毫秒。緩衝區命中表示在 PostgreSQL 快取中找到區塊;這不代表沒有進行任何工作。計畫估算值、緩衝區報告。
在這組測試資料中,兩個查詢都應傳回 5,000 列。每個合成內文包含 1,160 個 ASCII 位元組,因此內文查詢會傳回 5,800,000 個內文位元組,約 5.53 MiB,還不包括 ID 和通訊協定額外負擔。這是根據範例輸入計算出的數值,並非資料庫效能測量結果。程式中的 bodyBytes 欄位可讓你驗證兩者的差異。
一般查詢會在計畫測量之後執行,因此其經過時間反映了這種預熱後的執行順序。下結論前,請重複比較並交替執行順序。單次執行有助於診斷,但不足以建立可靠的效能比例。
為什麼擷取內文可能比 EXPLAIN 顯示的更耗時?
大型文字值可能經過壓縮,或透過 PostgreSQL 的 TOAST 機制儲存在主資料表之外。因此,擷取資料列的主要資料,與產生完整內文並傳送給用戶端,可能需要不同程度的工作。上方簡短且重複性高的測試資料旨在示範欄位涵蓋範圍,並不代表所有內文儲存模式。TOAST 儲存方式。
依預設,EXPLAIN ANALYZE 會捨棄查詢結果,不會將其轉換為可傳送的格式。在 PostgreSQL 17 及更新版本中,可加入 SERIALIZE TEXT 來檢查輸出轉換,包括必要時擷取外部儲存的值。PostgreSQL 17 新增了此選項。EXPLAIN 序列化行為、PostgreSQL 17 版本資訊。
若要嘗試,請將程式中現有的 EXPLAIN 範本列替換成以下片段,並重新執行整個程式:
text: `EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SERIALIZE TEXT) ${text}`,
在 PostgreSQL 14–16 中,請保留原本的列,並使用一般查詢計時器測量接收結果所需的時間。SERIALIZE 仍不會將結果資料傳送給用戶端,因此無法測量傳輸或 JavaScript 處理時間。EXPLAIN 的限制。
node-postgres 通常會將結果資料列收集為 JavaScript 物件。其 rowMode: 'array' 選項會改變結果的表示方式,但不會從 SQL 結果中移除已選取的內文值。因此,將 body 加入此查詢後,該選項無法恢復原先失去的欄位涵蓋範圍。資料列模式。
根據文章操作選擇合適的修改
如果桌面操作只需要 ID 清單,請保持選取欄位精簡。如果需要完整文章文字,請評估內文查詢的實際經過時間與結果大小。診斷的目標是在保留所需結果的前提下,找出不必要的工作。
如果畫面只顯示某分類中的前 50 篇文章,限制所需結果就是一種有意義的替代方案。在程式的 queries 陣列中,將內文查詢字串替換成以下 SQL:
SELECT id, body
FROM article_plan_demo
WHERE category = $1
ORDER BY id
LIMIT 50
這會刻意將任務從擷取所有符合條件的文章,改為擷取一頁依序排列的結果。分類固定時,現有的 (category, id) 索引提供了依 ID 排序的候選存取路徑。相符的 B-tree 索引可讓包含 LIMIT 的 ORDER BY 有效率地執行,因為找到足夠資料列後即可停止。實際選用的計畫仍應檢查。索引與 ORDER BY。
不要將 INCLUDE (body) 視為自動修復方案。包含欄位會重複儲存資料表中的資料、擴大索引,而且索引項目超過大小限制時可能導致插入失敗。小型負載欄位可以作為經過測量的實驗候選項目。INCLUDE 是 PostgreSQL 11 新增的功能;較早版本不支援。涵蓋索引的注意事項、PostgreSQL 11 版本資訊。
調查結束時,請確認三項具體結果:選取內文是否取消了僅索引掃描的資格、新的掃描方式是否有可接受的效能,以及完整結果傳送到 Node.js 需要多少時間。這些結果會告訴你,應保留目前計畫、減少要求的文章數,還是為實際查詢測試不同索引。
來源
- 僅索引掃描與涵蓋索引 — PostgreSQL 全球開發小組。
- 使用 EXPLAIN — PostgreSQL 全球開發小組。
- EXPLAIN — PostgreSQL 全球開發小組。
- VACUUM — PostgreSQL 全球開發小組。
- ANALYZE — PostgreSQL 全球開發小組。
- TOAST — PostgreSQL 全球開發小組。
- 索引與 ORDER BY — PostgreSQL 全球開發小組。
- 第 17 版 — PostgreSQL 全球開發小組。
- 第 11 版 — PostgreSQL 全球開發小組。
- node-postgres — node-postgres。
- 連線 — node-postgres。
- 查詢 — node-postgres。