简介
- 完全重新排序
- 原始表的一个子集,但采用不同的排序方式
- 预先计算的 aggregation (类似于 materialized view) ,但其排序 与 aggregation 保持一致。
投影如何工作?
- 合理使用主索引
- 预先计算聚合
使用 _part_offset 实现更智能的存储
_part_offset,为定义投影提供了一种新方式。
现在有两种定义投影的方式:
- 存储普通列 (原始行为) :投影包含完整 数据,可直接读取;当过滤条件与 投影的排序方式匹配时,性能会更快。
-
仅存储排序键 +
_part_offset:投影的工作方式类似于索引。 ClickHouse 使用投影的主索引来定位匹配的行,但会从 基础表中读取实际数据。这可以减少存储开销,但代价是 查询时 I/O 会略有增加。
_part_offset 间接引用其他列。
何时使用投影?
- 投影不允许为源表和 (隐藏的) 目标表设置不同的生存时间 (TTL),而 materialized views 允许使用不同的 TTL。
- 带有投影的表不支持轻量级更新和删除。
- Materialized Views 可以形成链式关系:一个 materialized view 的目标表 可以作为另一个 materialized view 的源表,依此类推。而投影 无法做到这一点。
- 投影定义不支持 join,但 Materialized Views 支持。不过,对带有投影的表执行的查询仍可自由使用 join。
- 投影定义不支持过滤 (
WHERE子句) ,但 Materialized Views 支持。不过,对带有投影的表执行的查询仍可自由过滤。
- 需要对数据进行完全重排序时。虽然投影中的表达式
理论上可以使用
GROUP BY,,但 materialized views 在维护聚合方面更高效。查询优化器也更可能 利用采用简单重排序的投影,即SELECT * ORDER BY x。 你还可以在该表达式中仅选择部分列,以减少存储 占用。 - 用户能够接受由此带来的存储占用增加 以及数据写入两次的额外开销。请测试其对插入速度的影响,并 评估存储开销。
示例
对不在主键中的列进行过滤
pickup_datetime 排序。
我们先编写一个简单的查询,找出所有乘客
给司机支付超过 200 美元小费的行程 ID:
请注意,由于这里是按 tip_amount 进行过滤,而它并不在 ORDER BY 中,因此 ClickHouse
不得不执行全表扫描。下面我们来加速这个查询。
为了保留原始表和查询结果,我们将创建一个新表,并使用 INSERT INTO SELECT 复制数据:
ALTER TABLE 语句和 ADD PROJECTION
语句:
MATERIALIZE PROJECTION
语句,以便其中的数据根据上面指定的查询进行物理排序和重写:
system.query_log 表,
来确认上面的查询确实使用了我们创建的投影:
使用 投影 加速英国房产成交价查询
town 和 price 都不在 ORDER BY 子句中,因此这两个查询都对全部 3003 万行进行了全表扫描:
INSERT INTO SELECT 复制数据:
prj_oby_town_price,它会生成一张额外的
(隐藏) 表,并创建一个按 town 和 price 排序的主索引,以
优化这样一条查询:列出特定 town 中成交价格最高的房产所在的 counties:
mutations_sync 设置
用于强制以同步方式执行。
我们创建并填充投影 prj_gby_county——这是一个额外的 (隐藏) 表,
会以增量方式为英国现有的 130 个郡预先计算 avg(price) 聚合值:
如果在投影中使用了
GROUP BY 子句,就像上面的 prj_gby_county
投影一样,那么该 (隐藏) 表的底层存储引擎
就会变为 AggregatingMergeTree,并且所有聚合函数都会被转换为
AggregateFunction。这可确保增量数据得到正确聚合。uk_price_paid_with_projections
及其两个投影:
如果我们现在再次运行该查询,列出伦敦成交价最高的三个房产所在的郡,
就会看到查询性能有所提升:
同样,对于列出英国平均成交价最高的三个郡的查询:
请注意,这两个查询都针对原始表,而且在我们
创建这两个投影之前,这两个查询都进行了全表扫描 (全部 3003 万行都从磁盘流式读取) 。
另外请注意,列出伦敦各郡中价格最高的三套房产所属郡的那个查询
会流式处理 217 万行。而当我们直接使用针对该查询优化的第二张表时,
只从磁盘流式读取了 8.192 万行。
之所以会有这种差异,是因为目前,上面提到的 optimize_read_in_order
优化还不支持投影。
我们查看 system.query_log 表,可以看到 ClickHouse
已自动为上面的两个查询使用了这两个投影 (请参见下方的
projections 列) :
更多示例
CREATE AS 和 INSERT INTO SELECT 创建该表的副本。
创建投影
toYear(date)、district 和 town 创建一个聚合投影:
optimize_use_projections,该设置默认处于启用状态。
查询 1. 每年平均价格
查询 2. 伦敦每年的平均房价
查询 3. 最昂贵的街区
(date >= '2020-01-01') 需要调整为与 projection 的维度一致 (toYear(date) >= 2020)) :
同样,结果没有变化,但请注意第 2 个查询的查询性能有所提升。
在单个查询中组合使用投影
_part_offset 支持基础上,ClickHouse 现在可以使用多个投影来加速带有多个过滤条件的单个查询。
需要注意的是,ClickHouse 仍然只会从一个投影 (或基表) 中读取数据,
但在读取之前,可以借助其他投影的主索引来剪除不必要的 parts。
这对于按多个列进行过滤的查询尤其有用,因为每个列
都可能对应不同的投影。
目前,该机制只能剪除整个 parts。尚不支持粒度级别的剪除。为演示这一点,我们定义这张表 (其中投影使用
_part_offset 列) ,
并插入五个与上图对应的示例行。
注意:该表为了演示使用了自定义设置,例如单行粒度
和禁用 parts 合并,这些设置不建议在生产环境中使用。
- 五个独立的 parts (每个插入的行一个)
- 每行对应一个主索引条目 (在基表和每个 projection 中)
- 每个 part 都恰好包含一行
region 和 user_id 过滤的查询。
由于基表的主索引是基于 event_date 和 id 构建的,因此
在这里派不上用场,所以 ClickHouse 会使用:
region_proj按区域裁剪 partsuser_id_proj进一步按user_id裁剪
EXPLAIN projections = 1 观察到,它展示了
ClickHouse 如何选择并应用 projections。
EXPLAIN 的输出 (如上所示) 展示了逻辑查询计划,从上到下:
最终,基表 5 个 parts 中只读取了 1 个。
通过结合多个 projection 的索引分析,ClickHouse 显著减少了扫描的数据量,
在保持较低存储开销的同时提升了性能。