建表时能用时间日期类型就不要使用字符串类型,在Hive中可以使用全字符串,但是在Clickhouse中不应使用。
Clickhouse底层将DateTime存储为Long类型,但是不建议存储Long类型,因为DateTime不需要经过函数处理,执行效率高,可读性好。
create table t_type2(
id UInt32,
sku_id String,
total_amount Decimal(16,2),
create_time Int32
)engine = ReplacingMergeTree(create_time)
partition by toYYYYMMDD(toDate(create_time))
primary key(id)
order by (id,sku_id)
在Clickhouse中空值是会影响性能的,因为要存储一个nullable列时需要创建一个额外的文件来存储null的标记,并且null列无法被索引,因此,应直接使用字段默认值表示为空,或者自定义一个在业务中午意义的值表示。
create table t_null(x Int8,y Nullable(Int8)) engine TinyLog;
insert into t_null values(1,null),(2,3);
select x+y from t_null;
分区应按照实际业务进行分区,一般选择按天进行分区。
索引列必须执行,Clickhouse中的索引列即排序列,通过order by执行,一般在查询条件中经常被用来充当筛选条件的属性被纳入进来,可以是单一的维度,也可以是组合维度的索引,通常是高级列在前、查询频率较大的列在前的原则,筛选后的数据满足在百万以内最佳。
index_granularity 是用来控制索引粒度的,默认是8192,不建议调整。
如果表中不是必须保留全量历史数据,建议指定TTL,避免手动删除过期数据,TTL也可以通过alter table进行修改。
1. Code: 252, e.displayText() = DB::Exception: Too many parts(304).
Merges are processing significantly slower than inserts
2. Code: 241, e.displayText() = DB::Exception: Memory limit (for query)
exceeded:would use 9.37 GiB (attempt to allocate chunk of 301989888
bytes), maximum: 9.31 GiB
解决办法:
配置项主要在 config.xml 或 users.xml 中, 基本上都在 users.xml 里。
config.xml 的配置项:https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/
users.xml 的配置项:https://clickhouse.tech/docs/en/operations/settings/settings/
Clickhouse不支持多数据目录,为了提升IO性能,可以挂载虚拟券组,一个券组绑定多块物理磁盘提升读写性能,多数据查询场景 SSD 会比普通机械硬盘快 2-3 倍。
Clickhouse的SQL优化是基于RBO(Rule Based Optimization)
将 visits_v1.tar 和 hits_v1.tar 上传到虚拟机,解压到 clickhouse 数据路径下
# 解压到 clickhouse 数据路径
sudo tar -xvf hits_v1.tar -C /var/lib/clickhouse
sudo tar -xvf visits_v1.tar -C /var/lib/clickhouse
# 修改所属用户
sudo chown -R clickhouse:clickhouse /var/lib/clickhouse/data/datasets
sudo chown -R clickhouse:clickhouse /var/lib/clickhouse/metadata/datasets
# 重启Clickhouse
sudo clickhouse restart
如果直接使用count()或者count(*),且没有where条件时,则直接会使用system.tables的total_rows,如:
explain select count(*) from hits_v1 hv ;
Optimized trivial count :这是对 count 的优化。
如果使用count具体字段,则不会有此优化:
EXPLAIN SELECT count(CounterID) FROM datasets.hits_v1;
下面语句子查询中有两个重复的 id 字段,会被去重:
EXPLAIN SYNTAX SELECT
a.UserID,
b.VisitID,
a.URL,
b.UserID
FROM
hits_v1 AS a
LEFT JOIN (
SELECT
UserID,
UserID as HaHa,
VisitID
FROM visits_v1) AS b
USING (UserID)
limit 3;
-- 返回优化语句:
SELECT
UserID,
VisitID,
URL,
b.UserID
FROM hits_v1 AS a
ALL LEFT JOIN
(
SELECT
UserID,
VisitID
FROM visits_v1
) AS b USING (UserID)
LIMIT 3
当group by有having子句,但是没有with cube、with rollup 、with totals修饰的时候,having过滤会下推到where提前过滤,例如下面,having name变成了where name在group by之前过滤:
EXPLAIN SYNTAX SELECT UserID FROM hits_v1 GROUP BY UserID HAVING UserID =
'8585742290196126178';
-- 返回优化语句
SELECT UserID
FROM hits_v1
WHERE UserID = \'8585742290196126178\'
GROUP BY UserID
子查询也支持谓词下推:
EXPLAIN SYNTAX
SELECT *
FROM
(
SELECT UserID
FROM visits_v1
)
WHERE UserID = '8585742290196126178'
-- 返回优化后的语句
SELECT UserID
FROM
(
SELECT UserID
FROM visits_v1
WHERE UserID = \'8585742290196126178\'
)
WHERE UserID = \'8585742290196126178\'
复杂场景:
EXPLAIN SYNTAX
SELECT * FROM (
SELECT
*
FROM
(
SELECT
UserID
FROM visits_v1)
UNION ALL
SELECT
*
FROM
(
SELECT
UserID
FROM visits_v1)
)
WHERE UserID = '8585742290196126178'
-- 返回优化后的语句
SELECT UserID
FROM
(
SELECT UserID
FROM
(
SELECT UserID
FROM visits_v1
WHERE UserID = \'8585742290196126178\'
)
WHERE UserID = \'8585742290196126178\'
UNION ALL
SELECT UserID
FROM
(
SELECT UserID
FROM visits_v1
WHERE UserID = \'8585742290196126178\'
)
WHERE UserID = \'8585742290196126178\'
)
WHERE UserID = \'8585742290196126178\'
聚合函数内的计算,会进行外推:
EXPLAIN SYNTAX
SELECT sum(UserID * 2)
FROM visits_v1
-- 优化后的sql
SELECT sum(UserID) * 2
FROM visits_v1
对于聚合键,也就是group by key使用min、max、any、聚合函数,则将函数消除:
EXPLAIN SYNTAX
SELECT
sum(UserID * 2),
max(VisitID),
max(UserID)
FROM visits_v1
GROUP BY UserID
-- 优化后的语句
SELECT
sum(UserID) * 2,
max(VisitID),
UserID
FROM visits_v1
GROUP BY UserID
重复的order by key会被删除:
EXPLAIN SYNTAX
SELECT *
FROM visits_v1
ORDER BY
UserID ASC,
UserID ASC,
VisitID ASC,
VisitID ASC
//返回优化后的语句:
select
……
FROM visits_v1
ORDER BY
UserID ASC,
VisitID ASC
重复的limit by key会被删除:
EXPLAIN SYNTAX
SELECT *
FROM visits_v1
LIMIT 3 BY
VisitID,
VisitID
LIMIT 10
--返回优化后的语句:
select
……
FROM visits_v1
LIMIT 3 BY VisitID
LIMIT 10
重复的using key会被删除:
EXPLAIN SYNTAX
SELECT
a.UserID,
a.UserID,
b.VisitID,
a.URL,
b.UserID
FROM hits_v1 AS a
LEFT JOIN visits_v1 AS b USING (UserID, UserID)
-- 返回优化后的语句:
SELECT
UserID,
UserID,
VisitID,
URL,
b.UserID
FROM hits_v1 AS a
ALL LEFT JOIN visits_v1 AS b USING (UserID)
如果子查询只返回一行数据,那么在引用的时候会使用标量进行替换:
EXPLAIN SYNTAX
WITH
(
SELECT sum(bytes)
FROM system.parts
WHERE active
) AS total_disk_usage
SELECT
(sum(bytes) / total_disk_usage) * 100 AS table_disk_usage,
table
FROM system.parts
GROUP BY table
ORDER BY table_disk_usage DESC
LIMIT 10;
-- 返回优化后的语句:
WITH CAST(0, \'UInt64\') AS total_disk_usage
SELECT
(sum(bytes) / total_disk_usage) * 100 AS table_disk_usage,
table
FROM system.parts
GROUP BY table
ORDER BY table_disk_usage DESC
LIMIT 10
如果开启了 optimize_if_chain_to_multiif 参数,三元运算符会被替换成 multiIf 函数:
EXPLAIN SYNTAX
SELECT number = 1 ? 'hello' : (number = 2 ? 'world' : 'atguigu')
FROM numbers(10)
settings optimize_if_chain_to_multiif = 1;
-- 返回优化后的语句:
SELECT multiIf(number = 1, \'hello\', number = 2, \'world\', \'atguigu\')
FROM numbers(10)
SETTINGS optimize_if_chain_to_multiif = 1
prewhere和where作用相同,用来过滤数据,不同之处在于prewhere只支持MergeTree系列的表,首先会读取指定的列的数据,来判断数据过滤,等待数据过滤之后再读取select字段来补全其余属性。
当查询列多余筛选列时使用prewhere可提升十倍查询性能,prewhere会自动优化执行过滤阶段的数据读取方式,降低IO。
在某些情况下,prewhere语句比where语句处理的数据量更少性能更高。
#关闭 where 自动转 prewhere(默认情况下, where 条件会自动优化成 prewhere)
set optimize_move_to_prewhere=0;
# 使用 where
select WatchID,
JavaEnable,
Title,
GoodEvent,
EventTime,
EventDate,
CounterID,
ClientIP,
ClientIP6,
RegionID,
UserID,
CounterClass,
OS,
UserAgent,
URL,
Referer,
URLDomain,
RefererDomain,
Refresh,
IsRobot,
RefererCategories,
URLCategories,
URLRegions,
RefererRegions,
ResolutionWidth,
ResolutionHeight,
ResolutionDepth,
FlashMajor,
FlashMinor,
FlashMinor2
from datasets.hits_v1 where UserID='3198390223272470366';
# 使用 prewhere 关键字
select WatchID,
JavaEnable,
Title,
GoodEvent,
EventTime,
EventDate,
CounterID,
ClientIP,
ClientIP6,
RegionID,
UserID,
CounterClass,
OS,
UserAgent,
URL,
Referer,
URLDomain,
RefererDomain,
Refresh,
IsRobot,
RefererCategories,
URLCategories,
URLRegions,
RefererRegions,
ResolutionWidth,
ResolutionHeight,
ResolutionDepth,
FlashMajor,
FlashMinor,
FlashMinor2
from datasets.hits_v1 prewhere UserID='3198390223272470366';
以下场景需要手动指定prewhere:
通过数据采样运算可极大提高数据分析的性能,数据采样只针对MergeTree表才有用,且在建表时需要指定采样策略:
SELECT Title,count(*) AS PageViews
FROM hits_v1
SAMPLE 0.1 -- 代表采样 10%的数据,也可以是具体的条数
WHERE CounterID =57
GROUP BY Title
ORDER BY PageViews DESC LIMIT 1000
数据量太大时避免使用select * 这种写法,字段越少,IO越低,性能也就越高。
数据量较大的表一定要建成分区表,减少数据的扫描量。
千万级以上数据量在使用order by查询时需要搭配where和limit语句一起使用。
虚拟列非常耗资源,不是必须不要在结果集上构建虚拟列。可以考虑在前端进行处理,或者在表中构造实际字段进行额外存储。
性能可提升十倍以上,uniqCombined底层采用类似HyperLogLog算法实现,能接受2%左右的数据误差,可直接使用这种去重方式提升查询性能,count(distinct) 会使用uniqExact精确去重。
千万级以上数据不建议使用distinct ,改为近似去重uniqCombined。
ClickHouse 的物化视图是一种查询结果的持久化,它确实是给我们带来了查询效率的提升。用户查起来跟表没有区别,它就是一张表,它也像是一张时刻在预计算的表,创建的过程它是用了一个特殊引擎,加上后来 as select,就是 create 一个 table as select 的写法。“查询结果集”的范围很宽泛,可以是基础表中部分数据的一份简单拷贝,也可以是多表 join 之后产生的结果或其子集,或者原始数据的聚合指标等等。所以,物化视图不会随着基础表的变化而变化,所以它也称为快照(snapshot)。
物化视图是把查询的结果根据相应的引擎存入到了磁盘或内存中。
优点:查询速度快,要是把物化视图这些规则全部写好,它比原数据查询快了很多,总
的行数少了,因为都预计算好了。
缺点:它的本质是一个流式数据的使用场景,是累加式的技术,所以要用历史数据做去
重、去核这样的分析,在物化视图里面是不太好用的。在某些场景的使用也是有限的。而且
如果一张表加了好多物化视图,在写这张表的时候,就会消耗很多机器的资源,比如数据带
宽占满、存储一下子增加了很多。
避免因单个慢查询引起的服务雪崩,除了可以为单个查询设置超时外,还可以设置周期熔断,在一个查询周期内,如果用户频繁进行慢查询操作超出规定阈值后将无法继续进行查询操作。
物理内存和虚拟内存的交换会导致查询变慢,如果资源允许的情况下请关闭虚拟内存。
为每一个账户添加join_use_nulls配置,坐标中的一条记录在右表中不存在,右表的相关字段会返回该字段相应数据类型的默认值,而不是标准SQL中的null。
批量写入数据时,必须控制每个批次的数据中涉及到的分区的数量,在写入之前最好对需要导入的数据进行排序,无序的数数据或者涉及到的分区太多,会导致Clickhouse无法及时对新导入的数据进行合并,从而影响查询性能。
CPU一般在50%左右会出现查询波动,达到70%会出现大范围的查询超时,CPU是最关键的指标,需要关注。
-- 创建小表
CREATE TABLE visits_v2
ENGINE = CollapsingMergeTree(Sign)
PARTITION BY toYYYYMM(StartDate)
ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID)
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from visits_v1 limit 10000;
-- 创建 join 结果表:避免控制台疯狂打印数据
CREATE TABLE hits_v2
ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from hits_v1 where 1=0;
当进行多表联查时,如果查询的数据仅从其中一张表出时,可考虑用IN而不是Join
insert into hits_v2
select a.* from hits_v1 a where a. CounterID in (select CounterID from
visits_v1);
-- 反例:使用 join
insert into table hits_v2
select a.* from hits_v1 a left join visits_v1 b on a. CounterID=b.
CounterID;
多表Join时要满足小表在右的原则,右表关联时被加载到内存中与左表进行比较,在Clickhouse中无论是Left Join、Right Join还是Inner Join永远都是拿着右表中的每一条记录去左表中查找该记录是否存在,所以右表必须是小表
-- 小表在右
insert into table hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b.
CounterID;
-- 大表在右
insert into table hits_v2
select a.* from visits_v2 b left join hits_v1 a on a. CounterID=b.
CounterID;
Clickhouse在Join查询时不会主动发起谓词下推的操作,需要每个子查询提前完成过滤操作,需要注意的是,是否执行谓词下推,对性能影响很大。
Explain syntax
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b.
CounterID
having a.EventDate = '2014-03-17';
Explain syntax
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b.
CounterID
having b.StartDate = '2014-03-17';
insert into hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b.
CounterID
where a.EventDate = '2014-03-17';
insert into hits_v2
select a.* from (
select * from
hits_v1
where EventDate = '2014-03-17'
) a left join visits_v2 b on a. CounterID=b. CounterID;
两张分布式表上的IN和Join之前必须加上GLOBAL关键字,右表只会在接收查询请求的那个节点查询一次,并将其分发到其他节点上,如果不加GLOBAL关键字的话,每个节点都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询N²次(N是该分布式表的分片数量),这就是查询放大,会带来很大开销。
将一些需要关联分析的业务创建成字典表进行join操作,前提是字典表不宜太大,因为字典表会常驻内存。
通过增加逻辑过滤可以减少数据扫描,达到提高执行速度及降低内存消耗的目的