【clickhouse】

clickhouse

概述

快如闪电

  • 采用列式存储

    1. 扫描范围小,只需要扫描这一列的数据
    2. 压缩率高,同一数据类型下,压缩效率好。磁盘io,数据传输小 默认使用 LZ4压缩。 总体可以达到8:1的压缩效果。
  • 向量化执行引擎

向量化执行简单理解就是消除程序中循环的优化。

每列的数据存储在一起,可以认为这些数据是以数组的方式存储的,基于这样的特征,当该列数据需要进行某一同样操作,可以使用SIMD进一步提升计算效率,即便运算的机器上不支持SIMD, 也可以通过一个循环来高效完成对这个数据块各个值的计算。

SIMD (Single Instruction Multiple Data) 即单条指令操作多条数据——原理即在CPU 寄存器层面实现数据的并行操作,从寄存器中访问数据的速度,是从内存访问数据速度的300倍,是从磁盘中访问数据速度的3000万倍 ClickHouse 目前使用SSE4.2 指令集实现向量化执行

举个例子: 为了制作8杯果汁,非向量化执行是用1台榨汁机重复循环8次
而向量化执行的方式是用8台榨汁机执行一次。即单条指令操作多条数据。
在cpu寄存器层面实现数据的并行操作。

  • 多线程并发处理
    向量化执行是通过数据级并行处理,提升性能。那么多线程处理就是通过线程并行的方式实现了性能的提升。

  • 分布式集群
    计算移动比数据移动更划算
    在各个服务器间通过网络传输数据的成本是高昂的,所以相比移动数据,更好的方式是 预先将数据分布到各台服务器,将数据的计算查询直接下推到数据所在的服务器。

  • 多主架构
    没有master slave的概念,每一台服务器都能对外提供读写服务。

缺点:

  1. 不支持事务,不支持真正的删除/更新
  2. 不支持高并发,官方建议qps为100,可以通过修改配置文件增加连接数,但是在服务器足够好的情况下
  3. SQL满足日常使用80%以上的语法,join写法比较特殊;最新版已支持类似SQL的join,但性能不好
  4. 尽量做1000条以上批量的写入,避免逐行insert或小批量的insert,update,delete操作
  5. Clickhouse快是因为采用了并行处理机制,即使一个查询,也会用服务器一半的CPU去执行,所以ClickHouse不能支持高并发的使用场景,默认单查询使用CPU核数为服务器核数的一半,安装时会自动识别服务器核数,可以通过配置文件修改该参数

clickhouse的表现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
参考博客: https://www.sfjvip.com/server/10159.html

Clickhouse替代ES后,日志查询速度提升了38倍!
参考博客: https://dbaplus.cn/news-148-3458-1.html

没有万能的技术,只有适合的应用场景。

查询语句

with 语句

  • 定义变量
  • 调用函数
  • 定义子查询
  • 在子查询中重复使用with
    说是有四种用法,实际上就只有一种就是定义变量

定义变量示例:

with ['900489594287239739','1000483842131477746'] as oid
    select *
    from t_order_info final
    where order_id in(oid);

调用函数: 可以访问select查询的列字段,并调用函数

with sum(sku_receipts+box_receipts) as total_receipts
select platform, shop_id, total_receipts
from t_order_food_info
where status = 1
  and order_id in ('900489594287239739', '1000483842131477746') group by platform, shop_id,order_id;

子查询

我把 with的作用理解为表达式别名

with (t1.platform = 1
  and t1.shop_id = '15983528161'
  and t1.order_time >= '2023-04-03 00:00:00'
  and t1.order_time <= '2023-04-03 23:59:59'
  and t1.account_id in (25)
) as fisql
select t1.account_id,
       t1.shop_id,
       t1.platform,
       sum(t1.food_receipts)                          food_receipts,
       sum(t1.box_receipts)                           box_receipts,
       sum(if(t1.food_cost > 0, t1.food_receipts, 0)) food_exact_receipts,
       sum(if(t1.box_cost > 0, t1.box_receipts, 0))   box_exact_receipts,
       sum(t1.food_cost)                              food_cost,
       sum(t1.box_cost)                               box_cost,
       sum(t1.food_sale)                              food_sale,
       sum(t1.box_sale)                               box_sale,
       sum(t1.food_num)                               food_num,
       sum(t1.box_num)                                box_num,
       max(t1.order_time)                             order_time
from t_food_profit_day t1 final
where fisql group by t1.account_id, t1.shop_id, t1.platform ;

而msyql 8.0后 使用with语句可以将结果当做一张临时表使用
with t1 as (select * from dkd_occ.dz_auth_token where shop_id = ‘xxxx’) select * from t1;

array join

这里我们建一张复杂的表来引入array join的概念:

create table t_order_and_food_info
(
    platform       UInt8,
    shop_id        String,
    order_id       String,
    original_price Int64,
    commission     Int64,
    receipts       Int64,
    payment        Int64,
    user_id        String,
    status         UInt8,
    order_time     DateTime,
    cancel_time    UInt64,
    completed_time UInt64,
    update_time    DateTime,
    food_info Nested (
        platform_spu_id String,
        platform_sku_id String,
        sku_price Int64,
        reduce_fee Int64,
        box_fee Int64,
        sku_num Int64,
        box_num Int64,
        sku_receipts Int64,
        box_receipts Int64
        )

) engine = ReplacingMergeTree(update_time)
      PARTITION BY toYYYYMMDD(order_time)
      ORDER BY (platform, shop_id, order_id)
      SETTINGS index_granularity = 8192;
      
-- 写入数据      
insert into t_order_and_food_info (platform, shop_id, order_id, original_price, commission, receipts, payment, user_id,
                                   status, order_time, cancel_time, completed_time, update_time,
                                   `food_info.platform_spu_id`, `food_info.platform_sku_id`, `food_info.sku_price`,
                                   `food_info.reduce_fee`, `food_info.box_fee`, `food_info.sku_num`,
                                   `food_info.box_num`, `food_info.sku_receipts`, `food_info.box_receipts`)
select t1.platform,
       t1.shop_id,
       t1.order_id,
       max(t1.original_price),
       max(t1.commission),
       max(t1.receipts),
       max(t1.payment),
       max(t1.user_id),
       max(t1.status),
       max(t1.order_time),
       max(t1.cancel_time),
       max(t1.completed_time),
       max(t1.update_time),
       groupArray(t2.platform_spu_id),
       groupArray(t2.platform_sku_id),
       groupArray(t2.sku_price),
       groupArray(t2.reduce_fee),
       groupArray(t2.box_fee),
       groupArray(t2.sku_num),
       groupArray(t2.box_num),
       groupArray(t2.sku_receipts),
       groupArray(t2.box_receipts)
from t_order_info t1 final ALL
         inner join t_order_food_info t2 on t1.order_id=t2.order_id
where t1.order_id='900503983124133840'
group by t1.platform, t1.shop_id, t1.order_id

查询得到:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用arrayjoin 展开嵌套对象
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

left array join
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数组中存在null值:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

join

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图所示:

join 子句可以对左右两张表进行连接 语法包含 连接精度和连接类型两个部分.

连接精度

  • ALL

    如果左表内的一行数据,右表内有多行数据与之对应(join_key) , 则返回右表所有与之匹配的数据.

  • ANY

    如果左表内的一行数据,右表内有多行数据与之对应(join_key) , 则返回右表第一行与之匹配的数据.

  • ASOF

    模糊连接, 允许连接键之后在追加顶一个一个模糊连接的匹配条件 asof_column.

    例子:

    select *
    from t_order_info t1   asof
    inner join t_order_food_info t2
    on t1.order_id = t2.order_id and t1.order_time<=t2.order_time;
    

就是关联条件不光有等式还有不等式

连接类型:

  • LEFT

  • RIGHT

  • FULL

  • CORSS

    笛卡尔积

  • INNER

    交集

join 使用的注意事项

  • 性能

    为了能够优化join查询性能,首先应该遵循左大右小的原则,即将数据量小的表放在右侧. 这是因为在执行join查询时,无论使用哪种连接方式,右表都会被全部加载到内存中与左表进行比较.

    其次 join查询目前没有缓存支持,这意味着每一次join查询,即便是连续执行相同的sql,也都会生成一次全新的执行计划.如果应用程序大量使用join查询,则需要进一步考虑借助上层的应用测的缓存或者使用JOIN表引擎改善性能.

    如果有大量属性补全的场景中即多个表的连接查询, 查询会转换成 两两连接的形式,这种滚雪球式的方式,查询可能会带来性能问题.

    **说明:**需要说明的是:Join表引擎更加通常的用途,是用于Join连接查询的右侧表。且Join表的数据是首先被写至内存,然后才被同步到磁盘文件上。这意味着两件事:1.Join表的查询速度很快,因为它的存在本来就是为了优化连接查询的速度;2.Join表不适合存放千万级以上的大表,否则会占用过多的服务器内存,它更适合存放需要经常查询的小表,且通常为join语句的右侧表。

  • 空值策略
    连接查询的空值是默认值填充,这与其他数据库所采取的策略不同.
    连接查询的空值策略可以通过 join_user_nulls 参数指定 默认为0 当值为1 时 ,空值由NULL 填充.

