理解Amazon Redshift服务概念、列式存储、MPP 架构与表设计原理

Amazon Redshift 是 AWS 提供的完全托管的 PB 级云数据仓库服务。它专为在线分析处理(OLAP)服务

数据库系统的设计哲学可以根据使用场景划分为两大类别:OLTP(Online Transaction Processing)和 OLAP(Online Analytical Processing)。理解这两者的理论分野对于把握 Redshift 的设计初衷至关重要。

  • OLTP 的理论根基可以追溯到 Edgar F. Codd 在 1970 年提出的关系模型(Relational Model)。该模型强调数据的原子性、一致性、隔离性和持久性(ACID),旨在支持高频的读写操作。典型的 OLTP 系统如 MySQL、PostgreSQL 面向交易处理,每秒处理成千上万条短事务,每次查询通常只涉及单条或少量记录。

  • OLAP 则是对关系模型的补充性演化。随着企业数据量的指数级增长,人们不再满足于仅查询当前交易状态,而是希望从历史数据中挖掘趋势、模式和洞察。OLAP 的核心假设是:查询模式以读为主,涉及大规模数据扫描、聚合计算和多维分析。1993 年,Edgar Codd 又提出了 OLAP 的 12 条规则,定义了多维数据模型、快速分析共享等特性。

为什么分析场景天然适合列式存储

从 I/O 模型的理论上分析,行式存储和列式存储在分析查询中表现差异显著。考虑一个典型的 OLAP 查询:

sql 复制代码
SELECT SUM(amount), AVG(amount)
FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
  AND region = 'North America'

该查询仅需要访问三列(amount、order_date、region),但如果使用行式存储,每次 I/O 操作会将整行数据(可能包含 50 列以上)从磁盘读入内存。这意味着 97% 的 I/O 数据是查询不需要的,造成严重的 I/O 浪费。列式存储从根本上解决了这一问题。每个 1MB 的数据块只存储单一列的值,当查询只需要三列时,I/O 量理论上减少到行式存储的 3/50。这种 I/O 优化在海量数据分析场景中带来数量级的性能提升。

从 CPU 缓存(CPU Cache)的角度分析,列式存储同样具有优势。现代 CPU 的 L1/L2/L3 缓存命中率对性能影响巨大。当同一列的数据连续存储时,CPU 预取器(Prefetcher)可以准确预测即将访问的数据并提前加载到缓存中,实现数据访问的流水线化。

在现代数据架构中,Redshift 通常位于数据管道的下游。典型的数据流动如下:
源系统

业务数据库、日志、API
数据湖 S3
ETL/ELT

Glue、dbt
Redshift

数据仓库
BI 工具

QuickSight、Tableau

传统的 ETL(Extract-Transform-Load)模式在数据到达目标系统前完成转换。而 ELT(Extract-Load-Transform)模式则先将原始数据加载到目标系统,再利用目标系统强大的计算能力进行转换。Redshift 的列式存储和 MPP 架构使其特别适合 ELT 模式:数据以原始格式批量加载,然后通过 SQL 定义的转换逻辑处理,最终生成分析用的宽表和聚合表。

Redshift 的高性能建立在两大数据仓库系统架构支柱上:

列式存储(Columnar Storage)

传统行式数据库将一行数据的所有列连续存储在磁盘上。而 Redshift 将同一列的数据连续存储。
列式存储
id: 1 2 3
name: 张三 李四 王五
age: 30 25 28
行式存储
(1,张三,30)
(2,李四,25)
(3,王五,28)

Redshift 的列式存储以 1MB 的数据块(Block)为基本单元。每个 Block 仅存储单一列的值,而非多列混合。这种设计在三个层面带来性能优势。

  • I/O 优化。分析查询通常只访问少数几列,列式存储只读取需要的列,跳过无关列。假设一张表有 100 列,查询只访问 5 列,列式存储的 I/O 量仅为行式存储的 5%。

  • 高压缩率。同一列的数据类型相同,值域范围有限,重复模式丰富。Redshift 的压缩算法通常能达到 3-10 倍的压缩率,远超行式存储。压缩不仅节省存储成本,更重要的是减少磁盘 I/O 和内存占用。

  • 向量化执行(Vectorized Execution)。Redshift 的查询引擎以列向量(Column Vector)为单位处理数据,而非单行元组。CPU 可以利用 SIMD(Single Instruction Multiple Data)指令在单次指令周期内对多个数据值并行执行相同操作,大幅提升 CPU 利用率。向量化执行与列式存储的结合是 Redshift 高性能的关键因素。

Zone Maps

Redshift 为每个 1MB Block 维护 Zone Map 元数据,记录该 Block 内某列的最小值(min)和最大值(max)。查询执行时,Redshift 先检查 Zone Map,如果目标值不在 min, max 范围内,则直接跳过该 Block,无需读取数据。

复制代码
假设 orders 表的 order_date 列按时间顺序存储:

Block 1: order_date 范围 [2024-01-01 ~ 2024-01-15]  → Zone Map: min=2024-01-01, max=2024-01-15
Block 2: order_date 范围 [2024-01-16 ~ 2024-01-31]  → Zone Map: min=2024-01-16, max=2024-01-31
Block 3: order_date 范围 [2024-02-01 ~ 2024-02-14]  → Zone Map: min=2024-02-01, max=2024-02-14
...

查询 WHERE order_date = '2024-02-10' 时:
→ 检查所有 Block 的 Zone Map
→ 跳过 Block 1 和 Block 2(目标值不在范围内)
→ 只读取 Block 3

