本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
虽然ClickHouse是列式存储
但是从平时的操作来说,和关系型数据库也没有太大差别(也是以表展示)
所以我之前一直以为ClickHouse的字段类型也就那么一些基础类型
后来才发现,ClickHouse支持的类型比我想象的要丰富的多
数组
一般情况下当我们有多对多的业务场景的时候
都会键一张中间表用于关联数据
比如说一篇文章有多个标签,一个标签可以对应多篇文章
我们建的中间表就像这样
| article_id | tag_id | 
|---|---|
| article_1 | tag_1 | 
| article_1 | tag_2 | 
| article_2 | tag_2 | 
| ... | ... | 
当需要根据标签来查询文章的时候
一般的逻辑就是先查出中间表中指定标签对应的文章ID,再根据文章ID查询文章数据(分两次查询或进行多表关联)
而如果字段支持数组类型就不需要中间表了
直接把文章的标签属性设置成数组就行了
| article_id | tag_ids | 
|---|---|
| article_1 | [tag_1,tag_2] | 
| article_2 | [tag_2] | 
| article_3 | [tag_3,tag_4] | 
| ... | ... | 
表定义
用ClickHouse可以这样指定列类型
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id  String,
    tag_ids     Array(String),
    create_time DateTime
) ENGINE = MergeTree
      ORDER BY create_time;Array表示数组类型,括号里面的String表示数组的元素类型
插入
            
            
              sql
              
              
            
          
          insert into t_article(article_id, tag_ids, create_time)
values ('article_1', ['tag_1','tag_2'], now()),
       ('article_2', ['tag_2'], now()),
       ('article_3', array('tag_3','tag_4'), now());可以使用[]或是array()函数来表示数组
查询
当我们需要查询有tag_1的文章时
            
            
              sql
              
              
            
          
          select * from t_article where has(tag_ids, 'tag_1');当我们需要查询有tag_1或tag_2的文章时
            
            
              sql
              
              
            
          
          select * from t_article where hasAny(tag_ids, ['tag_1','tag_2']);当我们需要查询有tag_1和tag_2的文章时
            
            
              sql
              
              
            
          
          select * from t_article where hasAll(tag_ids, ['tag_1','tag_2']);元组
元组有点类似没有字段名称的对象
我们可以指定每一个字段的类型
根据字段的顺序来确定数据的含义(如第一个值代表A,第二个值代表B)
文章有作者信息,按照顺序分别定义
- 
第一个字段是作者ID 
- 
第二个字段是作者名称 
- 
第三个字段是作者等级 
表定义
所以可以这样创建表
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id  String,
    /*作者ID,作者名称,作者等级*/
    author Tuple(String, String, UInt8),
    create_time DateTime
) ENGINE = MergeTree
      ORDER BY create_time;Tuple表示元组类型,括号里面可以定义1-n个数据类型
插入
            
            
              sql
              
              
            
          
          insert into t_article(article_id, author, create_time)
values ('article_1', ('author_1', 'author_name1', 1), now()),
       ('article_1', tuple('author_2', 'author_name2', 2), now());使用()或是tuple()函数来表示元组
查询
当我们需要使用元组中的某一个属性来作为条件的时候
            
            
              sql
              
              
            
          
          select * from t_article where author.1 = 'author_1' 
and tupleElement(author, 2) like 'author%';直接使用字段名称.字段下标(从1开始)或者使用tupleElement函数来获得某个属性
嵌套
嵌套有点类似于数组和元组的简单结合
表定义
还是以文章作者为例
使用嵌套类型的建表语句是这样的
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id  String,
    /*作者ID,作者名称,作者等级*/
    author      Nested(author_id String,
                    author_name String,
                    author_level UInt8),
    create_time DateTime
) ENGINE = MergeTree
      ORDER BY create_time;从表定义来看
相比于元组类型,嵌套类型可以指定每个字段的名称了
Nested表示嵌套类型,括号里面可以定义1-n个字段,很接近我们平时定义类的方式
插入
根据前两个类型的经验
我非常自信的写下了插入数据的脚本
            
            
              sql
              
              
            
          
          insert into t_article(article_id, author, create_time)
values ('article_1', ('author_1', 'author_name1', 1), now()),
       ('article_1', nested('author_2', 'author_name2', 2), now());因为Array类型有array()函数,Tuple类型有tuple()函数