final

  1. 老版本 单线程查询效率低 新版本虽然是多线程查询但是读取parts是单线程的 效率还是有损失 数据量较大时,使用group by 去重

  2. 在join情况下 final修饰的被join表的去重会失效

prewhere where

prewhere 目前只能作用于mergetree的表引擎,他可以看做是对where的优化,作用与where相同,均是用来
过滤数据.
不同之处在于:使用prewhere时,只会读取prewhere指定列的字段,用于数据过滤的条件判断,待数据过滤之后在读取select 需要查询的字段以补全其余属性.相对于where而言,他处理的数据量更少,性能更高.

//关闭自动优化where

set optimize_move_to_prewhere=0;
查看语法的解析:

explain syntax select * from t_order_info2  prewhere   order_id='900493920172345904' and seq=9999;
explain syntax select * from t_order_info2  where   order_id='900493920172345904' and seq=9999;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由上图可以看到在:
扫描的行数相同,但是扫描的数据量却不是一样的.

既然 clickhouse默认自动开启prewhere优化即:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

group by

分组

  • with rollup
    上卷
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    select platform,shop_id,count() from t_order_info final group by platform,shop_id with rollup;
  • with cube
    立方体
    select platform,shop_id,count() from t_order_info final where shop_id in('2128902185','16779106391') group by platform,shop_id with cube;
    select platform,shop_id,toYYYYMMDD(order_time) ,count() from t_order_info final where shop_id in('2128902185','16779106391') group by platform,shop_id,toYYYYMMDD(order_time) with cube;
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
  • with totals
    总计
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Having

和group by 组合使用,用于聚合后二次过滤
数据能在聚合前过滤,就在聚合前过滤,减少聚合需要处理的数据量.

order by

排序 order by a,b desc;
order by a desc,b asc;
select platform,shop_id,count() orderNum from t_order_info final group by platform,shop_id order by orderNum desc,platform asc;

NULL FIRST NULLLAST

排序字段如果为null值,排在最前面还是最后面

  1. 默认 NULLLAST
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

select order_id,arr from (select order_id, ['1','2',null] arr from t_order_info limit 10) t1 array join arr order by arr NULLS FIRST;

limit by

limit num by col1,col2…

select * from (select * from t_order_info limit 3 by platform,status) order by platform,status;
有两个平台1/2,每个平台的订单有两种状态0/1
这样展示的数据为:
1平台 状态为 0 3条数据
1平台 状态为 1 3条数据
2平台 状态为 0 3条数据
2平台 状态为 1 3条数据
总计12条数据

┌─platform─┬─shop_id─────┬─order_id────────────┬─original_price─┬─commission─┬─receipts─┬─payment─┬─user_id───────┬─status─┬──────────order_time─┬───cancel_time─┬─completed_time─┬─────────update_time─┐
│        1 │ 15983528161 │ 1100491672555025273 │              3 │          0 │        1 │       3 │ 11180244449   │      0 │ 2023-04-07 10:38:39 │ 1680835484000 │              0 │ 2023-04-11 09:46:44 │
│        1 │ 15983528161 │ 1100491732698106880 │              3 │          0 │        1 │       3 │ 11180244449   │      0 │ 2023-04-07 11:37:38 │ 1680839024000 │              0 │ 2023-04-11 09:46:44 │
│        1 │ 15983528161 │ 1100492244216409248 │              3 │          0 │        1 │       3 │ 11180244449   │      0 │ 2023-04-07 20:05:54 │ 1680869520000 │              0 │ 2023-04-11 09:46:44 │
│        1 │ 15983528161 │ 1100491582871352857 │              3 │          0 │        1 │       3 │ 11141627001   │      1 │ 2023-04-07 09:04:34 │             0 │  1680919294000 │ 2023-04-11 09:46:44 │
│        1 │ 16342776991 │ 900491082784936059  │           4288 │        257 │     3131 │    3388 │ 8775155077    │      1 │ 2023-04-07 00:44:31 │             0 │  1680802371000 │ 2023-04-11 14:14:34 │
│        1 │ 16342776991 │ 900491691027329754  │           9360 │        553 │     6722 │    7275 │ 10907957881   │      1 │ 2023-04-07 10:58:53 │             0 │  1680840182000 │ 2023-04-11 14:14:34 │
│        2 │ 160276429   │ 8036420106514762228 │              0 │          0 │        0 │       0 │ 11514370      │      0 │ 2023-04-06 10:47:56 │             0 │              0 │ 2023-04-10 18:43:43 │
│        2 │ 1135318573  │ 8056620106757167530 │           5279 │        146 │     1936 │    1979 │ 3100016733871 │      0 │ 2023-04-09 10:32:03 │             0 │              0 │ 2023-04-12 17:12:59 │
│        2 │ 160276429   │ 8036420106464403787 │              0 │          0 │        0 │       0 │ 11514370      │      0 │ 2023-04-06 14:18:03 │             0 │              0 │ 2023-04-10 18:43:43 │
│        2 │ 1135318573  │ 8025640106742737405 │           5880 │        146 │     1837 │    1980 │ 1351412106    │      1 │ 2023-04-09 09:03:36 │             0 │              0 │ 2023-04-12 17:12:59 │
│        2 │ 160276429   │ 8023590106463910331 │           1250 │         53 │      997 │    1050 │ 20279897      │      1 │ 2023-04-06 17:41:02 │             0 │              0 │ 2023-04-10 18:43:43 │
│        2 │ 1135318573  │ 8001040106792642915 │           6514 │        154 │     2038 │    2314 │ 921288130     │      1 │ 2023-04-09 09:21:07 │             0 │              0 │ 2023-04-12 17:12:59 │
└──────────┴─────────────┴─────────────────────┴────────────────┴────────────┴──────────┴─────────┴───────────────┴────────┴─────────────────────┴───────────────┴────────────────┴─────────────────────┘

limit num

limit 10
limit 5,10
limit 10 offset 5
第一句: 返回前十条数据
第二句: 从第5行开始返回条数据
第三句: 与第二句相同

union all

联合左右两边的子查询
Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序; (clickhouse 不支持)
Union All:对两个结果集进行并集操作,包括重复行,不进行排序;

数据类型

整型

整型子类有很多,clickhouse目前有:
UInt8,UInt16,UInt32,UInt64,UInt128,UInt256,
Int8,Int16,Int32,Int64,Int128,Int256

浮点型

Float32,Float64 建议尽可能以整数形式存储数据。

NaN and Inf

Nan 非数字
Inf 正无穷
-Inf 负无穷

小数

Decimal(P,S),Decimal32(S),Decimal64(S),Decimal128(s)
P - 精度。有效范围:[1:38],决定可以有多少个十进制数字(包括分数)。
S - 规模。有效范围:[0:P],决定数字的小数部分中包含的小数位数
对于不同的 P 参数值 Decimal 表示,以下例子都是同义的:P在[1:9] 使用Decimal32(S),P在[10:18]使用Decimal64(S),P在[19:38]使用Decimal128(S)
十进制值范围
Decimal32(S) - ( -1 * 10^(9 - S), 1 * 10^(9 - S) )
Decimal64(S) - ( -1 * 10^(18 - S), 1 * 10^(18 - S) )
Decimal128(S) - ( -1 * 10^(38 - S), 1 * 10^(38 - S) )

String

字符串可以任意长度的。它可以包含任意的字节集,包含空字节。

FixedString

固定长度 N 的字符串(N 必须是严格的正自然数)

UUID

通用唯一标识符(UUID)是一个16字节的数字,用于标识记录

日期

Date

不包含具体时间信息,只精确到天 同样也支持字符串写入:

create table  test_date(
    c1 Date
)ENGINE =Memory;

insert into test_date values
(toDate('2023-04-17')),('2023-04-18');
DateTime DateTime64

DateTime 包含日期+时分秒信息,精确到秒
DateTime 包含日期+时分秒 亚秒信息,精确到亚秒
语法 DateTime64(precision, [timezone])

复合类型

除基础数据类型外,clickhouse还提供了 数组,元组,枚举和嵌套

Array

数组有两种定义形式,常规方式为: array(T) 或者简写为 [T]:
select array(1,2) as a
select [1,2] as a

如果数组中有不同的数据类型如:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
他们之间的数据类型必须是兼容的
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
这种就会报错

Tuple

元组由1~n个元素组成,每个元素之间允许设置不同的数据类型,彼此之间不要求兼容
常规方式为: tuple(T) 或者简写为 (T)