Zone Map 本质上是一种稀疏索引(Sparse Index),以极低的元数据存储代价实现了 Block 级的数据过滤。Sort Key 与 Zone Map 配合使用时效果最佳,因为排序后的数据使每个 Block 的值域范围更集中,过滤效率更高。

I/O 放大问题

此外行式存储在分析查询中存在 I/O 放大问题假设一张表有 100 列、1000 万行,每行 1KB,单个 Block 大小为 1MB,则该表占用约 10000 个 Block。

执行一个仅访问 3 列的聚合查询时:

  • 行式存储:每个 Block 包含 1000 行完整数据,但查询只需要 3 列的值。因此每个 Block 的 997 列数据被无意义地读取到内存又丢弃。I/O 放大约为 100/3 ≈ 33 倍。
  • 列式存储:每个列占用约 10000/100 = 100 个 Block。查询需要的 3 列共 300 个 Block,I/O 放大约为 1 倍(无浪费)。

这种 I/O 放大效应在大规模数据上的累积影响是惊人的,也是列式存储成为 OLAP 系统标配的根本原因。

大规模并行处理(MPP)

Redshift 将数据分布在多个计算节点上,查询时所有节点并行处理各自的数据分片,最后汇总结果。
Compute Node 3
Slice 5
Slice 6
Compute Node 2
Slice 3
Slice 4
Compute Node 1
Slice 1
Slice 2
客户端查询
Leader Node

接收查询、生成执行计划、汇总结果

各组件的具体作用如下

  • Leader Node:集群的"大脑",不存储用户数据,也不参与实际计算。它的职责包括:接收客户端 SQL 连接、解析 SQL 生成抽象语法树(AST)、通过查询优化器生成最优执行计划、将执行计划编译为 C 代码片段、将编译后的代码分发到各 Compute Node、收集各节点的中间结果并做最终聚合、将结果返回客户端。Leader Node 本质上是一个协调者,所有客户端只与 Leader Node 通信。
  • Compute Node:实际执行数据处理的"肌肉"。每个 Compute Node 拥有独立的 CPU、内存和本地磁盘(或 SSD 缓存)。数据按照 Distribution Key 的哈希值被预先分配到各个 Compute Node 上,每个节点只处理自己本地存储的数据分片。节点数量可以在创建集群时指定(Provisioned)或自动调整(Serverless)。
  • Slice:每个 Compute Node 内部被进一步划分为多个 Slice(通常是每节点 2 个或更多,取决于节点类型)。Slice 是 Redshift 中最细粒度的并行处理单元。每个 Slice 拥有独立的内存空间和磁盘空间,独立执行 Leader 分配过来的代码片段。一个 Slice 只处理分配给自己的那部分数据行,与其他 Slice 完全不共享状态。这种设计使得并行度 = 节点数 × 每 Slice 数,例如 4 个 Compute Node 各 2 个 Slice = 8 路并行。
数据是如何分片的

假设有一张 sales 表,包含 1,000,000 行销售记录,定义为 DISTKEY(product_id),集群有 2 个 Compute Node、每个节点 2 个 Slice(共 4 个 Slice)。

分片过程如下。注意:本例哈希值为示意值,官方未公开 Redshift 使用的具体哈希函数和计算过程。由于相同 product_id 值的行必定落在同一个 Slice 上。这意味着当 sales 表与 products 表(同样以 product_id 作为 DISTKEY)做 JOIN 时,相同 product_id 的数据已经在同一个 Slice,无需跨节点传输。
集群节点 + Slice 分布
哈希分片流程(DISTKEY = product_id)
原始数据(sales 表 100万行)
节点 2
节点 1
均匀哈希分片
行1:product_id=101
行2:product_id=205
行3:product_id=301
行4:product_id=410
... 共 1,000,000 行数据
hash(101) = 847293

847293 % 4 = 1
hash(205) = 328194

328194 % 4 = 2
hash(301) = 614882

614882 % 4 = 0
hash(410) = 102937

102937 % 4 = 1
Slice 0

~250,000 行
Slice 1

~250,000 行
Slice 2

~250,000 行
Slice 3

~250,000 行

查询时的并行处理示意图如下
4 个 Slice 并行执行
Leader Node
Slice 3(Node2)
Slice 2(Node2)
Slice 1(Node1)
Slice 0(Node1)
局部聚合结果
局部聚合结果
局部聚合结果
局部聚合结果
下发执行代码
解析 SQL
生成执行计划
编译为 C 代码

下发给所有 Slice
收集局部聚合结果
全局最终聚合
返回最终结果给客户端
扫描本地25万行
本地聚合
扫描本地25万行
本地聚合
扫描本地25万行
本地聚合
扫描本地25万行
本地聚合

为什么这样分片是高效的?

  • 数据本地性:每个 Slice 只扫描本地磁盘上的数据,没有网络 I/O
  • 线性加速:4 个 Slice 并行扫描,理论上 1/4 的时间完成
  • 内存效率:每个 Slice 只需为自己的 ~250,000 行分配聚合内存,而不是为全表 1,000,000 行分配
  • JOIN 优化:如果 JOIN 的两张表使用相同 DISTKEY,数据已经Colocated,无需网络传输
Shared-Nothing 架构

MPP 的理论基础源于 1980 年代 Michael Stonebraker 等人提出的 Shared-Nothing 架构(无共享架构)。与传统 Shared-Memory(共享内存)架构和 Shared-Disk(共享磁盘)架构相比,Shared-Nothing 架构的每个节点拥有独立的 CPU、内存和磁盘,通过高速网络互联。

