一、 MPP架构和列式存储概念讲解
MPP 架构 和 列式存储这两项技术是现代高性能数据分析数据库(如 StarRocks、ClickHouse、Snowflake 等)的基石,理解了它们,你就明白了为什么这些数据库能如此之快。
1. MPP 架构 (Massively Parallel Processing)
什么是 MPP?
MPP 即大规模并行处理。你可以把它想象成一个分工极其明确、协作效率极高的"工厂流水线"。
- 传统数据库(如 MySQL):像一个老师傅,所有工作(接单、切菜、炒菜、装盘)都自己一个人完成。虽然也能做,但订单一多就忙不过来,成为瓶颈。
- MPP 数据库:像一条现代化的流水线,有无数个工人(节点)同时工作。每个工人只负责处理一道工序,大家齐心协力,同时处理海量任务。
MPP 的核心思想:"分而治之"
MPP 架构将一个大型集群(多台服务器)中的每个节点都视作一个独立的计算和存储单元。没有一个全局的主节点会成为瓶颈。
- 数据分布 :一张巨大的表(比如 100TB)的数据并不是存在一台机器上,而是被水平切分 ,分布到集群的数十、数百甚至数千个节点上。每个节点只存有一小片数据。
- 常用策略 :按照某个键(如
user_id
)进行 Hash 分片,保证相同user_id
的数据落在同一个节点上。
- 常用策略 :按照某个键(如
- 查询执行 :当你执行一个查询时(例如
SELECT COUNT(*) FROM big_table
):- 查询协调:一个主节点(Coordinator)接收你的 SQL 请求,并生成一个分布式执行计划。
- 并行计算 :它将这个计划下发到所有存有相关数据分片的节点上。
- 本地计算 :每个节点在自己的那一小份数据上同时地、并行地执行这个计算(比如各自数自己那里有多少行)。
- 结果汇总:各个节点将本地计算出的中间结果(比如各自的计数)返回给主节点。
- 最终聚合:主节点将这些中间结果汇总成最终结果(把所有计数加起来),返回给用户。
MPP 的优势:
- 高性能:多个节点并行工作,极大地缩短了处理时间。理论上,节点数量增加一倍,处理速度就能接近提升一倍(线性扩展)。
- 高扩展性:可以通过增加廉价的普通服务器(节点)来轻松扩展整个集群的处理能力,以应对数据量和计算量的增长。
- 无单点瓶颈:计算和存储都分散在各节点,没有共享资源争用的问题。
MPP 的劣势:
- 延迟敏感:节点间需要频繁交换数据(Shuffle),因此对网络延迟要求很高,通常要求高速局域网。
- 数据倾斜 :如果数据分片策略不好(比如某个
user_id
的数据特别多),会导致某个节点任务过重,成为"短板",影响整体速度。
2. 列式存储 (Columnar Storage)
什么是列式存储?
它与传统的行式存储相对应。
- 行式存储 :像把一行的所有数据(用户ID、姓名、年龄、地址...)紧紧挨着存在一起。类似于"记事本",一行一行地记录。
- 适用场景 :适合事务处理(OLTP),如频繁插入、更新、删除整行数据的操作。例如
SELECT * FROM users WHERE user_id = 123
。
- 适用场景 :适合事务处理(OLTP),如频繁插入、更新、删除整行数据的操作。例如
- 列式存储 :像把同一列的所有数据存在一起。所有用户ID存在一个地方,所有姓名存在另一个地方,所有年龄又存在别的地方。类似于"成绩单",先列出所有学生的语文成绩,再列出所有学生的数学成绩。
- 适用场景 :适合分析处理(OLAP),如对某几列进行聚合、筛选、计算。例如
SELECT AVG(age), department FROM users GROUP BY department
。
- 适用场景 :适合分析处理(OLAP),如对某几列进行聚合、筛选、计算。例如
列式存储的优势:
- 极高的压缩比:同一列的数据类型相同,数据模式相似(比如都是年龄数字,或者都是省份字符串),可以使用针对性的压缩算法(如 Run-Length Encoding、Delta Encoding、Dictionary Encoding),获得极高的压缩率。压缩不仅能节省存储空间,更能减少磁盘 I/O,因为从磁盘读出的数据块更小。
- 极少的 I/O 读取 :分析查询通常只关心少数几个列。例如,计算平均年龄的查询只需要读取
age
这一列。- 行式存储 :必须把整行数据(包括不需要的
name
,address
等)从磁盘读入内存,浪费大量 I/O 带宽。 - 列式存储 :只需要读取
age
这一列的数据,I/O 效率极高。
- 行式存储 :必须把整行数据(包括不需要的
- 更适合向量化执行:由于数据按列连续存储,CPU 可以一次性将一整列数据加载到高速缓存中,并利用 SIMD(单指令多数据)指令进行并行计算。这就像从"用勺子一粒一粒吃饭"变成了"用铲子一铲一铲吃饭",极大地提高了 CPU 的计算效率。
列式存储的劣势:
- 不适合频繁更新/点查:更新一行数据需要重写多个列的文件,成本很高。点查(查询单条记录的所有字段)也需要从多个列文件中读取数据并组装,性能较差。
3. 强强联合:MPP + 列式存储
像 StarRocks 这样的数据库,将两者结合,发挥了"1+1 >> 2"的效果:
- 数据存储 :数据按列 进行存储,并且被水平切分 分布到 MPP 集群的各个节点上。
- 查询处理 :当一个分析查询到来时:
- MPP 架构的主节点生成分布式计划。
- 每个数据节点 并行地在自己本地的列式数据上进行操作。
- 由于是列式存储,每个节点只读取查询所需的列 ,并利用向量化执行引擎极快地完成聚合、过滤等操作。
- 最后将中间结果汇总,得到最终答案。
案例对比:
假设有一张 10 亿行、100 列的表,要执行 SELECT province, COUNT(*) FROM sales WHERE year = 2023 GROUP BY province
。
- 传统行式数据库 :可能需要从磁盘读取所有 100 列 的 10 亿行 数据(数据量巨大),然后在内存中过滤出
year=2023
的行,再对province
分组计数。速度极慢。 - MPP + 列式数据库 :
- 主节点将任务分发给所有存储节点。
- 每个节点只读取 本地的
year
和province
这两列的数据(I/O 量减少 98%)。 - 每个节点利用向量化执行,并行地过滤
year=2023
的数据,并对province
进行本地计数。 - 各节点将(省份,计数)的中间结果发给主节点。
- 主节点做最终汇总。速度极快。
总结
特性 | MPP 架构 | 列式存储 |
---|---|---|
核心思想 | 分而治之,并行处理 | 按列组织数据 |
优化目标 | 计算速度,通过增加节点线性扩展性能 | 查询和压缩效率,减少 I/O,加速聚合 |
好比 | 工厂流水线,众人拾柴火焰高 | 成绩单,看单科成绩非常方便 |
劣势 | 网络要求高,怕数据倾斜 | 不适合频繁更新和点查 |
两者珠联璧合,共同造就了 StarRocks 在海量数据场景下极致的速度体验,使其成为实时数据分析的利器。
二、利用 MPP 和列式存储的特性进行表设计
在 StarRocks 中建表是性能调优的第一步,也是最关键的一步。巧妙地利用 MPP 和列式存储的特性进行表设计,能带来成倍的性能提升。
以下是如何在建表时更好地利用这两大特性的详细指南和最佳实践。
核心思路
- 利用 MPP:关键在于"数据分布"
- 目标:让数据均匀分散在各个 BE 节点上,最大化并行计算能力;并尽量减少节点间数据传输(Shuffle)。
- 手段 :精心选择分桶键(DISTRIBUTED BY HASH)。
- 利用列式存储:关键在于"降低I/O"和"加速计算"
- 目标:减少扫描数据量、提高压缩比、优化本地计算效率。
- 手段 :选择合适的数据类型、编码、压缩 ,并设计分区 和排序键。
1. 利用 MPP 架构进行数据分布设计
分桶(Bucketing) - DISTRIBUTED BY HASH(...)
这是实现 MPP 并行计算的基石。数据通过分桶键的 Hash 值被分散到不同的分桶(Tablet) 中,每个分桶都是数据移动、复制和计算的最小单元。
如何选择分桶键(Bucket Key)?
- 原则一:选择高基数列
- 为什么:确保数据能尽可能均匀地分布到各个分桶,避免数据倾斜。如果一个分桶的数据量远大于其他分桶,它就会成为查询的瓶颈。
- 好的选择 :
user_id
,order_id
,device_id
等唯一性或基数很高的列。 - 坏的选择 :
性别
、省份
(基数低,会导致数据严重倾斜)、NULL
(所有NULL值会hash到同一个桶)。
- 原则二:选择频繁用于查询过滤或连接的列
- 为什么 :StarRocks 的本地计算能力很强。如果查询条件或
JOIN
条件中包含分桶键,可以极大减少节点间的数据传输(Shuffle)。- 例如,如果按
user_id
分桶,那么WHERE user_id = 1001
的查询可以精准地只路由到一个分桶进行计算(Pruning)。 - 如果两个大表都按相同的列(如
user_id
)且相同桶数分桶,那么它们之间的JOIN
操作可以采取 Colocate Shuffle 策略,直接在本地进行关联,效率极高。
- 例如,如果按
- 为什么 :StarRocks 的本地计算能力很强。如果查询条件或
如何确定分桶数量?
- 推荐范围 :单个分桶的数据量建议在 100MB ~ 1GB 之间。
- 计算公式 :
分桶数量 ≈ 总数据量预估 / 1GB
- 限制:分桶数量一旦确定,后续不易修改。建表时需做好预估。对于中小表,建议设置不少于 8 个分桶以保证足够的并行度。
示例:
sql
SQL
CREATE TABLE user_behavior (
user_id INT,
item_id INT,
...
)
DUPLICATE KEY(user_id, item_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 12 -- 选择高基数的user_id作为分桶键,并设置12个分桶
PROPERTIES ("replication_num" = "3");
2. 利用列式存储进行存储优化
选择合适的数据类型
- 使用最小、最高效的数据类型 :
- 使用
INT
而不是BIGINT
(如果数值范围允许)。 - 使用
VARCHAR(n)
而不是TEXT
/STRING
,并指定合适的长度。 - 使用
DATE
或DATETIME
而不是字符串来表示时间。
- 使用
- 为什么:更小的数据类型占用更少的磁盘和内存空间,列式存储压缩效率更高,查询时在内存中能处理更多数据,CPU Cache 更友好。
使用编码和压缩 - encoding
和 compression
StarRocks 会自动为列选择编码和压缩(如 LZ4),但你也可以手动指定以优化性能。
-
低基数列(如枚举值、状态码) :使用
BITMAP_ENCODING
或LOWCARD_DICT_ENCODING
,压缩比极高。 -
高基数列(如ID、时间戳) :使用
PLAIN_ENCODING
或DELTA_ENCODING
。 -
可以在建表时指定:
sql
SQLPROPERTIES ( "replication_num" = "3", "compression" = "LZ4", -- 表级别的压缩算法 "storage_format" = "v2" -- 使用新版存储格式,通常更好 );
排序键和前缀索引 - DUPLICATE/PRIMARY/UNIQUE KEY(...)
在 StarRocks 中,DUPLICATE KEY
、PRIMARY KEY
和 UNIQUE KEY
决定了数据的排序和存储方式。
- 排序键(Sort Key):数据在每个分桶内是按照这些键排序后存储的。
- 前缀索引(Prefix Index):StarRocks 会为每 1024 行数据生成一个前缀索引项,内容是排序键的前 36 个字节。查询时,可以先通过前缀索引快速定位到数据块,再在块内进行精准查找。
如何设计排序键?
- 原则:将最常用作查询条件的列放在前面。
- 为什么 :排序和前缀索引可以极大地加速范围查询和等值查询。
- 查询
WHERE date = '2023-01-01' AND user_id = 100
,如果(date, user_id)
是排序键,可以快速定位到数据所在区域,极大减少需要扫描的数据行数。
- 查询
示例:
sql
SQL
CREATE TABLE user_behavior (
date DATE,
user_id INT,
item_id INT,
behavior_type VARCHAR(10),
timestamp BIGINT
)
DUPLICATE KEY(date, user_id, item_id) -- 排序键:日期 -> 用户 -> 商品
DISTRIBUTED BY HASH(user_id) BUCKETS 12;
这个设计使得 WHERE date = '2023-01-01'
这类按天查询的效率非常高。
3. 分区(Partitioning) - PARTITION BY RANGE(...)
分区不是 MPP 的直接特性,但它是列式存储查询加速的重要补充。
- 作用 :分区主要用于数据管理 和查询加速(分区剪枝)。
- 如何工作 :按照时间(通常是天)或其他范围将数据划分成不同的子目录。查询时,优化器可以根据条件直接跳过无关的分区,减少需要扫描的数据量。
分区键选择建议:
- 几乎总是使用时间字段 :如
date
、dt
。 - 分区数量不宜过多 :通常按天分区,单个分区数据量建议在 1GB 以上。过多的小分区会浪费元数据管理开销,影响 FE 性能。
示例:
sql
SQL
CREATE TABLE user_behavior (
date DATE, -- 分区键
user_id INT,
...
)
DUPLICATE KEY(date, user_id, ...)
PARTITION BY RANGE(date) -- 按天分区
(
PARTITION p20230101 VALUES [('2023-01-01'), ('2023-01-02')),
PARTITION p20230102 VALUES [('2023-01-02'), ('2023-01-03')),
...
)
DISTRIBUTED BY HASH(user_id) BUCKETS 12;
查询 WHERE date BETWEEN '2023-01-01' AND '2023-01-07'
只会扫描 p20230101
到 p20230107
这 7 个分区,而不是全表。
综合实战案例:电商订单表
假设我们要创建一张订单事实表。
sql
SQL
CREATE TABLE dwd_order_fact (
-- 时间维度 (分区键 & 排序键首位)
dt DATE COMMENT "订单日期",
order_time DATETIME COMMENT "订单时间",
-- 业务维度 (高基数,作为分桶键和排序键)
order_id BIGINT COMMENT "订单ID",
user_id BIGINT COMMENT "用户ID",
-- 其他维度
product_id INT COMMENT "商品ID",
province_code SMALLINT COMMENT "省份编码",
-- 指标(度量)
quantity INT COMMENT "数量",
revenue DECIMAL(10,2) COMMENT "订单金额",
coupon_amt DECIMAL(10,2) COMMENT "优惠券金额"
)
ENGINE=OLAP
-- 1. 列式存储优化:排序键设计,优先常用查询条件
DUPLICATE KEY(dt, order_id, user_id)
-- 2. MPP优化:选择高基数列user_id作为分桶键,数据均匀分布
DISTRIBUTED BY HASH(user_id) BUCKETS 16
-- 3. 分区管理:按天分区,方便管理并加速按天查询
PARTITION BY RANGE(dt)
(
START ("2023-01-01") END ("2024-01-01") EVERY (INTERVAL 1 DAY)
)
-- 4. 存储属性设置
PROPERTIES (
"replication_num" = "3",
"storage_format" = "v2",
"compression" = "LZ4"
);
设计解读:
- MPP并行 :数据按
user_id
Hash 分布到 16 个桶,均匀分散在集群各节点,并行计算。 - 列式存储 :
- 按
(dt, order_id, user_id)
排序,对按天查询和按订单/用户查询极度友好。 - 使用合适的数据类型(如
SMALLINT
表示省份)。 - 启用压缩。
- 按
- 分区裁剪:按天分区,查询特定日期范围时性能极佳。
- Colocate Join :如果用户维度表也按
user_id
分桶且桶数相同,两表关联时可避免Shuffle。
通过这样的设计,无论是面向用户的行为分析、按天的报表统计,还是多表关联查询,都能充分发挥 StarRocks MPP 和列式存储的威力。