select tuple(1,'hello') as a;
select (1,'hello') as a;
创建表时:
create table  test_tuple
(
    c1 Tuple(UInt8,String)
) ENGINE =Memory;

Enum

枚举 目前支持 Enum8 Enum16 两种类型,主要是取值范围.
枚举固定为(Sring:Int) key/value键值对形式订单数据 所以
Enum8 对应 (String:Int8)
Enum16 对应 (String:Int16)

create table test\_enum
(
c1 Enum8('success'=1,'failure'=0),
c2 Enum16('success'=1,'failure'=0)
) ENGINE =Memory;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
select * from test_enum where c1=‘success’ 也可以查询

可能有人会觉得 完全可以用字符串代替,为啥要专门搞个枚举类型? 这个是出于性能的考虑, 虽然枚举定义中key属于字符串,但是后续枚举的所有操作 (排序 分组 去重 过滤) 都是使用Int 类型的value值

key和value 是不允许重复的,key/value

Nested

嵌套类型,每个字段的嵌套层级只支持一级,即嵌套表内不支持继续使用嵌套类型.
嵌套本质是一种多维数组的结构,嵌套表中的每个字段都是一个数组,
并且行与行之间数组的长度无需对其,但是同一行还是需要对其的.

    create table test_nested
    (

        dep_no   UInt8,
        name String,
        members Nested(
            name String,
            age UInt8
            )
    ) ENGINE = Memory;


    insert into test_nested
    values (1,'技术部',['tom','jerry'],[22,25]),
           (2,'销售部',['lily'],[26]);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

特殊类型

Nullable

nullable 并不是一种数据类型,更像是一种辅助数据类型,需要与基础数据类型搭配使用,如 Nullable(Uint8)
不能和 元组和数组等复杂数据类型使用
其次应该慎用Nullable数据类型,不然会使查询和写入性能变慢.
正常情况下,每个列字段的数据会被写入[Column].bin 文件中. 如果一个列字段 Nullable 修饰后 会额外生成
一个[column].null.bin文件专门保存他的null值,这意味读取和写入数据时,需要一倍额外的文件操作.

Domain

域名类型 分为 IPv4 和 IPv6 两类 本质是对整形和字符串的进一步封装.
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

表引擎

万物皆为表,是clickhouse的一个非常有意思的一个设计思路,正因为clickhouse是一个数据库,所以clickhouse以表作为工具,是他与外部交互的接口层,在数据表背后无论连接的是本地文件还是HDFS,zookeeper等等终端用户面对的只有数据表,并使用sql来查询数据.

外部存储类型

直接从其他存储系统中读取数据如直接从HDFS或者mysql数据库中读取数据,这些表引擎只负责元数据的管理和数据查询,他们并不负责数据的写入. 数据文件由外部系统维护.
大致有: HDFS MYSQL JDBC kafka file(本地文件)

create table test.dkd_food_sku (
     id            UInt64 ,
    spu_id        Int64            comment '商品id',
    name          String    comment '规格名称',
    price         Decimal(20,5)  comment '规格价格',
    food_box_cost Decimal(6, 2)  comment '餐盒成本',
    food_cost     Decimal(6, 2)  comment '菜品成本',
    account_id    UInt64     comment '连锁账号id',
    delete_status UInt8     comment '删除标识',
    create_time   DateTime64(3),
    update_time   DateTime64(3)
)ENGINE =MySQL('192.168.0.17:59306','dkd_chain','dkd_food_sku','root','P12k10109gWoDH');

select * from dkd_food_sku;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

# test.topic
-- 创建topic  kafka-topics.sh --create  --topic test.topic  --bootstrap-server localhost:9092  --partitions 3  --replication-factor 1
create table test.test_topic
(
    taskId   String comment '任务id',
    taskName String comment '任务名称',
    taskTime DateTime64 comment '任务时间'
) ENGINE = Kafka()
      settings kafka_broker_list = 'uck1:9092',
          kafka_topic_list = 'test.topic',kafka_group_name = 'clickhouse',kafka_format = 'JSONEachRow',kafka_skip_broken_messages = 100;


create MATERIALIZED VIEW if not exists  test.test_topic_view
engine =MergeTree order by taskId  as select * from test_topic;

-- 生产数据
kafka-producer.sh --bootstrap-server localhost:9092   --topic test.topic
{"taskId":"1","taskName":"检查订单完整性","taskTime":"2023-04-22 14:15:03"}
{"taskId":"2","taskName":"统计店铺订单","taskTime":"2023-04-22 14:30:03"}
select * from test_topic_view;

自动创建文件的方式

自动创建文件
create table  test_file
(
      taskId   String comment '任务id',
    taskName String comment '任务名称',
    taskTime DateTime64 comment '任务时间'
)ENGINE =File('JSONEachRow');

insert into test_file values ('1','检查店铺订单',now());

-- 改动这个文件 并上传到docker指定位置
查询 发现数据可以被查询到

内存表引擎

面向内存查询,数据在内存中直接被访问,但是并不意味着内存类表引擎不支持物理存储,事实上,除了Memory表引擎之外,其余几款表引擎都会将数据写入磁盘,这是为了防止数据丢失,是一种故障恢复手段.而在数据表被加载时,他们会将数据全部加载到内存中. 对于表引擎来说是一把双刃剑 一方面 良好的查询性能 另一方面 消耗大量内存.

Memory

Merroy表引擎直接将数据保存在内存中,当clickhouse服务重启时,数据全部丢失.
所以在一些场景中,会将Memory作为测试表使用.
当Memory表创建后,写入数据,数据不会写入磁盘,也就不存在数据序列化,反序列化,读取和写入效率高
而且Memory表支持并发查询.

create table test_memory(
id UInt64,
name String,
salary Decimal(20,2)
)ENGINE =Memory;
insert into test_memory values (1,‘tom’,2000),(2,‘jerry’,2500);

重启服务 数据丢失

Set

set表引擎拥有物理存储的,数据首先会被写入内存,然后被同步到磁盘文件中.索引当服务重启是,他的数据不会丢失,当数据表重新加载时,文件数据会再次全量加载至内存中. set表引擎中的元素都是唯一的,不可重复的.
数据写入过程中,重复数据会被自动忽略.
特性: 他虽然支持正常的insert 写入 但是并不能直接使用select对其进行查询
set 表引擎只能间接的作为IN 查询的右侧条件被使用.

set表引擎的存储结构由两部分构成:
[num].bin 数据文件 保存了所有列字段的数据 其中num是一个自增id 每一批次 都会生成一个新的.bin文件
tmp 临时目录 数据文件会首先被写入到这个目录中,当一批数据写入完毕后,数据文件会被移出此目录.

    create table if not exists test_query_memory
    (
        eno        UInt64 comment '员工编号',
        ename      String comment '员工名称',
        age UInt64
    ) ENGINE = Memory;
    insert into test_query_memory values (1,'tom',10),(2,'jerry',12),(3,'lucy',13);
    select * from test_query_memory;

    create table tb_set(
        eno        UInt64 comment '员工编号'
    )engine=Set();

    insert into tb_set values (1),(2),(3),(1),(1);

    select * from test_query_memory where eno  in  tb_set

Join

join表引擎可以说是为了join查询而生的,他等同于将join查询进行了一次简单封装,在join表引擎的底层实现中
他与set表引擎共用了大部分处理逻辑,所以join和set有许多相似之处.
如: join表引擎的存储也由[num].bin数据文件和tmp临时目录两部分构成
数据首先写入内存,然后同步到磁盘临时目录 最终被写入.bin文件中.
不同之处在于: 既能作为join查询的右表,又能被直接查询.
Engine = Join(join_strictness,join_type,join_key1[,join_key2,...])

join_strictness
连接精度:决定了join查询是所使用的策略目前支持: ALL ANY ASOF
join_type
连接类型: 决定了JOIN查询组合左右两个数据集合的策略。 交集 并集 笛卡尔积 等等 INNER OUTTER CROSS
join_key
连接键
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ALL
如果左表一条数据,右表有多条数据与之匹配,则返回所有关联的数据。
ANY
如果左表一条数据,右表有多条数据与之匹配,则返回右表中第一行数据。
ASOF
是一种模糊连接,它允许在连接键之后追加一个模糊连接的匹配条件 asof_column

drop table if exists test.food_sale;
create table test.food_sale
(
    platform  UInt8,
    shop_id   UInt64,
    food_name String,
    food_id   UInt64,
    food_sale UInt64
) ENGINE = MergeTree order by (platform, shop_id, food_id);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (1, 1, '牛奶', 1001, 10);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (1, 1, '锅边', 1006, 5);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (2, 2, '花生', 1003, 15);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (2, 2, '瓜子', 1004, 28);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (2, 3, '鸡腿', 1005, 12);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (1, 4, '汉堡', 1006, 20);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (1, 5, '鸡蛋', 1007, 80);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (1, 6, '二手电脑', 1008, 150);
INSERT INTO test.food_sale (platform, shop_id, food_name, food_id, food_sale)
VALUES (2, 6, '二手手机', 1009, 89);