架构类型 特点 优点 缺点 代表产品
Shared-Memory 多 CPU 共享同一内存和磁盘 编程简单,数据共享容易 扩展性受限,内存带宽瓶颈 小型 SMP 服务器
Shared-Disk 多节点共享同一存储设备 存储统一管理,节点可自由扩展 存储 I/O 成为瓶颈 Oracle Exadata
Shared-Nothing 每个节点独立资源 线性扩展,高吞吐量 数据分布和节点协调复杂 Redshift、Teradata

Redshift 采用 Shared-Nothing 架构,每个 Compute Node 完全独立地处理分配给自己的数据分片,节点间仅通过网络传输中间结果。这种架构理论上可以实现接近线性的横向扩展:节点数翻倍,吞吐量接近翻倍。

那么节点扩缩容时的数据如何重新分片?当增加或减少 Compute Node 数量时,Slice 的总数发生变化,必然重新分片所有数据:

  1. 用户新的节点数量配置生效后,Redshift 预置新节点并创建新的 Slice 布局
  2. 对每张表的所有数据行重新计算 hash(DISTKEY) % 新Slice数,将数据分配到新的 Slice
  3. 扩容期间集群进入 modifying 状态,部分操作受限

不同节点类型的重新分片开销差异显著

节点类型 持久化存储 重新分片过程 扩容耗时
DC2(本地存储) 节点本地磁盘 所有数据必须通过网络在节点间物理搬迁 较长,与数据量正相关
RA3(RMS/S3) S3 + 本地 SSD 缓存 只需重新建立 Slice 到 S3 对象的映射关系,新节点缓存逐步预热 较短,无需搬移持久数据

这也是 RA3 优于 DC2 的关键场景之一,即RA3 的持久数据存储在 S3 上,扩容时不需要物理搬迁数据,只需更新元数据映射。

查询并行化流程

Redshift 的查询执行分为以下几个阶段:

  1. 查询编译:Leader Node 接收 SQL 语句,通过解析器生成抽象语法树(AST),经过查询优化器生成最优执行计划。Redshift 的查询编译器将执行计划编译为 C 代码,这些代码将被分发到各 Compute Node 的 Slice 上执行。编译过程会利用列式存储的统计信息(如每列的 distinct 值数量、数据分布直方图)来选择最优的 JOIN 策略和聚合方式。

  2. 执行计划生成:优化器决定多个关键策略:数据是否需要 Redistribution 或 Broadcast(根据表的分布方式和数据量)、使用 Hash JOIN 还是 Merge JOIN(根据数据是否已排序)、聚合使用 Hash Aggregation 还是 Sort Aggregation。执行计划以 Segment 为单位组织,每个 Segment 是一个可以独立并行执行的数据处理步骤。

  3. 任务分发:编译后的执行计划以 Segment 为单位分发到各 Compute Node,再由 Compute Node 分配给自己的各个 Slice。每个 Slice 独立接收并执行分配给自己的数据处理任务。任务分发通过 Redshift 内部的高速网络完成。

  4. 并行执行:所有 Slice 同时开始处理各自的数据分片。每个 Slice 扫描本地磁盘上属于自己的数据,执行过滤、投影、JOIN、聚合等操作。Slice 之间不共享状态,完全独立运行。如果某一步需要跨 Slice 的数据交换(如 Redistribution),则通过节点间的网络进行数据传输。

  5. 结果合并:各 Slice 的中间结果通过网络汇聚到 Leader Node。Leader Node 对这些部分结果做最终聚合(如 SUM、COUNT 的合并)、排序(ORDER BY)、去重(DISTINCT)等操作,生成最终结果集返回给客户端。

分布式 JOIN 策略

Redshift 支持三种分布式 JOIN 策略,选择哪种策略取决于数据分布方式和数据量:

  • Broadcast JOIN(广播连接):将小表的所有数据发送到所有节点。当一个小表与大表 JOIN 时,Redshift 选择将小表广播到每个节点,在本地完成 JOIN,避免大表数据的网络传输。适用于小维度表(通常数 MB 以内)与大事实表 JOIN 的场景。网络成本 = 小表数据量 × 节点数。
  • Redistribution(重分布):将两张表的数据按照 JOIN 列的哈希值重新分布到各节点。如果两张表都没有使用 JOIN 列作为 DISTKEY,或者分布方式不同,Redshift 需要将数据重新哈希分布,使得相同 JOIN 键值的行到达同一节点。这是最通用的 JOIN 策略,但网络成本最高(需要传输较大表的数据量)。
  • Colocated JOIN(同置连接):如果两张表使用相同的 DISTKEY,则 JOIN 的行已经位于同一节点,无需任何网络传输。这是最高效的 JOIN 方式------每个 Slice 直接在本地完成 JOIN,网络开销为零。这也是为什么选择正确的 DISTKEY 如此重要:它可以让频繁 JOIN 的大表之间实现 Colocated JOIN。

MPP 系统的理论加速比受 Amdahl 定律约束:加速比 = 1 / (S + (1-S)/N),其中 S 是串行执行部分的比例,N 是节点数。假设一个查询的串行部分占 5%(元数据操作、结果汇总),其余 95% 可完全并行化:

节点数 理论加速比 效率
2 1.90 95%
4 3.48 87%
8 5.93 74%
16 9.23 58%
32 10.84 34%

这解释了为什么单纯增加节点数并不总是最优的。因此Redshift 提供了 Concurrency Scaling(并发扩展)和 Elastic Concurrency Scaling 两种机制来应对突发工作负载:前者启动额外的临时集群处理并发查询,后者则在同一集群内动态扩展计算资源。