所以Nested类型有nested()函数,没毛病
怎么样,我的举一反三还不错吧
很可惜,聪明过头了(bushi
当执行插入语句的时候,ClickHouse报了这样一个错
            
            
              arduino
              
              
            
          
          Code: 16. DB::Exception: No such column author in table default.t_article没有author字段?这报错怎么牛头不对马嘴的,我们明明定义了一个嵌套类型的author字段
查询一下看看怎么个事
            
            
              sql
              
              
            
          
          select * from t_article;你猜怎么着?
| article_id | author.author_id | author.author_name | author.author_level | create_time | 
|---|
原来ClickHouse直接把字段拆开来了,怪不得没有author这个字段
修改了一下插入语句
            
            
              sql
              
              
            
          
          insert into t_article(article_id, author.author_id, author.author_name, author.author_level, create_time)
values ('article_1', 'author_1', 'author_name1', 1, now()),
       ('article_1', 'author_2', 'author_name2', 2, now());这样总没问题了吧
这回倒是没有字段不存在的报错了,但是
            
            
              css
              
              
            
          
          Code: 53. DB::Exception: Type mismatch in IN or VALUES section. Expected: Array(UInt8). Got: UInt64期望是数组?我们也没有数组类型的字段啊
既然报错说要数组,那就用数组试试?
            
            
              sql
              
              
            
          
          insert into t_article(article_id, author.author_id, author.author_name, author.author_level, create_time)
values ('article_1', ['author_1'], ['author_name1'], [1], now()),
       ('article_1', ['author_2'], ['author_name2'], [2], now());成功了,所以嵌套类型其实默认就是带数组的
嵌套类型更准确的表达其实是(伪代码)
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id  String,
    /*作者ID,作者名称,作者等级*/
    author      Array(对象(author_id String,
                    author_name String,
                    author_level UInt8)),
    create_time DateTime
) ENGINE = MergeTree
      ORDER BY create_time;当然,实际结构更像是这样
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id          String,
    /*作者ID,作者名称,作者等级*/
    author.author_id    Array(String),
    author.author_name  Array(String),
    author.author_level Array(UInt8),
    create_time         DateTime
) ENGINE = MergeTree
      ORDER BY create_time;不过嵌套属性对于数组的长度是会有校验的
如果我们的author.author_id,author.author_name,author.author_level的数组长度不一样,则会报错
            
            
              sql
              
              
            
          
          insert into t_article(article_id, author.author_id, author.author_name, author.author_level, create_time)
values ('article_3', ['author_3'], ['author_name3'], [1,2], now());在插入数据的时候,作者ID和名称都写1个值,等级写2个值
            
            
              c
              
              
            
          
          Code: 190. DB::Exception: Elements 'author.author_id' and 'author.author_level' of Nested data structure 'author' (Array columns) have different array sizes.ClickHouse会告诉我们数组的长度不同
另外嵌套类型只支持一级,没办法在嵌套类型中再定义嵌套类型
关于为什么只支持一级嵌套,假设支持二级嵌套(伪代码)
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id          String,
    /*作者ID,作者名称,作者等级*/
    author.author_id    Array(String),
    author.author_name  Array(String),
    author.author_level Array(Nested(level_id UInt8, level_name String)),
    create_time         DateTime
) ENGINE = MergeTree
      ORDER BY create_time;而Nested本身就是一个数组,所以author.author_level就变成了
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id          String,
    /*作者ID,作者名称,作者等级*/
    author.author_id    Array(String),
    author.author_name  Array(String),
    author.author_level Array(Array(对象(level_id UInt8, level_name String))),
    create_time         DateTime
) ENGINE = MergeTree
      ORDER BY create_time;这还能再展开吗。。。展不开,根本展不开
查询
既然嵌套本身是数组
那么用标签举例更合适一点
            
            
              sql
              
              
            
          
          CREATE TABLE t_article
(
    article_id  String,
    tags        Nested(tag_id String,
                    tag_name String),
    create_time DateTime
) ENGINE = MergeTree
      ORDER BY create_time;
            
            
              sql
              
              
            
          
          /* article_1 有 tag_1(tag_name1) 和 tag_2(tag_name2) 两个标签 */
/* article_2 有 tag_2(tag_name2) 一个标签 */
insert into t_article(article_id, tags.tag_id, tags.tag_name, create_time)
values ('article_1', ['tag_1','tag_2'], ['tag_name1','tag_name2'], now()),
       ('article_2', ['tag_2'], ['tag_name2'], now());查询标签名称模糊匹配name1
            
            
              sql
              
              
            
          
          select *
from t_article
where arrayExists(x -> x like '%name1%', tags.tag_name);arrayExists()函数会遍历数组,x就是每一个数组的元素
不得不说,这种lambda加sql的写法还挺有意思
多级嵌套
虽然ClickHouse自带的嵌套类型只能支持一级
但是如果Array能和Tuple组合不就类似于多级嵌套的对象了吗
            
            
              sql
              
              
            
          
          CREATE TABLE t_test
(
    test Array(Array(Tuple(Array(String), Tuple(String))))
) ENGINE = MergeTree
      ORDER BY test;
            
            
              sql
              
              
            
          
          insert into t_test(test)
values ([[(['test'], ('test'))]]);test字段包含了Array(Array),Array(Tuple),Tuple(Array),Tuple(Tuple)这四种组合
执行没有任何问题,也能查出来插入的数据
所以理论上我们是可以用Array和Tuple的组合来存储相对复杂的对象(除了没办法用字段名表示,只能用字段下标来确定数据)
总结
从支持的字段类型来看
ClickHouse像是介于关系型数据库和非关系型数据库之间的感觉
能够插入一些简单的对象
这样就有可能优化一些连表查询变为单表查询,简化逻辑提升性能