create table test_query_food_join
(
    area_id   UInt64 comment '区域id',
    area_name String COMMENT '区域名称',
    shop_id   UInt64,
    platform  UInt8
) ENGINE = Join(all, INNER, platform, shop_id);
insert into test_query_food_join
values (1, '闽侯区', 1, 1)
     , (2, '鼓楼区', 6, 1)
     , (3, '鼓楼区', 6, 2)
;


select sum(f1.food_sale) as sale, groupUniqArray(f1.shop_id), f2.area_id,f2.area_name
from food_sale f1
        all inner join test_query_food_join f2 on f1.platform=f2.platform and f1.shop_id = f2.shop_id
group by f2.area_id, f2.area_name order by  sale asc;

Buffer

Buffer表引擎完全使用内存装载数据,不支持文件的持久化存储,所以当服务重启之后,表内的数据会被清空,Buffer表引擎不是为了面向查询场景而设计的,他的作用是充当缓冲区的角色.

假设场景:
我们要向目标MergeTree表A写入一批数据,由于是高并发小批量写入数据,性能不好。此时引入buffer表。数据先写入buffer表中,当满足预设条件时,buffer表会自动将数据刷新到目标表中

存在丢失数据的风险 如果服务器异常重启,缓冲区中的数据就会丢失。正常停机,数据会从buffer刷新到目标表。
乱序 插入 Buffer 表的数据可能会以不同的顺序和不同的块最终出现在从属表中
复制表 如果目标表被复制,则在写入 Buffer 表时,复制表的一些预期特征会丢失。(待验证)
数据延迟
删源表或改源表的时候,建议 Buffer 表删了重建。
参考博客: https://www.cnblogs.com/MrYang-11-GetKnow/p/16111275.html

在写操作期间,数据被插入到num_layers多个随机缓冲区中。
Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)

  • database 目标表的数据库名称
  • table 目标表的名称 buffer表内的数据会自动刷新到目标表中
  • num_layers 可以理解成线程数,Buffer表会按照 num_layer的数量开启线程以并行的方式将数据刷新到目标表中,官方建议为16
    buffer表并不是实时刷新数据的,只有当阈值满足时,才会刷新,阈值有三组条件组成
  • min_time max_time 时间条件的最大最小值 单位为秒 从第一次向表内写入数据的时候开始计算
  • min_rows max_rows 写入行数的最大值和最小值
  • min_bytes max_bytes 写入数据的最大值和最小值
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    满足:
  • 三组条件中所有最小阈值都已满足,则触发刷新动作
  • 三组条件中有一个最大阈值满足(这里是超过最大值),则触发刷新动作
  • 如果写入一批数据的行数大于 max_rows 或者 数据大小大于 max_bytes,则数据直接写入目标表(不使用缓冲,直接批量入到目标表) 存疑?

还有一点需要注意,上述三组条件在每一个 layer 中都是单独的计算的,假设 num_layers 为 16,则 Buffer 表最多开启 16 个线程来响应数据的写入,它们以轮训的方式接收请求,在每个线程内会独立进行上述判断的过程。也就是说,假设一张 Buffer 表的 max_bytes 为 100 MB,num_layers 为 16,那么这张 Buffer 表能够同时处理的最大数据量约为 1600 MB


drop table if exists test_insert_buffer;
drop table if exists test_query_MergeTree;
create table if not exists test_query_MergeTree
(
    eno        UInt64 comment '员工编号',
    ename      String comment '员工名称',
    age UInt64,
    create_time  DateTime64(3)
) ENGINE = MergeTree order by eno;

-- Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)
create table  test_insert_buffer as test_query_MergeTree
ENGINE =Buffer(test,test_query_MergeTree,5,30,60,100,1000,10485760,104857600);
insert into  test_insert_buffer select number,generateUUIDv4(),rand64()%100,now64(3) from numbers(5);

select * from test_query_MergeTree;

日志类型表引擎

如果使用的数据量很小,如100万以下,面对数据查询的场景也比较简单,并且一次写入多次查询,那么使用日志类型引擎就是不错的选择.
不支持索引 分区等高级特性 也不支持并发读写
有: TinyLog Log 等等 略
参考: https://www.cnblogs.com/traditional/p/15218812.html

接口类型

有那么一类表引擎,他们自身并不存储数据,而是像粘合剂一样,整合其他数据表.

在使用这类表引擎时,不用担心底层的复杂性,而是像接口一样,为用户提供统一的访问页面.所以我们将他们归类为接口类表引擎.

主要的代表有两个: Merge Distributed

Merge

数据按年分表存储,如 order_2020 order_2021 order_2022 假如现在需要跨年度查询这些数据,Merge表引擎就是一种合适的选择,Merge表引擎 并不存储任何数据,也不支持数据写入,他只是负责合并多个结果查询的结果集.
被代理的数据表要求处于同一个数据库内,且拥有相同的表结构.
Engine=Merge('database','table_name')

create table t_order_info_2023_04_13
(
    platform       UInt8,
    shop_id        String,
    order_id       String,
    original_price Int64,
    commission     Int64,
    receipts       Int64,
    payment        Int64,
    user_id        String,
    status         UInt8,
    order_time     DateTime,
    cancel_time    UInt64,
    completed_time UInt64,
    update_time    DateTime
)
    engine = ReplacingMergeTree(update_time)
        ORDER BY (platform, shop_id, order_time, order_id);


create table t_order_info_2023_04_14
(
    platform       UInt8,
    shop_id        String,
    order_id       String,
    original_price Int64,
    commission     Int64,
    receipts       Int64,
    payment        Int64,
    user_id        String,
    status         UInt8,
    order_time     DateTime,
    cancel_time    UInt64,
    completed_time UInt64,
    update_time    DateTime
)
    engine = ReplacingMergeTree(update_time)
        ORDER BY (platform, shop_id, order_time, order_id);


create table t_order_info_2023_04_15
(
    platform       UInt8,
    shop_id        String,
    order_id       String,
    original_price Int64,
    commission     Int64,
    receipts       Int64,
    payment        Int64,
    user_id        String,
    status         UInt8,
    order_time     DateTime,
    cancel_time    UInt64,
    completed_time UInt64,
    update_time    DateTime
)
    engine = ReplacingMergeTree(update_time)
        ORDER BY (platform, shop_id, order_time, order_id);


insert into t_order_info_2023_04_13
select *
from t_order_info
where order_time >= '2023-04-13 00:00:00'
  and order_time < '2023-04-14 00:00:00';
insert into t_order_info_2023_04_14
select *
from t_order_info
where order_time >= '2023-04-14 00:00:00'
  and order_time < '2023-04-15 00:00:00';
insert into t_order_info_2023_04_15
select *
from t_order_info
where order_time >= '2023-04-15 00:00:00'
  and order_time < '2023-04-16 00:00:00';


create table t_order_info_all as t_order_info ENGINE =Merge('test','^t_order_info_');
select * from t_order_info_2023_04_13;
select _table,* from t_order_info_all;

as t_order_info 用于复制t_order_info的表结构

Distributed

在数据库业务,面对海量业务数据时,一种主流的做法是实施分片方案.即一张表横向拓展到多个数据实例中,其中每一个数据库实例成为一个分片.在数据写入时,需要根据业务规则均匀的将数据写入各个分片上,而数据查询时,需要在每个分片上分别查询,最总归并结果集. Distibuted表引擎就等同于数据库sharding的中间件,自身不存储任务数据,作为分布式表的一层透明代理.

MergeTree 表引擎(重点)

MergeTree 和 LSM Tree
MergeTree引擎族是ClickHouse强大功能的基础。MergeTree这个名词是在LSM Tree之上做减法而来——去掉了MemTable和Log。也就是说,向MergeTree引擎族的表插入数据时,数据会不经过缓冲而直接写到磁盘.

LSM树 全称log structured merge tree lsm树 并不像B+树,红黑树 是一颗严格的树状结构的数据结构. 他实际是一种存储结构或者叫存储的解决方案.
下图是 leveldb的数据写入过程:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一步 数据先顺序写入预写入日志WALog
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第二步 将数据写入到内存MemTableMemTable 的数据结构一般是跳表或者红黑树

内存内采用这种数据结构一方面支持内存内高速增删改查(时间复杂度O(logM)),另一方面可以保持有序,为写入磁盘中的SSTable打基础

第三步 Memtable存储的元素达到一定数量后,就会把它拷贝一份出来成为Immutable Memtable (不可变的Memtable)并且不能对其修改了,新增的数据都写入新的Memtable

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第四步 内存中的数据不可能无线的扩张下去,需要把内存里面Immutable Memtable 定期dump到到硬盘上的SSTable level 0层中,此步骤也称为Minor Compaction

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图是
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2. clickhouse 的MergeTee 和 leveldb的区别