Redshift Managed Storage (RMS)

现代 Redshift(RA3 节点类型和 Serverless)采用计算存储分离架构。RMS 的核心思想是:S3 是唯一的持久化存储,本地 SSD 只是有限容量的缓存层,RMS 在两者之间自动管理数据的上下移动。

计算存储分离(Disaggregated Storage)是云原生数据系统的演进趋势。传统紧耦合架构中,计算资源和存储资源绑定在同一物理节点上,扩展时必须同时扩展两者,造成资源浪费。分离架构允许:

  • 独立扩展:计算资源不足时只扩展计算节点(增加节点数或升级节点类型),存储空间不足时 RMS 自动将数据溢写到 S3,无需加节点,两者互不影响
  • 成本优化:按需付费,避免为峰值容量预置资源。计算资源按节点小时计费,存储按 GB-月计费,可以分别优化各自的成本
  • 高可用性:计算节点故障不影响数据持久性,因为数据持久化在 S3 上。新的计算节点可以快速重新加载缓存,恢复服务

RMS 中的数据生命周期

以下部分为推测,AWS 未公开具体的持久化时序细节

本地 SSD 容量是有限的,但表可以远大于 SSD 容量。 例如一个 RA3 节点本地 SSD 容量有限(具体容量取决于节点类型),但你可以存储 10TB 的数据------只是其中部分热数据在 SSD 上,其余在 S3 上,查询时按需加载。这就是"RMS 自动扩容到 S3"的含义:不需要加节点就能存储更多数据,超出 SSD 容量的部分自动由 S3 承载。

复制代码
写入数据(COPY / INSERT)
   ↓
数据首先写入本地 SSD 缓存(热数据)
同时异步持久化到 S3(保证耐久性)
   ↓
SSD 容量接近上限时,RMS 自动执行:
1. 识别最近最少访问的数据块(LRU 策略)
2. 确认这些数据块已持久化到 S3
3. 从本地 SSD 腾出空间给新写入的数据
   ↓
查询需要已淘汰的数据时:
1. 从 S3 加载对应的数据块到本地 SSD
2. 缓存中可能淘汰其他冷数据腾出空间
3. 后续对同一数据块的查询直接命中 SSD 缓存

RMS 与 DC2 的本质区别如下

DC2(本地存储) RA3 + RMS
数据存储上限 节点本地 SSD 容量(固定上限) S3 容量(几乎无限)
数据满了怎么办 必须加节点才能存更多数据 RMS 自动将冷数据迁移到 S3,本地 SSD 只保留热数据
存储与计算 绑死:加存储 = 加计算节点 独立:存储扩容不影响计算节点
扩容操作 手动弹性调整节点数 存储自动扩展,计算可独立弹性调整

RMS 的托管s3存储空间

Redshift 将数据持久化存储在 Amazon S3 中,利用 S3 的高耐久性设计(99.999999999%,即 11 个 9)确保数据安全。S3 的强一致性模型保证了写入后立即可读,避免了最终一致性带来的复杂性。数据在 S3 中以列式格式存储为多个对象,每个对象对应一个 Block 的数据。Redshift 内部维护元数据目录,记录 S3 对象与表、列、Block 的映射关系。

RMS 使用的 S3 存储空间由 Redshift 服务内部持有和管理,用户不需要(也无法)自行创建、配置或管理 S3 Bucket。具体表现为:

  • RMS 的 S3 存储空间不会出现在你自己 AWS 账号的 S3 Bucket 列表中,你无法通过 S3 Console/CLI/API 直接访问这些数据对象
  • S3 的存储费用打包在 Redshift 的 RA3 存储计费中(按每 TB-月收费)
  • 快照(Snapshot)也存储在 Redshift 管理的 S3 空间中,不需要指定目标 Bucket
  • 所有数据的加密、备份、冗余副本管理完全由 Redshift 服务自动处理

需要区分两种 S3 使用场景

场景 S3 的归属 谁管理 用途
RMS 内部存储 Redshift 服务内部持有 Redshift 自动管理 存储表数据、Block、快照
COPY / UNLOAD 用户自己的 S3 Bucket 用户自行管理 加载外部数据、导出查询结果
Redshift Spectrum 用户自己的 S3 Bucket 用户自行管理 直接查询数据湖中的外部数据

本地 SSD 缓存

每个 Compute Node 配备本地 NVMe SSD,用作热数据的缓存层。Redshift 使用 LRU(Least Recently Used)淘汰策略管理缓存:当缓存空间不足时,最近最少访问的数据块被淘汰回 S3。

缓存优化的核心目标是提高命中率。影响命中率的两个关键因素是:数据访问的局部性(访问模式是否集中在热点数据)和缓存容量(相对于数据总量的比例)。RA3 节点的本地缓存通常能容纳最近访问的热数据(具体比例取决于节点类型和数据总量)。

Redshift RMS 实现了自动的存储分层:

  • 热数据:最近频繁访问的数据保留在 Compute Node 的本地 NVMe SSD 上,延迟最低,通常是最近几天的查询热点
  • 温数据:访问频率降低的数据从 SSD 淘汰,但仍保留在 S3 的标准访问层,查询时需要从 S3 加载到 SSD
  • 冷数据:长期未访问的历史数据存储在 S3 的低成本层,存储费用最低,访问时需要从 S3 远端读取

注意:各层级的具体延迟范围(微秒/毫秒/百毫秒)为估算值,实际延迟取决于节点类型、网络条件和数据块大小。

