

# 阅读 Aurora DSQL EXPLAIN 计划
<a name="reading-dsql-explain-plans"></a>

了解如何阅读 EXPLAIN 计划是优化查询性能的关键。在本节中，我们将介绍 Aurora DSQL 查询计划的真实示例，展示不同扫描类型的行为，解释应用筛选条件的位置，并重点介绍优化的机会。

## 这些示例中使用的示例表
<a name="explain-plan-sample-tables"></a>

下面的示例引用两个表：`transaction` 和 `account`。

`transaction` 表没有主键，这将导致 Aurora DSQL 在查询表时执行全表扫描。

`account` 表对 `customer_id` 具有索引。该索引包括 `balance` 和 `status` 作为覆盖列，支持直接从索引中满足某些查询，而无需从基表中读取。但是，索引不包括 `created_at`，因此，引用该列的查询需要额外的表访问权限。

```
CREATE TABLE transaction (
    account_id uuid,
    transaction_date timestamp,
    description text
);

CREATE TABLE account (
    customer_id uuid,
    balance numeric,
    status varchar,
    created_at timestamp
);

CREATE INDEX ASYNC idx1 ON account (customer_id) INCLUDE (balance, status);
```

## 全面扫描示例
<a name="full-scan-example"></a>

Aurora DSQL 既有顺序扫描（功能上与 PostgreSQL 相同），也有全面扫描。这两者之间的唯一区别是，全面扫描可以对存储进行额外的筛选。因此，与顺序扫描相比，几乎始终优先选择全面扫描。由于相似性，我们将只介绍更有趣的全面扫描的示例。

全面扫描将主要用于没有主键的表。由于 Aurora DSQL 主键默认情况下为完全覆盖索引，因此在 PostgreSQL 使用顺序扫描的许多情况下，Aurora DSQL 很可能会对主键使用仅限索引扫描。与大多数其它数据库一样，没有索引的表的扩展性会很差。

```
EXPLAIN SELECT account_id FROM transaction WHERE transaction_date > '2025-01-01' AND description LIKE '%external%';
```

```
                                                   QUERY PLAN
----------------------------------------------------------------------------------------------------------------
 Full Scan (btree-table) on transaction  (cost=125100.05..177933.38 rows=33333 width=16)
   Filter: (description ~~ '%external%'::text)
   -> Storage Scan on transaction (cost=12510.05..17793.38 rows=66666 width=16)
        Projections: account_id, description
        Filters: (transaction_date > '2025-01-01 00:00:00'::timestamp without time zone)
        -> B-Tree Scan on transaction (cost=12510.05..17793.38 rows=100000 width=30)
```

此计划显示了在不同阶段应用的两个筛选条件。`transaction_date > '2025-01-01'` 条件应用于存储层，从而减少返回的数据量。稍后，在数据传输之后，在查询处理器中应用 `description LIKE '%external%'` 条件，这会降低效率。将更具选择性的筛选条件推送到存储层或索引层通常可以提高性能。

## 仅限索引扫描示例
<a name="index-only-scan-example"></a>

仅限索引扫描是 Aurora DSQL 中最优的扫描类型，因为它们可以最大限度地减少到存储层的往返次数，并且可以进行最多的筛选。但仅因为您看到了仅限索引扫描，并不意味着您拥有最好的计划。由于可能发生的筛选级别各不相同，因此仍要注意可能发生筛选的不同位置，这一点至关重要。

```
EXPLAIN SELECT balance FROM account 
WHERE customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb' 
AND balance > 100 
AND status = 'pending';
```

```
                                  QUERY PLAN
-------------------------------------------------------------------------------
 Index Only Scan using idx1 on account  (cost=725.05..1025.08 rows=8 width=18)
   Index Cond: (customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'::uuid)
   Filter: (balance > '100'::numeric)
   -> Storage Scan on idx1 (cost=12510.05..17793.38 rows=9 width=16)
        Projections: balance
        Filters: ((status)::text = 'pending'::text)
        -> B-Tree Scan on idx1 (cost=12510.05..17793.38 rows=10 width=30)
            Index Cond: (customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'::uuid)
```

在此计划中，首先在索引扫描期间评估索引条件 (`customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'`)，这是效率最高的阶段，因为它会限制从存储中读取的数据量。存储筛选条件 `status = 'pending'` 是在读取数据之后但在将数据发送到计算层之前应用的，从而减少了传输的数据量。最后，查询处理器筛选条件 `balance > 100` 在数据移动后最后运行，因此效率最低。其中，索引条件的性能最佳，因为它直接控制扫描的数据量。

## 索引扫描示例
<a name="index-scan-example"></a>

索引扫描与仅限索引扫描类似，不同之处在于前者需要另一个步骤，即调用基表。由于 Aurora DSQL 可以指定存储筛选条件，因此它能够对索引调用和查找调用指定存储筛选条件。

为了明确这一点，Aurora DSQL 将计划呈现为两个节点。这样，您可以清楚地看到添加包含列对从存储返回的行有多大帮助。

```
EXPLAIN SELECT balance FROM account 
WHERE customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'
AND balance > 100 
AND status = 'pending' 
AND created_at > '2025-01-01';
```

```
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Index Scan using idx1 on account  (cost=728.18..1132.20 rows=3 width=18)
   Filter: (balance > '100'::numeric)
   Index Cond: (customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'::uuid)
   -> Storage Scan on idx1 (cost=12510.05..17793.38 rows=8 width=16)
        Projections: balance
        Filters: ((status)::text = 'pending'::text)
        -> B-Tree Scan on account (cost=12510.05..17793.38 rows=10 width=30)
            Index Cond: (customer_id = '4b18a761-5870-4d7c-95ce-0a48eca3fceb'::uuid)
   -> Storage Lookup on account (cost=12510.05..17793.38 rows=4 width=16)
        Filters: (created_at > '2025-01-01 00:00:00'::timestamp without time zone)
        -> B-Tree Lookup on transaction (cost=12510.05..17793.38 rows=8 width=30)
```

 此计划显示了筛选是如何跨多个阶段发生的：
+  有关 `customer_id ` 的索引条件会尽早筛选数据。
+ 有关 `status` 的存储筛选条件会进一步缩小结果范围，然后再将结果发送到计算中。
+ 有关 `balance` 的查询处理器筛选条件将在稍后传输后应用。
+ 从基表中提取其它列时，会评估有关 `created_at` 的查找筛选条件。

将常用列添加为 `INCLUDE` 字段通常可以消除这种查找并提高性能。

## 最佳实践
<a name="best-practices"></a>
+ **将筛选条件与索引列对齐**，以便更早地推送筛选。
+ **使用 INCLUDE 列**以支持仅限索引扫描并避免查找。
+ 在调查性能问题时**验证行估计值**。Aurora DSQL 根据数据更改率在后台运行 `ANALYZE`，从而自动管理统计数据。如果估计值看起来不准确，则可以手动运行 `ANALYZE` 以立即刷新统计数据。
+ **避免对大型表进行未编入索引的查询**，以防止代价高昂的全面扫描。