MergeTree这个名词是在LSM Tree之上做减法而来——去掉了MemTable和Log
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以 levledb 适合于写多读少的场景 低延迟写入
clickhouse 适合写少读多的场景 写入性能并leveldb差但是读性能好

注意: 现在的clickhouse MergeTree 可以通过设置参数 实现 LSM Tree 的 低延迟写的特性
参考博客: https://blog.csdn.net/nazeniwaresakini/article/details/108596752

MergeTree的创建方式

MergeTree 在写入一批数据时,数据总是以数据片段的形式写入磁盘,且数据片段不可修改. 为避免片段过多,clickhouse会通过后台线程,定期合并这些数据片段,属于同分区的片段会被合并成一个新的片段,这种数据片段往复合并的特点也是合并树名称的

     CREATE TABLE [IF NOT EXISTS ] db_name.table_name{
       name1 type default value not null comment '',
       省略
    }ENGINE = MergeTree
    PARTITION BY expr]
    [ORDER BY uid]
    [primary key uid]
    [sample by uid]
    [settings name = value ,省略]
  • partition by[选填]
    分区键
    用于指定表数据以何种方式进行分区.
    分区键既可以是单个列字段,又可以通过元组的方式使用多个列字段,同时也支持使用列表达式.
    如果不声明分区键 clickhouse会生成一个all的分区. 合理使用分区键,可以有效的减少查询时,数据文件的扫描范围.

  • order by [必填]
    排序键
    用于指定在一个数据片段内,数据以何种标准排序. 默认情况下 主键和排序键相同.
    排序键既可以是单个列字段也可以是元组形式的多个列字段

  • primary key [选填]
    声明后 依照主键字段生成一级索引,用于加速表查询. 默认情况下, 主键与排序键相同. 所以通常使用 order by 代为指定主键, 无需刻意通过 primary key 声明. 所以数据和一级索引按照order by的规则升序排列
    与其他数据库不同,MergeTree主键允许存在重复数据(ReplacingMergeTree 可以去重)

  • settings [选填]
    配置

  1. index_granularity
    非常重要的参数 索引粒度 默认为8192 也就是说 MergeTree的索引在默认情况下,每间隔8192行数据才生成一条索引.
    通常情况下并不需要修改此参数.
    settings index_granularity = 8192
  2. index_granularity_bytes
    在19.11版本之前 clickhouse只支持固定大小的索引间隔,由index_granularity指定,默认8192.但是在新版本中增加了自适应索引间隔,即根据一批写入数据量的大小,动态划分间隔大小. 数据的划分体量,就是由index_granularity_bytes指定,默认为10M,设置为0表示不启动自适应功能.

注意: 这里的自适应索引间隔 针对的不是列而是行 他的意思是谁先满足条件就按谁来分割数据,生成索引.

drop table if exists test_primary;
create table if not exists test_primary(
    platform UInt64,
    name     UInt64,
    remark   FixedString(1024)
) engine = MergeTree()
      ORDER BY (platform, name)
      SETTINGS index_granularity = 1000,index_granularity_bytes = 10400,min_rows_for_wide_part = 0,min_bytes_for_wide_part=0;

insert into test_primary
select number,number,concat('tom',toString(number)) from numbers(50);
  1. enable_mixed_granularity_parts
    设置是否开启自适应缩影间隔的功能.默认开启

  2. min_rows_for_wide_part
    数据部分可以宽格式或紧凑格式存储。在Wide格式中,每一列存储在文件系统中的单独文件中,在Compact格式中,所有列存储在一个文件中。紧凑格式可用于提高小型频繁插入的性能。
    数据存储格式由表引擎的min_bytes_for_wide_part和min_rows_for_wide_part设置控制。如果数据部分中的字节数或行数小于相应设置的值,则该部分将以压缩格式存储。否则将以宽格式存储。如果未设置这些设置,数据部分将以宽格式存储。

  3. min_rows_for_compact_part
    数据首先会被写到内存和 WAL中,当触发 Merge 的时候,如果数据大于 x 行,就直接把合并后的分区写到磁盘
    这个特性好像是实验性质的,待求证.

  4. ttl
    time to live 顾名思义就是数据的存活时间,在MergeTree中可以为某个列字段或者某张表设置TTL。当时间到达时,如果是列字段,就删除这列数据,如果是表,则会删除这张表的数据。
    语法:
    INTERVAL [1-9][0-9]{0,n} [SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR]

    drop table if exists test_query_ttl;
    create table if not exists test_query_ttl
    (
    eno UInt64 comment ‘员工编号’,
    ename FixedString(1024) comment ‘员工名称’,
    dno UInt64 comment ‘部门编号’,
    dname String COMMENT ‘部门编号’,
    level UInt64 comment ‘职级0-10 0最高 薪水也最高’,
    salary UInt64 ttl createTime + INTERVAL 5 SECOND ,
    date DateTime64 comment ‘发放薪水日期’,
    createTime DateTime comment ‘数据创建时间’
    ) ENGINE = ReplacingMergeTree
    partition by toYYYYMM(date)
    order by (dno, level, eno) ttl createTime + INTERVAL 60 SECOND settings index_granularity = 100 ,min_rows_for_wide_part = 2000 ;

    INSERT INTO default.test_query_ttl (eno, ename, dno, dname, level, salary, date, createTime)
    VALUES (1, ‘tom’, 1, ‘研发部’, 1, 1000, now(), now());

    optimize table test_query_ttl final ;

MergeTree的存储结构

|-table_name
|   |-partition-1
|   |      |-checksums.txt
|   |      |-columns.txt
|   |      |-count.txt
|   |      |-[column].bin
|   |      |-[column].mrk
|   |      |-[column].mrk2
|   |      |-partition.data        使用分区键才会存在
|   |      |-minmax_[column].idx   使用分区键才会存在
|   |      |-primary.idx
|   |      |-skip_idx_[column].idx  使用二级索引才会存在
|   |      |-skip_idx_[column].mrk  使用二级索引才会存在
|   |-partition-2
|   |...
|   |-partition-n

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如下图所示 我们看到了分区后产生的两个文件

  • partition
    分区目录 余下各类文件(checksums.txt,columns.txt) 等等都是以分区目录的形式被组织存放的. 属于相同分区的数据,最终都会被合并到同一个分区目录下.而不同分区的数据永远不会被合并到一起.

  • primary.idx
    一级索引文件,使用二进制存储.用于存放稀疏索引,一个MergeTree表只能声明一次一级索引. 借助一级索引,在数据查询时,能够排除主键条件范围外的数据文件,有效减少扫描范围.

  • checksums.txt
    校验文件 使用二进制格式存储. 保存各类文件的大小和size的hash值,用于快速校验文件的完整性和正确性

  • columns.txt
    列信息文件 明文存储

  • count.txt
    计数文件 明文存储

  • [Column].bin
    数据文件 使用压缩格式存储 默认LZ4压缩格式 用于存储某一列的数据.

  • [Column].mrk

  • [Column].mrk2
    列字段标记文件,使用二进制格式存储. 标记文件中保存了.bin文件中数据的偏移量信息. 标记文件保存了一级索引和.bin的文件映射关系.
    mrk 是使用固定索引间隔产生的文件
    mrk2 是使用自适应索引产生的文件

  • partition.data 和 minmax_[Column].idx
    如果使用了分区键会额外生成primary.data 和 minmax_[Column].idx 这两个文件
    均使用二进制存储
    partition.data存储了当前分区下分区表达式最终生成的值
    minmax_[Column].idx 存储了当前分区下,分区字段对应原始字段的最大值和最小值
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • skip_idx_[column].idx 和 skip_idx_[column].mrk
    如果声明了二级索引,则会额外生成这两个文件,同样都是二进制存储.二级索引在clickhouse中又被成为跳数索引,目前有 minmax set ngrambf_v1 tokenbf_v1 四种类型
    这些跳数索引的目的和一级索引的相同,都是为了进一步减少扫描的数据范围,以加速整个查询过程.

4. MergeTree 数据写入过程和查询过程

数据分区

数据是以分区目录的形式进行组织,每个分区独立分开存储.

  • 分区规则