这一分层过程对用户透明,查询时 Redshift 自动将所需数据从相应层级加载到本地缓存。

Redshift Serverless

Redshift的部署模式有如下两种

类型 核心特点 计费方式 适用场景 节点 / 资源规格
Provisioned 手动选择节点类型、数量 按节点小时计费,空闲仍计费 负载稳定、可预测的业务 节点类型:RA3(计算存储分离,推荐)、DC2(本地 SSD)
Serverless 无需管理节点,自动扩缩容 按实际使用 RPU 计费,空闲不收费 负载间歇、不可预测的业务 容量范围:8 ~ 1024 RPU(8 的倍数)

Redshift Serverless 将基础设施抽象化,用户无需关心节点配置、集群管理等细节。系统内部维护一个容量池,根据查询负载自动分配计算资源。

当用户提交查询时,Serverless 从基线容量开始执行。如果查询需要的资源超过基线容量,系统会自动 burst(突发)到更高容量,处理完成后释放资源。这种机制特别适合波峰波谷明显的工作负载。基线容量定义了始终可用的最小计算资源,确保即使在负载高峰时也有足够的容量响应查询请求。基线容量越高,突发到峰值的时间越短,但固定成本也越高。

Redshift Serverless 按以下维度计费:

  • 计算费用:按 RPU-小时计费,实际使用的 RPU 数量乘以运行时间。例如 32 RPU 运行 1 小时 = 32 RPU-小时
  • 数据扫描费用:通过 Redshift Spectrum 查询 S3 数据时,按扫描的 TB 数计费(与 Athena 类似的按查询付费模式)
  • 存储费用:按 GB-月计费,与 Provisioned 集群的存储计费方式一致。存储在 RMS 中的数据量乘以单价

Namespace 与 Workgroup 的关系为,一个 Namespace 可以关联多个 Workgroup,实现不同工作负载的隔离。例如,为 BI 报表查询和 ETL 作业分别配置独立的 Workgroup,避免相互干扰。

分类 包含配置 / 资源
Namespace 数据库对象(表、Schema、视图)、IAM 策略与访问控制、加密密钥、跨 Workgroup 共享元数据
Workgroup RPU 基线 / 峰值容量、VPC 安全配置、跨可用区配置、公网 / 私有访问设置

表设计核心

Distribution Style

分布方式(Distribution Style)决定数据如何在各计算节点间分配,直接影响查询性能。

分布方式 说明 适用场景
DISTSTYLE KEY 按指定列的值哈希分布,相同值在同一节点 大表,经常用于 JOIN 的列
DISTSTYLE ALL 整张表复制到每个节点 小维度表(< 几百万行),频繁与大表 JOIN
DISTSTYLE EVEN 轮询均匀分布到所有节点 无明显 JOIN 列的表,或临时表
DISTSTYLE AUTO Redshift 自动选择最优策略 不确定时的默认选择

Redshift 使用一致性哈希(Consistent Hashing)的变体进行数据分片。每个 DISTKEY 列的值通过哈希函数映射到哈希桶(Hash Bucket),每个桶分配到特定的节点和 Slice。一致性哈希的核心优势在于:当节点数量变化时,只需要重新分配约 1/N 的数据(而非全部数据),减少数据迁移量。Redshift 的哈希算法将相同值映射到相同桶,从而保证 JOIN 时相同键值的数据位于同一节点。

sql 复制代码
-- 查看表的分布情况
SELECT
    node,
    slice,
    COUNT(*) as row_count
FROM svv_table_info
WHERE table_id = (SELECT table_id FROM pg_class WHERE relname = 'fact_sales')
GROUP BY node, slice
ORDER BY node, slice;
数据倾斜问题与检测

数据倾斜(Data Skew)是指数据在各节点间分布不均匀,导致某些节点处理的数据量远大于其他节点,成为查询瓶颈。

sql 复制代码
SELECT
    "table" AS table_name,
    SUM(rows) AS total_rows,
    MAX(rows) AS max_rows_per_slice,
    MIN(rows) AS min_rows_per_slice,
    MAX(rows) * 1.0 / NULLIF(SUM(rows), 0) AS skew_ratio
FROM svv_table_info
WHERE "table" = 'your_table_name'
GROUP BY "table";

如果 skew_ratio(最大 Slice 行数 / 平均 Slice 行数)显著大于 1,说明存在数据倾斜。

Colocation与网络传输优化

当两张表使用相同的 DISTKEY 时,JOIN 的行已经位于同一节点,无需网络传输。这种优化称为 Colocation。

sql 复制代码
-- 事实表和维度表使用相同的 DISTKEY
CREATE TABLE fact_sales (
    sale_id BIGINT,
    product_id BIGINT,  -- DISTKEY
    amount DECIMAL(10,2)
) DISTKEY(product_id);

CREATE TABLE dim_product (
    product_id BIGINT,  -- DISTKEY
    product_name VARCHAR(100)
) DISTKEY(product_id);

-- JOIN 时无需 Redistribution
SELECT p.product_name, SUM(s.amount)
FROM fact_sales s
JOIN dim_product p ON s.product_id = p.product_id
GROUP BY p.product_name;

分区和排序键

DISTKEY 选择

选择最优 DISTKEY 需要分析查询模式。关键原则是:

  1. 优先考虑 JOIN 列
  2. 选择高基数列
  3. 考虑查询频率

系统表查询可帮助分析使用分布键

sql 复制代码
-- 分析历史查询的 JOIN 模式
SELECT
    q.query_text,
    p.join_type,
    p.relation
