

 从补丁 198 开始，Amazon Redshift 将不再支持创建新的 Python UDF。现有的 Python UDF 将继续正常运行至 2026 年 6 月 30 日。有关更多信息，请参阅[博客文章](https://aws.amazon.com/blogs/big-data/amazon-redshift-python-user-defined-functions-will-reach-end-of-support-after-june-30-2026/)。

# 创建和解释查询计划
<a name="c-the-query-plan"></a>

您可以借助查询计划获取有关运行查询所需的各个操作的信息。在处理查询计划前，建议您先了解 Amazon Redshift 如何处理查询和如何创建查询计划。有关更多信息，请参阅 [查询计划和执行工作流程](c-query-planning.md)。

要创建查询计划，请运行 [EXPLAIN](r_EXPLAIN.md) 命令，后跟实际查询文本。查询计划提供以下信息：
+ 执行引擎将执行的操作，自下而上地阅读结果。
+ 每个操作执行的步骤的类型。
+ 每个操作中使用的表和列。
+ 每个操作中处理的数据量（以字节为单位），以行数和数据宽度计。
+ 操作的相对成本。*成本*是比较计划内的步骤的相对执行时间的度量。成本不提供有关实际执行时间或内存消耗的任何精确信息，也没有提供执行计划之间的有意义的比较。它可以指示查询中的哪些操作消耗最多的资源。

EXPLAIN 命令不实际运行查询。它只显示当查询在当前操作条件下运行时 Amazon Redshift 将执行的计划。如果您更改表的 schema 或数据后再次运行 [ANALYZE](r_ANALYZE.md) 以更新统计元数据，则查询计划可能会不同。

EXPLANE 的查询计划输出是查询执行的简化高级视图。它不描述并行查询处理的详细信息。要查看详细信息，请运行查询本身，然后从 SVL\$1QUERY\$1SUMMARY 或 SVL\$1QUERY\$1REPORT 视图获取查询摘要信息。有关使用这些视图的更多信息，请参阅[分析查询摘要](c-analyzing-the-query-summary.md)。

以下示例显示 EVENT 表上的简单 GROUP BY 查询的 EXPLAIN 输出：

```
explain select eventname, count(*) from event group by eventname;

                            QUERY PLAN
-------------------------------------------------------------------
XN HashAggregate  (cost=131.97..133.41 rows=576 width=17)
  ->  XN Seq Scan on event  (cost=0.00..87.98 rows=8798 width=17)
```

EXPLAIN 为每个操作返回以下指标：

**费用**  
对于比较计划内的操作非常有用的相对值。成本由被两个圆点分隔的十进制值组成，例如 `cost=131.97..133.41`。第一个值（在本例中为 131.97）提供返回此操作的第一行的相对成本。第二个值（在本例中为 133.41）提供完成操作的相对成本。查询计划的成本在读取计划时进行累积，因此本示例中的 HashAggregate 成本 (131.97..133.41) 包括其下面的序列扫描的成本 (0.00..87.98)。

**行数**  
要返回的估计行数。在此示例中，扫描预计将返回 8798 行。HashAggregate 运算符本身应返回 576 行（在从结果集中丢弃重复的事件名称之后）。  
行数估算基于 ANALYZE 命令生成的可用统计数据。如果最近未运行过 ANALYZE，则估算的可靠性会降低。

**宽度**  
平均行的估计宽度（以字节为单位）。在此示例中，平均行的宽度应为 17 个字节。

## EXPLAIN 运算符
<a name="EXPLAIN-operators"></a>

本节简要介绍了在 EXPLAIN 输出中最常见的运算符。有关运算符的完整列表，请参阅 SQL 命令部分中的[EXPLAIN](r_EXPLAIN.md)。

### 顺序扫描运算符
<a name="scan-operator"></a>

顺序扫描运算符 (Seq Scan) 指示表扫描。Seq Scan 扫描从开始到结束按顺序扫描表中的每一列，并计算每一行的查询约束（在 WHERE 子句中）。

### 联接运算符
<a name="join-operators"></a>

Amazon Redshift 根据要联接的表的物理设计、联接所需的数据的位置以及查询本身的特定要求来选择联接运算符。
+ **嵌套循环**

  最优化程度最差的联接，即嵌套循环，主要用于交叉联接（笛卡尔积）和一些不等式联接。
+ **哈希联接和哈希**

  哈希联接和哈希的运行速度通常比签到循环快，可用于内部联接以及左和右外部联接。这些运算符在联接列不是分配键*和*排序键的情况下用于联接表。哈希运算符为联接中的内部表创建哈希表；哈希联接运算符读取外部表，对联接列进行哈希处理，然后在内部哈希表中查找匹配项。
+ **合并联接**

  合并联接通常是最快的连接，用于内联接和外联接。合并联接不用于完全联接。此运算符在联接列都是分配键*和*排序键的情况下，以及未排序的联接表少于 20% 时用于联接表。它按顺序读取两个排序表并查找匹配的行。要查看未排序行的百分比，请查询 [SVV\$1TABLE\$1INFO](r_SVV_TABLE_INFO.md) 系统表。
+ **空间联接**

  通常是基于空间数据邻近度的快速联接，用于 `GEOMETRY` 和 `GEOGRAPHY` 数据类型。

### 聚合运算符
<a name="aggregate-operators"></a>

查询计划在涉及聚合函数和 GROUP BY 操作的查询中使用以下运算符。
+ **聚合**

  标量聚合函数（如 AVG 和 SUM）的运算符。
+ **HashAggregate**

  未排序分组聚合函数的运算符。
+ **GroupAggregate**

  已排序分组聚合函数的运算符。

### 排序运算符
<a name="sort-operators"></a>

当查询必须对结果集进行排序或合并时，查询计划使用以下运算符。
+ **排序**

  评估 ORDER BY 子句和其他排序操作，例如 UNION 查询和联接所需的排序、SELECT DISTINCT 查询和窗口函数。
+ **合并**

  根据从并行操作得到的临时排序结果，来生成最终排序结果。

### UNION、INTERSECT 和 EXCEPT 运算符
<a name="UNION-INTERSECT-and-EXCEPT-operators"></a>

查询计划将以下运算符用于涉及使用 UNION、INTERSECT 和 EXCEPT 进行集合操作的查询。
+ **Subquery**

  用于运行 UNION 查询。
+ **Hash Intersect Distinct**

  用于运行 INTERSECT 查询。
+ **SetOp Except**

  用于运行 EXCEPT（或 MINUS）查询。

### 其他运算符
<a name="other-operators"></a>

以下运算符也经常出现在例行查询的 EXPLAIN 输出中。
+ **唯一**

  消除 SELECT DISTINCT 查询和 UNION 查询的重复项。
+ **限制**

  处理 LIMIT 子句。
+ **窗口**

  运行窗口函数。
+ **结果**

  运行不涉及任何表访问的标量函数。
+ **子计划**

  用于特定的子查询。
+ **Network**

  将临时结果发送到领导节点，以待进一步处理。
+ **实体化**

  保存嵌套循环联接和某些合并联接的输入中的行。

## EXPLAIN 中的联接
<a name="joins-in-EXPLAIN"></a>

查询优化程序使用不同的联接类型来检索表数据，具体取决于查询和基础表的结构。EXPLAIN 输出引用了联接类型、使用的表以及在集群中分布表数据的方式，以描述查询的处理方式。

### 联接类型示例
<a name="join-types"></a>

下面的示例显示了查询优化程序可以使用的不同联接类型。查询计划中使用的联接类型取决于所涉表的物理设计。

#### 示例：对两个表进行哈希联接
<a name="hash-join-two-tables"></a>

以下查询在 CATID 列上将 EVENT 和 CATEGORY 联接起来。CATID 是 CATEGORY 的分配和排序键，但不适用于 EVENT。使用 EVENT 作为外部表并使用 CATEGORY 作为内部表来执行哈希联接。由于 CATEGORY 是较小的表，因此计划程序在查询处理过程中使用 DS\$1BCAST\$1INNER 将其副本广播到计算节点。此示例中的联接成本占计划累计成本的大部分。

```
explain select * from category, event where category.catid=event.catid;

                               QUERY PLAN
-------------------------------------------------------------------------
 XN Hash Join DS_BCAST_INNER  (cost=0.14..6600286.07 rows=8798 width=84)
   Hash Cond: ("outer".catid = "inner".catid)
   ->  XN Seq Scan on event  (cost=0.00..87.98 rows=8798 width=35)
   ->  XN Hash  (cost=0.11..0.11 rows=11 width=49)
         ->  XN Seq Scan on category  (cost=0.00..0.11 rows=11 width=49)
```

**注意**  
EXPLAIN 输出中运算符的缩进对齐有时表示这些操作不相互依赖，并且可以并行开始。在前面的示例中，尽管 EVENT 表上的扫描和哈希操作已对齐，但 EVENT 扫描必须等到哈希操作完全完成。

#### 示例：对两个表进行合并联接
<a name="merge-join-two-tables"></a>

以下查询还使用 SELECT \$1，但它在 LISTID 列上联接 SALES 和 LISTING，其中 LISTID 已设置为两个表的分配和排序键。选择合并联接，并且不需要对联接重新分配数据 (DS\$1DIST\$1NONE)。

```
explain select * from sales, listing where sales.listid = listing.listid;
QUERY PLAN
-----------------------------------------------------------------------------
XN Merge Join DS_DIST_NONE  (cost=0.00..6285.93 rows=172456 width=97)
  Merge Cond: ("outer".listid = "inner".listid)
  ->  XN Seq Scan on listing  (cost=0.00..1924.97 rows=192497 width=44)
  ->  XN Seq Scan on sales  (cost=0.00..1724.56 rows=172456 width=53)
```

以下示例演示了同一查询中的不同类型的联接。与前面的示例一样，SALES 和 LISTING 是合并联接的，但第三个表 EVENT 必须与合并联接的结果进行哈希联接。同样，哈希联接会产生广播成本。

```
explain select * from sales, listing, event
where sales.listid = listing.listid and sales.eventid = event.eventid;
                                  QUERY PLAN
----------------------------------------------------------------------------
XN Hash Join DS_BCAST_INNER  (cost=109.98..3871130276.17 rows=172456 width=132)
  Hash Cond: ("outer".eventid = "inner".eventid)
  ->  XN Merge Join DS_DIST_NONE  (cost=0.00..6285.93 rows=172456 width=97)
        Merge Cond: ("outer".listid = "inner".listid)
        ->  XN Seq Scan on listing  (cost=0.00..1924.97 rows=192497 width=44)
        ->  XN Seq Scan on sales  (cost=0.00..1724.56 rows=172456 width=53)
  ->  XN Hash  (cost=87.98..87.98 rows=8798 width=35)
        ->  XN Seq Scan on event  (cost=0.00..87.98 rows=8798 width=35)
```

#### 示例：联接、聚合和排序
<a name="join-aggregate-and-sort-example"></a>

以下查询运行 SALES 和 EVENT 表的哈希联接，然后执行聚合和排序操作，以考虑分组 SUM 函数和 ORDER BY 子句。初始排序运算符在计算节点上并行运行。然后，Network 运算符将结果发送到领导节点，其中合并运算符生成最终的排序结果。

```
explain select eventname, sum(pricepaid) from sales, event 
where sales.eventid=event.eventid group by eventname
order by 2 desc;
                                           QUERY PLAN
---------------------------------------------------------------------------------
 XN Merge  (cost=1002815366604.92..1002815366606.36 rows=576 width=27)
  Merge Key: sum(sales.pricepaid)
  ->  XN Network  (cost=1002815366604.92..1002815366606.36 rows=576 width=27)
        Send to leader
        ->  XN Sort  (cost=1002815366604.92..1002815366606.36 rows=576 width=27)
              Sort Key: sum(sales.pricepaid)
              ->  XN HashAggregate  (cost=2815366577.07..2815366578.51 rows=576 width=27)
                    ->  XN Hash Join DS_BCAST_INNER  (cost=109.98..2815365714.80 rows=172456 width=27)
                          Hash Cond: ("outer".eventid = "inner".eventid)
                          ->  XN Seq Scan on sales  (cost=0.00..1724.56 rows=172456 width=14)
                          ->  XN Hash  (cost=87.98..87.98 rows=8798 width=21)
                                ->  XN Seq Scan on event  (cost=0.00..87.98 rows=8798 width=21)
```

### 数据重新分配
<a name="data-redistribution"></a>

联接的 EXPLAIN 输出还指定在集群上移动数据以便进行联接的方法。数据移动可以采用广播方法或重新分配。在广播中，联接一侧的数据值将从每个计算节点复制到每个其他计算节点，以便每个计算节点最终得到数据的完整副本。在重新分配中，参与的数据值从其当前切片发送到新切片（可能位于不同节点上）。如果该分配键是联接列之一，则通常会重新分配数据以匹配参与联接的其他表的分配键。如果两个表在其中一个联接列上都没有分配键，则两个表都会被分配，或者内部表将广播到每个节点。

EXPLAIN 输出还引用内表和外表。首先扫描内部表，并显示在查询计划底部附近。内部表是用来探测匹配项的表。它通常保存在内存中，通常是哈希的源表，如果可能的话，是两者中较小的表。外部表是要与内部表匹配的行的源。它通常是从磁盘读取的。查询优化程序根据最新运行的 ANALYZE 命令中的数据库统计信息选择内表和外表。查询的 FROM 子句中的表顺序并不区分内部表和外部表。

您可以通过查询计划中的以下属性了解数据的移动方式，以便执行查询：
+ **DS\$1BCAST\$1INNER**

  将整个内部表的副本广播到所有计算节点。
+ **DS\$1DIST\$1ALL\$1NONE**

  无需重新分配，因为内部表已经使用 DISTSTYLE ALL 被分配到每个节点。
+ **DS\$1DIST\$1NONE**

  两个表都未重新分配。可以使用并置连接，因为相应的切片联接时不会在节点之间移动数据。
+ **DS\$1DIST\$1INNER**

  内部表重新分配。
+ **DS\$1DIST\$1OUTER**

  外部表重新分配。
+ **DS\$1DIST\$1ALL\$1INNER**

  整个内部表重新分配到单个切片，因为外部表使用 DISTSTYLE ALL。
+ **DS\$1DIST\$1BOTH**

  两个表都重新分配。