在中国区Amazon Redshift端到端实践包括数仓、数据湖、权限与共享等

环境配置

数据上传

创建 S3 Bucket

bash 复制代码
aws s3 mb s3://redshift-lab-data-xxxxxxxxxx --region cn-north-1

使用 Python 脚本生成 TICKIT 测试数据集(模拟票务系统):

python 复制代码
import csv
import random
from datetime import date, timedelta

# users.csv (500行)
with open('users.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['userid', 'username', 'firstname', 'lastname', 'city', 'state', 'email', 'phone', 'likesports', 'liketheatre', 'likeconcerts', 'likejazz', 'likeclassical', 'likeopera', 'likerock', 'likevegas', 'likebroadway', 'likemusicals'])
    for i in range(1, 501):
        writer.writerow([i, f'user{i}', f'First{i}', f'Last{i}', 'Beijing', 'CN', f'user{i}@test.com', f'1{i:010d}', 'true', 'true', 'true', 'false', 'false', 'false', 'true', 'false', 'false', 'false'])

# event.csv (800行)
with open('event.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['eventid', 'venueid', 'catid', 'dateid', 'eventname', 'starttime'])
    events = ['Concert', 'Theatre', 'Sports', 'Opera', 'Jazz']
    for i in range(1, 801):
        writer.writerow([i, random.randint(1, 80), random.randint(1, 11), random.randint(1, 365), f'{random.choice(events)} {i}', f'2024-{random.randint(1,12):02d}-{random.randint(1,28):02d} {random.randint(10,22):02d}:00:00'])

# category.csv (11行)
with open('category.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['catid', 'catgroupname', 'catname', 'catdesc'])
    cats = [('Sports', 'NFL', 'National Football League'), ('Sports', 'NBA', 'National Basketball Association'), 
            ('Sports', 'MLB', 'Major League Baseball'), ('Sports', 'NHL', 'National Hockey League'),
            ('Shows', 'Musicals', 'Musical theatre'), ('Shows', 'Opera', 'Opera'),
            ('Shows', 'Plays', 'Plays'), ('Concerts', 'Pop', 'Pop music'),
            ('Concerts', 'Classical', 'Classical music'), ('Concerts', 'Jazz', 'Jazz music'),
            ('Concerts', 'Rock', 'Rock music')]
    for i, (group, name, desc) in enumerate(cats, 1):
        writer.writerow([i, group, name, desc])

# venue.csv (80行)
with open('venue.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['venueid', 'venuename', 'venuecity', 'venuestate', 'venueseats'])
    venues = ['National Stadium', 'Great Hall', 'City Arena', 'Concert Hall', 'Sports Center']
    cities = ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu']
    for i in range(1, 81):
        writer.writerow([i, f'{random.choice(venues)} {i}', random.choice(cities), 'CN', random.randint(1000, 50000)])

# date.csv (365行)
with open('date.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['dateid', 'caldate', 'day', 'week', 'month', 'qtr', 'year', 'holiday'])
    start = date(2024, 1, 1)
    for i in range(1, 366):
        d = start + timedelta(days=i-1)
        writer.writerow([i, d.isoformat(), d.day, d.isocalendar()[1], d.month, (d.month-1)//3+1, d.year, 'false'])

# sales.csv (5000行)
with open('sales.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['salesid', 'listid', 'sellerid', 'buyerid', 'eventid', 'dateid', 'qtysold', 'pricepaid', 'commission', 'saletime'])
    for i in range(1, 5001):
        writer.writerow([i, random.randint(1, 5000), random.randint(1, 500), random.randint(1, 500), 
                        random.randint(1, 800), random.randint(1, 365), random.randint(1, 4), 
                        random.randint(50, 500), random.randint(5, 50), f'2024-{random.randint(1,12):02d}-{random.randint(1,28):02d} {random.randint(10,22):02d}:00:00'])

# listing.csv (5000行)
with open('listing.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['listid', 'sellerid', 'eventid', 'dateid', 'numtickets', 'priceperticket', 'totalprice', 'listtime'])
    for i in range(1, 5001):
        price = random.randint(50, 500)
        qty = random.randint(1, 10)
        writer.writerow([i, random.randint(1, 500), random.randint(1, 800), random.randint(1, 365),
                        qty, price, price*qty, f'2024-{random.randint(1,12):02d}-{random.randint(1,28):02d} {random.randint(10,22):02d}:00:00'])

print("数据文件生成完成!")

上传到 S3

bash 复制代码
# 逐个上传 CSV 文件
for file in users.csv event.csv category.csv venue.csv date.csv sales.csv listing.csv; do
    aws s3 cp $file s3://redshift-lab-data-xxxxxxxxxx/tickit/
done

生成的数据规模如下:

表名 行数 说明
users 500 用户表
event 800 活动表
category 11 类别表
venue 80 场馆表
date 365 日期表
sales 5000 销售表
listing 5000 列表表

Redshift Serverless 配置

创建 Namespace

bash 复制代码
aws --region cn-north-1 redshift-serverless create-namespace \
    --namespace-name lab-namespace \
    --db-name dev \
    --admin-username awsuser \
    --admin-user-password 'Awsuser123!' \
    --iam-roles 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'

创建 Workgroup。

  • RPU(Redshift Processing Unit)是 Serverless 的计算单位,每个 RPU = 16 GB 内存。8 RPU 是最小配置,适合开发和测试。Serverless 支持自动暂停功能,无查询时不产生计算费用。
bash 复制代码
aws --region cn-north-1 redshift-serverless create-workgroup \
    --workgroup-name lab-workgroup \
    --namespace-name lab-namespace \
    --base-capacity 8 \
    --vpc-id vpc-086d798xxxxxe2ae

Provisioned 集群配置

Redshift 提供两种节点类型:DC2 和 RA3。两者的核心区别如下:

特性 DC2 RA3
架构 计算存储耦合 计算存储分离
存储 本地 SSD S3 托管存储
性能 最好(本地 SSD 低延迟) 较好(S3 访问有网络延迟)
扩展性 固定存储,扩展不灵活 计算和存储独立扩展
弹性 需调整节点数 快速调整 RPU
状态 正在被淘汰(deprecated) 当前推荐类型

创建集群。中国区只允许创建 RA3 节点。

bash 复制代码
aws --region cn-north-1 redshift create-cluster \
    --cluster-identifier lab-provisioned \
    --node-type ra3.large \
    --number-of-nodes 2 \
    --master-username awsuser \
    --master-user-password 'Awsuser123!' \
    --db-name dev

IAM Role 配置

创建 IAM Role

bash 复制代码
# 创建信任策略
cat > trust-policy.json << 'EOF'
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {"Service": "redshift.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF

# 创建 Role
aws iam create-role \
    --role-name RedshiftLabRole \
    --assume-role-policy-document file://trust-policy.json

附加 S3 访问策略。

  • 后续执行 UNLOAD 操作时发现需要写入权限,添加了 s3:PutObjects3:DeleteObject。执行 Glue Crawler 时发现需要 Glue 权限,添加了完整的 glue:*logs:* 权限。
bash 复制代码
aws iam put-role-policy --role-name RedshiftLabRole \
    --policy-name S3AccessPolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    "arn:aws-cn:s3:::redshift-lab-data-xxxxxxxxxx",
                    "arn:aws-cn:s3:::redshift-lab-data-xxxxxxxxxx/*"
                ]
            }
        ]
    }'

关联到 Redshift

bash 复制代码
# 关联到 Serverless Namespace
aws --region cn-north-1 redshift-serverless update-namespace \
    --namespace-name lab-namespace \
    --iam-roles 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'

# 关联到 Provisioned 集群
aws --region cn-north-1 redshift modify-cluster-iam-roles \
    --cluster-identifier lab-provisioned \
    --add-iam-roles 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'

IAM Role 的作用如下

操作 需要 IAM Role 的原因
COPY 从 S3 读取数据加载到表
UNLOAD 将查询结果导出到 S3
Spectrum 读取 S3 中的外部表数据
Data Sharing 跨集群共享数据

可以关联多个 IAM Role,不同操作使用不同权限,实现权限隔离。

连接集群

Redshift 支持多种连接方式,本文使用 psycopg2 直接连接(走 VPC 内网)。另一种方式是 Redshift Data API(aws redshift-data execute-statement),它通过 AWS API 提交 SQL,不需要网络可达,但延迟更高、无法跑事务。

Query Editor V2

Query Editor V2 提供三种连接方式:

连接方式 认证方式 访问 Glue 适用场景
Federated user AWS IAM 身份 支持 需要访问 Glue Data Catalog
Database user 数据库用户名密码 受限 简单直接的管理操作
Secrets Manager 从 Secrets Manager 读取 支持 企业环境集中凭据管理

问题:使用 Database user 连接时出现错误 "Error accessing the database 'awsdatacatalog'"。这是因为 Database user 没有 IAM 身份,无法访问 Glue Data Catalog。解决方式是切换到 Federated user 连接,或者通过 External Schema + IAM Role 方式访问。

连接验证

sql 复制代码
-- Serverless 端点验证
SELECT current_user, current_database();
-- 结果: ('awsuser', 'dev')

-- Provisioned 端点验证
SELECT current_user, current_database();
-- 结果: ('awsuser', 'dev')
Data API

Data API 是 Redshift 提供的无连接、异步的 HTTP 查询接口,免去了管理 JDBC/ODBC 连接池的麻烦。

  • 无需管理连接:HTTPS 调用 + IAM 鉴权
  • 异步执行:execute_statement 立即返回 query_id,后续轮询 describe_statement
  • AWS SDK 集成:Python(boto3)、Java、Node.js、Go 等都直接支持
  • 典型用途:Lambda 调用 Redshift、跨账户查询、API Gateway 暴露报表服务

示意图如下
#mermaid-svg-GmWrvAPO6CQxuMsy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GmWrvAPO6CQxuMsy .error-icon{fill:#552222;}#mermaid-svg-GmWrvAPO6CQxuMsy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GmWrvAPO6CQxuMsy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GmWrvAPO6CQxuMsy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GmWrvAPO6CQxuMsy .marker.cross{stroke:#333333;}#mermaid-svg-GmWrvAPO6CQxuMsy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GmWrvAPO6CQxuMsy p{margin:0;}#mermaid-svg-GmWrvAPO6CQxuMsy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster-label text{fill:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster-label span{color:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster-label span p{background-color:transparent;}#mermaid-svg-GmWrvAPO6CQxuMsy .label text,#mermaid-svg-GmWrvAPO6CQxuMsy span{fill:#333;color:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy .node rect,#mermaid-svg-GmWrvAPO6CQxuMsy .node circle,#mermaid-svg-GmWrvAPO6CQxuMsy .node ellipse,#mermaid-svg-GmWrvAPO6CQxuMsy .node polygon,#mermaid-svg-GmWrvAPO6CQxuMsy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GmWrvAPO6CQxuMsy .rough-node .label text,#mermaid-svg-GmWrvAPO6CQxuMsy .node .label text,#mermaid-svg-GmWrvAPO6CQxuMsy .image-shape .label,#mermaid-svg-GmWrvAPO6CQxuMsy .icon-shape .label{text-anchor:middle;}#mermaid-svg-GmWrvAPO6CQxuMsy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GmWrvAPO6CQxuMsy .rough-node .label,#mermaid-svg-GmWrvAPO6CQxuMsy .node .label,#mermaid-svg-GmWrvAPO6CQxuMsy .image-shape .label,#mermaid-svg-GmWrvAPO6CQxuMsy .icon-shape .label{text-align:center;}#mermaid-svg-GmWrvAPO6CQxuMsy .node.clickable{cursor:pointer;}#mermaid-svg-GmWrvAPO6CQxuMsy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GmWrvAPO6CQxuMsy .arrowheadPath{fill:#333333;}#mermaid-svg-GmWrvAPO6CQxuMsy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GmWrvAPO6CQxuMsy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GmWrvAPO6CQxuMsy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GmWrvAPO6CQxuMsy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GmWrvAPO6CQxuMsy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GmWrvAPO6CQxuMsy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster text{fill:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy .cluster span{color:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GmWrvAPO6CQxuMsy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GmWrvAPO6CQxuMsy rect.text{fill:none;stroke-width:0;}#mermaid-svg-GmWrvAPO6CQxuMsy .icon-shape,#mermaid-svg-GmWrvAPO6CQxuMsy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GmWrvAPO6CQxuMsy .icon-shape p,#mermaid-svg-GmWrvAPO6CQxuMsy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GmWrvAPO6CQxuMsy .icon-shape .label rect,#mermaid-svg-GmWrvAPO6CQxuMsy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GmWrvAPO6CQxuMsy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GmWrvAPO6CQxuMsy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GmWrvAPO6CQxuMsy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-GmWrvAPO6CQxuMsy .appStyle>*{fill:#FF9900!important;stroke:#333!important;color:#fff!important;}#mermaid-svg-GmWrvAPO6CQxuMsy .appStyle span{fill:#FF9900!important;stroke:#333!important;color:#fff!important;}#mermaid-svg-GmWrvAPO6CQxuMsy .appStyle tspan{fill:#fff!important;}#mermaid-svg-GmWrvAPO6CQxuMsy .rsStyle>*{fill:#1E88E5!important;stroke:#333!important;color:#fff!important;}#mermaid-svg-GmWrvAPO6CQxuMsy .rsStyle span{fill:#1E88E5!important;stroke:#333!important;color:#fff!important;}#mermaid-svg-GmWrvAPO6CQxuMsy .rsStyle tspan{fill:#fff!important;} HTTPS 调用
异步提交
轮询 query_id
返回 status / 结果
应用 / Lambda
Data API
Redshift

与 psycopg2 直连的对比

特性 psycopg2 / JDBC Data API
连接管理 需维护连接池 无连接,HTTPS 调用
执行模式 同步阻塞 异步,轮询结果
认证方式 用户名密码 IAM 身份(或 Secrets Manager)
适用场景 长连接应用、ETL Lambda、API Gateway、无服务器
网络要求 需 VPC 内访问 公网可达即可

使用示例(boto3)

python 复制代码
import boto3
import time

client = boto3.client("redshift-data", region_name="cn-north-1")

# 提交查询(异步)
resp = client.execute_statement(
    WorkgroupName="lab-workgroup",       # Serverless 用 Workgroup
    # DbClusterIdentifier="lab-provisioned",  # Provisioned 用集群名
    Database="dev",
    DbUser="awsuser",
    Sql="SELECT COUNT(*) FROM tickit.sales",
)
query_id = resp["Id"]
print(f"Submitted: {query_id}")

# 轮询结果
while True:
    desc = client.describe_statement(Id=query_id)
    status = desc["Status"]
    if status in ("FINISHED", "FAILED", "ABORTED"):
        break
    print(f"Status: {status} ...")
    time.sleep(1)

# 获取结果
if status == "FINISHED":
    result = client.get_statement_result(Id=query_id)
    print(result["Records"])

表创建与数据加载

TICKIT 是 Redshift 实验常用的示例数据集,模拟票务系统,包含用户、活动、场馆、销售等核心实体。
#mermaid-svg-FA814iOwRhPXSRDr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FA814iOwRhPXSRDr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FA814iOwRhPXSRDr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FA814iOwRhPXSRDr .error-icon{fill:#552222;}#mermaid-svg-FA814iOwRhPXSRDr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FA814iOwRhPXSRDr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FA814iOwRhPXSRDr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FA814iOwRhPXSRDr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FA814iOwRhPXSRDr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FA814iOwRhPXSRDr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FA814iOwRhPXSRDr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FA814iOwRhPXSRDr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FA814iOwRhPXSRDr .marker.cross{stroke:#333333;}#mermaid-svg-FA814iOwRhPXSRDr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FA814iOwRhPXSRDr p{margin:0;}#mermaid-svg-FA814iOwRhPXSRDr .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-FA814iOwRhPXSRDr .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-FA814iOwRhPXSRDr .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-FA814iOwRhPXSRDr .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-FA814iOwRhPXSRDr .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-FA814iOwRhPXSRDr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FA814iOwRhPXSRDr .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-FA814iOwRhPXSRDr .node rect,#mermaid-svg-FA814iOwRhPXSRDr .node circle,#mermaid-svg-FA814iOwRhPXSRDr .node ellipse,#mermaid-svg-FA814iOwRhPXSRDr .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FA814iOwRhPXSRDr .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-FA814iOwRhPXSRDr .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-FA814iOwRhPXSRDr .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FA814iOwRhPXSRDr .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-FA814iOwRhPXSRDr .edgeLabel .label text{fill:#333;}#mermaid-svg-FA814iOwRhPXSRDr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} buys
sells
in
in
hosts
belongs_to
on
on
on
USERS
int
userid
PK
string
username
string
firstname
string
lastname
string
city
string
state
SALES
int
salesid
PK
int
listid
FK
int
sellerid
FK
int
buyerid
FK
int
eventid
FK
int
dateid
FK
int
qtysold
decimal
pricepaid
decimal
commission
timestamp
saletime
LISTING
EVENT
int
eventid
PK
int
venueid
FK
int
catid
FK
int
dateid
FK
string
eventname
timestamp
starttime
VENUE
CATEGORY
DATE

创建表

sql 复制代码
-- 创建 schema
DROP SCHEMA IF EXISTS tickit CASCADE;
CREATE SCHEMA tickit;

-- 用户表 - 小表,使用 ALL 分布
CREATE TABLE tickit.users (
    userid integer NOT NULL,
    username varchar(50),
    firstname varchar(50),
    lastname varchar(50),
    city varchar(50),
    state char(2),
    email varchar(100),
    phone varchar(20),
    likesports boolean,
    liketheatre boolean,
    likeconcerts boolean,
    likejazz boolean,
    likeclassical boolean,
    likeopera boolean,
    likerock boolean,
    likevegas boolean,
    likebroadway boolean,
    likemusicals boolean
)
DISTSTYLE ALL;

-- 类别表 - 小表,使用 ALL 分布
CREATE TABLE tickit.category (
    catid integer NOT NULL,
    catgroup varchar(50),
    catname varchar(50),
    catdesc varchar(100)
)
DISTSTYLE ALL;

-- 场馆表 - 小表,使用 ALL 分布
CREATE TABLE tickit.venue (
    venueid integer NOT NULL,
    venuename varchar(100),
    venuecity varchar(50),
    venuestate char(2),
    venueseats integer
)
DISTSTYLE ALL;

-- 日期表 - 小表,使用 ALL 分布
CREATE TABLE tickit.date (
    dateid integer NOT NULL,
    caldate date,
    day integer,
    week integer,
    month integer,
    qtr integer,
    year integer,
    holiday boolean
)
DISTSTYLE ALL;

-- 活动表 - 中等表,使用 ALL 分布
CREATE TABLE tickit.event (
    eventid integer NOT NULL,
    venueid integer,
    catid integer,
    dateid integer,
    eventname varchar(200),
    starttime timestamp
)
DISTSTYLE ALL;

-- 列表表 - 大表,按 listid 分布,按 listtime 排序
CREATE TABLE tickit.listing (
    listid integer NOT NULL,
    sellerid integer,
    eventid integer,
    dateid integer,
    numtickets integer,
    priceperticket decimal(10,2),
    totalprice decimal(10,2),
    listtime timestamp
)
DISTKEY (listid)
SORTKEY (listtime);

-- 销售表 - 最大表,按 listid 分布,按 saletime 排序
CREATE TABLE tickit.sales (
    salesid integer NOT NULL,
    listid integer,
    sellerid integer,
    buyerid integer,
    eventid integer,
    dateid integer,
    qtysold integer,
    pricepaid decimal(10,2),
    commission decimal(10,2),
    saletime timestamp
)
DISTKEY (listid)
SORTKEY (saletime);

表设计策略

表名 分布方式 排序键 设计理由
users DISTSTYLE ALL 小表,广播到所有节点避免重分布
category DISTSTYLE ALL 极小表,只有 11 行
venue DISTSTYLE ALL 小表,只有 80 行
date DISTSTYLE ALL 小表,只有 365 行
event DISTSTYLE ALL 中等表,与其他表 JOIN 时广播
listing DISTKEY (listid) SORTKEY (listtime) 大表,与 sales 共置,按时间查询
sales DISTKEY (listid) SORTKEY (saletime) 最大表,与 listing 共置,按时间查询

概念 - DISTSTYLE 和 SORTKEY

  • DISTSTYLE ALL:表的完整副本存在每个计算节点上。适合小表(维度表),JOIN 时不需要在节点间移动数据。
  • DISTKEY (列):按指定列的哈希值将数据分布到不同节点。适合大表(事实表),相同 DISTKEY 的数据在同一节点,JOIN 效率高。
  • SORTKEY (列):数据在节点内按指定列排序存储。适合时间范围查询(如 WHERE saletime >= '2024-06-01'),可以跳过大量无关数据块。

listingsales 都用 listid 作为 DISTKEY,这样两表 JOIN 时数据已经在同一节点(称为"colocation"),避免网络传输。

从 S3 加载数据

COPY 命令参数说明:

  • IAM_ROLE:指定访问 S3 的 IAM Role。中国区必须使用 arn:aws-cn: 前缀。
  • REGION:S3 Bucket 所在区域。中国区必须显式指定 cn-north-1
  • CSV:源文件格式为 CSV。
  • IGNOREHEADER 1:跳过第一行(表头)。
sql 复制代码
-- 加载用户表
COPY tickit.users
FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/users.csv'
IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'
REGION 'cn-north-1'
CSV IGNOREHEADER 1;

-- 加载其他表(使用相同模式)
COPY tickit.category FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/category.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;
COPY tickit.venue FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/venue.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;
COPY tickit.date FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/date.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;
COPY tickit.event FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/event.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;
COPY tickit.listing FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/listing.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;
COPY tickit.sales FROM 's3://redshift-lab-data-xxxxxxxxxx/tickit/sales.csv' IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' REGION 'cn-north-1' CSV IGNOREHEADER 1;

验证加载结果

sql 复制代码
SELECT 'users' AS tbl, COUNT(*) AS rows FROM tickit.users
UNION ALL SELECT 'category', COUNT(*) FROM tickit.category
UNION ALL SELECT 'venue', COUNT(*) FROM tickit.venue
UNION ALL SELECT 'date', COUNT(*) FROM tickit.date
UNION ALL SELECT 'event', COUNT(*) FROM tickit.event
UNION ALL SELECT 'listing', COUNT(*) FROM tickit.listing
UNION ALL SELECT 'sales', COUNT(*) FROM tickit.sales
ORDER BY 1;

执行计划分析

sql 复制代码
EXPLAIN
SELECT e.eventname, v.venuename, s.qtysold, s.pricepaid
FROM tickit.sales s
JOIN tickit.event e ON s.eventid = e.eventid
JOIN tickit.venue v ON e.venueid = v.venueid
WHERE s.saletime >= '2024-06-01'
ORDER BY s.saletime;

执行计划输出:

复制代码
XN Merge  (cost=28.60..81.09 rows=2866 width=60)
  Merge Key: s.saletime
  ->  XN Network  (cost=28.60..81.09 rows=2866 width=60)
        Send to leader
        ->  XN Hash Join DS_DIST_ALL_NONE  (cost=28.60..81.09 rows=2866 width=60)
              Hash Cond: ("outer".venueid = "inner".venueid)
              ->  XN Hash Join DS_DIST_ALL_NONE  (cost=26.00..70.43 rows=2866 width=45)
                    Hash Cond: ("outer".eventid = "inner".eventid)
                    ->  XN Seq Scan on sales s  (cost=0.00..36.25 rows=2901 width=32)
                          Filter: (saletime >= '2024-06-01')
                    ->  XN Hash  (cost=8.00..8.00 rows=800 width=21)
                          ->  XN Seq Scan on event e  (cost=0.00..8.00 rows=800 width=21)
              ->  XN Hash  (cost=0.80..0.80 rows=80 width=23)
                    ->  XN Seq Scan on venue v  (cost=0.00..0.80 rows=80 width=23)

执行计划解读:

节点 含义
XN Seq Scan 顺序扫描整张表,最基本的读取方式
XN Hash Join 哈希连接:把小表建成哈希表,再用大表去探测。适合等值 JOIN
DS_DIST_ALL_NONE 数据不需要在节点间重分布,因为维度表用了 DISTSTYLE ALL,每个节点都有完整副本
XN Merge 归并排序(因为 ORDER BY saletime)
XN Network Send to leader 将结果发送到 leader 节点汇总
Filter: (saletime >= '2024-06-01') 谓词过滤,在扫描 sales 表时直接跳过不满足条件的行

#mermaid-svg-SAiAi26EvxOuFd3e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SAiAi26EvxOuFd3e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SAiAi26EvxOuFd3e .error-icon{fill:#552222;}#mermaid-svg-SAiAi26EvxOuFd3e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SAiAi26EvxOuFd3e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SAiAi26EvxOuFd3e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SAiAi26EvxOuFd3e .marker.cross{stroke:#333333;}#mermaid-svg-SAiAi26EvxOuFd3e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SAiAi26EvxOuFd3e p{margin:0;}#mermaid-svg-SAiAi26EvxOuFd3e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SAiAi26EvxOuFd3e .cluster-label text{fill:#333;}#mermaid-svg-SAiAi26EvxOuFd3e .cluster-label span{color:#333;}#mermaid-svg-SAiAi26EvxOuFd3e .cluster-label span p{background-color:transparent;}#mermaid-svg-SAiAi26EvxOuFd3e .label text,#mermaid-svg-SAiAi26EvxOuFd3e span{fill:#333;color:#333;}#mermaid-svg-SAiAi26EvxOuFd3e .node rect,#mermaid-svg-SAiAi26EvxOuFd3e .node circle,#mermaid-svg-SAiAi26EvxOuFd3e .node ellipse,#mermaid-svg-SAiAi26EvxOuFd3e .node polygon,#mermaid-svg-SAiAi26EvxOuFd3e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SAiAi26EvxOuFd3e .rough-node .label text,#mermaid-svg-SAiAi26EvxOuFd3e .node .label text,#mermaid-svg-SAiAi26EvxOuFd3e .image-shape .label,#mermaid-svg-SAiAi26EvxOuFd3e .icon-shape .label{text-anchor:middle;}#mermaid-svg-SAiAi26EvxOuFd3e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SAiAi26EvxOuFd3e .rough-node .label,#mermaid-svg-SAiAi26EvxOuFd3e .node .label,#mermaid-svg-SAiAi26EvxOuFd3e .image-shape .label,#mermaid-svg-SAiAi26EvxOuFd3e .icon-shape .label{text-align:center;}#mermaid-svg-SAiAi26EvxOuFd3e .node.clickable{cursor:pointer;}#mermaid-svg-SAiAi26EvxOuFd3e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SAiAi26EvxOuFd3e .arrowheadPath{fill:#333333;}#mermaid-svg-SAiAi26EvxOuFd3e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SAiAi26EvxOuFd3e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SAiAi26EvxOuFd3e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SAiAi26EvxOuFd3e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SAiAi26EvxOuFd3e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SAiAi26EvxOuFd3e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SAiAi26EvxOuFd3e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SAiAi26EvxOuFd3e .cluster text{fill:#333;}#mermaid-svg-SAiAi26EvxOuFd3e .cluster span{color:#333;}#mermaid-svg-SAiAi26EvxOuFd3e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SAiAi26EvxOuFd3e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SAiAi26EvxOuFd3e rect.text{fill:none;stroke-width:0;}#mermaid-svg-SAiAi26EvxOuFd3e .icon-shape,#mermaid-svg-SAiAi26EvxOuFd3e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SAiAi26EvxOuFd3e .icon-shape p,#mermaid-svg-SAiAi26EvxOuFd3e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SAiAi26EvxOuFd3e .icon-shape .label rect,#mermaid-svg-SAiAi26EvxOuFd3e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SAiAi26EvxOuFd3e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SAiAi26EvxOuFd3e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SAiAi26EvxOuFd3e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Leader 节点

解析 SQL,生成执行计划
计算节点 1
计算节点 2
计算节点 N
各节点并行执行

Seq Scan + Hash Join
结果汇总到 Leader

Merge + 排序
返回给客户端

Redshift 查询优化器依赖表的统计信息(行数、列值分布、NULL 比例等)来选择最优执行计划。COPY 加载空表时会自动执行 ANALYZE,大量数据变更后建议手动执行。Redshift 也会在低负载时自动运行 ANALYZE。

sql 复制代码
-- 分析销售表
ANALYZE tickit.sales;

SVV_TABLE_INFO - 查看表信息

sql 复制代码
SELECT "table", size, tbl_rows, estimated_visible_rows
FROM SVV_TABLE_INFO
WHERE "table" IN ('users','sales','listing','event','category','venue','date');
table size (MB) tbl_rows estimated_visible_rows
users 42 500 500
event 18 800 800
date 22 365 365
category 14 11 11
listing 2816 5000 5000
venue 16 80 80
sales 3328 5000 5000

注意tbl_rows 包含已删除但未回收的行(称为"幽灵行"),estimated_visible_rows 是真实可见行数。两者一致说明没有未清理的删除操作。查询时 table 是 SQL 保留字,必须用双引号包裹。

VACUUM 回收

Redshift 的 DELETE 和 UPDATE 不会立即释放磁盘空间,需要 VACUUM 回收。

sql 复制代码
-- 重新排序未排序的行
VACUUM SORT ONLY tickit.sales;

VACUUM 类型对比:

类型 作用 场景
VACUUM DELETE ONLY 回收已删除行空间 大量 DELETE 后
VACUUM SORT ONLY 重新排序未排序行 大量无序 INSERT 后
VACUUM RECLUSTER 只排序未排序部分 推荐的日常维护方式
VACUUM FULL DELETE + SORT 完整维护
BOOST 选项 使用额外资源加速 维护窗口期,会阻塞 DML

Redshift 会在低负载时自动执行 VACUUM DELETE 和 VACUUM SORT,大多数情况下无需手动干预。


Spectrum 数据湖

Redshift Spectrum 是 Redshift 的数据湖查询引擎,允许用 SQL 直接查询 S3 中的数据,无需加载到 Redshift。Spectrum 运行在 AWS 托管的独立计算层,不消耗 Redshift 自身的计算资源。
#mermaid-svg-XBwIIj8EgmbuIYsR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XBwIIj8EgmbuIYsR .error-icon{fill:#552222;}#mermaid-svg-XBwIIj8EgmbuIYsR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XBwIIj8EgmbuIYsR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XBwIIj8EgmbuIYsR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XBwIIj8EgmbuIYsR .marker.cross{stroke:#333333;}#mermaid-svg-XBwIIj8EgmbuIYsR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XBwIIj8EgmbuIYsR p{margin:0;}#mermaid-svg-XBwIIj8EgmbuIYsR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster-label text{fill:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster-label span{color:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster-label span p{background-color:transparent;}#mermaid-svg-XBwIIj8EgmbuIYsR .label text,#mermaid-svg-XBwIIj8EgmbuIYsR span{fill:#333;color:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR .node rect,#mermaid-svg-XBwIIj8EgmbuIYsR .node circle,#mermaid-svg-XBwIIj8EgmbuIYsR .node ellipse,#mermaid-svg-XBwIIj8EgmbuIYsR .node polygon,#mermaid-svg-XBwIIj8EgmbuIYsR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XBwIIj8EgmbuIYsR .rough-node .label text,#mermaid-svg-XBwIIj8EgmbuIYsR .node .label text,#mermaid-svg-XBwIIj8EgmbuIYsR .image-shape .label,#mermaid-svg-XBwIIj8EgmbuIYsR .icon-shape .label{text-anchor:middle;}#mermaid-svg-XBwIIj8EgmbuIYsR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XBwIIj8EgmbuIYsR .rough-node .label,#mermaid-svg-XBwIIj8EgmbuIYsR .node .label,#mermaid-svg-XBwIIj8EgmbuIYsR .image-shape .label,#mermaid-svg-XBwIIj8EgmbuIYsR .icon-shape .label{text-align:center;}#mermaid-svg-XBwIIj8EgmbuIYsR .node.clickable{cursor:pointer;}#mermaid-svg-XBwIIj8EgmbuIYsR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XBwIIj8EgmbuIYsR .arrowheadPath{fill:#333333;}#mermaid-svg-XBwIIj8EgmbuIYsR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XBwIIj8EgmbuIYsR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XBwIIj8EgmbuIYsR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XBwIIj8EgmbuIYsR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XBwIIj8EgmbuIYsR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XBwIIj8EgmbuIYsR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster text{fill:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR .cluster span{color:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XBwIIj8EgmbuIYsR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XBwIIj8EgmbuIYsR rect.text{fill:none;stroke-width:0;}#mermaid-svg-XBwIIj8EgmbuIYsR .icon-shape,#mermaid-svg-XBwIIj8EgmbuIYsR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XBwIIj8EgmbuIYsR .icon-shape p,#mermaid-svg-XBwIIj8EgmbuIYsR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XBwIIj8EgmbuIYsR .icon-shape .label rect,#mermaid-svg-XBwIIj8EgmbuIYsR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XBwIIj8EgmbuIYsR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XBwIIj8EgmbuIYsR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XBwIIj8EgmbuIYsR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读取元数据
读取元数据
Redshift 集群

提交 SQL 查询
Spectrum Layer

独立计算资源

谓词下推、聚合
Amazon S3

数据湖

Parquet / CSV / ORC / JSON
AWS Glue Data Catalog

外部表元数据

Spectrum 查询执行流程:

  1. Redshift 解析 SQL,向 Glue 请求表元数据
  2. Redshift 生成执行计划,将查询下推到 Spectrum 层
  3. Spectrum 根据元数据直接从 S3 读取数据
  4. 谓词下推在 Spectrum 层执行,只返回满足条件的行
  5. 结果返回 Redshift 进行最终处理

核心特点:

  • 独立计算层:不消耗 Redshift 集群计算资源
  • 弹性扩展:根据查询复杂度自动扩展
  • 谓词下推:过滤条件下推到 Spectrum 层执行
  • 支持多种格式:Parquet、ORC、JSON、CSV 等

UNLOAD 导出数据到 S3

执行时报错 S3ServiceException: User is not authorized to perform: s3:PutObject。原因是 IAM Role 只有 S3 读取权限(s3:GetObject + s3:ListBucket),UNLOAD 需要写入权限。更新 IAM Policy 添加 s3:PutObjects3:DeleteObject

sql 复制代码
UNLOAD ('SELECT * FROM tickit.sales')
TO 's3://redshift-lab-data-xxxxxxxxxx/spectrum/sales_'
IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'
PARQUET REGION 'cn-north-1';

UNLOAD 参数解释:

参数 含义
PARQUET 列式存储格式,查询时只读取需要的列,大幅减少 I/O
自动分片 Redshift 按节点自动生成多个 Parquet 文件(0000_part_00, 0001_part_00...)

创建 Glue 数据库和 Crawler

bash 复制代码
# 创建 Glue 数据库
aws --region cn-north-1 glue create-database --database-input '{"Name":"spectrumdb"}'

# 创建 Crawler
aws --region cn-north-1 glue create-crawler \
  --name sales-crawler \
  --role 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole' \
  --database-name spectrumdb \
  --targets '{"S3Targets":[{"Path":"s3://redshift-lab-data-xxxxxxxxxx/spectrum/"}]}'

# 启动 Crawler
aws --region cn-north-1 glue start-crawler --name sales-crawler

创建外部 Schema 并查询

sql 复制代码
CREATE EXTERNAL SCHEMA spectrum_schema
FROM DATA CATALOG
DATABASE 'spectrumdb'
IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxxx:role/RedshiftLabRole'
REGION 'cn-north-1';

SELECT * FROM spectrum_schema.spectrum LIMIT 5;

FROM DATA CATALOG元数据从 AWS Glue Data Catalog 取。Redshift 还支持其他来源:

子句 元数据来源 用途
FROM DATA CATALOG AWS Glue Data Catalog 查 S3
FROM HIVE METASTORE EMR 上的 Hive Metastore 查 EMR 管理的 Hive 表
FROM POSTGRES DATABASE RDS/Aurora PostgreSQL 联邦查询,直接查 RDS
FROM MYSQL DATABASE RDS/Aurora MySQL 联邦查询,直接查 RDS
FROM REDSHIFT 另一个 Redshift db 跨数据库查询

内部表和外部表

最佳实践:将热数据放在 Redshift 内部表,冷数据/历史数据放在 S3,通过 UNION ALL 视图统一查询。

特性 内部表 外部表
数据位置 Redshift Managed Storage Amazon S3
元数据管理 Redshift 内部 AWS Glue Data Catalog
查询性能 最快(本地 SSD 缓存) 较慢(网络读取 S3)
存储成本 较高 较低
数据共享 通过 Data Sharing 任何能访问 S3 的服务

谓词下推

Spectrum 的核心优化特性之一是谓词下推(Predicate Pushdown)。过滤条件在 Spectrum 层执行,只将满足条件的数据传输到 Redshift,减少网络流量。

sql 复制代码
-- 这个查询的 WHERE 条件会在 Spectrum 层执行
SELECT * FROM spectrum_schema.spectrum
WHERE saletime >= '2024-06-01';

查看 Spectrum 查询详情

sql 复制代码
SELECT * FROM SYS_EXTERNAL_QUERY_DETAIL;

该系统视图显示 Spectrum 外部查询的执行详情,包括:

  • 读取的数据量
  • 扫描的文件数
  • 谓词下推情况
  • 分区裁剪情况

查询 Iceberg 表

Apache Iceberg 是一种开放表格式(Open Table Format),把 S3 上的数据文件管理成一张"事务性"的表。它解决了传统数据湖(Hive 风格)的痛点:

痛点 Hive Iceberg
行级 UPDATE/DELETE 不支持,只能整分区重写 支持
模式演进 加列受限,删除/重命名危险 全套 schema evolution
分区演进 改分区策略要重建数据 隐式分区演进
时间旅行查询 支持(AS OF SNAPSHOT_ID
一致性 列目录易出错 元数据文件 + 快照保证一致性

Iceberg 表的物理结构

复制代码
icebergspectrum/icebergdb.db/clickstream_iceberg_tbl/
├── data/                    <-- 实际数据文件(Parquet)
│   └── customer=1/
│       └── visityearmonth=199801/
│           └── *.parquet
└── metadata/                <-- 元数据文件
    ├── *.metadata.json      <-- schema、分区、当前快照指针
    ├── snap-*.avro          <-- 快照(一次提交)
    └── *-manifest-list.avro <-- manifest 列表 -> manifest 文件 -> data 文件

每次 DDL/DML 都生成新的快照。manifest 文件中保存数据文件路径 + 分区列 min/max,用于查询裁剪。

创建 Iceberg 表

在 cn-north-1 环境,我们使用 Athena 创建 Iceberg 表:

复制代码
-- Athena SQL
CREATE TABLE iceberg_lab.sales
WITH (
  table_type = 'ICEBERG',
  is_external = false,
  location = 's3://redshift-lab-data-xxxxxxxxx/iceberg/sales/',
  format = 'PARQUET'
) AS SELECT * FROM (
  VALUES 
    (1, 'Alice', 'east', 1000.00),
    (2, 'Bob', 'west', 2500.00),
    (3, 'Carol', 'east', 1800.00)
) AS t(id, name, region, amount);

完成后 S3 路径下出现 data/metadata/ 两个子目录。Glue Catalog 中可以看到表,Table format = Apache Iceberg

配置 Lake Formation 权限

本账户下的 Iceberg 表需要 Lake Formation 权限管理:

shell 复制代码
# 添加 IAM policy
aws iam put-role-policy --role-name RedshiftLabRole --policy-name LakeFormationAccess \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["lakeformation:*"],"Resource":"*"}]}'

# 授予数据库和表权限
aws lakeformation grant-permissions \
  --principal DataLakePrincipalIdentifier="arn:aws-cn:iam::${ACCOUNT}:role/RedshiftLabRole" \
  --permissions ALL \
  --resource '{"Database":{"Name":"iceberg_lab"}}'

aws lakeformation grant-permissions \
  --principal DataLakePrincipalIdentifier="arn:aws-cn:iam::${ACCOUNT}:role/RedshiftLabRole" \
  --permissions ALL \
  --resource '{"Table":{"DatabaseName":"iceberg_lab","Name":"sales"}}'
在 Redshift 创建外部 Schema 并查询
sql 复制代码
CREATE EXTERNAL SCHEMA iceberg_schema
FROM DATA CATALOG
DATABASE 'iceberg_lab'
IAM_ROLE 'arn:aws-cn:iam::xxxxxxxxx:role/RedshiftLabRole';

SELECT * FROM iceberg_schema.sales ORDER BY id;
-- 结果: 3 行数据
Iceberg DML 操作(通过 Athena)
sql 复制代码
-- INSERT
INSERT INTO iceberg_lab.sales VALUES (4, 'Dave', 'west', 3200.00), (5, 'Eve', 'east', 750.00);

-- UPDATE
UPDATE iceberg_lab.sales SET amount = amount * 1.1 WHERE region = 'east';

-- 通过 Spectrum 验证
SELECT * FROM iceberg_schema.sales ORDER BY id;
-- 结果: 5 行,east 区域金额增加 10%
快照历史验证

S3 metadata 目录显示了 3 个快照文件:

复制代码
iceberg/sales2/metadata/
├── 00000-4fb063c4-...metadata.json  (初始创建)
├── 00001-b42f3613-...metadata.json  (INSERT 后)
├── 00002-65a2569e-...metadata.json  (UPDATE 后,当前)
├── snap-8316465339333943354-...avro (快照1)
├── snap-6543216669922434155-...avro (快照2)
└── snap-6946948048392951822-...avro (快照3,当前)

每次 DML 操作生成新的快照文件:

复制代码
iceberg/sales/metadata/
├── 00000-...metadata.json  (CREATE TABLE)
├── 00001-...metadata.json  (INSERT 后)
├── 00002-...metadata.json  (UPDATE 后,当前)

Spectrum 查询调优

Spectrum 查询的成本主要由 S3 扫描量决定。要让 Spectrum 查询又快又省,核心思路有三条:

  1. 分区裁剪(Partition Pruning):通过分区列过滤,跳过整批文件
  2. 存储格式优化:用列式格式(Parquet)替代行式(CSV),并合并小文件
  3. 谓词下推(Predicate Pushdown):让过滤、聚合在 Spectrum 层完成,只把结果传给 Redshift

实验使用 SSB(Star Schema Benchmark)+ clickstream 数据集:

  • 维度表(建在 Redshift 内部):customer(300 万行)、dwdate(2556 行)
  • 事实表(外部表 in S3):uservisits 38 亿行,分别提供 CSV(10 文件/分区)和 Parquet(1 文件/分区)两个版本

需要 SSB/clickstream 数据集(38 亿行),仅记录理论和方法,没有进行实际测试

创建外部 Schema

sql 复制代码
CREATE EXTERNAL SCHEMA clickstream
FROM DATA CATALOG DATABASE 'clickstream'
IAM_ROLE default
CREATE EXTERNAL DATABASE IF NOT EXISTS;

外部表 clickstream.uservisits_csv10clickstream.uservisits_parquet1 通过 Glue Crawler 扫描 S3 路径自动建立,分区列为 customervisityearmonth

性能诊断系统视图

视图 适用场景 用途
SVL_S3QUERY_SUMMARY Provisioned Spectrum 查询的实际运行统计
SYS_EXTERNAL_QUERY_DETAIL Serverless 同上,新版统一系统视图
EXPLAIN 通用 查看查询计划,识别 Spectrum 步骤
sql 复制代码
-- Serverless 查 Spectrum 统计
SELECT query_id, file_location,
       end_time - start_time AS elapsed,
       total_partitions, qualified_partitions,
       scanned_files, returned_rows, returned_bytes
FROM SYS_EXTERNAL_QUERY_DETAIL
ORDER BY query_id, start_time DESC;
基线:未利用分区
sql 复制代码
SELECT c.c_name, c.c_mktsegment, t.prettyMonthYear, SUM(uv.adRevenue)
FROM clickstream.uservisits_csv10 uv
RIGHT OUTER JOIN customer c ON c.c_custkey = uv.custKey
INNER JOIN (
  SELECT DISTINCT d_yearmonthnum, (d_month||','||d_year) prettyMonthYear
  FROM dwdate WHERE d_yearmonthnum >= 199810) t
  ON uv.yearMonthKey = t.d_yearmonthnum
WHERE c.c_custkey <= 3
GROUP BY 1,2,3,uv.yearMonthKey
ORDER BY 1,2,uv.yearMonthKey;

S3 扫描行数:约 37.6 亿,扫描文件数:5040。

优化 1:使用 customer 分区列

把 JOIN 条件从合成键 custKey 改为分区列 customer

sql 复制代码
RIGHT OUTER JOIN customer c ON c.c_custkey = uv.customer

EXPLAIN 中可以看到 Filter: ((customer <= 3) ...) 出现在 PartitionInfo 扫描上,说明分区裁剪生效。S3 扫描行数减半到约 19 亿,速度提升 2 倍

优化 2:同时使用 customer + visityearmonth 分区
sql 复制代码
ON uv.visitYearMonth = t.d_yearmonthnum  -- 改用分区列

S3 扫描行数下降到 6627 万(精准命中),扫描文件数降到 90,速度比原始查询快 22.5 倍

优化 3:换成 Parquet + 谓词下推
sql 复制代码
SELECT c.c_name, c.c_mktsegment, t.prettyMonthYear, uv.totalRevenue
FROM (
  SELECT customer, visitYearMonth, SUM(adRevenue) totalRevenue
  FROM clickstream.uservisits_parquet1
  WHERE customer <= 3 AND visitYearMonth >= 199810
  GROUP BY customer, visitYearMonth) uv
RIGHT OUTER JOIN customer c ON c.c_custkey = uv.customer
INNER JOIN (
  SELECT DISTINCT d_yearmonthnum, (d_month||','||d_year) prettyMonthYear
  FROM dwdate WHERE d_yearmonthnum >= 199810) t
  ON uv.visitYearMonth = t.d_yearmonthnum
ORDER BY c.c_name, c.c_mktsegment, uv.visitYearMonth;

EXPLAIN 输出包含 S3 Aggregate 步骤,意味着聚合在 Spectrum 层完成。返回到 Redshift 的行数从 6627 万降到 9 行,查询时间约 4 秒。

优化效果总结

优化阶段 S3 扫描行数 扫描文件数 相对提升
基线(无分区) 37.6 亿 5040 1x
+customer 分区 19 亿 2520 2x
+visityearmonth 分区 6627 万 90 22.5x
Parquet + 谓词下推 返回 9 行 90 约 100x

Data Sharing

Data Sharing 允许一个 Redshift 端点(Producer)将数据实时共享给其他端点(Consumer),无需物理复制数据。
#mermaid-svg-LmH7T2YaggV35u7A{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LmH7T2YaggV35u7A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LmH7T2YaggV35u7A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LmH7T2YaggV35u7A .error-icon{fill:#552222;}#mermaid-svg-LmH7T2YaggV35u7A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LmH7T2YaggV35u7A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LmH7T2YaggV35u7A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LmH7T2YaggV35u7A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LmH7T2YaggV35u7A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LmH7T2YaggV35u7A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LmH7T2YaggV35u7A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LmH7T2YaggV35u7A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LmH7T2YaggV35u7A .marker.cross{stroke:#333333;}#mermaid-svg-LmH7T2YaggV35u7A svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LmH7T2YaggV35u7A p{margin:0;}#mermaid-svg-LmH7T2YaggV35u7A .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LmH7T2YaggV35u7A .cluster-label text{fill:#333;}#mermaid-svg-LmH7T2YaggV35u7A .cluster-label span{color:#333;}#mermaid-svg-LmH7T2YaggV35u7A .cluster-label span p{background-color:transparent;}#mermaid-svg-LmH7T2YaggV35u7A .label text,#mermaid-svg-LmH7T2YaggV35u7A span{fill:#333;color:#333;}#mermaid-svg-LmH7T2YaggV35u7A .node rect,#mermaid-svg-LmH7T2YaggV35u7A .node circle,#mermaid-svg-LmH7T2YaggV35u7A .node ellipse,#mermaid-svg-LmH7T2YaggV35u7A .node polygon,#mermaid-svg-LmH7T2YaggV35u7A .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LmH7T2YaggV35u7A .rough-node .label text,#mermaid-svg-LmH7T2YaggV35u7A .node .label text,#mermaid-svg-LmH7T2YaggV35u7A .image-shape .label,#mermaid-svg-LmH7T2YaggV35u7A .icon-shape .label{text-anchor:middle;}#mermaid-svg-LmH7T2YaggV35u7A .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LmH7T2YaggV35u7A .rough-node .label,#mermaid-svg-LmH7T2YaggV35u7A .node .label,#mermaid-svg-LmH7T2YaggV35u7A .image-shape .label,#mermaid-svg-LmH7T2YaggV35u7A .icon-shape .label{text-align:center;}#mermaid-svg-LmH7T2YaggV35u7A .node.clickable{cursor:pointer;}#mermaid-svg-LmH7T2YaggV35u7A .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LmH7T2YaggV35u7A .arrowheadPath{fill:#333333;}#mermaid-svg-LmH7T2YaggV35u7A .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LmH7T2YaggV35u7A .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LmH7T2YaggV35u7A .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LmH7T2YaggV35u7A .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LmH7T2YaggV35u7A .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LmH7T2YaggV35u7A .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LmH7T2YaggV35u7A .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LmH7T2YaggV35u7A .cluster text{fill:#333;}#mermaid-svg-LmH7T2YaggV35u7A .cluster span{color:#333;}#mermaid-svg-LmH7T2YaggV35u7A div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LmH7T2YaggV35u7A .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LmH7T2YaggV35u7A rect.text{fill:none;stroke-width:0;}#mermaid-svg-LmH7T2YaggV35u7A .icon-shape,#mermaid-svg-LmH7T2YaggV35u7A .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LmH7T2YaggV35u7A .icon-shape p,#mermaid-svg-LmH7T2YaggV35u7A .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LmH7T2YaggV35u7A .icon-shape .label rect,#mermaid-svg-LmH7T2YaggV35u7A .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LmH7T2YaggV35u7A .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LmH7T2YaggV35u7A .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LmH7T2YaggV35u7A :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Datashare
Datashare
Datashare
Producer

ETL/数据入库

存储所有数据
Consumer A

BI 报表团队
Consumer B

数据科学
Consumer C

营销分析

核心特点:

  • 零拷贝:Consumer 直接读取 Producer 数据
  • 实时一致:Producer 提交的数据变更立即可见
  • 计算隔离:Consumer 使用独立计算资源
  • 跨部署支持:Provisioned 与 Serverless 之间可共享

Producer 端(Serverless)

sql 复制代码
-- 获取当前 Namespace
SELECT current_namespace;
-- 输出: 3f4cf42a-be9f-47d0-8128-f1a87b40780e

-- 创建 Datashare
CREATE DATASHARE tickit_share SET PUBLICACCESSIBLE TRUE;

-- 添加要共享的 schema
ALTER DATASHARE tickit_share ADD SCHEMA tickit;

-- 添加要共享的表
ALTER DATASHARE tickit_share ADD TABLE tickit.sales;
ALTER DATASHARE tickit_share ADD TABLE tickit.users;

-- 查看 Datashare 详情
SHOW DATASHARES;
SELECT * FROM SVV_DATASHARE_OBJECTS;

-- 授权给 Consumer(使用 Consumer 的 Namespace)
GRANT USAGE ON DATASHARE tickit_share TO NAMESPACE '4ab34c5b-d1bb-4320-a702-a225a887e18d';

Consumer 端(Provisioned)

sql 复制代码
-- 获取当前 Namespace
SELECT current_namespace;
-- 输出: 4ab34c5b-d1bb-4320-a702-a225a887e18d

-- 从 Datashare 创建本地数据库
CREATE DATABASE tickit_db
FROM DATASHARE tickit_share
OF NAMESPACE '3f4cf42a-be9f-47d0-8128-f1a87b40780e';

-- 使用三段式命名查询
SELECT COUNT(*) FROM tickit_db.tickit.sales;
-- 结果: 5000

-- 创建 External Schema 简化查询
CREATE EXTERNAL SCHEMA tickit_shared
FROM REDSHIFT DATABASE 'tickit_db' SCHEMA 'tickit';

-- 两段式命名查询
SELECT COUNT(*) FROM tickit_shared.sales;
-- 结果: 5000

本地数据与共享数据 JOIN

Consumer 可以将本地数据与共享数据进行 JOIN:

sql 复制代码
-- JOIN 本地表和共享表
SELECT l.listid, l.numtickets, s.qtysold, s.pricepaid
FROM tickit.listing l
JOIN tickit_db.tickit.sales s ON l.listid = s.listid
LIMIT 10;

结果如下

权限模型与用户隔离

Redshift 的访问控制基于三层权限模型:

  • Schema USAGE: 控制用户能否"看到"schema 及其内部的表。没有这个权限,用户甚至不知道 schema 里有什么
  • Table SELECT/INSERT/DELETE: 控制对具体表的操作权限
  • Column 权限: 更细粒度,可以限制用户只能访问某些列
  • 行级安全(RLS): 最高粒度,可以限制用户只能看到满足条件的行

#mermaid-svg-SEQEudeQPgtF4VNp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SEQEudeQPgtF4VNp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SEQEudeQPgtF4VNp .error-icon{fill:#552222;}#mermaid-svg-SEQEudeQPgtF4VNp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SEQEudeQPgtF4VNp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SEQEudeQPgtF4VNp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SEQEudeQPgtF4VNp .marker.cross{stroke:#333333;}#mermaid-svg-SEQEudeQPgtF4VNp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SEQEudeQPgtF4VNp p{margin:0;}#mermaid-svg-SEQEudeQPgtF4VNp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SEQEudeQPgtF4VNp .cluster-label text{fill:#333;}#mermaid-svg-SEQEudeQPgtF4VNp .cluster-label span{color:#333;}#mermaid-svg-SEQEudeQPgtF4VNp .cluster-label span p{background-color:transparent;}#mermaid-svg-SEQEudeQPgtF4VNp .label text,#mermaid-svg-SEQEudeQPgtF4VNp span{fill:#333;color:#333;}#mermaid-svg-SEQEudeQPgtF4VNp .node rect,#mermaid-svg-SEQEudeQPgtF4VNp .node circle,#mermaid-svg-SEQEudeQPgtF4VNp .node ellipse,#mermaid-svg-SEQEudeQPgtF4VNp .node polygon,#mermaid-svg-SEQEudeQPgtF4VNp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SEQEudeQPgtF4VNp .rough-node .label text,#mermaid-svg-SEQEudeQPgtF4VNp .node .label text,#mermaid-svg-SEQEudeQPgtF4VNp .image-shape .label,#mermaid-svg-SEQEudeQPgtF4VNp .icon-shape .label{text-anchor:middle;}#mermaid-svg-SEQEudeQPgtF4VNp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SEQEudeQPgtF4VNp .rough-node .label,#mermaid-svg-SEQEudeQPgtF4VNp .node .label,#mermaid-svg-SEQEudeQPgtF4VNp .image-shape .label,#mermaid-svg-SEQEudeQPgtF4VNp .icon-shape .label{text-align:center;}#mermaid-svg-SEQEudeQPgtF4VNp .node.clickable{cursor:pointer;}#mermaid-svg-SEQEudeQPgtF4VNp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SEQEudeQPgtF4VNp .arrowheadPath{fill:#333333;}#mermaid-svg-SEQEudeQPgtF4VNp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SEQEudeQPgtF4VNp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SEQEudeQPgtF4VNp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SEQEudeQPgtF4VNp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SEQEudeQPgtF4VNp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SEQEudeQPgtF4VNp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SEQEudeQPgtF4VNp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SEQEudeQPgtF4VNp .cluster text{fill:#333;}#mermaid-svg-SEQEudeQPgtF4VNp .cluster span{color:#333;}#mermaid-svg-SEQEudeQPgtF4VNp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SEQEudeQPgtF4VNp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SEQEudeQPgtF4VNp rect.text{fill:none;stroke-width:0;}#mermaid-svg-SEQEudeQPgtF4VNp .icon-shape,#mermaid-svg-SEQEudeQPgtF4VNp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SEQEudeQPgtF4VNp .icon-shape p,#mermaid-svg-SEQEudeQPgtF4VNp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SEQEudeQPgtF4VNp .icon-shape .label rect,#mermaid-svg-SEQEudeQPgtF4VNp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SEQEudeQPgtF4VNp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SEQEudeQPgtF4VNp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SEQEudeQPgtF4VNp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 能看到 schema 存在
能操作具体表
能访问特定列
用户/角色
Schema USAGE 权限
Table SELECT/INSERT 权限
Column 权限(可选)
行级安全(RLS,可选)

创建者(如 awsuser)默认拥有所有权限,而其他用户默认什么都看不到。需要显式授权:

sql 复制代码
-- 让某个用户能看到 tickit schema 里的表
GRANT USAGE ON SCHEMA tickit TO someuser;
GRANT SELECT ON ALL TABLES IN SCHEMA tickit TO someuser;

-- 或者授权给所有人(PUBLIC = 所有当前和未来用户)
GRANT USAGE ON SCHEMA tickit TO PUBLIC;
GRANT SELECT ON ALL TABLES IN SCHEMA tickit TO PUBLIC;

与 Query Editor V2 连接方式的关系

连接方式 映射的数据库用户 权限来源
Federated user RedshiftDbUser 标签决定 需要 IAM Principal Tag + 显式 GRANT
Database user/password 直接用 awsuser 管理员账号,通常拥有全部权限
Secrets Manager 由 Secret 中的凭据决定 取决于 Secret 存储的用户权限

用 Federated user 连接时,如果映射到的数据库用户没有被授权访问 tickit schema,就看不到那些表。

查看当前权限

sql 复制代码
-- 查看当前用户
SELECT current_user;

-- 查看 schema 上的权限分配
SELECT
  nspname AS schema_name,
  usename,
  has_schema_privilege(usename, nspname, 'USAGE') AS can_use,
  has_schema_privilege(usename, nspname, 'CREATE') AS can_create
FROM pg_namespace, pg_user
WHERE nspname NOT LIKE 'pg_%'
  AND nspname != 'information_schema'
ORDER BY nspname, usename;

-- 查看表级权限
SELECT
  schemaname,
  tablename,
  usename,
  has_table_privilege(usename, schemaname || '.' || tablename, 'SELECT') AS can_select,
  has_table_privilege(usename, schemaname || '.' || tablename, 'INSERT') AS can_insert,
  has_table_privilege(usename, schemaname || '.' || tablename, 'DELETE') AS can_delete
FROM pg_tables, pg_user
WHERE schemaname = 'tickit'
ORDER BY tablename, usename;

最佳实践

  • 生产环境中,为不同角色创建专用数据库用户,按需授权(最小权限原则)
  • 避免使用 PUBLIC 授权,它会给所有当前和未来用户授予权限
  • 利用 Schema 隔离不同业务域的数据(如 tickitanalyticsstaging
  • 对敏感数据考虑使用列级权限或行级安全(RLS)

行级安全(RLS)与列级安全(CLS)

Redshift 支持两种细粒度数据安全机制:

  • 行级安全(Row-Level Security, RLS) :基于策略控制用户能访问哪些
  • 列级安全(Column-Level Security, CLS) :基于 GRANT 控制用户能访问哪些

两者可以组合使用,实现最小权限原则。

创建用户和角色
  • 角色(Role)是RBAC 的核心。先创建角色并赋予策略,再将角色分配给用户。管理角色比直接管理用户权限更高效,尤其用户多时。
sql 复制代码
CREATE USER rls_user1 PASSWORD 'RlsUser123!' CREATEDB;
CREATE USER rls_user2 PASSWORD 'RlsUser123!' CREATEDB;
CREATE USER rls_admin PASSWORD 'RlsAdmin123!' CREATEDB;

CREATE ROLE sales_east;
CREATE ROLE sales_west;
CREATE ROLE sales_all;

GRANT ROLE sales_east TO rls_user1;
GRANT ROLE sales_west TO rls_user2;
GRANT ROLE sales_all TO rls_admin;
创建测试数据
sql 复制代码
CREATE SCHEMA security_demo;

CREATE TABLE public.regional_sales (
    id INTEGER,
    region VARCHAR(20),
    product VARCHAR(50),
    amount DECIMAL(10,2),
    commission DECIMAL(10,2),
    salesperson VARCHAR(50)
);

INSERT INTO public.regional_sales VALUES
(1, 'east', 'Widget A', 1000.00, 100.00, 'Alice'),
(2, 'east', 'Widget B', 2500.00, 250.00, 'Bob'),
(3, 'west', 'Gadget X', 1800.00, 180.00, 'Carol'),
(4, 'west', 'Gadget Y', 3200.00, 320.00, 'Dave'),
(5, 'east', 'Widget C', 750.00, 75.00, 'Eve'),
(6, 'west', 'Gadget Z', 4100.00, 410.00, 'Frank');
创建 RLS 策略
  • WITH 声明策略涉及的列和数据类型,USING 定义过滤条件。条件中使用 current_user 函数动态判断当前登录用户。
sql 复制代码
-- 东区策略:rls_user1 只能看 east
CREATE RLS POLICY region_rls
WITH (region VARCHAR(20))
USING (
  CASE
    WHEN current_user = 'rls_user1' THEN region = 'east'
    ELSE TRUE
  END
);

-- 西区策略:rls_user2 只能看 west
CREATE RLS POLICY west_rls
WITH (region VARCHAR(20))
USING (
  CASE
    WHEN current_user = 'rls_user2' THEN region = 'west'
    ELSE TRUE
  END
);

-- 管理员策略:看所有
CREATE RLS POLICY all_rls
WITH (region VARCHAR(20))
USING (TRUE);
附加策略到表并启用 RLS
sql 复制代码
-- 附加策略到角色
ATTACH RLS POLICY region_rls ON public.regional_sales TO ROLE sales_east;
ATTACH RLS POLICY west_rls ON public.regional_sales TO ROLE sales_west;
ATTACH RLS POLICY all_rls ON public.regional_sales TO ROLE sales_all;

-- 启用 RLS
ALTER TABLE public.regional_sales ROW LEVEL SECURITY ON;

验证策略是否附加成功:

sql 复制代码
SELECT polname, grantee FROM svv_rls_attached_policy ORDER BY 2, 1;

结果:

polname grantee
all_rls sales_all
region_rls sales_east
west_rls sales_west
验证 RLS 效果

使用 SET SESSION AUTHORIZATION 模拟不同用户:

rls_user1(只看 east)

sql 复制代码
SET SESSION AUTHORIZATION rls_user1;
SELECT id, region, product, amount FROM public.regional_sales ORDER BY id;
id region product amount
1 east Widget A 1000.00
2 east Widget B 2500.00
5 east Widget C 750.00

rls_user2(只看 west)

sql 复制代码
SET SESSION AUTHORIZATION rls_user2;
SELECT id, region, product, amount FROM public.regional_sales ORDER BY id;
id region product amount
3 west Gadget X 1800.00
4 west Gadget Y 3200.00
6 west Gadget Z 4100.00

rls_admin(看全部):

sql 复制代码
SET SESSION AUTHORIZATION rls_admin;
SELECT id, region, product, amount FROM public.regional_sales ORDER BY id;
id region product amount
1 east Widget A 1000.00
2 east Widget B 2500.00
3 west Gadget X 1800.00
4 west Gadget Y 3200.00
5 east Widget C 750.00
6 west Gadget Z 4100.00

每个用户只能看到自己权限范围内的数据行。

列级安全(CLS)

CLS 使用标准 GRANT 语法,在列级别授权:

  • 如果用户通过角色(ROLE)或直接获得了全表 SELECT 权限,列级限制会被覆盖。必须先 REVOKE 全表权限,再 GRANT 列级权限。
sql 复制代码
-- 撤销全表 SELECT(重要!全表授权会覆盖列级授权)
REVOKE SELECT ON public.regional_sales FROM ROLE sales_east;

-- 只授权特定列
GRANT SELECT (id, region, product, amount) ON public.regional_sales TO rls_user1;

-- 管理员授权全部列
GRANT SELECT ON public.regional_sales TO rls_admin;

验证:

sql 复制代码
-- rls_user1 尝试访问 commission 列(被拒绝)
SET SESSION AUTHORIZATION rls_user1;
SELECT id, commission FROM public.regional_sales;
-- 错误: permission denied for relation regional_sales

-- rls_user1 访问允许的列(成功)
SELECT id, region, product, amount FROM public.regional_sales;
-- 返回 east 区域的 3 行数据(RLS + CLS 同时生效)
RLS 相关系统视图
sql 复制代码
-- 查看所有 RLS 策略
SELECT poldb, polname, polqual, polmodifiedby
FROM svv_rls_policy;

-- 查看策略与表的附加关系
SELECT relschema, relname, polname, grantee
FROM svv_rls_attached_policy;

-- 查看哪些表启用了 RLS
SELECT datname, relschema, relname, is_rls_on
FROM svv_rls_relation
WHERE is_rls_on = 't';

监控与运维

Redshift Serverless 使用 SYS_ 前缀的系统视图:

视图 用途
SYS_QUERY_HISTORY 查询历史(状态、耗时、缓存命中)
SYS_QUERY_DETAIL 查询执行详细步骤
SYS_EXTERNAL_QUERY_DETAIL Spectrum 外部查询详情
SYS_LOAD_HISTORY COPY 加载历史
SYS_LOAD_ERROR_DETAIL 加载错误详情
SYS_SERVERLESS_USAGE Serverless 计算和存储使用量

查询示例

sql 复制代码
-- 最近 10 个查询
SELECT query_id, status, start_time, end_time,
       result_cache_hit, elapsed_time/1000 AS elapsed_ms
FROM sys_query_history
WHERE status IN ('success', 'running', 'queued')
ORDER BY start_time DESC
LIMIT 10;

-- Serverless 使用摘要
SELECT start_time, end_time, compute_seconds, compute_capacity, data_storage
FROM sys_serverless_usage
ORDER BY start_time DESC;

-- COPY 加载历史
SELECT query_id, table_name, data_source, loaded_rows, loaded_bytes
FROM sys_load_history
ORDER BY query_id DESC;

Redshift 会缓存查询结果。如果相同查询再次执行且底层数据未变化,直接返回缓存结果(毫秒级响应)。

sql 复制代码
-- 查看是否命中缓存
SELECT query_id, result_cache_hit, elapsed_time
FROM sys_query_history
ORDER BY start_time DESC
LIMIT 5;

可以在 Namespace -> Security and encryption 中启用审计日志,查看如下内容:

  • Connection log:连接/断开事件
  • User log:用户创建/删除/权限变更
  • User activity log:所有执行的 SQL 语句

参考资料

相关推荐
Omics Pro1 小时前
基因泰克:检测级虚拟细胞基准!大语言模型+智能体
大数据·数据库·人工智能·机器学习·语言模型·自然语言处理·r语言
Quincy_Freak1 小时前
工具分享|基于 SQLiteGo 的国产系统离线数据处理方案
大数据·数据库·数据分析·arm·国产系统·银河麒麟·aarch64
爱笑的源码基地2 小时前
智慧班牌源码:从后端SpringBoot到前端Vue2的全栈实现
java·大数据·云计算·源码·程序代码·智慧校园源码·智慧班牌源码
人工智能培训2 小时前
数字孪生赋能建筑行业 解锁工程全周期智慧管理
大数据·人工智能·机器学习·prompt·agent
计算机安禾2 小时前
【算法分析与设计】第21篇:回溯法的状态空间树与剪枝函数设计
大数据·人工智能·算法·机器学习·数据挖掘·剪枝
captain_AIouo2 小时前
攻克行业技术痛点,GPT Image2重塑电商AI生图标准
大数据·人工智能·经验分享·gpt·aigc
garmin Chen2 小时前
Elasticsearch(2):JavaRestClient操作Elasticsearch全流程实战指南
java·大数据·elasticsearch·搜索引擎
兴通物联科技3 小时前
条码防重防错防漏防呆:工业数据采集的全链路风控技术方案
大数据·物联网·计算机视觉·计算机外设·硬件架构
czzxxxxxx3 小时前
知识IP卡在变现第一步:创客匠人用一套陪跑系统回答“谁来陪你落地”
大数据·人工智能