FROM stl_query q
JOIN stl_plan_info p ON q.query = p.query
WHERE q.starttime > CURRENT_DATE - 7
  AND p.relation IS NOT NULL
ORDER BY q.starttime DESC
LIMIT 100;

当指定 DISTSTYLE AUTO 时,Redshift 根据以下规则自动选择分布方式:

  • 小表(< 10MB):自动使用 ALL 分布,将整张表复制到每个节点。小表的数据量不大,复制的存储开销可忽略,但能确保任何 JOIN 都无需 Redistribution
  • 大表:根据查询模式自动分析,如果存在频繁 JOIN 的列则选择 KEY 分布(以主键或 JOIN 列为 DISTKEY),如果无明显 JOIN 列则选择 EVEN 分布(轮询均匀分布)

AUTO 分布是一个合理的起点,但对于生产环境的关键表,手动指定 DISTKEY 通常能获得更好的性能。

而轮询分布(EVEN)保证数据在数学上均匀分布到所有 Slice。每个新行依次分配到下一个 Slice,周而复始。这种分布方式的优势是理论上绝对均衡,不受数据分布影响。缺点是 JOIN 时通常需要 Redistribution。

Sort Key

排序键(Sort Key)决定数据在磁盘上的物理排列顺序,配合 Zone Maps 实现高效的数据跳过。

假设 orders 表按 order_date 排序:

复制代码
Block 1: order_date [2020-01-01 ~ 2020-01-15]  ← Zone Map: min=2020-01-01, max=2020-01-15
Block 2: order_date [2020-01-16 ~ 2020-01-31]
Block 3: order_date [2020-02-01 ~ 2020-02-14]
...

查询 WHERE order_date = '2020-02-10' 时:

  • 跳过 Block 1 和 Block 2(Zone Map 排除)
  • 只读取 Block 3
Compound Sort Key

Compound Sort Key按定义顺序依次排序多列数据。第一列具有最高优先级,只有当第一列值相同时才比较第二列,以此类推。这种排序方式在数据结构上对应 B-Tree 索引,查询时可以利用有序性进行二分查找。

sql 复制代码
CREATE TABLE orders (
    order_id BIGINT,
    order_date DATE,
    customer_id BIGINT,
    amount DECIMAL(10,2)
) SORTKEY(order_date, customer_id);

上述表中,数据首先按 order_date 排序,相同日期的记录再按 customer_id 排序。查询 WHERE order_date = '2024-01-15' 可以利用 Zone Map 高效跳过不相关的日期块。

Interleaved Sort Key

Interleaved Sort Key对所有排序列赋予同等权重,使用 Z-Order Curve(Z 阶曲线)将多维坐标映射到一维空间。Z-Order Curve 的核心思想是:将多维数据的二进制表示交叉编织,形成一维排序值。

复制代码
假设两列:x (2位) 和 y (2位)

x=0, y=0 → 00 00 → 0000 → 0
x=0, y=1 → 00 01 → 0001 → 1
x=1, y=0 → 01 00 → 0100 → 4
x=1, y=1 → 01 01 → 0101 → 5
...

Interleaved Sort Key 适合多列等权重过滤的场景,但维护成本较高:增量数据插入会破坏排序,需要定期 VACUUM。

排序键和 JOIN 算法

已排序的数据可以使用 Merge JOIN 算法,其时间复杂度为 O(n+m)(两表已排序),优于 Hash JOIN 的 O(n+m)(构建哈希表)。如果两张表都按 JOIN 列排序,Redshift 会自动选择 Merge JOIN。

sql 复制代码
-- 两表都按 product_id 排序
CREATE TABLE fact_sales (...) SORTKEY(product_id);
CREATE TABLE dim_product (...) SORTKEY(product_id);

-- 查询优化器会自动选择 Merge JOIN
SELECT * FROM fact_sales s
JOIN dim_product p ON s.product_id = p.product_id;

排序键选择策略如下

  • 单列排序键:选择 WHERE 子句中最常用的单个过滤列。简单高效,Zone Map 过滤效果最好
  • 多列 Compound 排序键:将高选择性的列放在前面(第一列决定主排序)。适合 WHERE col1 = ? AND col2 = ? 这种前缀匹配的查询模式
  • 时间列:时间戳列是优秀的排序键候选,因为时间范围查询极为常见(如"最近 7 天"、"本月"),且时间戳天然递增,与 COPY 追加写入完美配合
  • 避免在排序键列上使用压缩:排序键列应使用 RAW 编码(不压缩),因为压缩后的数据需要先解压才能比较,会抵消排序带来的 Zone Map 过滤优势

Zone Map 的更新机制

Zone Map在以下操作时自动维护:

  • COPY:批量加载数据时,新数据写入新的 Block,每个新 Block 的 min/max 值被准确计算并写入 Zone Map。这是维护 Zone Map 最高效的方式
  • INSERT:单条或小批量插入时,数据追加到表的末尾或已有 Block 中,相关 Block 的 Zone Map 被更新。但频繁小批量 INSERT 会导致 Block 内的 min/max 范围扩大,降低过滤效率
  • VACUUM:重新排序数据后,重建所有 Block 的 Zone Map。VACUUM 后的 Zone Map 最精确,因为排序后的数据使每个 Block 的值域范围最小

关键点:增量插入(尤其是小批量 INSERT)可能导致 Zone Map 不准确,因为每个 Block 可能包含来自不同时间点的数据,min/max 范围变大,过滤效果下降。定期 VACUUM SORT 可以恢复 Zone Map 的有效性。