类型样例数据分区表达式分区id
无分区键all
整形18,19,20 partition by age 分区1: 18 分区2: 19 分区3:20
日期2023-02-01,2023-02-02partition by eventTime / partition by toYYYYMMDD(eventTime) 分区1: 20230201 分区2: 20230202
其他'篮球','足球' partition by sport 分区1: 1512212132323232323 分区2: 16ww111111111111111 实际上是hash值
- 分区目录的命名规则 `PartitionId`-`minBlockNum`-`maxBlockNum`-`level`
  1. partitionId 无需赘言,就是分区id

  2. MinBlockNum和MaxBlockNum
    顾名思义就是 最小数据块编号和最大数据块编号
    clickhouse这里的数据块不要和后面的数据压缩块混淆了,完全不同的两个概念.
    这里的BlockNum 是一个整形自增编号,如果将这个数字设为变量n,那么n在单张MergeTree数据表内全局累加,n从1开始,每当创建一个分区目录n就会累加1如
    对一个新分区而言: minBlockNum=maxBlockNum
    202301_1_1_0
    202302_2_2_0
    202301_3_3_0
    分区合并时:
    属于同一个分区的多个目录,在合并之后会生成一个全新的目录,目录中的索引和数据文件也会相应的合并
    其中
    MinBlockNum 取同样分区内中最小的blockNum
    MaxBlockNum 取同一分区内中最大的blockNum
    level 取同一分区内最大的level+1

  3. level
    合并层级,可以理解为这个分区被合并的次数,或者这个分区的年龄.
    level与block不同,他不是全局累加的,他的初始值为0,针对分区,相同的分区合则相应分区内计数累加1

数据存储
  • 数据压缩块
    在.bin文件中数据压缩块的形式组织
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个压缩块的体积,按照其压缩前的数据字节大小,被严格控制在64kb-1mb之间
分别有两个参数指定 min_compress_block_size 64kb 和 max_compress_block_size 1mb
而一个压缩块的最终大小则和 一个间隔 index_grannularity 内的数据的实际大小有关

Merge数据的具体写入过程:

  1. 如果单个批次的数据大小<64kb 则继续获取下一批数据直到累积到size>=64kb时 生成下一个压缩数据块
  2. 如果单个批次的数据大小在64kb-1mb之间 则直接生成下一个压缩数据块
  3. 如果单个批次的数据大小>1mb 则截断1mb数据压缩生成下一个压缩块剩余数据按照上述规则生成下一个压缩块

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 数据标记 mrk

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据的写入过程:
一批数据写入->内存中排序->生成分区目录->按照索引间隔的设置分割数据生成一级索引
->按照一级索引生成mrk标记文件->生成.bin数据压缩文件

数据的批量写入

By default, each insert sent to ClickHouse causes ClickHouse to immediately create a part on storage containing the data from the insert together with other metadata that needs to be stored. Therefore sending a smaller amount of inserts that each contain more data, compared to sending a larger amount of inserts that each contain less data, will reduce the number of writes required. Generally, we recommend inserting data in fairly large batches of at least 1,000 rows at a time, and ideally between 10,000 to 100,000 rows. To achieve this, consider implementing a buffer mechanism such as using the Buffer table Engine to enable batch inserts, or use asynchronous inserts (see asynchronous inserts).
默认情况下,每个批次的数据写入,都会导致clickhouse生成一个分区目录.用来存储数据和元数据.
因此 发送次数较小但是每个批次包含较多数据的方式 要比 发送次数较多但是每个批次包含数据较小的 方式 高效.
可以减少大量文件写入. 我们推荐在向clickhouse写入大量数据时,最好每个批次至少有1000行.理想情况下为10,000到100,000行.
无论插入的大小如何,我们建议将插入的数量保持在每秒一个插入左右。提出这个建议的原因是,创建的部分会在后台合并为更大的部分(为了优化读取查询的数据),每秒发送太多的插入查询会导致后台合并无法跟上新部分的数量。

数据查询过程

数据查询的本质可以看做一个不断减小数据查询范围的过程。在最理想的情况下,MergeTree首先可以依次借助 分区索引 一级索引 二级索引 将数据扫描范围压缩至最小,然后借助数据标记,将需要解压和计算的数据范围缩至最小。
例子:

drop table if exists test_query_table1;
create table if not exists test_query_table1
(
    eno    UInt64 comment '员工编号',
    ename  FixedString(1024) comment '员工名称',
    dno    UInt64 comment '部门编号',
    dname  String COMMENT '部门编号',
    level  UInt64 comment '职级0-10 0最低 薪水也最高',
    salary UInt64 COMMENT '薪水',
    date   DateTime64 comment '发放薪水日期'
) ENGINE = ReplacingMergeTree
      partition by toYYYYMM(date)
      order by (dno, level,eno) settings index_granularity = 10 ,min_rows_for_wide_part = 2;

ALTER TABLE test_query_table1
    ADD INDEX skip_index salary TYPE minmax GRANULARITY 2;


insert into test_query_table1
select number,concat('员工',toString(number)),intDiv(number,100)+1,concat('部门',toString(intDiv(number,100)+1)),intDiv(number,10),(intDiv(number,10))*100+500
       ,addMonths(now(),0) from numbers(1000);


select * from test_query_table1 where dno=1 and salary<600 and toYYYYMM(date)=202304 ;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2023.02.06 11:41:35.376698 [ 53 ] {e5e047ee-21e7-4212-95d6-094488e6feee} <Debug> default.test_query_table1 (abaafe9a-2cd6-4ab2-a483-04fb0f8dbe3a) (SelectExecutor): Indexskip_indexhas dropped 1/2 granules. 2023.02.06 11:41:35.376790 [ 53 ] {e5e047ee-21e7-4212-95d6-094488e6feee} <Debug> default.test_query_table1 (abaafe9a-2cd6-4ab2-a483-04fb0f8dbe3a) (SelectExecutor): Selected 1/3 parts by partition key, 1 parts by primary key, 10/80 marks by primary key, 5 marks to read from 1 ranges 2023.02.06 11:41:35.377023 [ 53 ] {e5e047ee-21e7-4212-95d6-094488e6feee} <Debug> MergeTreeInOrderSelectProcessor: Reading 1 ranges in order from part 202302_1_1_0, approx. 500 rows starting from 0 2023.02.06 11:41:35.378659 [ 53 ] {e5e047ee-21e7-4212-95d6-094488e6feee} <Information> executeQuery: Read 500 rows, 531.26 KiB in 0.004447518 sec., 112422 rows/sec., 116.65 MiB/sec.
分区键索引: Selected 1/3 parts by partition key
主键索引: 1 parts by primary key, 10/80 marks by primary key
二级索引: Index skip_index has dropped 1/2 granules => 5 marks to read from 1 ranges
1marks -> 100 rows 5 marks-> 500 rows
executeQuery: Read 500 rows, 531.26 KiB in 0.004447518 sec., 112422 rows/sec., 116.65 MiB/sec.
所以这里扫描的是500rows
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二级索引:
又称跳数索引,他有一个参数 granularity 定义了聚合信息汇总粒度. 换言之, granularity 定义了一行跳数索引能够跳过多少个index_granularity的区间数据.

跳数索引的类型:
set minmax 等等

  1. minmax 记录了一段数据内的最大值和最小值
  2. set 记录了声明字段或者表达式 在这段区间内取值. set(number)

5. MergeTree族其他表引擎

  • 表引擎
表引擎特点
MergeTree有主键,写入数据的主键是可以重复
ReplacingMergeTree为数据去重而设计的,可以在合并分区时删除重复的数据
SummingMergeTree用于仅关心聚合结果,而不关心具体明细的场景。
在合并分区时,按照预先定义的条件,聚合汇总数据,将同一分区下的多行数据根据排序键聚合成一行。 这里排序键和主键可以不同,原因在于 用户可以根据某个主键筛选数据但是根据排序键汇总。即 select 统计结果 from table where 主键 group by 排序键 比如统计一个省的每个高中的本科录取人数 省就是主键 省+高中 就是排序键
AggregatingMergeTree数据立方体,是SummingMergeTree的升级版 提供一种特殊的数据类型,以二进制形式存储中间结果。通常与物化视图搭配使用
CollapsingMergeTree折叠合并树,以增代删
VersionedCollapsingMergeTree折叠合并树,以增代删,增加并发场景下的
  • SummingMergeTree
    用于仅关心聚合结果,而不关心具体明细的场景。
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = SummingMergeTree([columns])
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

SummingMergeTree中的 [columns] 表示需要聚合的列,如果没有指定,表示对主键之外的所有数值型字段进行聚合。
一般情况下我们不需要单独指定主键字段,ORDER BY 字段就是主键字段。但是在 AggregatingMergeTree 和 SummingMergeTree 表中可以指定 PRIMARY KEY 和 ORDER BY 字段不同,因为如果不单独定义主键,而聚合键字段又比较多,就会导致主键过多,降低写效率

drop table if exists test_sum_merge_tree;
create table if not exists test_sum_merge_tree
(
    platform       UInt8,
    shop_id        String,
    original_price Int64,
    receipts       Int64,
    order_num      UInt8,
    order_time     DateTime
) ENGINE = SummingMergeTree((receipts, original_price, order_num))
      ORDER BY (platform, shop_id, order_time) primary key (platform, shop_id);

insert into test_sum_merge_tree
select platform, shop_id, original_price, receipts, 1 order_num, toStartOfDay(order_time)
from t_order_info final
where status = 1;

