=> EXPLAIN SELECT * FROM bookings
WHERE book_ref = '9AC0C6' AND total_amount = 48500.00;
QUERY PLAN
---------------------------------------------
Index Scan using bookings_pkey on bookings
(cost=0.43..8.45 rows=1 width=21)
Index Cond: (book_ref = '9AC0C6'::bpchar)
Filter: (total_amount = 48500.00)
(4 rows)
Index Cond 中仅列出可通过索引检查的条件。需要在表上重新验证的额外条件则单独显示在 Filter 行中。
因此,对索引和表的访问并未分为两个独立的计划节点,而是在同一个 Index Scan 节点中执行。尽管存在专门的 Tid Scan 节点3,它根据已知的标识符从表中读取行版本:
=> EXPLAIN SELECT * FROM bookings WHERE ctid = '(0,1)'::tid;
QUERY PLAN
---------------------------------------------------------
Tid Scan on bookings (cost=0.00..4.01 rows=1 width=21)
TID Cond: (ctid = '(0,1)'::tid)
(2 rows)
成本估算
索引扫描的评估由索引访问评估和表页读取评估组成。
显然,评估中的索引部分完全取决于具体的访问方法。对于 B-树,主要开销在于读取索引页和处理这些页中的行。具体读取多少页和行,可以通过已知的数据总量和条件的选择性来确定。索引页访问具有随机性(逻辑上相邻的页在物理上可能以不可预测的方式分布)。评估还包含从根节点下降到叶节点的 CPU 资源消耗以及计算所需表达式的开销 4。(参见 335)
表部分的评估考虑了访问页的成本和处理所有读取的行版本所需的 CPU 时间。重要的是,输入/输出评估不仅取决于索引访问的选择性,还取决于行版本在磁盘上的物理位置与访问方法输出其标识符的顺序之间的相关性。
=> EXPLAIN SELECT * FROM bookings WHERE book_ref < '100000';
QUERY PLAN
---------------------------------------------
Index Scan using bookings_pkey on bookings
(cost=0.43..4640.91 rows=132999 width=21)
Index Cond: (book_ref < '100000'::bpchar)
(3 rows)
根据规划器的评估,条件的选择性为:
=> SELECT round(132999::numeric/reltuples::numeric, 4)
FROM pg_class WHERE relname = 'bookings';
round
--------
0.0630
(1 row)
=> CREATE INDEX ON bookings(book_date);
=> SET enable_seqscan = off;
=> SET enable_bitmapscan = off;
=> EXPLAIN SELECT * FROM bookings
WHERE book_date < '2016-08-23 12:00:00+03';
QUERY PLAN
---------------------------------------------------------------------
Index Scan using bookings_book_date_idx on bookings
(cost=0.43..57121.48 rows=132403 width=21)
Index Cond: (book_date < '2016-08-23 12:00:00+03'::timestamp w...
(3 rows)
=> SET effective_cache_size = '8kB';
=> EXPLAIN SELECT * FROM bookings
WHERE book_date < '2016-08-23 12:00:00+03';
QUERY PLAN
---------------------------------------------------------------------
Index Scan using bookings_book_date_idx on bookings
(cost=0.43..532745.48 rows=132403 width=21)
Index Cond: (book_date < '2016-08-23 12:00:00+03'::timestamp w...
(3 rows)
=> EXPLAIN SELECT book_ref FROM bookings WHERE book_ref < '100000';
QUERY PLAN
-------------------------------------------------
Index Only Scan using bookings_pkey on bookings
(cost=0.43..4640.91 rows=132999 width=7)
Index Cond: (book_ref < '100000'::bpchar)
(3 rows)
名称可能让人误以为 Index Only Scan 节点从不访问表,但事实并非如此。PostgreSQL 中的索引不包含判断行可见性的信息,因此访问方法会返回所有符合条件的版本数据,无论其对当前事务是否可见。可见性随后由索引机制检查。(参见 89)
=> SELECT relpages, relallvisible
FROM pg_class WHERE relname = 'bookings';
relpages | relallvisible
----------+---------------
13488 | 13446
(1 row)
仅索引扫描的评估与普通索引扫描不同,其与表访问相关的输入/输出成本按未包含在可见性映射中的页的比例计算。(行版本处理的 CPU 成本保持不变。)
由于本例中所有页上的所有行版本对所有事务都可见,因此成本评估中实际上完全排除了表输入/输出成本:
=> WITH costs(idx_cost, tbl_cost) AS (
SELECT
(
SELECT round(
current_setting('random_page_cost')::real * pages +
current_setting('cpu_index_tuple_cost')::real * tuples +
current_setting('cpu_operator_cost')::real * tuples
)
FROM (
SELECT relpages * 0.0630 AS pages,
reltuples * 0.0630 AS tuples
FROM pg_class WHERE relname = 'bookings_pkey'
) c
) AS idx_cost,
(
SELECT round(
(1 - frac_visible) * -- доля страниц вне карты видимости
current_setting('seq_page_cost')::real * pages +
current_setting('cpu_tuple_cost')::real * tuples
)
FROM (
SELECT relpages * 0.0630 AS pages,
reltuples * 0.0630 AS tuples,
relallvisible::real/relpages::real AS frac_visible
FROM pg_class WHERE relname = 'bookings'
) c
) AS tbl_cost
)
SELECT idx_cost, tbl_cost, idx_cost + tbl_cost AS total
FROM costs;
idx_cost | tbl_cost | total
----------+----------+-------
2457 | 1333 | 3790
(1 row)
=> CREATE TEMP TABLE bookings_tmp WITH (autovacuum_enabled = off)
AS SELECT * FROM bookings ORDER BY book_ref;
=> ALTER TABLE bookings_tmp ADD PRIMARY KEY(book_ref);
=> ANALYZE bookings_tmp;
=> EXPLAIN (analyze, timing off, summary off, buffers off)
SELECT book_ref FROM bookings_tmp WHERE book_ref < '100000';
QUERY PLAN
---------------------------------------------------------------------
Index Only Scan using bookings_tmp_pkey on bookings_tmp
(cost=0.43..4641.91 rows=132999 width=7) (actual rows=132109.0...
Index Cond: (book_ref < '100000'::bpchar)
Heap Fetches: 132109
Index Searches: 1
(5 rows)
清理创建了可见性映射,因此无需再检查:
=> VACUUM bookings_tmp;
=> EXPLAIN (analyze, timing off, summary off, buffers off)
SELECT book_ref FROM bookings_tmp WHERE book_ref < '100000';
QUERY PLAN
---------------------------------------------------------------------
Index Only Scan using bookings_tmp_pkey on bookings_tmp
(cost=0.43..3794.91 rows=132999 width=7) (actual rows=132109.0...
Index Cond: (book_ref < '100000'::bpchar)
Heap Fetches: 0
Index Searches: 1
(5 rows)
=> CREATE UNIQUE INDEX ON bookings(book_ref) INCLUDE (book_date);
=> BEGIN;
=> ALTER TABLE bookings DROP CONSTRAINT bookings_pkey CASCADE;
NOTICE: drop cascades to constraint tickets_book_ref_fkey on table
tickets
ALTER TABLE
=> ALTER TABLE bookings ADD CONSTRAINT bookings_pkey PRIMARY KEY
USING INDEX bookings_book_ref_book_date_idx; -- новый индекс
NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index
"bookings_book_ref_book_date_idx" to "bookings_pkey"
ALTER TABLE
=> ALTER TABLE tickets
ADD FOREIGN KEY (book_ref) REFERENCES bookings(book_ref);
=> COMMIT;
=> EXPLAIN SELECT book_ref, book_date
FROM bookings WHERE book_ref < '100000';
QUERY PLAN
---------------------------------------------------------------------
Index Only Scan using bookings_pkey on bookings (cost=0.43..438...
Index Cond: (book_ref < '100000'::bpchar)
(2 rows)
说明
人们常常将此类 INCLUDE 索引称为“覆盖索引”,这是错误的。索引是否为覆盖索引,取决于其列集是否覆盖了特定查询所需的所有列。无论这些列是作为键列还是通过 INCLUDE 子句添加的,都不影响判断。此外,同一个索引对一个查询可能是覆盖的,而对另一个查询则不是。
若查询条件作用于表的多个字段,且为这些字段创建了不同索引,则位图扫描可同时使用多个索引15。为每个索引构建行版本位图,随后根据条件逻辑合并:若表达式由 AND 连接,则进行按位逻辑与;若由 OR 连接,则进行按位逻辑或。
例如:
=> EXPLAIN (costs off)
SELECT *
FROM bookings
WHERE book_date < '2016-08-28'
AND total_amount > 250000;
QUERY PLAN
---------------------------------------------------------------------
Bitmap Heap Scan on bookings
Recheck Cond: ((total_amount > '250000'::numeric) AND (book_da...
-> BitmapAnd
-> Bitmap Index Scan on bookings_total_amount_idx
Index Cond: (total_amount > '250000'::numeric)
-> Bitmap Index Scan on bookings_book_date_idx
Index Cond: (book_date < '2016-08-28 00:00:00+03'::tim...
(7 rows)