zoukankan      html  css  js  c++  java
  • ClickHouse介绍(四)ClickHouse使用操作

    ClickHouse使用操作

    这章主要介绍在ClickHouse使用的各个操作的注意点。常规的统一语法不做详细介绍。

    1. Join操作

    在ClickHouse中,对连接操作定义了不同的精度,包含ALL、ANY和ASOF三种类型,默认为ALL。可以通过join_default_strictness配置修改默认精度(位于system.setting表中)。下面分别说明这3种精度。

    首先建表并插入测试数据:

    --表join_tb1
    CREATE TABLE join_tb1
    (
        `id` String,
        `name` String,
        `time` DateTime
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(time)
    ORDER BY id
    
    --表 join_tb2
    CREATE TABLE join_tb2
    (
        `id` String,
        `rate` UInt8,
        `time` DateTime
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(time)
    ORDER BY id
    
    --表 join_tb3
    CREATE TABLE join_tb3
    (
        `id` String,
        `star` UInt8
    )
    ENGINE = MergeTree
    ORDER BY id
    
    --插入数据
    INSERT INTO join_tb1 VALUES 
    ('1', 'ClickHouse', '2019-05-01 12:00:00')
    ('2', 'Spark', '2019-05-01 12:30:00')
    ('3', 'ElasticSearch', '2019-05-01 13:00:00')
    ('4', 'HBase', '2019-05-01 13:30:00')
    (NULL, 'ClickHouse', '2019-05-01 14:00:00')
    (NULL, 'Spark', '2019-05-01 14:30:00')
    
    INSERT INTO join_tb2 VALUES 
    ('1', 100, '2019-05-01 11:55:00')
    ('1', 105, '2019-05-01 11:50:00')
    ('2', 90, '2019-05-01 12:01:00')
    ('3', 80, '2019-05-01 13:10:00')
    ('5', 70, '2019-05-01 14:00:00')
    ('6', 60, '2019-05-01 13:55:00')
    
    INSERT INTO join_tb3 VALUES 
    ('1', 1000)
    ('2', 900)

    1.1. ALL

    如果左表内的一行数据,在右表中有多行数据与之连接匹配,则返回右表种全部连接的数据。连接依据为:left.key=right.key。

    SELECT a.id, a.name, b.rate FROM join_tb1 AS a ALL INNER JOIN join_tb2 AS b ON a.id=b.id
    
    SELECT
        a.id,
        a.name,
        b.rate
    FROM join_tb1 AS a
    ALL INNER JOIN join_tb2 AS b ON a.id = b.id
    
    ┌─id─┬─name──────────┬─rate─┐
    │ 1  │ ClickHouse    │  100 │
    │ 1  │ ClickHouse    │  105 │
    │ 2  │ Spark         │   90 │
    │ 3  │ ElasticSearch │   80 │
    └────┴───────────────┴──────┘

    1.2. ANY

    如果左表内的一行数据,在右表中有多行数据与之连接匹配,则仅返回右表中第一行连接的数据。连接依据同样为:left.key=right.key

    SELECT
        a.id,
        a.name,
        b.rate
    FROM join_tb1 AS a
    ANY INNER JOIN join_tb2 AS b ON a.id = b.id
    
    ┌─id─┬─name──────────┬─rate─┐
    │ 1  │ ClickHouse    │  100 │
    │ 2  │ Spark         │   90 │
    │ 3  │ ElasticSearch │   80 │
    └────┴───────────────┴──────┘

    1.3. ASOF

    ASOF 是一种模糊连接,允许在连接键之后追加定义一个模糊连接的匹配条件asof_column,例如:

    SELECT
        a.id,
        a.name,
        b.rate,
        a.time,
        b.time
    FROM join_tb1 AS a
    ASOF INNER JOIN join_tb2 AS b ON (a.id = b.id) AND (a.time >= b.time)
    
    ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐
    │ 1  │ ClickHouse │  1002019-05-01 12:00:002019-05-01 11:55:00 │
    │ 2  │ Spark      │   902019-05-01 12:30:002019-05-01 12:01:00 │
    └────┴────────────┴──────┴─────────────────────┴─────────────────────┘

    根据官网介绍的语法:

    SELECT expressions_list
    FROM table_1
    ASOF LEFT JOIN table_2
    ON equi_cond AND closest_match_cond

    https://clickhouse.tech/docs/en/sql-reference/statements/select/join/

    ASOF会先以 left.key = right.key 进行连接匹配,然后根据AND 后面的 closest_match_cond(也就是这里的a.time >= b.time)过滤出最符合此条件的第一行连接匹配的数据。

     另一种写法是使用USING,语法为:

    SELECT expressions_list
    FROM table_1
    ASOF JOIN table_2
    USING (equi_column1, ... equi_columnN, asof_column)

    举例:

    SELECT
        a.id,
        a.name,
        b.rate,
        a.time,
        b.time
    FROM join_tb1 AS a
    ASOF INNER JOIN join_tb2 AS b USING (id, time)
    
    Query id: 075f7e4a-7355-4e11-ae3b-0e3275912a3e
    
    ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐
    │ 1  │ ClickHouse │  1002019-05-01 12:00:002019-05-01 11:55:00 │
    │ 2  │ Spark      │   902019-05-01 12:30:002019-05-01 12:01:00 │
    └────┴────────────┴──────┴─────────────────────┴─────────────────────┘

    对 asof_colum 字段的使用有2点需要注意:

    1. asof_column 必须是整型、浮点型和日期型这类有序序列的数据类型
    2. asof_column不能是数据表内的唯一字段,也就是说连接键(JOIN KEY)和asof_column不能是同一字段

    1.4. Join性能

    在执行JOIN时,ClickHouse对执行的顺序没有特别优化,JOIN操作会在WHERE以及聚合查询前运行。

    JOIN操作结果不会缓存,所以每次JOIN操作都会生成一个全新的执行计划。如果应用程序会大量使用JOIN,则需进一步考虑借助上层应用侧的缓存服务或使用JOIN表引擎来改善性能(JOIN表引擎不支持ASOF精度)。JOIN表引擎会在内存中保存JOIN结果。

    在某些情况下,IN的效率比JOIN要高。

    在使用JOIN连接维度表时,JOIN操作可能并不会特别高效,因为右则表对每个query来说,都需要加载一次。在这种情况下,外部字典(external dictionaries)的功能会比JOIN性能更好。

    1.5. JOIN的内存限制

    默认情况下,ClickHouse使用Hash Join 算法。它会将右侧表(right_table)加载到内存,并为它创建一个hash table。在达到了内存使用的一个阈值后,ClickHouse会转而使用Merge Join 算法。

    可以通过以下参数限制JOIN操作消耗的内存:

    1. max_rows_in_join:限制hash table中的行数
    2. max_bytes_in_join:限制hash table的大小

    在达到任何上述limit后,ClickHouse会以join_overflow_mode 的参数进行动作。此参数包含2个可选值:

    1. THROW:抛出异常并终止操作
    2. BREAK:终止操作但并不抛出异常

    2. WHERE与PREWHERE子句

    WHERE可以通过表达式来过滤数据,如果过滤条件恰好为主键字段,则可以进一步借助索引加速查询,所以WHERE子句是决定查询语句是否能使用索引的判断依据(前提是表引擎支持索引)。

    除此之外,ClickHouse还提供了PREWHERE子句用于条件过滤,它可以更有效地进行过滤优化,仅用于MergeTree表系列引擎。

    PREWHERE与WHERE不同之处在于:使用PREWHERE时,首先只会去PREWHERE指定的列字段数据,用于数据过滤的条件判断。在数据过滤之后再读取SELECT声明的列字段以补全其余属性。所以在一些场合下,PREWHERE相比WHERE而言,处理的数据更少,性能更高。

    默认情况下,即使在PREWHERE子句没有显示指定的情况下,它也会自动移动到WHERE条件到PREWHERE阶段。

    下面做个对比:

    # 默认自动开启了PREWHERE,查询速度为:
    select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1;
    
    …
    6535088 rows in set. Elapsed: 1.428 sec. Processed 8.87 million rows, 863.90 MB (6.21 million rows/s., 604.82 MB/s.)
    
    # 关闭PREWHERE
    set optimize_move_to_prewhere=0
    
    
    # 关闭自动PREWHERE,查询速度为
    6535088 rows in set. Elapsed: 1.742 sec. Processed 8.87 million rows, 864.55 MB (5.09 million rows/s., 496.20 MB/s.)

    可以看到2条语句处理的数据总量没有变化,但是其数据处理量稍有降低(PREWHERE为863.90MB),且每秒吞吐量上升(PREWHER为604.82MB/s,WHERE为496.20MB/s)。

    对比2条语句的执行计划:

    # PREWHERE
    explain select WatchID, Title, GoodEvent from hits_v1 prewhere JavaEnable=1;
    
    EXPLAIN
    SELECT
        WatchID,
        Title,
        GoodEvent
    FROM hits_v1
    PREWHERE JavaEnable = 1
    
    Query id: 103fd24a-e718-4304-9f75-4900528c1d1a
    
    ┌─explain───────────────────────────────────────────────────────────────────┐
    │ Expression ((Projection + Before ORDER BY))                               │
    │   SettingQuotaAndLimits (Set limits and quota after reading from storage) │
    │     ReadFromStorage (MergeTree)                                           │
    └───────────────────────────────────────────────────────────────────────────┘
    
    # WHERE
    explain select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1;
    
    EXPLAIN
    SELECT
        WatchID,
        Title,
        GoodEvent
    FROM hits_v1
    WHERE JavaEnable = 1
    
    Query id: 9b470524-1320-4e9f-bade-cf8c2c9944c8
    
    ┌─explain─────────────────────────────────────────────────────────────────────┐
    │ Expression ((Projection + Before ORDER BY))                                 │
    │   Filter (WHERE)                                                            │
    │     SettingQuotaAndLimits (Set limits and quota after reading from storage) │
    │       ReadFromStorage (MergeTree)                                           │
    └─────────────────────────────────────────────────────────────────────────────┘

    可以看到相比WHERE语句,PREWHERE语句的执行计划省去了一次Filter操作。

    3. Group By

    Group By的用法非常常见,ClickHouse中执行聚合查询时,若是SELECT后面只声明了聚合函数,则GROUP BY 关键字可以省略:

    SELECT
        SUM(data_compressed_bytes) AS compressed,
        SUM(data_uncompressed_bytes) AS uncompressed
    FROM system.parts
    
    Query id: e38e3ec1-968d-4442-ba7d-b8555f27e0d0
    
    ┌─compressed─┬─uncompressed─┐
    │ 18510739429445387666 │
    └────────────┴──────────────┘

    聚合查询还能配合WITH ROLLUP、WITH CUBE和WITH TOTALS三种修饰符获取额外的汇总信息。

    3.1. WITH ROLLUP

    ROLLUP便是上卷数据,按聚合键从右到左,基于聚合函数依次生成分组小计和总计。如果设聚合键的个数为n,则最终会生成小计的个数为n+1。例如:

    SELECT
        table,
        name,
        SUM(bytes_on_disk)
    FROM system.parts
    GROUP BY
        table,
        name
        WITH ROLLUP
    ORDER BY table ASC
    
    ┌─table──────────────────────────────────────────┬─name───────────────────────────────────┬─SUM(bytes_on_disk)─┐
    │                                                │                                        │         1857739143 │
    │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │                                        │                638 │
    │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ 953e60a1e8747360786c2b70a223788d_2_4_1 │                318 │
    │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ acb795a12c7ba41b0ed4c3d94a008ecd_1_3_1 │                320 │
    │ agg_table                                      │                                        │                358 │
    │ agg_table                                      │ 201909_2_2_0358

    可以看到第1行是一个汇总,统计的SUM(bytes_on_disk)的总行数。而每个table字段都有一个汇总(例如.inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 表第一行以及agg_table 第一行)。

    3.2. WITH CUBE

    CUBE也是数仓里重要的概念,基于聚合键之间所有的组合生成统计信息。如果聚合键的个数为n,则最终聚合数据的个数为2的n次方。例如:

    --建表
    CREATE TABLE person
    (
        `id` int,
        `name` String,
        `course` String,
        `year` DateTime,
        `points` int
    )
    ENGINE = MergeTree
    ORDER BY id
    
    --插入数据
    INSERT INTO person VALUES
     (1, 'jane', 'CS', '2021-01-02 11:00:00', 50),
     (2, 'tom', 'CS', '2021-01-03 11:00:00', 60),
     (3, 'bob', 'BS', '2021-01-03 11:00:00', 50),
     (4, 'alice', 'BS', '2021-01-01 11:00:00', 40),
     (5, 'jane', 'ACC', '2021-01-02 11:00:00', 70),
     (6, 'bob', 'ACC', '2021-01-03 11:00:00', 90),
     (7, 'jane', 'MATH', '2021-01-04 11:00:00', 100)
    
    --Cube计算
    SELECT
        name,
        course,
        year,
        AVG(points)
    FROM person
    GROUP BY
        name,
        course,
        year
    WITH CUBE
    
    
    ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐
    │ jane  │ ACC    │ 2021-01-02 11:00:0070 │
    │ bob   │ ACC    │ 2021-01-03 11:00:0090 │
    │ alice │ BS     │ 2021-01-01 11:00:0040 │
    …
    
    ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
    │      │        │ 2021-01-01 11:00:0040 │
    │      │        │ 2021-01-03 11:00:0066.66666666666667 │
    │      │        │ 2021-01-02 11:00:0060 │
    │      │        │ 2021-01-04 11:00:00100 │
    └──────┴────────┴─────────────────────┴───────────────────┘
    ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
    │      │        │ 1970-01-01 00:00:0065.71428571428571 │
    └──────┴────────┴─────────────────────┴───────────────────┘

    可以看到结果中会生成 8 个统计结果(部分结果已省略)。

    3.3. WITH TOTALS

    WITH TOTALS会基于聚合函数对所有数据进行统计(比原结果多一行总的统计结果),例如:

    SELECT
        name,
        course,
        year,
        AVG(points)
    FROM person
    GROUP BY
        name,
        course,
        year
        WITH TOTALS
    
    ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐
    │ jane  │ ACC    │ 2021-01-02 11:00:0070 │
    │ bob   │ ACC    │ 2021-01-03 11:00:0090 │
    │ alice │ BS     │ 2021-01-01 11:00:0040 │
    │ jane  │ CS     │ 2021-01-02 11:00:0050 │
    │ jane  │ MATH   │ 2021-01-04 11:00:00100 │
    │ tom   │ CS     │ 2021-01-03 11:00:0060 │
    │ bob   │ BS     │ 2021-01-03 11:00:0050 │
    └───────┴────────┴─────────────────────┴─────────────┘
    
    Totals:
    ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
    │      │        │ 1970-01-01 00:00:0065.71428571428571 │
    └──────┴────────┴─────────────────────┴───────────────────┘

    4. 查看SQL执行计划

    ClickHouse目前并没有直接提供EXPLAIN的详细查询计划,当前EXPLAIN仅是输出一个简单的计划。不过我们仍可以借助后台服务日志来实现此功能,例如执行以下语句即可看到详细的执行计划:

    clickhouse-client --password xxx --send_logs_level=trace <<<'select * from tutorial.hits_v1' > /dev/null

    打印信息如下(仅截取关键信息):

    tutorial.hits_v1  (SelectExecutor): Key condition: unknown
    => 查询未使用主键索引
    
    tutorial.hits_v1  (SelectExecutor): MinMax index condition: unknown
    => 未使用分区索引
    
    tutorial.hits_v1  (SelectExecutor): Not using primary index on part 201403_1_29_2
    => 未在分区 201403_1_29_2 下使用primary index
    
    tutorial.hits_v1  (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 1094 marks by primary key, 1094 marks to read from 1 ranges
    => 选择了1个分区,共计1094个marks
    
    executeQuery: Read 8873898 rows, 7.88 GiB in 21.9554721 sec., 404177 rows/sec., 367.50 MiB/sec.
    => 读取 8873898条数据,7.88G 数据,耗时21.955秒…
    
    MemoryTracker: Peak memory usage (for query): 361.67 MiB.
    => 消耗内存量

    下面优化一下查询:

    clickhouse-client --password xxx --send_logs_level=trace <<<"select WatchID from tutorial.hits_v1 where EventDate='2014-03-17'" > /dev/null

    打印结果为:

    InterpreterSelectQuery: MergeTreeWhereOptimizer: condition "EventDate = '2014-03-17'" moved to PREWHERE
    => 自动调用了PREWHERE
    
    tutorial.hits_v1 (SelectExecutor): Key condition: (column 1 in [16146, 16146])
    => 使用了主键索引
    
    tutorial.hits_v1 (SelectExecutor): MinMax index condition: (column 0 in [16146, 16146])
    => 使用了分区索引
    
    tutorial.hits_v1 (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 755 marks by primary key, 755 marks to read from 64 ranges
    => 根据分区键选择了一个分区
    
    executeQuery: Read 6102294 rows, 58.19 MiB in 0.032661599 sec., 186833902 rows/sec., 1.74 GiB/sec.
    => 读到的数据,以及速度
    
    MemoryTracker: Peak memory usage (for query): 11.94 MiB.
    => 消耗内存量

    总的来说,ClickHouse未直接通过EXPLAIN语句提供查看语句执行的详细过程,但是可以变相的将日志设置到DEBUG或是TRACE级别,实现此功能,并分析SQL的执行日志。

  • 相关阅读:
    设置linux session 编码
    在masterpage中添加对usercontrol的引用
    首页的sitecontent地址
    iis的路径
    设置repeater每行多少个的方法
    updatepannel的使用
    取caml查询结果的前多少行
    设置视频自动播放
    网站集与网站
    notepad++ 死机 找到没保存的文件
  • 原文地址:https://www.cnblogs.com/zackstang/p/14676729.html
Copyright © 2011-2022 走看看