DWS 列存表分区创建原理详解
在华为云 DWS 中,列存表的分区机制是影响查询性能和存储效率的核心设计之一。理解其底层原理,才能避开生产环境中的常见坑。
分区的本质:物理文件隔离
DWS 列存表以 CU(Compression Unit,压缩单元) 为最小存储单位,每个 CU 默认包含 60000 行数据,压缩后落盘为独立文件。分区在物理层面就是将这些 CU 文件按分区键的范围归组到不同目录下,每个分区拥有独立的元数据和文件空间。
查询时,优化器根据 WHERE 条件中的分区键值,直接跳过不满足条件的分区目录,这一机制称为分区裁剪(Partition Pruning)。被裁剪掉的分区不参与扫描,I/O 大幅减少,这是分区提升查询性能的根本原因。
列存分区与行存分区的关键差异
行存表支持 INTERVAL 自动分区,数据写入时若超出现有分区范围会自动创建新分区。列存表不支持这一特性,所有分区必须提前手动创建。
sql
-- 行存表:支持 INTERVAL 自动分区
CREATE TABLE orders_row (
order_id BIGINT,
created_at TIMESTAMP
)
WITH (ORIENTATION = ROW)
DISTRIBUTE BY HASH(order_id)
PARTITION BY RANGE (created_at)
INTERVAL ('1 day') -- 自动按天建分区
(
PARTITION p_init VALUES LESS THAN (TIMESTAMP '2025-01-01 00:00:00')
);
-- 列存表:不支持 INTERVAL,必须手动预建
CREATE TABLE orders_col (
order_id BIGINT,
created_at TIMESTAMP
)
WITH (ORIENTATION = COLUMN)
DISTRIBUTE BY HASH(order_id)
PARTITION BY RANGE (created_at)
(
PARTITION p_2025_01 VALUES LESS THAN (TIMESTAMP '2025-02-01 00:00:00'),
PARTITION p_2025_02 VALUES LESS THAN (TIMESTAMP '2025-03-01 00:00:00'),
PARTITION p_2025_03 VALUES LESS THAN (TIMESTAMP '2025-04-01 00:00:00'),
PARTITION p_future VALUES LESS THAN (MAXVALUE) -- 兜底分区,防止写入报错
);
MAXVALUE兜底分区是生产环境必备,一旦数据超出已有分区范围,数据会落入兜底分区而不是直接报错。
CU 与分区粒度的关系
分区粒度过细会导致 CU 无法填满 60000 行就强制落盘,产生大量小 CU 碎片,压缩率下降,查询需要打开更多文件,性能反而变差。
sql
-- 不推荐:按小时分区,一年产生 8760 个分区,CU 碎片严重
PARTITION BY RANGE (feature_hour)
(
PARTITION p_2025_05_01_00 VALUES LESS THAN (TIMESTAMP '2025-05-01 01:00:00'),
PARTITION p_2025_05_01_01 VALUES LESS THAN (TIMESTAMP '2025-05-01 02:00:00'),
... -- 分区数量爆炸,元数据开销极大
);
-- 推荐:按天分区,查询按小时过滤同样能分区裁剪
PARTITION BY RANGE (feature_hour)
(
PARTITION p_2025_05_01 VALUES LESS THAN (TIMESTAMP '2025-05-02 00:00:00'),
PARTITION p_2025_05_02 VALUES LESS THAN (TIMESTAMP '2025-05-03 00:00:00'),
PARTITION p_2025_05_03 VALUES LESS THAN (TIMESTAMP '2025-05-04 00:00:00'),
PARTITION p_future VALUES LESS THAN (MAXVALUE)
);
单个分区数据量建议在 1000 万行以上,按天或按月分区是最常见的合理选择。
定时预建分区
列存表需要配合定时任务提前建好未来分区,避免数据堆入兜底分区失去裁剪效果。
sql
-- 创建预建分区的存储过程
CREATE OR REPLACE PROCEDURE rc_dws.add_daily_partitions(days_ahead INT DEFAULT 7)
LANGUAGE plpgsql AS $$
DECLARE
v_date TIMESTAMP;
v_part_name VARCHAR(64);
BEGIN
v_date := date_trunc('day', NOW());
FOR i IN 0..days_ahead LOOP
v_part_name := 'p_' || to_char(v_date + (i || ' days')::INTERVAL, 'YYYY_MM_DD');
IF NOT EXISTS (
SELECT 1 FROM pg_partition
WHERE parentid = 'rc_dws.orders_col'::regclass
AND partitionname = v_part_name
) THEN
EXECUTE format(
'ALTER TABLE rc_dws.orders_col
ADD PARTITION %I VALUES LESS THAN (TIMESTAMP %L)',
v_part_name,
date_trunc('day', NOW()) + ((i + 1) || ' days')::INTERVAL
);
RAISE NOTICE '创建分区: %', v_part_name;
END IF;
END LOOP;
END;
$$;
-- 注册每天凌晨 1 点执行,提前预建未来 7 天分区
SELECT dbms_job.submit(
what => 'CALL rc_dws.add_daily_partitions(7);',
next_date => TRUNC(SYSDATE + 1) + 1/24,
interval => 'TRUNC(SYSDATE + 1) + 1/24'
);
Min/Max 索引与分区裁剪的协同
每个 CU 头部记录该列的 Min/Max 值(Zone Map),查询引擎在分区内部还会做二次过滤,跳过不满足谓词的 CU 块。可以通过 EXPLAIN 验证分区裁剪是否生效:
sql
-- 查看执行计划,确认是否触发分区裁剪
EXPLAIN SELECT * FROM rc_dws.orders_col
WHERE created_at >= '2025-05-01' AND created_at < '2025-05-02';
-- 执行计划中出现以下关键字说明裁剪生效
-- Partitions: p_2025_05_01 ← 只扫描目标分区
-- (Partition Eliminated: 29) ← 29 个分区被跳过
冷数据清理
分区表最大的运维价值之一是可以直接 DROP 历史分区,比 DELETE 快几个数量级:
sql
-- 清理 3 个月前的历史数据,秒级完成
ALTER TABLE rc_dws.orders_col DROP PARTITION p_2025_01;
-- 或转移到冷存储表空间,降低存储成本
ALTER TABLE rc_dws.orders_col MOVE PARTITION p_2025_02 TABLESPACE cold_obs_tbs;
小结
DWS 列存分区 = 物理文件隔离 + 分区裁剪 + CU Zone Map 三层机制协同工作。分区粒度选对了性能事半功倍,选错了碎片和元数据开销反而拖慢整体表现。核心原则只有一条:分区粒度服务于查询模式,而不是数据写入频率。