select sum(receipts),toStartOfDay(order_time) order_day
from t_order_info final
where platform = 1
  and shop_id = '16539918604'
  and order_day = '2023-04-08 00:00:00' group by platform,shop_id ,order_day;

optimize table test_sum_merge_tree final ;
  • AggregatingMergeTree
    数据立方体,是SummingMergeTree的升级版

物化视图+聚合表引擎
利用物化视图进行预聚合的时候,可以联合countState, sumState,uniqState ,maxState ,minState 以及countMerge , sumMerge , uniqMerge , maxMerge,minMerge 进行操作

drop table if exists agg_view;
create MATERIALIZED VIEW agg_view
            ENGINE =AggregatingMergeTree()
                partition by toYYYYMM(order_day)
                order by (platform, shop_id,order_day)
                primary key (platform,shop_id)
                POPULATE
as
select platform,
       shop_id,
       sumState(original_price) sale,
       sumState(receipts) food_receipts,
       sumState(status) order_num,
       avgState(payment) avg_payment,
       toStartOfDay(order_time) order_day
from t_order_info where status=1
group by platform, shop_id, order_day;

select platform,
       shop_id, sumMerge(order_num),avgMerge(avg_payment)
from agg_view
group by platform, shop_id;
  • CollapsingMergeTree
    CollapsingMergeTree就是一种通过以增代删的思路,支持行级数据修改和删除的表引擎。它通过定义一个sign标记位字段,记录数据行的状态。如果sign标记为1,则表示这是一行有效的数据;如果sign标记为-1,则表示这行数据需要被删除。当CollapsingMergeTree分区合并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。

每次需要新增数据时,写入一行sign标记为1的数据;需要删除数据时,则写入一行sign标记为-1的数据

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = CollapsingMergeTree(sign)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
CREATE TABLE emp_collapsingmergetree
(
    emp_id     UInt16 COMMENT '员工id',
    name       String COMMENT '员工姓名',
    work_place String COMMENT '工作地点',
    age        UInt8 COMMENT '员工年龄',
    depart     String COMMENT '部门',
    salary     Decimal32(2) COMMENT '工资',
    sign       Int8
) ENGINE = CollapsingMergeTree(sign) ORDER BY (emp_id, name) PARTITION BY work_place;
==CollapsingMergeTree同样是以ORDER BY排序键作为判断数据唯一性的依据。==
-- 插入新增数据,sign=1表示正常数据
INSERT INTO emp_collapsingmergetree VALUES (1,'tom','上海',25,'技术部',2000,1);

-- 更新上述的数据
-- 首先插入一条与原来相同的数据(ORDER BY字段一致),并将sign置为-1
INSERT INTO emp_collapsingmergetree VALUES (1,'tom','上海',25,'技术部',2000,-1);

-- 再插入更新之后的数据
INSERT INTO emp_collapsingmergetree VALUES (1,'tom','上海',25,'技术部',3000,1);

select emp_id,name, salary from emp_collapsingmergetree final ;
select emp_id,name,sum(salary*sign) salary from emp_collapsingmergetree group by emp_id,name having sum(sign)>0

分数数据折叠不是实时的,需要后台进行Compaction操作,用户也可以使用手动合并命令,但是效率会很低,一般不推荐在生产环境中使用。

值得注意的是:CollapsingMergeTree对于写入数据的顺序有着严格要求,否则导致无法正常折叠。

-- 插入新增数据,sign=1表示正常数据
INSERT INTO emp_collapsingmergetree VALUES (1,'tom','上海',25,'技术部',2000,-1);

-- 更新上述的数据
-- 首先插入一条与原来相同的数据(ORDER BY字段一致),并将sign置为-1
INSERT INTO emp_collapsingmergetree VALUES (1,'tom','上海',25,'技术部',2000,1);


select emp_id,name, salary from emp_collapsingmergetree final ;
select emp_id,name,sum(salary*sign) salary from emp_collapsingmergetree group by emp_id,name having sum(sign)>0;

同时数据重复插入也要考虑

  • VersionedCollapsingMergeTree表引擎
    CollapsingMergeTree表引擎对于数据写入乱序的情况下,不能够实现数据折叠的效果。VersionedCollapsingMergeTree表引擎的作用与CollapsingMergeTree完全相同,它们的不同之处在于,VersionedCollapsingMergeTree对数据的写入顺序没有要求,在同一个分区内,任意顺序的数据都能够完成折叠操作。
    VersionedCollapsingMergeTree使用version列来实现乱序情况下的数据折叠。
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
drop table if exists emp_versioned;
CREATE TABLE emp_versioned
(
    emp_id     UInt16 COMMENT '员工id',
    name       String COMMENT '员工姓名',
    work_place String COMMENT '工作地点',
    age        UInt8 COMMENT '员工年龄',
    depart     String COMMENT '部门',
    salary     Decimal32(2) COMMENT '工资',
    sign       Int8,
    version    Int8
) ENGINE = VersionedCollapsingMergeTree(sign, version) ORDER BY (emp_id, name) PARTITION BY work_place;

-- 先插入需要被删除的数据,即sign=-1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,-1,1);
-- 再插入sign=1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,1,1);
select emp_id,name, salary from emp_versioned final ;
select emp_id,name,sum(salary*sign) salary from emp_versioned group by emp_id,name having sum(sign)>0;

可见上面虽然在插入数据乱序的情况下,依然能够实现折叠的效果。之所以能够达到这种效果,是因为在定义version字段之后,VersionedCollapsingMergeTree会自动将version作为排序条件并增加到ORDER BY的末端,就上述的例子而言,最终的排序字段为ORDER BY emp_id,name,version desc

不能保证数据是否会存在重复插入的情况下:

drop table if exists emp_versioned;
CREATE TABLE emp_versioned
(
    emp_id     UInt16 COMMENT '员工id',
    name       String COMMENT '员工姓名',
    work_place String COMMENT '工作地点',
    age        UInt8 COMMENT '员工年龄',
    depart     String COMMENT '部门',
    salary     Decimal32(2) COMMENT '工资',
    sign       Int8,
    version    Int8
) ENGINE = VersionedCollapsingMergeTree(sign, version) ORDER BY (emp_id, name) PARTITION BY work_place;

-- 先插入需要被删除的数据,即sign=-1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,-1,1);
-- 再插入sign=1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,1,1);
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,1,1);
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',30000,1,2);
select emp_id,name, salary from emp_versioned final ;
select emp_id,name,sum(salary*sign) salary from emp_versioned group by emp_id,name having sum(sign)>0;

表引擎的组合

Replicated + 【Replacing/Summing/Aggregating/Collapsing/VersionedCollapsing/Graphite】 + MergeTree
2*(6+1) =14 种组合方式

6. 删除与更新

alter table table_name update column=value where condition;
alter table table_name delete where condition;
Clickhouse通过alter方式实现更新、删除,它把update、delete操作叫做mutation(突变)。
标准SQL的更新、删除操作是同步的,即客户端要等服务端反回执行结果(通常是int值);而Clickhouse的update、delete是通过异步方式实现的,当执行update语句时,服务端立即反回,但是实际上此时数据还没变,而是排队等着。

  • Mutation具体过程
    首先,使用where条件找到需要修改的分区;
    然后,重建每个分区,用新的分区替换旧的,分区一旦被替换,就不可回退;
    对于每个分区,可以认为是原子性的;但对于整个mutation,如果涉及多个分区,则不是原子性的。
  • 使用建议
    按照官方的说明,update/delete 的使用场景是一次更新大量数据,也就是where条件筛选的结果应该是一大片数据。
    举例:alter table test update status=1 where status=0 and day=‘2020-04-01’,一次更新一天的数据。
    那么,能否一次只更新一条数据呢?例如:alter table test update pv=110 where id=100
    当然也可以,但频繁的这种操作,可能会对服务造成压力。这很容易理解,如上文提到,更新的单位是分区,如果只更新一条数据,那么需要重建一个分区;如果更新100条数据,而这100条可能落在3个分区上,则需重建3个分区;相对来说一次更新一批数据的整体效率远高于一次更新一行。
    对于频繁单条更新的这种场景,建议使用ReplacingMergeTree引擎来变相解决。

副本与分片

副本

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

insert_quorum
默认 insert_quorum=0 如果 insert_quorum clickhouse会进一步监控已完成写操作的副本数量,只有当已写入的副本数>=insert_quorum 才认为这次写入完成。如果由于任何原因,成功写入的副本数量未达到insert_quorum,则认为写入失败,ClickHouse将从所有已写入数据的副本中删除插入的块

查询数据的时候 不会访问zookeeper
利用副本自动同步数据的特性可以分担负载,向一个节点写入数据,有两个节点可以查询数据。

分片