VACUUM SORT 的必要性

增量数据插入会破坏原有的物理排序顺序。随着时间推移,新插入的数据散布在磁盘的各个位置,Zone Map 的过滤效果逐渐失效。VACUUM SORT 重新组织数据,恢复排序顺序和 Zone Map 有效性。

sql 复制代码
-- 执行 VACUUM(仅排序,不删除已删除行)
VACUUM SORT ONLY table_name;

-- 执行完整 VACUUM(排序 + 删除已标记删除的行)
VACUUM table_name;

压缩编码

Redshift 对每一列独立应用压缩算法,减少存储空间和 I/O。

常见编码有

  • AZ64:Amazon 自研压缩算法,适合数值类型(INTEGER、BIGINT、DECIMAL)和日期时间类型(DATE、TIMESTAMP),利用 SIMD 指令并行解压,在压缩率和查询性能之间达到最优平衡
  • LZO:适合长字符串(VARCHAR),特别是自由文本(产品描述、用户评论、JSON 字符串),提供高压缩比和良好的解压速度
  • ZSTD:Facebook 开发的通用压缩算法,支持几乎所有数据类型,压缩率通常优于 LZO,且不会出现压缩后体积反而变大的情况
  • RAW:不压缩,用于排序键列和布尔类型(BOOLEAN)。保持原始数据格式以确保排序比较和 Zone Map 的准确性
  • BYTEDICT:字典编码,将低基数码值(唯一值 < 256)映射为 1 字节代码,适合性别、国家代码、状态码等列

COMPUPDATE PRESET 参数让 COPY 命令根据列数据类型自动选择最佳压缩编码。

AZ64

AZ64 是 Amazon 为 Redshift 开发的专有压缩算法,设计目标是在压缩率和 CPU 效率之间取得最佳平衡。

  • CPU 效率:解压缩路径经过专门优化,充分利用 SIMD(单指令多数据)指令,在单次 CPU 周期内并行处理多个数据值,解压速度远快于通用压缩算法
  • 压缩率:针对数值类型和日期类型的统计分布特性优化(数值列通常有较窄的值域范围),在数值和日期类型上通常具有良好的压缩表现
  • 支持的数据类型:SMALLINT、INTEGER、BIGINT、DECIMAL、DATE、TIMESTAMP、TIMESTAMPTZ,覆盖了数据仓库中最常见的数值和日期列

对于数值和日期类型的列,AZ64 是推荐的编码选择。

压缩的代价是解压缩的 CPU 开销。压缩率越高,解压缩越耗时。在实际工作中,需要权衡:

  • I/O 密集型查询:查询需要扫描大量数据行,瓶颈在磁盘 I/O。此时高压缩率是有利的------压缩减少了需要从磁盘读取的数据量,I/O 节省的收益大于解压缩的 CPU 开销
  • CPU 密集型查询:查询涉及大量计算(复杂聚合、字符串操作、UDF),瓶颈在 CPU。此时应考虑使用较低压缩率或 RAW 编码,避免解压缩占用额外的 CPU 资源
排序键列不压缩

排序键列使用 RAW 编码(不压缩)的原因在于排序操作的特殊性。压缩后的数据需要先解压缩才能比较,解压缩操作会抵消排序带来的性能优势。此外,压缩后的数据顺序与原始顺序可能不同,影响 Zone Map 的准确性。

自动压缩分析

COPY 命令的 COMPUPDATE PRESET 参数启用自动压缩分析。Redshift 会:

  1. 采样加载数据的前 100,000 行
  2. 对每种编码算法进行压缩测试
  3. 选择压缩率最高的编码
  4. 将编码信息写入表元数据
sql 复制代码
COPY fact_sales
FROM 's3://bucket/path/'
IAM_ROLE 'arn:aws:iam::account:role/role'
COMPUPDATE PRESET AUTO;

也可以手动运行 ANALYZE COMPRESSION 命令对已有数据进行分析:

sql 复制代码
ANALYZE COMPRESSION fact_sales;

数据加载

COPY 是 Redshift 加载数据的首选方式,远优于 INSERT:

COPY 的优势如下

  • 从 S3 并行加载多个文件(每个 Slice 独立读取)
  • 自动压缩编码分析
  • 支持多种格式:CSV、JSON、Parquet、ORC、Avro
  • 支持压缩文件:GZIP、LZO、ZSTD、BZIP2

并行加载的工作原理

COPY 命令的并行加载机制充分利用了 Redshift 的 MPP 架构。当执行 COPY 时,Redshift 将 S3 上的文件列表分配给各 Slice,每个 Slice 独立读取自己负责的文件子集。

复制代码
假设集群有 6 个 Slice,S3 路径包含 12 个文件:

Slice 1 → file-0001, file-0007
Slice 2 → file-0002, file-0008
Slice 3 → file-0003, file-0009
Slice 4 → file-0004, file-0010
Slice 5 → file-0005, file-0011
Slice 6 → file-0006, file-0012

文件数量与 Slice 数量的匹配对加载性能影响显著。最佳实践是文件数 = Slice 数的倍数,确保各 Slice 负载均衡。

Manifest 文件的格式与作用?Manifest 文件是一个 JSON 格式的文件,精确指定要加载的文件列表、格式和压缩方式。

json 复制代码
{
  "entries": [
    {
      "url": "s3://bucket/data/file-0001.csv.gz",
      "mandatory": true
    },
    {
      "url": "s3://bucket/data/file-0002.csv.gz",
      "mandatory": true
    }
  ]
}

