CREATE TABLE test_t (
id UInt32,
product String,
amount Decimal(16, 2),
create_time UInt32 -- 这里使用了整数存储时间
) ENGINE = ReplacingMergeTree(create_time)
PARTITION BY toYYYYMMDD(toDate(create_time)) -- 需要转换一次,否则报错
PRIMARY KEY id
ORDER BY id
除了日期类型和数值类型不用字符串表示之外,Null 也是拖累性能的一个罪魁祸首,因为官方已经指出 Null 会影响性能了。因为存储 Nullable 类型的列时,需要创建一个额外的文件来存储 Null 标记,并且 Nullable 类型的列无法被索引。因此除了极特殊的情况,否则不要将列设置为 Nullable,可以用一个不可能出现的默认值、或者在业务中无意义的来代指空,例如将 id 设置为 -1 表示该商品没有 id,而不是使用 Null。
分区和索引
分区粒度根据业务特点决定,但不宜过粗或者过细,如果数据之间是严格按照时间来划分,比如经常要按天、按月或者按年汇总处理,那么不妨选择按天分区或者按月分区;如果数据按照地区来划分,比如经常针对不同的地区单独汇总,那么不妨按照地区分区。那么分区到底要分多少个区呢?以单表一亿条数据为例,分区大小控制在 10 到 30 个最好。所以如果按照时间分区,那么我们一般都会按天、按月分区,至于按分钟分区则 dark不必,因为这样分区目录就太多了。
还有指定索引列,默认通过 ORDER BY 指定。ORDER BY 在 ClickHouse 中是最重要的,因为分区内的排序通过 ORDER BY 指定,主键(索引)默认也是由 ORDER BY 指定,即使我们显式地使用 PRIMARY KEY 不使用 ORDER BY,那么主键也必须是排序键的前缀。当然这里的 ORDER BY 指的是建表时的 ORDER BY,不是查询语句中的 ORDER BY。
然后我们在通过 ORDER BY 指定索引列的时候,应该指定查询中经常被用来充当筛选条件的列,可以是单一维度,也可以是组合维度,如果是组合维度,那么索引列要满足查询频率大的在前原则。还有基数特别大的不适合做索引列,基数大指的就是那些重复数据非常少的列。
index_granularity 是用来控制索引粒度的,默认是 8192,如非必须不建议调整。另外,如果一张表不是必须要保留全量历史数据,则建议指定 TTL,可以免去手动清理过期历史数据的麻烦,TTL 也可以通过 ALTER TABLE 语句随时修改。
写入和删除优化
尽量不要执行单条或小批量删除、插入操作,这样会产生小分区文件,给后台 Merge 任务带来巨大压力。
不要一次写入太多分区,或者数据写入太快,数据写入太快会导致 Merge 速度跟不上而报错,一般建议每秒钟发起 2 ~ 3 此写入操作,每次操作写入 2w ~ 5 w 条数据(依服务器性能而定)。
我们知道配置文件位于 /etc/clickhouse-server 目录下,里面有 config.xml 和 users.xml,我们之前一直说 config.xml,但其实 users.xml 也非常重要。它们都表示服务端的配置,而区别主要在于 config.xml 里面的配置是无法覆盖的,我们在命令行经常会使用 set 命令将某个参数进行修改,这些参数则是放在 users.xml 中。当然一个设置即可以在 users.xml 中出现,也可以在 config.xml 中出现,服务端首先会从 config.xml 中找,找不到再去 config.xml 中找。
而我们修改配置主要是为了调整 CPU、内存、IO,瓶颈主要在这里。因为 ClickHouse 会有后台线程 Merge 数据,所以非常的吃 CPU;当然加载数据,对内存也是一个考量;同理还有 IO,因为要从磁盘上读取大量数据。
下面来介绍与这三个配置有关的参数。
1)CPU
background_pool_size:位于 users.xml 中,非常重要的一个参数,表示后台线程池内的线程数量,Merge 线程就是在该线程池中执行,该线程池不仅仅是给 Merge 线程用的。默认值为 16,允许的前提下建议改成 CPU 个数的二倍。所以 ClickHouse 不建议和 HDFS、Yarn 等一起部署,因为 ClickHouse 太吃资源了,不然也达不到如此可观的速度
background_schedule_pool_size:位于 users.xml 中,表示执行后台任务的线程数,默认值为 128,允许的前提下建议改成 CPU 个数的二倍
background_distributed_schedule_pool_size:位于 users.xml 中,表示分布式发送执行后台任务的线程数,默认值为 16,允许的前提下建议改成 CPU 个数的二倍
max_concurrent_queries:位于 config.xml 中,表示最大并发处理的请求数(包含 SELECT、INSERT 等等),默认值为 100,推荐 150 ~ 300,不够再加
max_threads:位于 users.xml 中,表示单个查询所能使用的最大 CPU 个数,默认是 CPU 核数
**以上是关于 CPU 相关的设置,如果发现机器吃不消了,那么不妨减少一下线程数。 **
2)Memory
max_memory_usage:位于 users.xml 中,表示单次 Query 占用内存的最大值,该值可以设置的大一些,这样可以提高集群查询的上限。当然也要保留一些给 OS,比如 128G 的内存,设置为 100G 即可
max_bytes_before_external_group_by:表示 GROUP BY 使用的内存的最大值,一旦超过这个最大值,那么会刷新到磁盘进行 GROUP BY,一般按照 max_memory_usage 的一半设置即可。因为 ClickHouse 聚合分两个阶段,查询并建立中间数据、合并中间数据
max_bytes_before_external_sort:表示 ORDER BY 使用的内存的最大值,一旦超过这个最大值,那么会刷新到磁盘进行 ORDER BY。如果不设置该值,那么当内存不够的时候直接报错,设置了该值,ORDER BY 在内存不够的时候可以基于磁盘完成,但是速度相对就慢了(实际测试发现慢得多,甚至无法接受)。该参数和上一个参数都在 users.xml 中设置。
max_table_size_to_drop:位于 config.xml 中,应用于需要删除表或分区的情况,默认是 50 GB,意思是如果删除 50 GB 以上的数据会失败。建议设置为 0,表示无论分区表 多大都可以删除
和 HDFS 不同,ClickHouse 不支持设置多数据目录,为了提升 IO 性能,可以挂载虚拟券组(将多块磁盘虚拟成一块磁盘),通过一个券组绑定多块物理磁盘提升读写性能。或者使用 SSD,但是成本就比较高了。
ClickHouse 语法优化规则
很多数据库底层都内置了优化器,定义好了许多的优化规则,用于给我们的 SQL 语句进行优化,比如大小表 JOIN、谓词下推等等,就是为了避免开发人员执行慢查询。
那么 ClickHouse 会对哪些查询进行优化呢?我们来看一下。
COUNT 优化
我们说如果统计一张表有多少行,那么使用 count() 或者 count(*) 即可,此时会直接读取 count.txt。还记得这个 count.txt 文件吗?我们在介绍 MergeTree 的时候说过,该文件里面存储了表的行数,当使用 count() 或者 count(*) 的时候,直接读取该文件即可,此时是不需要全表扫描的。类似于关系型数据库也是如此,MySQL 在使用 count() 的时候也是直接计算的 B+ 树的叶子结点个数。
但当我们 count 一个字段的时候,那么就必须要全表扫描了,而且我们说过 count 字段的时候统计的是该字段中非空的值的个数。如果该字段中没有空值,count(字段) 的结果和 count()、count(*) 是相等的。
对比输出信息的话,我们看到 count(字段) 进行了全表扫描。
再比如 count(1),我们看看它会不会被优化:
satori :) EXPLAIN SYNTAX SELECT count(1) FROM datasets.hits_v1;
EXPLAIN SYNTAX
SELECT count(1)
FROM datasets.hits_v1
Query id: f59337ec-58e3-4a37-b00d-eaa796f54f65
┌─explain───────────────┐
│ SELECT count() │
│ FROM datasets.hits_v1 │
└───────────────────────┘
2 rows in set. Elapsed: 0.004 sec.
因为 1 是一个整型,没有什么实际意义,所以直接变成了 count()。
在 SQL 中,谓词就是返回 boolean 值的函数,或隐式转换为 bool 的函数,说白了你就简单理解为 WHERE 语句即可。而谓词下推指的是将过滤表达式尽可能移动至靠近数据源的位置,从事后过滤变成事前过滤。
举个最简单的栗子就是 WHERE 和 HAVING,我们知道 WHERE 是发生在 GROUP BY 之前的,HAVAING 发生在 GROUP BY 之后。
SELECT UserID, count() FROM datasets.hits_v1
GROUP BY UserID HAVING UserID = 1785640464950496314;
┌──────────────UserID─┬─count()─┐
│ 1785640464950496314 │ 105 │
└─────────────────────┴─────────┘
上面这行 SQL 语句执行的时候虽然没有任何问题,但很明显这是一个糟糕的 SQL 语句,因为要先对将近 900 万的数据进行聚合,然后选择 UserID 为 1785640464950496314 的记录。既然如此,那我们为什么不能先把 UserID 为 1785640464950496314 的记录选出来,然后再单独进行聚合呢?这样的话数据量会少很多。
我们看到在优化之后的 SQL 语句将条件从 HAVING 移到了 WHERE,所以将过滤表达式尽可能移动至靠近数据源的位置,在计算之前先将无用数据过滤掉,这个过程就是谓词下推。
当然谓词下推不仅仅是这里的 HAVING,子查询也支持,举个栗子,我们要根据 UserID 从 hits_v1 表中查询几个用户的记录,但是这些值必须存在于 visits_v1 的 UserID 字段中。
SELECT UserID, URL FROM datasets.hits_v1
WHERE UserID IN (329024891984319329, 3341630990649416532, 3444082748272603552);
显然这是非常简单的,但如果我们规定 UserID 还必须要出现在 visits_v1 表的 UserID 字段中,那么要怎么做呢?最简单的做法就是一个条件即可。
SELECT UserID, URL
FROM datasets.hits_v1
WHERE UserID IN (329024891984319329, 3341630990649416532, 3444082748272603552)
AND UserID IN (SELECT UserID FROM datasets.visits_v1);
但很明显这条语句就不是最优解,因为子查询会扫描全表,也就是 visits_v1 会全量读取。既然 UserID 要在两个表中都出现,那么就应该优先把过滤条件放在子查询里面。
SELECT UserID, URL
FROM datasets.hits_v1
WHERE UserID IN
(SELECT UserID -- 数据量大的话,还可以进行去重
FROM datasets.visits_v1
WHERE UserID IN (329024891984319329, 3341630990649416532, 3444082748272603552));
这种做法显然更优,因为 visits_v1 不需要全量读取,但 ClickHouse 目前还做不了这种优化,ClickHouse 所能做的子查询谓词下推还是很有限的。当然不光是子查询,相比 Hive,ClickHouse 所做的优化非常有限,不同的 SQL 语句效率相差十倍以上都是很正常的,因为我们写 SQL 就不可以肆无忌惮。
作为表进行 JOIN 的子查询会消除重复字段
如果子查询中重复选择了某个字段,那么当它作为表进行 JOIN 的时候,会去除重复字段。
EXPLAIN SYNTAX
SELECT a.UserID, b.VisitID, a.URL
FROM datasets.hits_v1 a LEFT JOIN (
SELECT UserID, UserID, VisitID
FROM datasets.visits_v1
USING(UserID) LIMIT 3
这里进行 JOIN 的右表是一个子查询,而在这个子查询里面我们选择了两次 UserID,那么 ClickHouse 会进行优化,变成只选择一次。
可能有人好奇,如果我给第二个 UserID 起一个别名会怎么样呢?答案是即使起了别名,仍然只会选择一次。
注意:这里的子查询在 JOIN 的时候会删除重复字段,但如果不是在 JOIN 的时候就不一样了。
EXPLAIN SYNTAX
SELECT UserID, URL
FROM (
SELECT UserID, UserID, URL
FROM datasets.hits_v1
) LIMIT 3
这里的子查询当中我们选择了两个 UserID,那么 ClickHouse 会不会变成一个呢?
我们看到并没有优化掉,所以 ClickHouse 所做的优化还是比较有限的。
聚合计算外推
什么是聚合计算外推呢?举个栗子:
SELECT sum(RequestNum * 2) FROM datasets.hits_v1;
你觉得上面的 SQL 有能够优化的地方吗?我们看看 ClickHouse 是如何做的。
没优化的时候,相当于是在 sum 之前先给每一条数据做一次乘法运算,然后进行 sum;优化之后则是先进性 sum,最后只对总和进行一次乘法运算,显然后者更优。
聚合函数消除
我们在使用 GROUP BY 的时候有一个限制,那就是 SELECT 中没有使用聚合函数的字段必须出现在分组字段中。举几个栗子:
-- 字段 b 出现在了 SELECT 中,并且没有使用聚合函数,所以它一定要出现在分组字段(GROUP BY)中
-- 但没有出现,所以报错
SELECT a, b, count(c) FROM t GROUP BY a;
-- 此时没有问题,a 和 b 都出现在 GROUP BY 中
-- 至于字段 c,它是以 count(c) 的形式出现的,使用了聚合函数,所以没问题
SELECT a, b, count(c) FROM t GROUP BY a, b;
-- 这条语句也是非法的,因为 b 没有出现在 GROUP BY 中,至于 count(b) 和 b 无关
-- 既然 SELECT 中出现了没有使用聚合函数的字段 b,那么它就必须要出现在 GROUP BY 中
SELECT a, b, count(b), count(c) FROM t GROUP BY a;
当然这些属于基础内容了,我主要想表达的是,我们可不可以对分组字段使用聚合函数呢?比如说:
SELECT max(UserID), max(Age), min(Age), sum(Age) FROM datasets.hits_v1 GROUP BY Age
很明显是可以的,但是这么做没有任何意义,因为分组就是把分组字段对应的值相同的归为一组,比如这里的 age,所以每一组的 age 的值都是一样的。既然都一样,那么做聚合就没有太大意义,因此 ClickHouse 会那些对分组字段使用 min、max、any 的聚合函数给删掉。因为每一组的所有值都是一样的,最小值、最大值、第一行的值,三者之间没差别。
我们看到聚合函数 min、max 被剥掉了,只留下了 Age 字段,因为分组字段的值都是一样的,min、max、any 没有意义。但聚合函数仅限于 max、min、any,如果是 sum 就不会了,虽然从业务的角度上来说也没有太大意义,但毕竟 sum 涉及到加法运算,所以它不会被剥掉。
删除重复的 ORDER BY KEY
类似于消除重复字段,如果指定了多个相同的排序字段,那么只会保留一个。
EXPLAIN SYNTAX SELECT UserID FROM datasets.hits_v1
ORDER BY Age, Age, Age DESC, Age DESC;
我们看到即使不同的排序,也会只保留相同排序字段的第一个,因为同一个字段即升序又降序本身就很奇怪。
删除重复的 LIMIT BY KEY
还记得 LIMIT BY 吗?"LIMIT N BY 字段" 表示按照字段进行分组,然后选出每组的前 N 条数据。如果 BY 后面的字段重复了,那么也会删除掉。
EXPLAIN SYNTAX SELECT UserID, URL FROM datasets.hits_v1
LIMIT 3 BY UserID, UserID
删除重复的 USING KEY
USING 也是如此,直接看例子吧。
EXPLAIN SYNTAX
SELECT a.UserID, a.UserID, b.VisitID, a.URL, b.UserID
FROM datasets.hits_v1 a LEFT JOIN datasets.visits_v1 b USING(UserID, UserID);
USING 里面指定了两个 UserID,那么会变成一个。
USING 里面指定了两个 UserID,那么会变成一个,即使我们指定了前缀,比如 USING(a.UserID, b.UserID),那么依旧会被优化成一个 UserID。
但是说实话,删除重复的 ORDER BY KEY、LIMIT BY KEY、USING KEY,正常情况下很难出现,因为谁会没事故意将一个字段重复写两遍啊。当然字段多了倒是有可能发生,因为字段一多就可能忘记某个字段已经写过一遍了,但是字段少的情况下几乎不可能发生。
所以这个 ClickHouse 的优化机制有点把人当傻子,大概感觉就是当你写了一个 1 + 1 = 3,那么它能帮你改成 1 + 1 = 2,然而查询一旦复杂,它就无法优化了。所以一切还需要我们来保证,当然后续 ClickHouse 的优化机制会变得越来越完善。
这个主要体现在 WITH 子句上面,我们最开始介绍 WITH 子句的时候说过,WITH 子句可以给一个普通的表达式赋值,也可以给一个查询赋值,但查询只能返回一行数组。最终会将其作为一个标量,后续查询时直接用这个标量进行替换即可。这背后也是 ClickHouse 给我们做的优化,举个例子:
WITH (SELECT sum(data_uncompressed_bytes) FROM system.columns) AS total_bytes
SELECT database,
(sum(data_uncompressed_bytes) / total_bytes) * 100 AS database_disk_usage
FROM system.columns
GROUP BY database
ORDER BY database_disk_usage DESC
注意 WITH 语句中的 total_bytes,它是对 data_uncompressed_bytes 进行 sum 所得到的结果,如果在查询中多次使用 total_bytes,那么难道每次都要计算一遍吗?显然不是的,这个值是提前算好的,是一个标量,因为只有一行数据,如果都多列就是一个元组。后续使用的都是已经算好的值。
三元运算符优化
如果开启了 optimize_if_chain_to_multiif 参数,那么三元运算符会被替换成 ,multiIf 函数,之前说过,这里就不再赘述了。
介绍了一些 ClickHouse 的优化规则,我们来看一下在编写 SQL 时如何手动进行优化,或者说有哪些可以优化的点。因为 ClickHouse 的优化规则(或者说内置的优化器)实在太简单了,但凡有点关系型数据库经验的人都不会那么写,因此在编写 SQL 语句的时候只能靠我们保证质量。那么来看看都有哪些注意的点。
PREWHERE 替代 WHERE
PREWHERE 和 WHERE 语句的作用相同,用来过滤数据,但是 PREWHERE 只支持 MergeTree 系列的引擎。WHERE 语句是读取所有的字段,然后进行数据过滤,而 PREWHERE 则是指定了哪些字段就读取哪些字段。比如 age > 18 and length > 160,WHERE 的话会读取全部字段,然后进行数据过滤,再根据 SELECT 中指定的字段进行丢弃。PREWHERE 则是只读取 age 和 length 两个字段,因为过滤条件只有这两个字段,而将数据过滤之后,再根据 SELECT 中指定的字段进行补全(或丢弃)。
所以两者的区别在于读取的数据量不同,当查询列明显多于筛选列时,使用 PREWHERE 可以十倍提升性能。当然这些我们之前在介绍子句的时候说过了,并且我们说过 ClickHouse 会自动将 WHERE 优化成 PREWHERE,因此我们直接用 WHERE 就好。当然我们还说了有几种情况,ClickHouse 不会自动优化,因为在这几种情况下,优化带来的性能提升不大,具体可以回去看看。
EXPLAIN SELECT * FROM datasets.hits_v1 WHERE UserID = 610708775678702928
在使用 * 的时候,ClickHouse 会自动展开成所有字段,然后重点是我们看到 WHERE 被替换成了 PREWHERE,证明确实会自动优化,当然我们说过可以通过设置 optimize_move_to_prewhere 为 1、0 进行开启、关闭,默认是开启的。另外这个配置可以通过 set 设置,那么它位于哪里呢?没错,显然是 users.xml 中。
当我们要求数据的实时性高于数据的准确性时,数据采样就很有用了。记得在大四实习的时候,当时负责给各大上市公司做审计,由于数据量非常庞大,算一次要花上好几个小时。所以每次都先采样,只算百分之 10 到百分之 20 的数据,如果得出来的结果符合正常预期,那么再跑全量数据,这样会稳妥一些。如果上来就跑全量数据,最后发现结果算的不对就尴尬了。
当然我当时选择采样只是简单的对程序的准确性进行一些检测,但实际生产中的程序基本上都是准确的,这个时候如果用户执行了一个查询,那么为了很快的给出结果,选择随机采样是最合适的方式。以我之前的经验,如果数据倾斜不严重的话,那么采样 10% 的数据和全量数据计算出来的结果差别很小,当然具体怎么做还是要取决于你的业务。
SELECT ... FROM ... SAMPLE 0.1
WHERE ...
SAMPLE 放在 FROM 之后、WHERE 之前,至于具体用法之前已经说过了,可以回头看一下。
列裁剪与分区裁剪
ClickHouse 非常适合存储大数据量的宽表,因此我们应该避免使用 SELECT * 操作,这是一个非常影响的操作。应当对列进行裁剪,只选择你需要的列,因为字段越少,消耗的 IO 资源就越少,从而性能就越高。
而分区裁剪就是只读取需要分区,在过滤条件中指定,所以设计一个合适的分区表对后期查询是非常有帮助的。。
ORDER BY 应当于 WHERE、LIMIT 一起使用
对千万级以上的数据集进行排序的时候一定要搭配 WHERE 或 LIMIT 使用,可能有人觉得我只是排个序而已,为啥还要有这么多限制。因为事实上我们很少会对数据进行全局排序,而且数据量一大,全局排序的话内存很容易爆掉;如果设置了 max_bytes_before_external_sort,那么全局排序会在磁盘上进行,此时速度又是一个难以忍受的地方。因此在使用 ORDER BY 的时候,需要搭配 WHERE、LIMIT。
避免构建虚拟列
虚拟列指的就是我们自己构造出来的字段,而在原表中是没有的,举个栗子:
SELECT A, A + 1 FROM table;
A 是表中的字段,但 A + 1 明显不是,它是我们构造出来的,所以叫虚拟列。但如果非必须的话,最好不要再结果集上构造虚拟机列,因为虚拟列非常消耗资源性能,可以考虑在拿到数据之后由前后端进行处理。
uniqCombined 替代 count(DISTINCT)
我们之前说过 ClickHouse 提供了一些语法糖,例如这里的 count(DISTINCT column) 实际上就是 countDistinct(column),只不过 ClickHouse 提供了类似于关系型数据库中 count(DISTINCT) 语法。并且在具体执行的时候,底层都对应 uniqExact 函数,举个例子:
SELECT count(DISTINCT UserID), countDistinct(UserID), uniqExact(UserID)
FROM datasets.hits_v1;
但还是建议使用 count(DISTINCT),因为这几个都是等价的,那么自然选一个看起来最熟悉的。那么这个我们说的 uniqCombined 有什么关系呢?原因是 uniqExact 是精确去重并统计数量,如果我们在数量上对统计的数据的误差有一定的容忍性,那么可以使用 uniqCombined,该函数使用类似 HyperLogLog 的算法,在速度上可以提升 10 倍以上,但牺牲了一些准确率。
JOIN 操作
在介绍 JOIN 之前我先说两句,很多公司在设计关系型数据库的表结构时,都会遵循相应的范式,但对于数据仓库而言是完全不需要的,数仓的重点在于分层。对于 OLAP 型的列式存储数据库而言,尤其是 ClickHouse,能不用 JOIN 就不用 JOIN,最好是单表操作,因此这就需要我们保证数据有冗余度,但这在数仓建设中完全 OK 。并且对于 ClickHouse 而言,它的 JOIN 也是比较奇葩的,那么它是怎么做的呢?
首先不管是 LEFT 还是 RIGHT,当 A 表和 B 表进行 JOIN 的时候,ClickHouse 都会将 B 表加载到内存,然后遍历 A 表数据,查询 B 表中有没有能与之关联上的数据,因此这就引出了第一个优化的原则:当大小表 JOIN 的时候,要保证小表在右侧。
-- hits_v1 的数据量要远大于 visits_v1,然后我们来 JOIN 试一下
-- 这里为了避免输出大量信息,我们使用 count(*) 代替
SELECT count(*) FROM datasets.hits_v1 a
LEFT JOIN datasets.visits_v1 b
USING(CounterID);
-- 上面是 hits_v1 作为左表进行左关联,等价于如下:将 hits_v1 作为右表进行右关联
SELECT count(*) FROM datasets.visits_v1 b
RIGHT JOIN datasets.hits_v1 a
USING(CounterID);
我们看到效率确实有差异,而 JOIN 之后的数据量比 hits_v1 表还要多,说明中间产生了笛卡尔积。如果不想产生笛卡尔积,那么只需要在 LEFT JOIN 和 RIGHT JOIN 的前面加上 ANY 即可,默认是 ALL。
但还是上面那句话,能不用 JOIN 就不要用 JOIN,当涉及到两张表的时候,看看是否可以用子查询来替代。
然后是谓词下推,我们举个栗子:
SELECT a.UserID, a.Age, b.CounterID
FROM datasets.hits_v1 a
LEFT JOIN datasets.visits_v1 b
USING(CounterID) WHERE a.EventDate = '2020-04-17'
上面会先对两张表进行 JOIN,完事之后再进行过滤,既然如此的话,那么为什么不能先过滤然后再进行 JOIN 呢?
SELECT a.*, b.CounterID
FROM (SELECT UserID, Age FROM datasets.hits_v1 WHERE EventDate = '2020-04-17') a
LEFT JOIN datasets.visits_v1 b
USING(CounterID)
下面的做法会比上面要快,因为在 JOIN 之前就将数据过滤掉了一部分,不要小看这一点,有时对性能的影响很大。并且 ClickHouse 不会主动帮我们发起谓词下推的操作,需要我们自己手动完成。可能有人好奇,如果将过滤条件放在 ON 子句后面会怎么样,答案是会报错,因为 ClickHouse 不允许 ON 子句中出现过滤条件。
以上就是 ClickHouse 关于优化方面的内容,说实话都比较简单,总之重点就是我们在写 SQL 的时候一定要注意,不能够随心所欲,因为 ClickHouse 所能做到的优化是非常有限的。