虽然副本可以降低数据丢失的风险,并提升查询性能(分摊查询,读写分离)。
但是仍有一个问题没有解决,数据表的容量问题。即每个副本表都保存了全量数据。当数据量十分庞大时,靠副本不能解决单表的性能瓶颈。需要进一步将数据水平切分,即分片

搭建集群:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建分布式表:

CREATE TABLE default.city_zone_local on cluster ck_cluster
(
    `city_id`   UInt64,
    `city_name` String COMMENT '城市名称',
    `zone_type` UInt8 comment '区域类型(南方 0 北方 1)',
    `zone_name` String comment '区域类型(南方 北方)',
    update_time DateTime64 comment '更新时间'
) ENGINE = ReplacingMergeTree(update_time) ORDER BY city_id;


CREATE TABLE default.city_zone_all on cluster ck_cluster AS default.city_zone_local
ENGINE = Distributed(ck_cluster, default, city_zone_local,zone_type);


insert into default.city_zone_all
values (1, '武汉市', 0, '南方', now64());
insert into default.city_zone_all
values (2, '北京市', 1, '北方', now64());
insert into default.city_zone_all
values (3, '南京市', 0, '南方', now64());
insert into default.city_zone_all
values (4, '福州市', 0, '南方', now64());

insert into default.city_zone_all
values (5, '南昌市', 0, '南方', now64());
insert into default.city_zone_all
values (6, '成都市', 0, '南方', now64());
insert into default.city_zone_all
values (7, '深圳市', 0, '南方', now64());
insert into default.city_zone_all
values (8, '西安市', 1, '北方', now64());

数据经过路由
zone_type=0的被转发到分片1
zone_type=1的数据被转发到了分片2
由于没有使用副本表引擎,分片1的数据并没有被节点2分享.

创建分布式表+副本表:

CREATE TABLE if not exists default.city_local_replica on cluster ck_cluster
(
    city_id     UInt64,
    city_name   String COMMENT '城市名称',
    zone_type   UInt8 comment '区域类型(南方 0 中部 1, 北方2)',
    zone_name   String comment '区域类型(南方 中部 北方)',
    update_time DateTime64 comment '更新时间'
) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/default/city_local_replica', '{replica}',
           update_time) ORDER BY city_id;

CREATE TABLE default.city_all_replica on cluster ck_cluster
AS default.city_local_replica
ENGINE = Distributed(ck_cluster, default, city_local_replica,zone_type);

insert into default.city_all_replica
values (1, '武汉市', 0, '南方', now64());

insert into default.city_all_replica
values (3, '南京市', 0, '南方', now64());
insert into default.city_all_replica
values (4, '福州市', 0, '南方', now64());

insert into default.city_all_replica
values (8, '西安市', 1, '北方', now64());
insert into default.city_all_replica
values (2, '北京市', 1, '北方', now64());

此时 数据被分成两部分 一部分存储到分片1,一部分存储到分片2, 分片1的数据有两个副本(节点1主副本 节点2 普通副本)
虽然上面是直接写入分布式表,但是现实中是不推荐大家这样做的,
一般 本地表写入+分布式表查询
查询会达到其中一个节点上,这个节点在向任务下发到各个分片,分片查询到结果,汇总到这个节点,这个节点在将数据返回给客户端.所以会存在节点热点问题,选择好的查询节点也很重要.

参考博客:https://blog.csdn.net/BIackMamba/article/details/119424839?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-119424839-blog-102466116.235%5Ev32%5Epc_relevant_default_base3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-119424839-blog-102466116.235%5Ev32%5Epc_relevant_default_base3&utm_relevant_index=5

集群方案: 参考博客 https://blog.csdn.net/m0_67550015/article/details/126179889

  • 分布式表查询放大
CREATE TABLE if not exists default.city_local_replica_global on cluster ck_cluster
(
    city_id     UInt64,
    city_name   String COMMENT '城市名称',
    zone_type   UInt8 comment '区域类型(南方 0 中部 1, 北方2)',
    zone_name   String comment '区域类型(南方 中部 北方)',
    update_time DateTime64 comment '更新时间'
) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/default/city_local_replica_global', '{replica}',
           update_time) ORDER BY city_id;

CREATE TABLE default.city_all_replica_global on cluster ck_cluster
AS default.city_local_replica_global
ENGINE = Distributed(ck_cluster, default, city_local_replica_global,zone_type);

insert into default.city_all_replica_global
values (1, '武汉市', 0, '南方', now64());

insert into default.city_all_replica_global
values (3, '南京市', 0, '南方', now64());
insert into default.city_all_replica_global
values (4, '福州市', 0, '南方', now64());

insert into default.city_all_replica_global
values (1, '西安市', 1, '北方', now64());
insert into default.city_all_replica_global
values (3, '北京市', 1, '北方', now64());

select * from city_all_replica_global final ;

查询 和 西安市 南京市 相同城市id的 的城市信息
– 查看日志发现 出现了 n*n 次查询 (n为表的分片数量)
set distributed_product_mode = ‘allow’;
select *
from default.city_all_replica_global
where city_id in (
select city_id from default.city_all_replica_global where city_name in (‘南京市’, ‘西安市’)
)

– 经过日志可得到是子查询的发大导致的 ,那么将子查询改一下
select *
from default.city_all_replica_global
where city_id in (
select city_id from default.city_local_replica_global where city_name in (‘南京市’, ‘西安市’)
)

发现结果不一致

– 引入golbal in/join 语法

select *
from default.city_all_replica_global
where city_id global in (
select city_id from default.city_all_replica_global where city_name in (‘南京市’, ‘西安市’)
)

根据日志可以印证我们的猜想: 查询 n+n次

视图和物化视图

clickhouse 拥有普通和物化两种视图。普通视图只是一层简单的查询代理。

CREATE VIEW [IF NOT EXISTS] [db_name.] view_name as select …

普通视图不会存储任何数据,他只是一层单纯的SELECT 查询映射,起着简化查询,明晰语义的作用,对查询性能不会有任何增强。

create view if not exists default.simple_view_query as
select sum(food_sale.food_sale), groupUniqArray(food_sale.shop_id), test_query_food_join.area_id
from food_sale
         inner join test_query_food_join  on food_sale.shop_id = test_query_food_join.shop_id
group by test_query_food_join.area_id

物化视图

CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER] [TO[db.]name]
[ENGINE = engine] [POPULATE] AS SELECT …

物化视图只同步插入的数据,对源表数据的任何更改(如更新、删除、删除分区等)都不会更改物化视图数据

删除视图可使用 DROP VIEW,虽然 DROP TABLE 也可以删除视图,但是不建议使用

如果指定POPULATE,则在创建视图时将现有表数据插入到视图中,就像创建CREATE table。。。选择。否则,查询仅包含创建视图后插入到表中的数据。我们不建议使用POPULATE,因为在视图创建期间插入到表中的数据不会插入其中。

例子:

create MATERIALIZED VIEW if not exists  default.view_query engine =MergeTree order by shop_id  populate as select * from  food_sale;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

create MATERIALIZED VIEW if not exists  default.view_query engine =MergeTree order by shop_id   as select * from  food_sale;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
新写入的数据会刷新到物化视图中
INSERT INTO default.food_sale (shop_id, food_name, food_id, food_sale)
VALUES (1, ‘牛奶’, 1001, 10);
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
想要以前的数据也刷新到物化视图中:需要执行:
insert into default.view_query select * from food_sale;

查询优化

建表优化

  • 日期使用datetime 而不使用 int/string
    很多日期函数都接受datetime类型的参数
    避免不必要的转化

  • 避免使用空值存储(Avoid Nullable Columns)
    Nullable(T)
    可空列(例如Nullable(UInt8))创建一个UInt8类型的单独列。每当用户使用可空列时,都必须处理这个附加列。这将导致使用额外的存储空间,并且几乎总是会对性能产生负面影响。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 多存储数据 2.无法被索引
  • 分区和索引
    缩小查询范围
  • 表参数
    1. 自适应索引
      ClickHouse 19.11.8版本,社区就引入了自适应
      index_granularity = 8192 索引粒度
      index_granularity_bytes = 10m
      SETTINGS index_granularity_bytes = 0取消自适应索引粒度

    2. 数据过期时间(ttl)

    3. 最大并发处理的请求数(默认100)
      因为clickhouse高效利用硬件资源,多线程执行任务,导致cpu被有效利用,
      可能 io还没有达到瓶颈 而cpu就达到瓶颈了,所以并发请求不能太高.

  • ttl
    建表指定TTL
    如果表不是必须保存全量历史数据,建议指定TTL,以免去手动清除过期数据的麻烦。

写入和删除优化

  • 尽量不要执行单条或者小批量插入操作 这样会造成小分区文件给后台Merge造成较大压力
  • 不要一次写入太多分区或者写入数据太快 这样导致merge速度跟不上而报错

有些图片看不了 贴上我的笔记分享地址:https://note.youdao.com/s/M4CZhSQP

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值