使用 Manifest 的场景:

  • 只加载部分文件,排除特定文件
  • 加载跨多个 S3 前缀的文件
  • 确保精确的文件列表,避免 S3 列表不一致问题
sql 复制代码
COPY fact_sales
FROM 's3://bucket/manifest.json'
IAM_ROLE 'arn:aws:iam::account:role/role'
MANIFEST;

COPY 命令的执行分为两个阶段:

  1. 数据加载阶段
  2. 元数据更新阶段

这种两阶段设计确保了加载的原子性:如果加载失败,元数据不会更新,已加载的部分对用户不可见。

COPY 命令提供多种错误处理机制:

  • MAXERROR:指定 COPY 允许的最大错误行数。超过此阈值则整个 COPY 事务回滚。例如 MAXERROR 100 允许最多 100 行数据格式错误,超过则失败
  • STL_LOAD_ERRORS:系统视图,记录每次加载失败的详细信息(错误类型、出错行号、原始数据、错误原因),用于排查数据质量问题
  • STL_LOAD_COMMITS:系统视图,记录成功完成的 COPY 事务信息(加载的文件名、行数、字节数),用于验证数据加载完整性
sql 复制代码
-- 查看加载错误
SELECT *
FROM stl_load_errors
ORDER BY starttime DESC
LIMIT 10;

INSERT 性能为何较差

与 COPY 相比,INSERT 的性能较差,原因包括:

  1. WAL 日志:每条 INSERT 都需要写入预写日志(Write-Ahead Log)以保证事务持久性,日志写入的 I/O 开销是性能瓶颈
  2. 网络往返:每条 INSERT 语句需要一次客户端到 Leader Node 的网络往返,批量 INSERT 也需要多次往返,延迟累积严重
  3. 单行处理:INSERT 逐行处理,无法利用 COPY 的并行加载和批量优化机制。COPY 将整个文件分片到各 Slice 并行处理,而 INSERT 只能串行写入

对于大规模数据加载,始终使用 COPY 而非 INSERT。如果需要增量更新,使用 MERGE 命令配合 staging table。

对于需要增量更新(插入新行 + 更新已有行)的场景,推荐使用 MERGE 命令配合 staging table:

sql 复制代码
-- 1. 将增量数据加载到 staging 表
COPY staging_sales
FROM 's3://bucket/incremental/'
IAM_ROLE 'arn:aws:iam::account:role/role';

-- 2. 使用 MERGE 同步到目标表
MERGE INTO fact_sales f
USING staging_sales s
ON f.sale_id = s.sale_id
WHEN MATCHED THEN
    UPDATE SET amount = s.amount, updated_at = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
    INSERT (sale_id, product_id, amount, sale_date)
    VALUES (s.sale_id, s.product_id, s.amount, s.sale_date);

这种模式的优势在于:COPY 处理批量加载的高吞吐量,MERGE 处理精确的增量更新。

性能最佳实践如下

  • 将大文件拆分为多个小文件(文件数 = Slice 数的倍数)
  • 文件大小均匀,避免数据倾斜
  • 使用 manifest 文件精确控制加载哪些文件
  • 加载前排序数据(按 Sort Key 顺序)可避免后续 VACUUM SORT
  • 避免在加载时同时运行大量并发查询,COPY 会占用大量资源

参考文档

  1. 数据仓库系统架构(Leader Node、Compute Node、Slice)
  2. 列式存储(Columnar Storage)
  3. Amazon Redshift 性能(MPP、列式存储、压缩、查询优化器)
  4. 分布方式(Distribution Styles:AUTO、EVEN、KEY、ALL)
  5. 数据分布查询优化(数据倾斜、Colocation)
  6. 排序键(Sort Keys:Compound、Interleaved)
  7. Interleaved Sort Key 与 Z-Order Curve
  8. VACUUM 命令(SORT ONLY、FULL、REINDEX)
  9. Vacuuming Tables(自动排序、删除回收)
  10. 压缩编码(AZ64、LZO、ZSTD、BYTEDICT、RLE、Delta)
  11. ANALYZE COMPRESSION 命令
  12. 自动压缩加载(COMPUPDATE)
  13. COPY 命令(从 S3 加载数据)
  14. 并行加载(数据文件拆分策略)
  15. Manifest 文件格式与使用
  16. MERGE 命令(UPSERT)
  17. Serverless 计算容量(RPU)
  18. Serverless Workgroup 与 Namespace
相关推荐
●VON21 分钟前
AtomGit Flutter鸿蒙客户端:用户资料
flutter·华为·架构·跨平台·harmonyos·鸿蒙
SL-staff27 分钟前
Web 白板技术架构深度解析:从渲染到协作的选型哲学
前端·架构
前端冒菜师44 分钟前
别急着做 Agent,AI 工程化的第一步是 Skill 化
架构·ai编程
Patrick_Wilson1 小时前
为省一次回归测试,该不该把多个改动堆进一条分支?
git·ci/cd·架构
oqX0Cazj22 小时前
2026超火Go-Zero实战:从架构原理到高并发接口落地,彻底解决接口超时、雪崩问题
开发语言·架构·golang
蝎子莱莱爱打怪2 小时前
XZLL-IM干货系列 02|Protobuf 协议设计:从 JSON 切到二进制,每条消息省了 60%
后端·面试·架构
Java识堂2 小时前
如何对微服务进行拆分?
微服务·云原生·架构
●VON2 小时前
AtomGit Flutter鸿蒙客户端:收藏仓库
flutter·架构·跨平台·harmonyos·鸿蒙