• clickhouse笔记


    clickhouse笔记

    1 clickhouse特点

    1.1 完备的DBMS功能

    1. DDL(数据定义语言):可以动态地创建、修改或删除数据库、表和视图,而无须重启服务。

    2. DML(数据操作语言):可以动态查询、插入、修改或删除数据。

    3. 权限控制:可以按照用户粒度设置数据库或者表的操作权限,保障数据的安全性。

    4. 分布式管理:提供集群模式,能够自动管理多个数据库节点。

    5. 数据备份与恢复:提供了数据备份导出与导入恢复机制,满足生产环境的要求。

    1.2 列式存储与数据压缩

    1. 按列存储与按行存储相比,前者可以有效减少查询时所需扫描的数据量

    2. 能更好的压缩数据,压缩前:abcdefghi_bcdefghi 压缩后:abcdefghi_(9,8)。数据库的瓶颈通常在IO,很高的压缩比,可以大大减轻数据读取的压力,提高响应速度

    3. 除去字符串类型,其他类型的字段通常是固定长度的,而且在磁盘和内存的字节顺序通常是一致的,可以直接映射,省去了解析的过程

    4. 列式存储可以向量化的处理一个字段。可以将一个列的一整块连续数据读入CPU cache,效率非常高

      'B|C|D', 'B|D', 'A|C', 'B|D' ,'B|C', 'B|C|D'
      A,B,C,D
      [0,1,1,1]
      [0,1,0,1]
      [1,0,1,0]
      [0,1,0,1]
      [0,1,1,0]
      [0,1,1,1]
      

    1.3 关系模型与SQL查询

    ClickHouse使用关系模型描述数据并提供了传统数据库的概念(数据库、表、视图和函数等)。与此同时,ClickHouse完全使用SQL作为查询语言(支持GROUP BY、ORDER BY、JOIN、IN等大部分标准SQL)

    1.4 多线程与分布式

    ClickHouse在数据存取方面,既支持分区(纵向扩展,利用多线程原理),也支持分片(横向扩展,利用分布式原理),可以说是将多线程和分布式的技术应用到了极致。

    1.5 多主架构

    ClickHouse则采用多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果。它天然规避了单点故障的问题,非常适合用于多数据中心、异地多活的场景

    1.6 数据分片与分布式查询

    数据分片是将数据进行横向切分,这是一种在面对海量数据的场景下,解决存储和查询瓶颈的有效手段,是一种分治思想的体现

    ClickHouse提供了本地表(Local Table)与分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。

    第2章 安装与部署

    2.1 ClickHouse的安装过程

    1.centos7 取消打开文件限制
    ulimit -n # 查看打开文件下载
    ulimit -a # 查看详情
    vim /etc/security/limits.conf
    * soft nofile 1024000
    * hard nofile 1024000
    hive - nofile 1024000
    hive - nproc 1024000
    vim /etc/security/limits.d/20-nproc.conf
    #加大普通用户限制 也可以改为unlimited
    * soft nproc 409600
    root soft nproc unlimited
     
    shutdown -r now 重启服务
    
    2.centos7取消selinux
    getenforce # 查看
    /usr/sbin/sestatus -v # 查看
     
    vim /etc/selinux/config # 关闭selinux服务
    将SELINUX=enforcing改为SELINUX=disabled
     
    shutdown -r now 重启服务
    
    3.关闭防火墙
    firewall-cmd --state # 查看防火墙状态
    systemctl stop firewalld # 禁用防火墙
    systemctl start firewalld # 启动防火墙
    
    4.安装依赖
    yum install -y curl pygpgme yum-utils coreutils epel-release libtool *unixODBC*
    
    5.下载安装包

    http://repo.yandex.ru/clickhouse/rpm/stable/x86_64/

    clickhouse-client-20.6.6.7-2.noarch.rpm                                     11-Sep-2020 09:55    102K
    clickhouse-client-20.6.7.4-2.noarch.rpm                                     18-Sep-2020 13:16    102K
    clickhouse-client-20.7.2.30-2.noarch.rpm                                    31-Aug-2020 16:20    102K
    clickhouse-client-20.7.3.7-2.noarch.rpm                                     18-Sep-2020 12:43    102K
    clickhouse-client-20.8.2.3-2.noarch.rpm                                     08-Sep-2020 12:46    115K
    clickhouse-client-20.8.3.18-2.noarch.rpm                                    18-Sep-2020 12:23    115K
    clickhouse-common-static-20.6.6.7-2.x86_64.rpm                              11-Sep-2020 09:55    120M
    clickhouse-common-static-20.6.7.4-2.x86_64.rpm                              18-Sep-2020 13:16    120M
    clickhouse-common-static-20.7.2.30-2.x86_64.rpm                             31-Aug-2020 16:20    120M
    clickhouse-common-static-20.7.3.7-2.x86_64.rpm                              18-Sep-2020 12:44    120M
    clickhouse-common-static-20.8.2.3-2.x86_64.rpm                              08-Sep-2020 12:46    137M
    clickhouse-common-static-20.8.3.18-2.x86_64.rpm                             18-Sep-2020 12:23    137M
    clickhouse-server-20.6.6.7-2.noarch.rpm                                     11-Sep-2020 09:56    125K
    clickhouse-server-20.6.7.4-2.noarch.rpm                                     18-Sep-2020 13:17    125K
    clickhouse-server-20.7.2.30-2.noarch.rpm                                    31-Aug-2020 16:21    126K
    clickhouse-server-20.7.3.7-2.noarch.rpm                                     18-Sep-2020 12:44    126K
    clickhouse-server-20.8.2.3-2.noarch.rpm                                     08-Sep-2020 12:46    139K
    clickhouse-server-20.8.3.18-2.noarch.rpm                                    18-Sep-2020 12:24    139K
    
    6.创建安装目录
    mkdir -p /usr/local/clickhouse
    
    [root@localhost clickhouse]# ll
    总用量 141008
    -rw-r--r--. 1 root root    117705 9月  22 14:02 clickhouse-client-20.8.3.18-2.noarch.rpm
    -rw-r--r--. 1 root root 144128776 9月  22 14:04 clickhouse-common-static-20.8.3.18-2.x86_64.rpm
    -rw-r--r--. 1 root root    142403 9月  22 14:01 clickhouse-server-20.8.3.18-2.noarch.rpm
    
    rpm -ivh *.rpm   # 安装clickhouse
    
    rpm -Uvh *.rpm   # 在线升级clickhouse版本  在升级的过程中 原来的配置将会保存下来
    
    rpm -e clickhouse-client-20.3.8.53-2.noarch   # 卸载
    rpm -e clickhouse-common-static-20.3.8.53-2.x86_64   # 卸载
    rpm -e clickhouse-server-20.3.8.53-2.noarch   # 卸载
    
    7.配置文件
    [root@localhost clickhouse-server]# pwd
    /etc/clickhouse-server
    
    [root@localhost clickhouse-server]# ll
    总用量 76
    drwxr-xr-x. 2 root root     6 8月  26 09:52 config.d
    -rw-r--r--. 1 root root 22601 8月  26 09:54 config.xml     # clickhouse配置文件
    -rw-r--r--. 1 root root 33738 9月  18 06:29 config.xml.rpmnew
    lrwxrwxrwx. 1 root root    41 8月  26 09:52 preprocessed -> /var/lib/clickhouse//preprocessed_configs
    drwxr-xr-x. 2 root root     6 8月  26 09:52 users.d
    -rw-r--r--. 1 root root  5522 8月  26 10:06 users.xml    # 用户的配置文件
    -rw-r--r--. 1 root root  5587 9月  18 06:29 users.xml.rpmnew
    
    8.config.xml配置文件详解
    <?xml version="1.0"?>
    <yandex>
        <logger>
            <level>trace</level>
            <log>/var/log/clickhouse-server/clickhouse-server.log</log>    <!-- 日志存放目录 -->
            <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>  <!-- 错误日志存放目录 -->
            <size>1000M</size>   <!-- 文件的大小。文件达到大小后,ClickHouse将对其进行存档并重命名,并在其位置创建一个新的日志文件 -->
            <count>10</count>  <!--  ClickHouse存储的已归档日志文件的数量 -->
        </logger>
        <http_port>8123</http_port> <!-- 通过HTTP连接到服务器的端口 -->
        <tcp_port>9000</tcp_port>  <!--通过TCP协议与客户端进行通信的端口,即ClickHouse端口。-->
        <mysql_port>9004</mysql_port>  <!--通过MySQL协议与客户端通信的端口。-->
        <https_port>8443</https_port>   <!-- 通过HTTP连接到服务器的端口 -->
        <tcp_port_secure>9440</tcp_port_secure>  <!--通过TCP协议与客户端进行通信的端口,即ClickHouse端口。 与OpenSSL设置一起使用。-->
        <openSSL>
            <server>
                <certificateFile>/etc/clickhouse-server/server.crt</certificateFile>  <!--PEM格式的客户端/服务器证书文件的路径。如果privateKeyFile包含证书,则可以忽略它-->
                <privateKeyFile>/etc/clickhouse-server/server.key</privateKeyFile>  <!--具有PEM证书的秘密密钥的文件的路径。该文件可能同时包含密钥和证书-->
                <dhParamsFile>/etc/clickhouse-server/dhparam.pem</dhParamsFile> <!--dh文件路径-->
                <verificationMode>none</verificationMode> <!--检查节点证书的方法-->
                <loadDefaultCAFile>true</loadDefaultCAFile> <!--指示将使用OpenSSL的内置CA证书。可接受的值:true,false-->
                <cacheSessions>true</cacheSessions>  <!--启用或禁用缓存会话。必须与sessionIdContext结合使用。可接受的值:true,false。-->
                <disableProtocols>sslv2,sslv3</disableProtocols>  <!--不允许使用的协议-->
                <preferServerCiphers>true</preferServerCiphers>  <!--首选服务器密码-->
            </server>
    
            <client>
                <loadDefaultCAFile>true</loadDefaultCAFile> <!--指示将使用OpenSSL的内置CA证书。可接受的值:true,false-->
                <cacheSessions>true</cacheSessions> <!--启用或禁用缓存会话。必须与sessionIdContext结合使用。可接受的值:true,false。-->
                <disableProtocols>sslv2,sslv3</disableProtocols>  <!--不允许使用的协议-->
                <preferServerCiphers>true</preferServerCiphers> <!--首选服务器密码-->
                <invalidCertificateHandler>  <!--用于验证无效证书的类-->
                    <name>RejectCertificateHandler</name>
                </invalidCertificateHandler>
            </client>
        </openSSL>
    
        <interserver_http_port>9009</interserver_http_port>  <!--于在ClickHouse服务器之间交换数据的端口。-->
    
        <listen_host>0.0.0.0</listen_host>  <!--限制来源主机的请求  允许所有-->
        <listen_host>127.0.0.1</listen_host>  <!--限制来源主机的请求 只允许本机访问-->
    
        <max_connections>4096</max_connections>  <!--最大连接数-->
        <keep_alive_timeout>3</keep_alive_timeout>  <!--ClickHouse在关闭连接之前等待传入请求的秒数。 默认为3秒。-->
    
        <max_concurrent_queries>100</max_concurrent_queries>  <!--同时处理的最大请求数。-->
    
        <max_open_files>262144</max_open_files> <!--打开最大的文件数,默认最大值-->
    
        <uncompressed_cache_size>8589934592</uncompressed_cache_size>  <!--表引擎从MergeTree使用的未压缩数据的缓存大小(以字节为单位,8G)-->
        
        <mark_cache_size>5368709120</mark_cache_size>  <!--标记缓存的大小,用于MergeTree系列的表中(以字节为单位,5G)-->
    
        <path>/var/lib/clickhouse/</path>  <!--数据的目录路径-->
        
        <storage_configuration>   <!--多个存储路径-->      
            <disks>
                <disk_name_a> <!--自定义磁盘名称 -->
                    <path>/var/lib/clickhouse1/</path>  <!--必填项,用于指定磁盘路径 -->
                    <keep_free_space_bytes>1073741824</keep_free_space_bytes>  <!--选填项,以字节为单位,用于定义磁盘的预留空间。-->
                </disk_name_a>
    
                <disk_name_b>
                    <path>/var/lib/clickhouse2/</path>  <!--必填项,用于指定磁盘路径 -->
                    <keep_free_space_bytes>1073741824</keep_free_space_bytes>  <!--选填项,以字节为单位,用于定义磁盘的预留空间。-->
                </disk_name_b>
            </disks>
    	</storage_configuration>  
    
        <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>  <!--用于处理大型查询的临时数据的路径。-->
    
       	<tmp_policy>tmp</tmp_policy>  <!--用于存储临时文件。如果未设置,则使用tmp_path,否则将忽略它。-->
    
        <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>  <!--存储用户文件的目录-->
    
        <users_config>users.xml</users_config>  <!--用户配置文件,可以配置用户访问、profiles、quota、查询等级等-->
    
        <default_profile>default</default_profile> <!--默认设置配置文件-->
    
    
        <default_database>default</default_database>  <!--默认数据库-->
    
        <timezone>Europe/Moscow</timezone>  <!--服务器的时区,定为UTC时区或地理位置  莫斯科时间-->
    
        <!-- <umask>022</umask> -->
        
        <mlock_executable>false</mlock_executable>
    
        <include_from>/etc/metrica.xml</include_from>  <!--带替换文件的路径-->
        
        <zookeeper incl="zookeeper-servers" optional="true" />  <!--ClickHouse与ZooKeeper群集进行交互的设置-->
    
        <macros incl="macros" optional="true" />  <!--复制表的参数替换,如果不使用复制表,则可以省略-->
    
        <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>  <!--重新加载内置词典的时间间隔(以秒为单位),默认3600。可以在不重新启动服务器的情况下“即时”修改词典-->
    
        <max_session_timeout>3600</max_session_timeout>  <!-- 最大会话超时时间  单位秒-->
    
        <default_session_timeout>60</default_session_timeout>  <!--默认会话超时时间  单位秒-->
    
        <graphite>   <!--将数据发送到Graphite,它是一款企业级监控。-->
            <host>localhost</host>  <!--Graphite服务器-->
            <port>42000</port>  <!--Graphite服务器上的端口-->
            <timeout>0.1</timeout>  <!--发送超时时间,以秒为单位-->
            <interval>60</interval>  <!--发送间隔,以秒为单位-->
            <root_path>one_min</root_path>  <!--密钥的前缀-->
            <hostname_in_path>true</hostname_in_path>  <!--讲机名追加到根路径(默认= true)-->
    
            <metrics>true</metrics>  <!--从system.metrics表发送数据-->
            <events>true</events>  <!--从system.events表发送在该时间段内累积的增量数据-->
            <events_cumulative>false</events_cumulative>  <!--从system.events表发送累积数据-->
            <asynchronous_metrics>true</asynchronous_metrics>  <!--从system.asynchronous_metrics表发送数据-->
        </graphite>
       
        <query_log>  <!--通过log_queries = 1 在用户配置表中设置,记录了ClickHouse服务中所有已经执行的查询记录。查询记录在system.query_log表中-->
            <database>system</database>  <!--库名-->
            <table>query_log</table>  <!--表名-->
            <partition_by>toYYYYMM(event_date)</partition_by>  <!--自定义分区键-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
        </query_log>
    
        <trace_log>  <!--trace_log系统表操作的设置。   采样查询探查器收集的堆栈跟踪-->
            <database>system</database>   <!--库名-->
            <table>trace_log</table>  <!--表名-->
            <partition_by>toYYYYMM(event_date)</partition_by>   <!--自定义分区键-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
        </trace_log>
    
        <query_thread_log>  <!--使用log_query_threads = 1设置,在用户配置表中设置 记录了所有线程的执行查询的信息-->
            <database>system</database>  <!--库名-->
            <table>query_thread_log</table>   <!--表名-->
            <partition_by>toYYYYMM(event_date)</partition_by>   <!--自定义分区键-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
        </query_thread_log>
    
        <part_log>  <!--记录了MergeTree系列表引擎的分区操作日志。如添加或合并数据-->
            <database>system</database>  <!--库名-->
            <table>part_log</table>  <!--表名-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
        </part_log>
    
        <text_log>  <!--log日志记录了ClickHouse运行过程中产生的一系列打印日志,包括INFO、DEBUG和Trace-->
            <database>system</database>   <!--库名-->
            <table>text_log</table>  <!--表名-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
            <level></level>
        </text_log>
    
        <metric_log> <!--用于将system.metrics和system.events中的数据汇聚到一起-->
            <database>system</database>   <!--库名-->
            <table>metric_log</table>  <!--表名-->
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>  <!--将数据从内存中的缓冲区刷新到表的时间间隔-->
            <collect_interval_milliseconds>1000</collect_interval_milliseconds>  <!--收集间隔-->
        </metric_log>
    
        <dictionaries_config>*_dictionary.xml</dictionaries_config>  <!--外部词典的配置文件的路径,在config配置文件中指定。路径可以包含通配符*和?的绝对或则相对路径-->
    
        <compression incl="clickhouse_compression">  <!--MergeTree引擎表的数据压缩设置。配置模板如-->
            <case>
                <min_part_size>10000000000</min_part_size>        <!-- 数据部分的最小大小 -->
                <min_part_size_ratio>0.01</min_part_size_ratio>   <!-- 数据部分大小与表大小的比率 -->
                <method>LZ4</method>  <!--压缩算法,zstd和lz4-->
            </case>
    
        </compression>
    
        <distributed_ddl>  <!--分布式ddl配置 create drop alter -->
            <path>/clickhouse/task_queue/ddl</path>  <!--分布式DDL在ZooKeeper内使用的根路径为-->
        </distributed_ddl>
    
        <max_table_size_to_drop>0</max_table_size_to_drop>  <!--删除表的限制,默认50G,0表示不限制。如果MergeTree表的大小超过max_table_size_to_drop(以字节为单位),则无法使用DROP查询将其删除-->
        <max_partition_size_to_drop>0</max_partition_size_to_drop>  <!--删除分区限制 默认50G,0表示不限制-->
    
        <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>  <!--包含输入格式文件的目录路径-->
    
    
        <query_masking_rules>  <!--基于Regexp的规则,应用于查询以及所有日志消息。再其存储在服务器日志中,system.query_log,system.text_log,system.processes表以及以日志形式发送给客户端。这样可以防止SQL查询中的敏感数据泄漏记录到日志中-->
            <rule>
                <name>hide SSN</name>  <!--规则名称-->
                <regexp>\b\d{3}-\d{2}-\d{4}\b</regexp>  <!--正则表达式-->
                <replace>000-00-0000</replace>  <!--替换,敏感数据的替换字符串(默认为可选-六个星号)-->
            </rule>
        </query_masking_rules>
        
        <disable_internal_dns_cache>1</disable_internal_dns_cache>  <!--禁用内部DNS缓存,默认0-->
    </yandex>
    
    9.users.xml配置详解
    <?xml version="1.0"?>
    <yandex>
        <profiles>
            <default>
                <max_memory_usage>10000000000</max_memory_usage>  <!--单个查询最大内存使用量-->
                <max_memory_usage_for_user>0</max_memory_usage_for_user>  <!--在单个ClickHouse服务进程中,以用户为单位进行统计,单个用户在运行查询时,限制使用的最大内存用量,默认值为0,即不做限制-->
                <max_memory_usage_for_all_queries>0</max_memory_usage_for_all_queries><!--在单个ClickHouse服务进程中,所有运行的查询累加在一起,限制使用的最大内存用量,默认为0不做限制-->
                <max_partitions_per_insert_block>100</max_partitions_per_insert_block><!--在单次INSERT写入的时候,限制创建的最大分区个数,默认值为100个。如果超出这个阈值数目,将会得到异常-->
                <max_rows_to_group_by>0</max_rows_to_group_by> <!--在执行GROUP BY聚合查询的时候,限制去重后的聚合KEY的最大个数,默认值为0,即不做限制-->
                <max_bytes_before_external_group_by>0</max_bytes_before_external_group_by> <!--在执行GROUP BY聚合查询的时候,限制使用的最大内存用量,默认值为0,即不做限制-->
                <use_uncompressed_cache>0</use_uncompressed_cache>  <!--是否使用未压缩块的缓存。接受0或1。默认情况下,0(禁用)-->
    
                <load_balancing>random</load_balancing> <!--指定用于分布式查询处理的副本选择算法 默认:Random
    			random是默认的负载均衡算法,在ClickHouse的服务节点中,拥有一个全局计数器errors_count,当服务发生任何异常时,该计数累积加1。而random算法会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则在它们之中随机选择一个
    			nearest_hostname可以看作random算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则选择集群配置中host名称与当前host最相似的一个
    			in_order同样可以看作random算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则按照集群配置中replica的定义顺序逐个选择。
    			first_or_random可以看作in_order算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,它首先会选择集群配置中第一个定义的replica,如果该replica不可用,则进一步随机选择一个其他的replica。
    -->
                <log_queries> 1</log_queries>  <!--用户配置开启query_log查询日志,记录了ClickHouse服务中所有已经执行的查询记录-->
                <log_query_threads> 1</log_query_threads>  <!--用户配置开启query_thread_log日志 记录了所有线程的执行查询的信息-->
                <part_log>1</part_log> <!--用户配置开启part_log日志,记录了MergeTree系列表引擎的分区操作日志-->
                <text_log>1</text_log>  <!--用户配置开启text_log日志 记录了ClickHouse运行过程中产生的一系列打印日志,包括INFO、DEBUG和Trace-->
                <metric_log>1</metric_log> <!--用户配置开启metric_log日志 用于将system.metrics和system.events中的数据汇聚到一起-->
            </default>
            
            <readonly>  <!--读权限、写权限和设置权限  readonly:用户名-->
                <readonly>0</readonly>  <!--不进行任何限制(默认值)-->
                <readonly>1</readonly>  <!--只拥有读权限(只能执行SELECT、EXISTS、SHOW和DESCRIBE)-->
                <readonly>2</readonly>  <!--拥有读权限和设置权限(在读权限基础上,增加了SET查询)-->
                <allow_ddl>0</allow_ddl>  <!--·当取值为0时,不允许DDL查询  create drop alter-->
                <allow_ddl>1</allow_ddl>  <!--·当取值为1时,允许DDL查询(默认值)-->
            </readonly>
        </profiles>
    
        <users>  <!--用户配置-->
            <default>  <!--用户名-->
                <password></password>  <!--置登录密码  免密码登入,支持明文、SHA256加密和double_sha1加密三种形式-->
                <password>123</password>   <!--明文密码-->
                <password_sha256_hex>a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3</password_sha256_hex>  <!--SHA256加密  # echo -n 123 | openssl dgst -sha256-->
                <password_double_sha1_hex>23ae809ddacaf96af0fd78ed04b6a265e05aa257</password_double_sha1_hex>  <!--double_sha1加密  # echo -n 123 | openssl dgst -sha1 -binary | openssl dgst -sha1-->
                
                <networks incl="networks" replace="replace">  <!--限制IP访问-->
                    <ip>::/0</ip>   --允许任何地址访问
                    <ip>127.0.0.1</ip>  --允许本地访问
                    <ip>192.168.107.216</ip> --允许该IP访问
                </networks>
                
                <allow_databases>  <!--限制访问数据库   不指定表示不限制-->
                    <database>default</database>  <!--库名称-->
                    <database>test_dictionaries</database>
                </allow_databases>
                
                <databases>  <!--限制访问表的数据   不指定表示不限制-->
                    <database_name><!--数据库名称-->
                        <table_name><!--表名称-->
                            <filter> id < 10</filter><!--数据过滤条件-->
                        </table_name>
                	</database_name>
               	</databases>
                        
                <profile>default</profile>
    
                <quota>default</quota>
            </default>
        </users>
    
        <quotas>  <!--限制资源被过度使用,当使用的资源数量达到阈值时,那么正在进行的操作会被自动中断-->
            <default>  <!--自定义名称,全局唯一-->
                <interval>  <!--配置时间间隔,每个时间内的资源消耗限制-->
                    <duration>3600</duration>  <!--表示累积的时间周期,单位是秒-->
                    <queries>0</queries>  <!--表示在周期内允许执行的查询次数,0表示不限制-->
                    <errors>0</errors> <!--表示在周期内允许发生异常的次数,0表示不限制-->
                    <result_rows>0</result_rows>  <!--表示在周期内允许查询返回的结果行数,0表示不限制-->
                    <read_rows>0</read_rows>  <!--表示在周期内在分布式查询中,允许远端节点读取的数据行数,0表示不限制。-->
                    <execution_time>0</execution_time>  <!--表示周期内允许执行的查询时间,单位是秒,0表示不限制。-->
                </interval>
            </default>
        </quotas>
    </yandex>
    
    10.metrika.xml详解
    vim /etc/metrika.xml
    
    <yandex>
    <clickhouse_remote_servers>  <!-- 集群设置 -->
        <shunwang>   <!-- clickhouse显示名称  可以自己修改 -->
            <shard>  <!-- 一分片 -->
                <weight>1</weight>  <!--分片权重-->
                <internal_replication>false</internal_replication>  <!-- 是否开启自动复制 -->
                <replica>  <!-- 副本 -->
                    <host>192.168.104.91</host>  <!--指定部署了ClickHouse节点的服务器地址-->
                    <port>9000</port>  <!--指定ClickHouse服务的TCP端口-->
                    <user>default</user>  <!--ClickHouse用户,默认为default  选填-->
                    <password></password>  <!--ClickHouse的用户密码,默认为空字符串  选填-->
                    <secure>9440</secure>  <!--SSL连接的端口,默认为9440  选填-->
                    <compression>true</compression>  <!--是否开启数据压缩功能,默认为true  选填-->
                </replica>
                <replica>  <!-- 副本 -->
                    <host>192.168.104.92</host>  <!--指定部署了ClickHouse节点的服务器地址-->
                    <port>9000</port>  <!--指定ClickHouse服务的TCP端口-->
                    <user>default</user>  <!--ClickHouse用户,默认为default  选填-->
                    <password></password>  <!--ClickHouse的用户密码,默认为空字符串  选填-->
                    <secure>9440</secure>  <!--SSL连接的端口,默认为9440  选填-->
                    <compression>true</compression>  <!--是否开启数据压缩功能,默认为true  选填-->
                </replica>
            </shard>
            <shard>
                <weight>1</weight>  <!--分片权重-->
                <internal_replication>false</internal_replication>  <!-- 是否开启自动复制 -->
                <replica>  <!-- 副本 -->
                    <host>192.168.104.93</host>  <!--指定部署了ClickHouse节点的服务器地址-->
                    <port>9000</port>  <!--指定ClickHouse服务的TCP端口-->
                    <user>default</user>  <!--ClickHouse用户,默认为default  选填-->
                    <password></password>  <!--ClickHouse的用户密码,默认为空字符串  选填-->
                    <secure>9440</secure>  <!--SSL连接的端口,默认为9440  选填-->
                    <compression>true</compression>  <!--是否开启数据压缩功能,默认为true  选填-->
                </replica>
                <replica>  <!-- 副本 -->
                    <host>192.168.104.94</host>  <!--指定部署了ClickHouse节点的服务器地址-->
                    <port>9000</port>  <!--指定ClickHouse服务的TCP端口-->
                    <user>default</user>  <!--ClickHouse用户,默认为default  选填-->
                    <password></password>  <!--ClickHouse的用户密码,默认为空字符串  选填-->
                    <secure>9440</secure>  <!--SSL连接的端口,默认为9440  选填-->
                    <compression>true</compression>  <!--是否开启数据压缩功能,默认为true  选填-->
                </replica>
            </shard>
        </shunwang>
    </clickhouse_remote_servers>
    
    <macros>
        <shard>01</shard>  <!--分片编号不能重复   如果是副本要相同-->
        <replica>192.168.104.91</replica>  <!--副本名称,创建复制表时有用,每个节点不同,整个集群唯一,建议使用主机名-->
    </macros>
    
    <networks>
       <ip>::/0</ip>
    </networks>
    
    <!-- zookeeper 配置 集群高可用 -->
    <zookeeper-servers>
        <node index="1">
            <host>192.168.104.91</host>
            <port>2181</port>
        </node>
        <node index="2">
            <host>192.168.104.92</host>
            <port>2181</port>
        </node>
        <node index="3">
            <host>192.168.104.93</host>
            <port>2181</port>
        </node>
        <session_timeout_ms>30000</session_timeout_ms>  <!--客户端会话的最大超时(以毫秒为单位)-->
        <operation_timeout_ms>10000</operation_timeout_ms>
        <root>/path/to/zookeeper/node</root>   <!--用作ClickHouse服务器使用的znode的根的znode-->
        <identity>user:password</identity>  <!--用户和密码,ZooKeeper可能需要这些用户和密码才能访问请求的znode-->
    </zookeeper-servers>
    
    <clickhouse_compression>  <!--MergeTree引擎表的数据压缩设置。配置模板如-->
       	<case>
        	<min_part_size>10000000000</min_part_size>        <!-- 数据部分的最小大小 -->
    		<min_part_size_ratio>0.01</min_part_size_ratio>   <!-- 数据部分大小与表大小的比率 -->
    		<method>LZ4</method>  <!--压缩算法,zstd和lz4-->
        </case>
    </clickhouse_compression>
    </yandex>
    
    11.其他配置文件

    (1)/etc/security/limits.d/clickhouse.conf:文件句柄数量的配置,默认值如下所示。该配置也可以通过config.xml的max_open_files修改。

    # cat /etc/security/limits.d/clickhouse.conf
    
    clickhouse      soft    nofile  262144 
    clickhouse      hard    nofile  262144
    

    (2)/etc/cron.d/clickhouse-server:cron定时任务配置,用于恢复因异常原因中断的ClickHouse服务进程,其默认的配置如下。

    # cat /etc/cron.d/clickhouse-server 
    
    # */10 * * * * root (which service > /dev/null 2>&1 && (service clickhouse-server  condstart ||:)) || /etc/init.d/clickhouse-server condstart > /dev/null 2>&1
    
    12.启动clickhouse
    service clickhouse-server start # 启动server端clickhouse-server 单机版clickhouse部署完成
    service clickhouse-server stop # 停止server端clickhouse-server
    service clickhouse-server restart # 重启server端clickhouse-server
    service clickhouse-server status # 查看server端服务开启/关闭状态
    
    clickhouse-client --multiline -h 192.168.104.91 --port 9000 -u ck --password shunwang
    # multiline:可以同时执行多条语句默认 ; 结束
    # -h:ip
    # --port:端口
    # -u:账号
    # --password:密码
    # --database/-d:登录的数据库,默认值为default
    # --time/-t:在非交互式执行时,会打印每条SQL的执行时间
    # --query/-q:只能在非交互式查询时使用,用于指定SQL语句   --query="SELECT 1;SELECT 2;"
    
    select * from system.clusters # 查看是否成功  显示下面信息  集群搭建成功
    

    第3章 数据定义

    3.1 ClickHouse的数据类型

    3.1.1 基础类型

    基础类型只有数值、字符串和时间三种类型,没有Boolean类型,但可以使用整型的0或1替代。

    1.数值类型

    数值类型分为整数、浮点数和定点数三类

    1)Int

    Int8、Int16、Int32和Int64指代4种大小的Int类型,其末尾的数字正好表明了占用字节的大小(8位=1字节)

    ClickHouse支持无符号的整数,使用前缀U表示

    2)Float

    ClickHouse直接使用Float32和Float64代表单精度浮点数以及双精度浮点数

    :) SELECT toFloat32('0.12345678901234567890') as a , toTypeName(a)
    ┌──────a─┬─toTypeName(toFloat32('0.12345678901234567890'))─┐
    │ 0.12345679 │ Float32                                          │
    └────────┴───────────────────────────────┘
     
    :) SELECT toFloat64('0.12345678901234567890') as a , toTypeName(a)
    ┌────────────a─┬─toTypeName(toFloat64('0.12345678901234567890'))─┐
    │ 0.12345678901234568 │ Float64                                          │
    └─────────────┴──────────────────────────────┘
    
    3)Decimal

    如果要求更高精度的数值运算,则需要使用定点数。ClickHouse提供了Decimal32、Decimal64和Decimal128三种精度的定点数。可以通过两种形式声明定点:简写方式有Decimal32(S)、Decimal64(S)、Decimal128(S)三种,原生方式为Decimal(P,S),其中:

    ·P代表精度,决定总位数(整数部分+小数部分),取值范围是1~38;

    ·S代表规模,决定小数位数,取值范围是0~P。

    在进行加法运算时,S取最大值。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相加后S=4:

    :) SELECT toDecimal64(2,4) + toDecimal32(2,2)
     
    ┌─plus(toDecimal64(2, 4), toDecimal32(2, 2))─┐
    │ 4.0000                                       │
    └───────────────────────────┘
    

    在进行减法运算时,其规则与加法运算相同,S同样会取最大值。例如toDecimal32(4,4)与toDecimal64(2,2)相减后S=4:

    :) SELECT toDecimal32(4,4) - toDecimal64(2,2)
    ┌─minus(toDecimal32(4, 4), toDecimal64(2, 2))┐
    │ 2.0000                                     │
    └────────────────────────────┘
    

    在进行乘法运算时,S取两者S之和。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相乘后S=4+2=6:

    :) SELECT toDecimal64(2,4) * toDecimal32(2,2)
    ┌─multiply(toDecimal64(2, 4), toDecimal32(2, 2))┐
    │ 4.000000                                      │
    └─────────────────────────────┘
    

    在进行除法运算时,S取被除数的值,此时要求被除数S必须大于除数S,否则会报错。例如toDecimal64(2,4)与toDecimal32(2,2)相除后S=4:

    :) SELECT toDecimal64(2,4) / toDecimal32(2,2)
    ┌─divide(toDecimal64(2, 4), toDecimal32(2, 2))┐
    │  1.0000                                      │
    └───────────────────────────┘
    

    2.字符串类型
    1)String

    字符串由String定义,长度不限。因此在使用String的时候无须声明大小。

    2)FixedString

    FixedString类型和传统意义上的Char类型有些类似,对于一些字符有明确长度的场合,可以使用固定长度的字符串。定长字符串通过FixedString(N)声明,其中N表示字符串长度。FixedString使用null字节填充末尾字符

    :) SELECT toFixedString('abc',5) , LENGTH(toFixedString('abc',5)) AS LENGTH
    ┌─toFixedString('abc', 5)─┬─LENGTH─┐
    │ abc                      │ 5       │
    └────────────────┴──────┘
    
    3)UUID

    UUID是一种数据库常见的主键类型,在ClickHouse中直接把它作为一种数据类型。UUID共有32位,它的格式为8-4-4-4-12。如果一个UUID类型的字段在写入数据时没有被赋值,则会依照格式使用0填充,例如:

    CREATE TABLE UUID_TEST (
        c1 UUID,
        c2 String
    ) ENGINE = Memory;
    --第一行UUID有值
    INSERT INTO UUID_TEST SELECT generateUUIDv4(),'t1'
    --第二行UUID没有值
    INSERT INTO UUID_TEST(c2) VALUES('t2')
     
    :) SELECT * FROM UUID_TEST
    ┌─────────────────────c1─┬─c2─┐
    │ f36c709e-1b73-4370-a703-f486bdd22749 │ t1 │
    └───────────────────────┴────┘
    ┌─────────────────────c1─┬─c2─┐
    │ 00000000-0000-0000-0000-000000000000 │ t2 │
    └───────────────────────┴────┘
    
    3.时间类型

    时间类型分为DateTime、DateTime64和Date三类。ClickHouse目前没有时间戳类型。时间类型最高的精度是秒,也就是说,如果需要处理毫秒、微秒等大于秒分辨率的时间,则只能借助UInt类型实现。

    1)DateTime

    DateTime类型包含时、分、秒信息,精确到秒,支持使用字符串形式写入:

    CREATE TABLE Datetime_TEST (
        c1 Datetime
    ) ENGINE = Memory
    --以字符串形式写入
    INSERT INTO Datetime_TEST VALUES('2019-06-22 00:00:00')
     
     SELECT c1, toTypeName(c1) FROM Datetime_TEST
    ┌──────────c1─┬─toTypeName(c1)─┐
    │ 2019-06-22 00:00:00 │  DateTime        │
    └─────────────┴───────────┘
    
    2)DateTime64

    DateTime64可以记录亚秒,它在DateTime之上增加了精度的设置,例如:

    CREATE TABLE Datetime64_TEST (
        c1 Datetime64(2)    
    ) ENGINE = Memory
    --以字符串形式写入
    INSERT INTO Datetime64_TEST VALUES('2019-06-22 00:00:00')
     
     SELECT c1, toTypeName(c1) FROM Datetime64_TEST
    ┌─────────────c1─┬─toTypeName(c1)─┐
    │ 2019-06-22 00:00:00.00 │ DateTime       │
    └───────────────┴──────────┘
    
    3)Date

    Date类型不包含具体的时间信息,只精确到天,它同样也支持字符串形式写入:

    CREATE TABLE Date_TEST (
        c1 Date
    ) ENGINE = Memory
     
    --以字符串形式写入
    INSERT INTO Date_TEST VALUES('2019-06-22')
    SELECT c1, toTypeName(c1) FROM Date_TEST
    ┌─────────c1─┬─toTypeName(c1)─┐
    │ 2019-06-22       │ Date            │
    └───────────┴──────────┘
    

    3.1.2 复合类型

    除了基础数据类型之外,ClickHouse还提供了数组、元组、枚举和嵌套四类复合类型。这些类型通常是其他数据库原生不具备的特性。拥有了复合类型之后,ClickHouse的数据模型表达能力更强了。

    1.Array

    数组有两种定义形式,常规方式array(T):

    SELECT array(1, 2) as a , toTypeName(a)
    ┌─a───┬─toTypeName(array(1, 2))─┐
    │ [1,2] │ Array(UInt8)              │
    └─────┴────────────────┘
    

    或者简写方式[T]:

    SELECT [1, 2]
    

    在定义表字段时,数组需要指定明确的元素类型

    CREATE TABLE Array_TEST (
        c1 Array(String)
    ) engine = Memory
    
    2.Tuple

    元组类型由1~n个元素组成,每个元素之间允许设置不同的数据类型,且彼此之间不要求兼容。

    SELECT tuple(1,'a',now()) AS x, toTypeName(x)
    ┌─x─────────────────┬─toTypeName(tuple(1, 'a', now()))─┐
    │ (1,'a','2019-08-28 21:36:32') │ Tuple(UInt8, String, DateTime)    │
    └───────────────────┴─────────────────────┘
    

    或者简写方式(T):

    SELECT (1,2.0,null) AS x, toTypeName(x)
    ┌─x──────┬─toTypeName(tuple(1, 2., NULL))───────┐
    │ (1,2,NULL) │ Tuple(UInt8, Float64, Nullable(Nothing)) │
    └───────┴──────────────────────────┘
    

    在定义表字段时,元组也需要指定明确的元素类型:

    CREATE TABLE Tuple_TEST (
        c1 Tuple(String,Int8)
    ) ENGINE = Memory;
    
    3.Enum

    ClickHouse支持枚举类型,这是一种在定义常量时经常会使用的数据类型。ClickHouse提供了Enum8和Enum16两种枚举类型,它们除了取值范围不同之外,别无二致。枚举固定使用(String:Int)Key/Value键值对的形式定义数据,所以Enum8和Enum16分别会对应(String:Int8)和(String:Int16)

    CREATE TABLE Enum_TEST (
        c1 Enum8('ready' = 1, 'start' = 2, 'success' = 3, 'error' = 4)
    ) ENGINE = Memory;
    

    在定义枚举集合的时候,有几点需要注意。首先,Key和Value是不允许重复的,要保证唯一性。其次,Key和Value的值都不能为Null,但Key允许是空字符串。在写入枚举数据的时候,只会用到Key字符串部分,但是在后续对枚举的所有操作中(包括排序、分组、去重、过滤等),会使用Int类型的Value值。

    INSERT INTO Enum_TEST VALUES('ready');
    INSERT INTO Enum_TEST VALUES('start');
    
    4.Nested

    嵌套类型,顾名思义是一种嵌套表结构。一张数据表,可以定义任意多个嵌套类型字段,但每个字段的嵌套层级只支持一级,即嵌套表内不能继续使用嵌套类型

    CREATE TABLE nested_test (
        name String,
        age  UInt8 ,
        dept Nested(
            id UInt8,
            name String
        )
    ) ENGINE = Memory;
    
    INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001,10002], ['研发部','技术支持中心','测试部']);
    --行与行之间,数组长度无须对齐
    INSERT INTO nested_test VALUES ('bruce' , 30 , [10000,10001], ['研发部','技术支持中心']);
    

    在访问嵌套类型的数据时需要使用点符号,例如:

    SELECT name, dept.id, dept.name FROM nested_test
    ┌─name─┬─dept.id──┬─dept.name─────────────┐
    │ bruce │ [16,17,18] │ ['研发部','技术支持中心','测试部'] │
    └────┴───────┴────────────────────┘
    

    4.1.3 特殊类型

    1.Nullable

    准确来说,Nullable并不能算是一种独立的数据类型,它更像是一种辅助的修饰符,需要与基础数据类型一起搭配使用。Nullable类型与Java8的Optional对象有些相似,它表示某个基础数据类型可以是Null值。其具体用法如下所示:

    CREATE TABLE Null_TEST (
        c1 String,
        c2 Nullable(UInt8)
    ) ENGINE = TinyLog;
    通过Nullable修饰后c2字段可以被写入Null值:
    INSERT INTO Null_TEST VALUES ('nauu',null)
    INSERT INTO Null_TEST VALUES ('bruce',20)
    SELECT c1 , c2 ,toTypeName(c2) FROM Null_TEST
    ┌─c1───┬───c2─┬─toTypeName(c2)─┐
    │ nauu   │ NULL    │ Nullable(UInt8) │
    │ bruce  │ 20      │ Nullable(UInt8) │
    └─────┴──────┴───────────┘
    

    在使用Nullable类型的时候还有两点值得注意:首先,它只能和基础类型搭配使用,不能用于数组和元组这些复合类型,也不能作为索引字段;其次,应该慎用Nullable类型,包括Nullable的数据表,不然会使查询和写入性能变慢。因为在正常情况下,每个列字段的数据会被存储在对应的[Column].bin文件中。如果一个列字段被Nullable类型修饰后,会额外生成一个[Column].null.bin文件专门保存它的Null值。这意味着在读取和写入数据时,需要一倍的额外文件操作。

    2.Domain

    域名类型分为IPv4和IPv6两类,本质上它们是对整型和字符串的进一步封装。IPv4类型是基于UInt32封装的,它的具体用法如下所示:

    CREATE TABLE IP4_TEST (
        url String,
        ip IPv4
    ) ENGINE = Memory;
    INSERT INTO IP4_TEST VALUES ('www.nauu.com','192.0.0.0')
    SELECT url , ip ,toTypeName(ip) FROM IP4_TEST
    ┌─url──────┬─────ip─┬─toTypeName(ip)─┐
    │ www.nauu.com │ 192.0.0.0 │ IPv4             │
    └────────┴───────┴──────────┘
    

    (1)出于便捷性的考量,例如IPv4类型支持格式检查,格式错误的IP数据是无法被写入的,例如:

    INSERT INTO IP4_TEST VALUES ('www.nauu.com','192.0.0')
    Code: 441. DB::Exception: Invalid IPv4 value.
    

    (2)出于性能的考量,同样以IPv4为例,IPv4使用UInt32存储,相比String更加紧凑,占用的空间更小,查询性能更快。IPv6类型是基于FixedString(16)封装的,它的使用方法与IPv4别无二致

    3.2 如何定义数据表

    3.2.1 数据库

    创建数据库的完整语法如下所示:

    CREATE DATABASE IF NOT EXISTS db_name [ENGINE = engine]
    

    IF NOT EXISTS表示如果已经存在一个同名的数据库,则会忽略后续的创建过程;

    [ENGINE=engine]表示数据库所使用的引擎类型(是的,你没看错,数据库也支持设置引擎)。

    数据库目前一共支持5种引擎,如下所示。

    ·Ordinary:默认引擎,在绝大多数情况下我们都会使用默认引擎,使用时无须刻意声明。在此数据库下可以使用任意类型的表引擎。

    ·Dictionary:字典引擎,此类数据库会自动为所有数据字典创建它们的数据表

    ·Memory:内存引擎,用于存放临时数据。此类数据库下的数据表只会停留在内存中,不会涉及任何磁盘操作,当服务重启后数据会被清除。

    ·Lazy:日志引擎,此类数据库下只能使用Log系列的表引擎

    ·MySQL:MySQL引擎,此类数据库下会自动拉取远端MySQL中的数据,并为它们创建MySQL表引擎的数据表

    CREATE DATABASE [IF NOT EXISTS] db_name [ON CLUSTER cluster]
    ENGINE = MySQL('host:port', 'database' , 'user', 'password')
    --host:port — 链接的MySQL地址。
    --database — 链接的MySQL数据库。
    --user — 链接的MySQL用户。
    --password — 链接的MySQL用户密码。
    
    SHOW databases;   -- 显示数据库
    use cloud_joy_yun;   -- 进入数据库
    SHOW tables;   -- 显示表
    DROP DATABASE [IF EXISTS] db_name  -- 删除数据库
    

    3.2.2 数据表

    第一种是常规定义方法,它的完整语法如下所示:

    CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
        name1 [type] [DEFAULT] [COMMENT],
        name2 [type] [DEFAULT] [COMMENT],
        省略…
    ) ENGINE = engine
    
    CREATE TABLE hits_v1 ( 
        Title String,
        URL String ,
        EventTime DateTime
    ) ENGINE = Memory;
    

    第二种定义方法是复制其他表的结构,具体语法如下所示:

    CREATE TABLE [IF NOT EXISTS] [db_name1.]table_name AS [db_name2.] table_name2 [ENGINE = engine]
    
    --创建新的数据库
    CREATE DATABASE IF NOT EXISTS new_db 
    --将default.hits_v1的结构复制到new_db.hits_v1
    CREATE TABLE IF NOT EXISTS new_db.hits_v1 AS default.hits_v1 ENGINE = TinyLog
    

    上述语句将会把default.hits_v1的表结构原样复制到new_db.hits_v1,并且ENGINE表引擎可以与原表不同。

    第三种定义方法是通过SELECT子句的形式创建,它的完整语法如下:

    CREATE TABLE [IF NOT EXISTS] [db_name.]table_name ENGINE = engine AS SELECT …
    CREATE TABLE IF NOT EXISTS hits_v1_1 ENGINE = Memory AS SELECT * FROM hits_v1  --表结构和数据都会在
    DROP TABLE [IF EXISTS] [db_name.]table_name
    

    3.2.3 临时表

    ClickHouse也有临时表的概念,创建临时表的方法是在普通表的基础之上添加temporary关键字,它的完整语法如下所示:

    CREATE temporary TABLE [IF NOT EXISTS] table_name (
        name1 [type] [DEFAULT] [COMMENT],
        name2 [type] [DEFAULT] [COMMENT],
    )
    

    相比普通表而言,临时表有如下两点特殊之处:

    1. 它的生命周期是会话绑定的,所以它只支持Memory表引擎,如果会话结束,数据表就会被销毁;
    2. 临时表不属于任何数据库,所以在它的建表语句中,既没有数据库参数也没有表引擎参数。
    3. 临时表和普通可以同时存在,优先级临时表大于普通表

    3.2.4 视图

    ClickHouse拥有普通和物化两种视图,其中物化视图拥有独立的存储,而普通视图只是一层简单的查询代理。创建普通视图的完整语法如下所示:

    CREATE VIEW [IF NOT EXISTS] [db_name.]view_name AS SELECT ...
    

    物化视图支持表引擎,数据保存形式由它的表引擎决定,创建物化视图的完整语法如下所示:

    CREATE [materialized] VIEW [IF NOT EXISTS] [db.]table_name [TO[db.]name] [ENGINE = engine] [POPULATE] AS SELECT ...
    

    物化视图创建好之后,如果源表被写入新数据,那么物化视图也会同步更新。物化视图目前并不支持同步删除,如果在源表中删除了数据,物化视图的数据仍会保留。

    3.2.5 追加新字段

    假如需要对一张数据表追加新的字段,可以使用如下语法:

    ALTER TABLE tb_name ADD COLUMN [IF NOT EXISTS] name [type] [default_expr] [AFTER name_after]
    

    例如,在数据表的末尾增加新字段:

    ALTER TABLE testcol_v1 ADD COLUMN OS String DEFAULT 'mac'
    

    或是通过AFTER修饰符,在指定字段的后面增加新字段:

    ALTER TABLE testcol_v1 ADD COLUMN IP String AFTER ID
    

    对于数据表中已经存在的旧数据而言,新追加的字段会使用默认值补全。

    3.2.6 修改数据类型

    如果需要改变表字段的数据类型或者默认值,需要使用下面的语法:

    ALTER TABLE tb_name modify COLUMN [IF EXISTS] name [type] [default_expr]
    ALTER TABLE testcol_v1 modify COLUMN IP IPv4  --将String类型的IP字段修改为IPv4类型是可行的
    

    3.2.7 修改备注

    做好信息备注是保持良好编程习惯的美德之一,所以如果你还没有为列字段添加备注信息,那么就赶紧行动吧。追加备注的语法如下所示:

    ALTER TABLE tb_name COMMENT COLUMN [IF EXISTS] name 'some comment'
    ALTER TABLE testcol_v1 COMMENT COLUMN ID '主键ID'  --为ID字段增加备注:
    
    DESC testcol_v1   --使用DESC查询可以看到上述增加备注的操作已经生效:
    ┌─name─────┬─type──┬─comment─┐
    │ ID          │ String │ 主键ID    │
    └─────────┴─────┴──────┘
    

    3.2.8 删除已有字段

    假如要删除某个字段,可以使用下面的语句:

    ALTER TABLE tb_name DROP COLUMN [IF EXISTS] name
    ALTER TABLE testcol_v1 DROP COLUMN URL  --执行下面的语句删除URL字段
    

    3.2.9 移动数据表

    在Linux系统中,mv命令的本意是将一个文件从原始位置A移动到目标位置B,但是如果位置A与位置B相同,则可以变相实现重命名的作用。ClickHouse的RENAME查询就与之有着异曲同工之妙,RENAME语句的完整语法如下所示:

    RENAME TABLE [db_name11.]tb_name11 TO [db_name12.]tb_name12, [db_name21.]tb_name21 TO [db_name22.]tb_name22, ...
    RENAME TABLE default.testcol_v1 TO db_test.testcol_v2  --testcol_v1从default默认数据库被移动到了db_test数据库,同时数据表被重命名为testcol_v2:
    

    3.2.10 清空数据表

    假设需要将表内的数据全部清空,而不是直接删除这张表,则可以使用TRUNCATE语句,它的完整语法如下所示:

    TRUNCATE TABLE [IF EXISTS] [db_name.]tb_name
    TRUNCATE TABLE db_test.testcol_v2  --将db_test.testcol_v2的数据一次性清空
    

    3.3 数据分区的基本操作

    目前只有MergeTree系列的表引擎支持数据分区。

    3.3.1 查询分区信息

    SELECT partition_id,name,table,database FROM system.parts WHERE table = 'partition_v2'
    ┌─partition_id─┬─name───────┬─table─────┬─database┐
    │ 201905         │ 201905_1_1_0_6 │ partition_v2 │ default │
    │ 201910         │ 201910_3_3_0_6 │ partition_v2 │ default │
    │ 201911         │ 201911_4_4_0_6 │ partition_v2 │ default │
    │ 201912         │ 201912_5_5_0_6 │ partition_v2 │ default │
    └──────────┴──────────┴─────────┴──────┘
    

    3.3.2 删除指定分区

    ALTER TABLE tb_name DROP PARTITION partition_expr   --删除一个指定分区
    ALTER TABLE partition_v2 DROP PARTITION 201907 --假如现在需要更新partition_v2数据表整个7月份的数据,则可以先将7月份的分区删除:
    INSERT INTO partition_v2 VALUES ('A004-update','www.bruce.com', '2019-07-02'),…  --然后将整个7月份的新数据重新写入,就可以达到更新的目的:
    

    3.3.3 复制分区数据

    ClickHouse支持将A表的分区数据复制到B表,这项特性可以用于快速数据写入、多表间数据同步和备份等场景,它的完整语法如下:

    ALTER TABLE B REPLACE PARTITION partition_expr FROM A
    

    不过需要注意的是,并不是任意数据表之间都能够相互复制,它们还需要满足两个前提条件:

    1. ·两张表需要拥有相同的分区键;
    2. ·它们的表结构完全相同。

    假设数据表partition_v2与先前的partition_v1分区键和表结构完全相同,那么应先在partition_v1中写入一批8月份的新数据:

    INSERT INTO partition_v1 VALUES ('A006-v1','www.v1.com', '2019-08-05'),('A007-v1','www.v1.com', '2019-08-20')
    -- 再执行下面的语句:
    ALTER TABLE partition_v2 REPLACE PARTITION 201908 FROM partition_v1  
    --即能够将partition_v1的整个201908分区中的数据复制到partition_v2:
    SELECT * from partition_v2 ORDER BY EventTime
    ┌─ID───────┬─URL──────┬─EventTime─┐
    │ A000         │ www.nauu.com │ 2019-05-01 │
    │ A001         │ www.nauu.com │ 2019-05-02 │
    省略…
    │ A004-update  │ www.bruce.com │ 2019-07-02 │
    │ A006-v1      │ www.v1.com    │ 2019-08-05 │
    │ A007-v1      │ www.v1.com    │ 2019-08-20 │
    └─────────┴─────────┴───────┘
    

    3.3.4 重置分区数据

    如果数据表某一列的数据有误,需要将其重置为初始值,此时可以使用下面的语句实现:

    ALTER TABLE tb_name CLEAR COLUMN column_name IN PARTITION partition_expr
    

    对于默认值的含义,笔者遵循如下原则:如果声明了默认值表达式,则以表达式为准;否则以相应数据类型的默认值为准。例如,执行下面的语句会重置partition_v2表内201908分区的URL数据重置。

    ALTER TABLE partition_v2 CLEAR COLUMN URL in PARTITION 201908
    

    查验数据后会发现,URL字段已成功被全部重置为空字符串了(String类型的默认值)。

    SELECT * from partition_v2
    ┌─ID────┬─URL─┬──EventTime┐
    │ A006-v1 │      │ 2019-08-05 │
    │ A007-v1 │      │ 2019-08-20 │
    └──────┴────┴────────┘
    

    3.3.5 卸载与装载分区

    表分区可以通过DETACH语句卸载,分区被卸载后,它的物理数据并没有删除,而是被转移到了当前数据表目录的子目录下。而装载分区则是反向操作,它能够将子目录下的某个分区重新装载回去。卸载与装载这一对伴生的操作,常用于分区数据的迁移和备份场景。卸载某个分区的语法如下所示:

    ALTER TABLE tb_name DETACH PARTITION partition_expr
    ALTER TABLE partition_v2 DETACH PARTITION 201908  --例如,执行下面的语句能够将partition_v2表内整个8月份的分区卸载:
    

    此时再次查询这张表,会发现其中2019年8月份的数据已经没有了。而进入partition_v2的磁盘目录,则可以看到被卸载的分区目录已经被移动到了detached目录中:

    # pwd
    /chbase/data/data/default/partition_v2/detached
    # ll
    total 4
    drwxr-x---. 2 clickhouse clickhouse 4096 Aug 31 23:16 201908_4_4_0
    

    记住,一旦分区被移动到了detached子目录,就代表它已经脱离了ClickHouse的管理,ClickHouse并不会主动清理这些文件。这些分区文件会一直存在,除非我们主动删除或者使用ATTACH语句重新装载它们。装载某个分区的完整语法如下所示:

    ALTER TABLE tb_name attach PARTITION partition_expr
    ALTER TABLE partition_v2 attach PARTITION 201908  --再次执行下面的语句,就可以将刚才已被卸载的201908分区重新装载回去:
    

    3.4 分布式DDL执行

    将一条普通的DDL语句转换成分布式执行十分简单,只需加上ON CLUSTER cluster_name声明即可。例如,执行下面的语句后将会对ch_cluster集群内的所有节点广播这条DDL语句:

    CREATE TABLE partition_v3 ON cluster ch_cluster( 
        ID String,
        URL String,
        EventTime Date
    ) ENGINE =  MergeTree()
    PARTITION BY toYYYYMM(EventTime)
    ORDER BY ID
    

    3.5 数据的写入

    第一种是使用VALUES格式的常规语法:第二种是使用指定格式的语法:

    INSERT INTO [db.]table [(c1, c2, c3…)] FORMAT format_name data_set
    INSERT INTO partition_v2 FORMAT CSV \
    'A0017','www.nauu.com', '2019-10-01' \
    'A0018','www.nauu.com', '2019-10-01'   -- CSV格式写入为例:
    

    第三种是使用SELECT子句形式的语法:

    INSERT INTO [db.]table [(c1, c2, c3…)] SELECT ...
    INSERT INTO partition_v2 SELECT * FROM partition_v1  --通过SELECT子句可将查询结果写入数据表,假设需要将partition_v1的数据写入partition_v2,则可以使用下面的语句:
    

    3.6 数据的删除与修改

    DELETE语句的完整语法如下所示:

    ALTER TABLE [db_name.]table_name DELETE WHERE filter_expr
    ALTER TABLE partition_v2 DELETE WHERE ID = 'A003'  --删除partition_v2表内所有ID等于A003的数据:
    

    数据修改除了需要指定具体的列字段之外,整个逻辑与数据删除别无二致,它的完整语法如下所示:

    ALTER TABLE [db_name.]table_name UPDATE column1 = expr1 [, ...] WHERE filter_expr
    ALTER TABLE partition_v2 UPDATE URL = 'www.wayne.com',OS = 'mac' WHERE ID IN (SELECT ID FROM partition_v2 WHERE EventTime = '2019-06-01')  --根据WHERE条件同时修改partition_v2内的URL和OS字段:
    

    3.7 表的查询

    SELECT WatchID FROM hits_v1   --从数据表中取数
    SELECT MAX_WatchID FROM (SELECT MAX(WatchID) AS MAX_WatchID FROM hits_v1)  --从子查询中取数
    

    第4章 MergeTree原理解析

    属合并树(MergeTree)表引擎及其家族系列(*MergeTree)最为强大,在生产环境的绝大部分场景中,都会使用此系列的表引擎。只有合并树系列的表引擎才支持主键索引、数据分区、数据副本和数据采样这些特性,同时也只有此系列的表引擎支持ALTER相关操作。

    合并树家族自身也拥有多种表引擎的变种。其中MergeTree作为家族中最基础的表引擎,提供了主键索引、数据分区、数据副本和数据采样等基本能力,而家族中其他的表引擎则在MergeTree的基础之上各有所长。例如ReplacingMergeTree表引擎具有删除重复数据的特性,而SummingMergeTree表引擎则会按照排序键自动聚合数据。如果给合并树系列的表引擎加上Replicated前缀,又会得到一组支持数据副本的表引擎,例如ReplicatedMergeTree、ReplicatedReplacingMergeTree、ReplicatedSummingMergeTree等。合并树表引擎家族如图6-1所示。

    4.1 MergeTree的创建方式与存储结构

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

    4.1.1 MergeTree的创建方式

    CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
        name1 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
        name2 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
        省略...
    ) ENGINE = MergeTree()
    [PARTITION BY expr]
    [ORDER BY expr]
    [PRIMARY KEY expr]
    [SAMPLE BY expr]
    [SETTINGS name=value, 省略...]
    
    • (1)PARTITION BY [选填]:分区键,用于指定表数据以何种标准进行分区。分区键既可以是单个列字段,也可以通过元组的形式使用多个列字段,同时它也支持使用列表达式。如果不声明分区键,则ClickHouse会生成一个名为all的分区

    • (2)ORDER BY [必填]:排序键,用于指定在一个数据片段内,数据以何种标准排序。默认情况下主键(PRIMARY KEY)与排序键相同。排序键既可以是单个列字段,例如ORDER BY CounterID,也可以通过元组的形式使用多个列字段,例如ORDER BY(CounterID,EventDate)。当使用多个列字段排序时,以ORDER BY(CounterID,EventDate)为例,在单个数据片段内,数据首先会以CounterID排序,相同CounterID的数据再按EventDate排序。

    • (3)PRIMARY KEY [选填]:主键,顾名思义,声明后会依照主键字段生成一级索引,用于加速表查询。默认情况下,主键与排序键(ORDER BY)相同,所以通常直接使用ORDER BY代为指定主键,无须刻意通过PRIMARY KEY声明。所以在一般情况下,在单个数据片段内,数据与一级索引以相同的规则升序排列。与其他数据库不同,MergeTree主键允许存在重复数据(ReplacingMergeTree可以去重)。

    • (4)sample BY [选填]:抽样表达式,用于声明数据以何种标准进行采样。如果使用了此配置项,那么在主键的配置中也需要声明同样的表达式,例如:

    •      省略...
         ) ENGINE = MergeTree()
         ORDER BY (CounterID, EventDate, intHash32(UserID)
         SAMPLE BY intHash32(UserID)
                   
      SELECT CounterID FROM hits_v1 SAMPLE 0.1  --随机返回10%的数据
      SELECT count() FROM hits_v1 SAMPLE 10000  --随机返回1000条数据
      SELECT CounterID FROM hits_v1 SAMPLE 0.4 OFFSET 0.5  --偏移50%数据开始采样40%数据
      
    • (5)SETTINGS:index_granularity [选填]:index_granularity对于MergeTree而言是一项非常重要的参数,它表示索引的粒度,默认值为8192。也就是说,MergeTree的索引在默认情况下,每间隔8192行数据才生成一条索引,其具体声明方式如下所示:

    •      省略...
      ) ENGINE = MergeTree()
       省略... 
      SETTINGS index_granularity = 8192;
      
    • (6)SETTINGS:enable_mixed_granularity_parts [选填]:设置是否开启自适应索引间隔的功能,默认开启。

    • (7)SETTINGS:index_granularity_bytes [选填]根据每一批次写入数据的体量大小,动态划分间隔大小。而数据的体量大小,正是由index_granularity_bytes参数控制的,默认为10M(10×1024×1024),设置为0表示不启动自适应功能

    • (8)SETTINGS:merge_with_ttl_timeout [选填]:TTL默认的合并频率,默认86400秒,即1天

    4.1.2 MergeTree的存储结构

    MergeTree表引擎中的数据是拥有物理存储的,数据会按照分区目录的形式保存到磁盘之上,其完整的存储结构如图6-2所示。

    从图6-2中可以看出,一张数据表的完整物理结构分为3个层级,依次是数据表目录、分区目录及各分区下具体的数据文件。接下来就逐一介绍它们的作用。

    (1)partition:分区目录,余下各类数据文件(primary.idx、[Column].mrk、[Column].bin等)都是以分区目录的形式被组织存放的,属于相同分区的数据,最终会被合并到同一个分区目录

    (2)checksums.txt:校验文件,使用二进制格式存储。它保存了余下各类文件(primary.idx、count.txt等)的size大小及size的哈希值,用于快速校验文件的完整性和正确性。

    (3)columns.txt:列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息,例如:

    $ cat columns.txt
    columns format version: 1
    4 columns:
    'ID' String
    'URL' String
    'Code' String
    'EventTime' Date
    

    (4)count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数,例如:

    $ cat count.txt 
    8
    

    (5)primary.idx:一级索引文件,使用二进制格式存储。用于存放稀疏索引,一张MergeTree表只能声明一次一级索引(通过ORDER BY或者PRIMARY KEY)。

    (6)[Column].bin:数据文件,使用压缩格式存储,默认为LZ4压缩格式,用于存储某一列的数据

    (7)[Column].mrk:列字段标记文件,使用二进制格式存储。标记文件中保存了.bin文件中数据的偏移量信息。标记文件与稀疏索引对齐,又与.bin文件一一对应,所以MergeTree通过标记文件建立了primary.idx稀疏索引与.bin数据文件之间的映射关系

    (8)[Column].mrk2:如果使用了自适应大小的索引间隔,则标记文件会以.mrk2命名。它的工作原理和作用与.mrk标记文件相同。

    (9)partition.dat与minmax_[Column].idx:如果使用了分区键,例如PARTITION BY EventTime,则会额外生成partition.dat与minmax索引文件,它们均使用二进制格式存储。partition.dat用于保存当前分区下分区表达式最终生成的值;而minmax索引用于记录当前分区下分区字段对应原始数据的最小和最大值

    (10)skp_idx_[Column].idx与skp_idx_[Column].mrk:如果在建表语句中声明了二级索引,则会额外生成相应的二级索引与标记文件,它们同样也使用二进制存储。二级索引在ClickHouse中又称跳数索引

    4.2 数据分区

    4.2.1 数据的分区规则

    • (1)不指定分区键:如果不使用分区键,即不使用PARTITION BY声明任何分区表达式,则分区ID默认取名为all,所有的数据都会被写入这个all分区。
    • (2)使用整型:如果分区键取值属于整型(兼容UInt64,包括有符号整型和无符号整型),且无法转换为日期类型YYYYMMDD格式,则直接按照该整型的字符形式输出,作为分区ID的取值。
    • (3)使用日期类型:如果分区键取值属于日期类型,或者是能够转换为YYYYMMDD格式的整型,则使用按照YYYYMMDD进行格式化后的字符形式输出,并作为分区ID的取值。
    • (4)使用其他类型:如果分区键取值既不属于整型,也不属于日期类型,例如String、Float等,则通过128位Hash算法取其Hash值作为分区ID的取值。

    如果通过元组的方式使用多个分区字段,则分区ID依旧是根据上述规则生成的,只是多个ID之间通过“-”符号依次拼接。例如按照上述表格中的例子,使用两个字段分区:

    PARTITION BY (length(Code),EventTime)
    -- 则最终的分区ID会是下面的模样:
    2-20190501
    2-20190611
    

    4.2.2 分区目录的命名规则

    一个完整分区目录的命名公式如下所示:

    PartitionID_MinBlockNum_MaxBlockNum_Level
    

    上图中,201905表示分区目录的ID;1_1分别表示最小的数据块编号与最大的数据块编号;而最后的_0则表示目前合并的层级。接下来开始分别解释它们的含义:

    • (1)PartitionID:分区ID,无须多说,关于分区ID的规则在上一小节中已经做过详细阐述了。
    • (2)MinBlockNum和MaxBlockNum:顾名思义,最小数据块编号与最大数据块编号。ClickHouse在这里的命名似乎有些歧义,很容易让人与稍后会介绍到的数据压缩块混淆。但是本质上它们毫无关系,这里的BlockNum是一个整型的自增长编号。如果将其设为n的话,那么计数n在单张MergeTree数据表内全局累加,n从1开始,每当新创建一个分区目录时,计数n就会累积加1。对于一个新的分区目录而言,MinBlockNum与MaxBlockNum取值一样,同等于n,例如201905_1_1_0、201906_2_2_0以此类推。但是也有例外,当分区目录发生合并时,对于新产生的合并目录MinBlockNum与MaxBlockNum有着另外的取值规则
    • (3)Level:合并的层级,可以理解为某个分区被合并过的次数,或者这个分区的年龄。数值越高表示年龄越大

    4.2.3 分区目录的合并过程

    MergeTree完全不同,伴随着每一批数据的写入(一次INSERT语句),MergeTree都会生成一批新的分区目录。即便不同批次写入的数据属于相同分区,也会生成不同的分区目录。也就是说,对于同一个分区而言,也会存在多个分区目录的情况。在之后的某个时刻(写入后的10~15分钟,也可以手动执行optimize TABLE table_name语句),ClickHouse会通过后台任务再将属于相同分区的多个目录合并成一个新的目录。已经存在的旧分区目录并不会立即被删除,而是在之后的某个时刻通过后台任务被删除(默认8分钟)。

    属于同一个分区的多个目录,在合并之后会生成一个全新的目录,目录中的索引和数据文件也会相应地进行合并。新目录名称的合并方式遵循以下规则,其中:

    1. ·MinBlockNum:取同一分区内所有目录中最小的MinBlockNum值。
    2. ·MaxBlockNum:取同一分区内所有目录中最大的MaxBlockNum值。
    3. ·Level:取同一分区内最大Level值并加1。

    4.3 一级索引

    4.3.1 稀疏索引

    primary.idx文件内的一级索引采用稀疏索引实现。

    稠密索引中每一行索引标记都会对应到一行具体的数据记录。而在稀疏索引中,每一行索引标记对应的是一段数据,而不是一行

    稀疏索引的优势是显而易见的,它仅需使用少量的索引标记就能够记录大量数据的区间位置信息,且数据量越大优势越为明显。以默认的索引粒度(8192)为例,MergeTree只需要12208行索引标记就能为1亿行数据记录提供索引。由于稀疏索引占用空间小,所以primary.idx内的索引数据常驻内存,取用速度自然极快。

    4.3.2 索引粒度

    MergeTree按照索引粒度

    4.3.3 索引数据的生成规则

    例如第0(81920)行CounterID取值57,第8192(81921)行CounterID取值1635,而第16384(8192*2)行CounterID取值3266,最终索引数据将会是5716353266。

    如果使用多个主键,例如ORDER BY(CounterID,EventDate),则每间隔8192行可以同时取CounterID与EventDate两列的值作为索引值,具体如图6-9所示。

    4.3.4 索引的查询过程

    现在有一份测试数据,共192行记录。其中,主键ID为String类型,ID的取值从A000开始,后面依次为A001、A002……直至A192为止。MergeTree的索引粒度index_granularity=3,根据索引的生成规则,primary.idx文件内的索引数据。

    根据索引数据,MergeTree会将此数据片段划分成192/3=64个小的MarkRange,两个相邻MarkRange相距的步长为1。其中,所有MarkRange(整个数据片段)的最大数值区间为[A000,+inf)。

    在引出了数值区间的概念之后,对于索引的查询过程就很好解释了。索引查询其实就是两个数值区间的交集判断。其中,一个区间是由基于主键的查询条件转换而来的条件区间;而另一个区间是刚才所讲述的与MarkRange对应的数值区间。

    整个索引查询过程可以大致分为3个步骤。

    (1)生成查询条件区间:首先,将查询条件转换为条件区间。即便是单个值的查询条件,也会被转换成区间的形式,例如下面的例子。

    WHERE ID = 'A003'
    ['A003', 'A003']
     
    WHERE ID > 'A000' 
    ('A000', +inf)
     
    WHERE ID < 'A188'
    (-inf, 'A188')
     
    WHERE ID LIKE 'A006%'
    ['A006', 'A007')
    

    (2)递归交集判断:以递归的形式,依次对MarkRange的数值区间与条件区间做交集判断。从最大的区间[A000,+inf)开始:

    1. ·如果不存在交集,数据不存在。
    2. ·如果存在交集,且MarkRange步长大于8(end-start),则将此区间进一步拆分成8个子区间,并重复此规则,继续做递归交集判断。
    3. ·如果存在交集,且MarkRange不可再分解(步长小于8),则记录MarkRange并返回。

    (3)合并MarkRange区间:将最终匹配的MarkRange聚在一起,合并它们的范围。

    4.4 二级索引

    除了一级索引之外,MergeTree同样支持二级索引。二级索引又称跳数索引,由数据的聚合信息构建而成。根据索引类型的不同,其聚合信息的内容也不同。跳数索引的目的与一级索引一样,也是帮助查询时减少数据扫描的范围。

    SET allow_experimental_data_skipping_indices = 1  --跳数索引在默认情况下是关闭的,需要设置allow_experimental_data_skipping_indices才能使用:
    

    跳数索引需要在CREATE语句内定义,它支持使用元组和表达式的形式声明,其完整的定义语法如下所示:

    INDEX index_name expr TYPE index_type(...) GRANULARITY granularity
    

    与一级索引一样,如果在建表语句中声明了跳数索引,则会额外生成相应的索引与标记文件(skp_idx_[Column].idx与skp_idx_[Column].mrk)。

    4.4.1 跳数索引的类型

    目前,MergeTree共支持4种跳数索引,分别是minmax、set、ngrambf_v1和tokenbf_v1。一张数据表支持同时声明多个跳数索引,例如:

    CREATE TABLE skip_test (
        ID String,
        URL String,
        Code String,
        EventTime Date,
        INDEX a ID TYPE minmax GRANULARITY 5,
        INDEX b(length(ID) * 8) TYPE set(2) GRANULARITY 5,
        INDEX c(ID,Code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5,
        INDEX d ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
    ) ENGINE = MergeTree()
    省略...
    

    (1)minmax:minmax索引记录了一段数据内的最小和最大极值,其索引的作用类似分区目录的minmax索引,能够快速跳过无用的数据区间,示例如下所示:

    INDEX a ID TYPE minmax GRANULARITY 5 --上述示例中minmax索引会记录这段数据区间内ID字段的极值。极值的计算涉及每5个index_granularity区间中的数据。
    

    (2)set:set索引直接记录了声明字段或表达式的取值(唯一值,无重复),其完整形式为set(max_rows),其中max_rows是一个阈值,表示在一个index_granularity内,索引最多记录的数据行数。如果max_rows=0,则表示无限制,例如:

    INDEX b(length(ID) * 8) TYPE set(100) GRANULARITY 5  --set索引会记录数据中ID的长度*8后的取值。其中,每个index_granularity内最多记录100条。
    

    (3)ngrambf_v1:ngrambf_v1索引记录的是数据短语的布隆表过滤器,只支持String和FixedString数据类型。这些参数是一个布隆过滤器的标准输入,如果你接触过布隆过滤器,应该会对此十分熟悉。它们具体的含义如下:

    1. n:token长度,依据n的长度将数据切割为token短语。
    2. size_of_bloom_filter_in_bytes:布隆过滤器的大小。
    3. number_of_hash_functions:布隆过滤器中使用Hash函数的个数。
    4. random_seed:Hash函数的随机种子。
    INDEX c(ID,Code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5  --ngrambf_v1索引会依照3的粒度将数据切割成短语token,token会经过2个Hash函数映射后再被写入,布隆过滤器大小为256字节。
    

    (4)tokenbf_v1:tokenbf_v1索引是ngrambf_v1的变种,同样也是一种布隆过滤器索引。tokenbf_v1去除了短语token的处理方法外,其他与ngrambf_v1是完全一样的。tokenbf_v1会自动按照非字符的、数字的字符串分割token,具体用法如下所示:

    INDEX d ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
    

    4.5 数据存储

    4.5.1 各列独立存储

    在MergeTree中,数据按列存储。而具体到每个列字段,数据也是独立存储的,每个列字段都拥有一个与之对应的.bin数据文件。也正是这些.bin文件,最终承载着数据的物理存储。数据文件以分区目录的形式被组织存放,所以在.bin文件中只会保存当前分区片段内的这一部分数据。按列独立存储的设计优势显而易见:一是可以更好地进行数据压缩(相同类型的数据放在一起,对压缩更加友好),二是能够最小化数据扫描的范围。

    4.5.2 压缩数据块

    一个压缩数据块由头信息和压缩数据两部分组成。头信息固定使用9位字节表示,具体由1个UInt8(1字节)整型和2个UInt32(4字节)整型组成,分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小,具体如图6-14所示。

    通过ClickHouse提供的clickhouse-compressor工具,能够查询某个.bin文件中压缩数据的统计信息

    clickhouse-compressor --stat  < /var/lib/clickhouse/data/cloud_joy_yun/cloud_friend/202007_1_1_0/cloud_id.bin
    
    --执行后,会看到如下信息:
    65536   12000
    65536   14661
    65536   4936
    65536   7506
    省略…
    

    其中每一行数据代表着一个压缩数据块的头信息,其分别表示该压缩块中未压缩数据大小和压缩后数据大小。

    每个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在64KB~1MB,其上下限分别由min_compress_block_size(默认65536)与max_compress_block_size(默认1048576)参数指定。而一个压缩数据块最终的大小,则和一个间隔(index_granularity)内数据的实际大小相关。

    MergeTree在数据具体的写入过程中,会依照索引粒度(默认情况下,每次取8192行),按批次获取数据并进行处理。如果把一批数据的未压缩大小设为size,则整个写入过程遵循以下规则:

    (1)单个批次数据size<64KB:如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size>=64KB时,生成下一个压缩数据块。

    (2)单个批次数据64KB<=size<=1MB:如果单个批次数据大小恰好在64KB与1MB之间,则直接生成下一个压缩数据块。

    (3)单个批次数据size>1MB:如果单个批次数据直接超过1MB,则首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。此时,会出现一个批次数据生成多个压缩数据块的情况。

    经过上述的介绍后我们知道,一个.bin文件是由1至多个压缩数据块组成的,每个压缩块大小在64KB~1MB之间。多个压缩数据块之间,按照写入顺序首尾相接,紧密地排列在一起。

    在.bin文件中引入压缩数据块的目的至少有以下两个:其一,虽然数据被压缩后能够有效减少数据大小,降低存储空间并加速数据传输效率。其二,通过压缩数据块,可以在不读取整个.bin文件的情况下将读取粒度降低到压缩数据块级别,从而进一步缩小数据读取的范围。

    4.6 数据标记

    4.6.1 数据标记的生成规则

    为了能够与数据衔接,数据标记文件也与.bin文件一一对应。即每一个列字段[Column].bin文件都有一个与之对应的[Column].mrk数据标记文件,用于记录数据在.bin文件中的偏移量信息。

    一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。它们分别表示在此段数据区间内,在对应的.bin压缩文件中,压缩数据块的起始偏移量;以及将该数据压缩块解压后,其未压缩数据的起始偏移量。.mrk文件内标记数据的示意。

    4.6.2 数据标记的工作方式

    MergeTree在读取数据时,必须通过标记数据的位置信息才能够找到所需要的数据。整个查找过程大致可以分为读取压缩数据块和读取数据两个步骤。为了便于解释,这里继续使用测试表hits_v1中的真实数据进行说明。

    首先,左侧的标记数据做一番解释说明。JavaEnable字段的数据类型为UInt8,所以每行数值占用1字节。而hits_v1数据表的index_granularity粒度为8192,所以一个索引片段的数据大小恰好是8192B。压缩数据块的生成规则,如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size>=64KB时,生成下一个压缩数据块。因此在JavaEnable的标记文件中,每8行标记数据对应1个压缩数据块(1B*8192=8192B,64KB=65536B,65536/8192=8)。其左侧的标记数据中,8行数据的压缩文件偏移量都是相同的,因为这8行标记都指向了同一个压缩数据块。而在这8行的标记数据中,它们的解压缩数据块中的偏移量,则依次按照8192B(每行数据1B,每一个批次8192行数据)累加,当累加达到65536(64KB)时则置0。因为根据规则,此时会生成下一个压缩数据块。

    理解了上述标记数据之后,接下来就开始介绍MergeTree具体是如何定位压缩数据块并读取数据的。

    (1)读取压缩数据块:在查询某一列数据时,MergeTree无须一次性加载整个.bin文件,而是可以根据需要,只加载特定的压缩数据块。而这项特性需要借助标记文件中所保存的压缩文件中的偏移量。

    (2)读取数据:在读取解压后的数据时,MergeTree并不需要一次性扫描整段解压数据,它可以根据需要,以index_granularity的粒度加载特定的一小段。为了实现这项特性,需要借助标记文件中保存的解压数据块中的偏移量。

    4.7 对于分区、索引、标记和压缩数据的协同总结

    4.7.1 写入过程

    数据写入的第一步是生成分区目录,伴随着每一批数据的写入,都会生成一个新的分区目录。在后续的某一时刻,属于相同分区的目录会依照规则合并到一起;接着,按照index_granularity索引粒度,会分别生成primary.idx一级索引(如果声明了二级索引,还会创建二级索引文件)、每一个列字段的.mrk数据标记和.bin压缩数据文件。

    4.7.2 查询过程

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

    4.7.3 数据标记与压缩数据块的对应关系

    1.多对一

    多个数据标记对应一个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size小于64KB时,会出现这种对应关系。

    以hits_v1测试表的JavaEnable字段为例。JavaEnable数据类型为UInt8,大小为1B,则一个间隔内数据大小为8192B。所以在此种情形下,每8个数据标记会对应同一个压缩数据块,如图6-22所示。

    2.一对一

    一个数据标记对应一个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size大于等于64KB且小于等于1MB时,会出现这种对应关系。

    3.一对多

    以hits_v1测试表的URL字段为例。URL数据类型为String,大小根据实际内容而定。如图6-24所示,编号45的标记对应了2个压缩数据块。

    一个数据标记对应多个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size直接大于1MB时,会出现这种对应关系。

    第5章 MergeTree系列表引擎

    除了基础表引擎MergeTree之外,常用的表引擎还有ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree和VersionedCollapsingMergeTree。每一种合并树的变种,在继承了基础MergeTree的能力之后,又增加了独有的特性。其名称中的“合并”二字奠定了所有类型MergeTree的基因,它们的所有特殊逻辑,都是在触发合并的过程中被激活的。在本章后续的内容中,会逐一介绍它们的特点以及使用方法。

    5.1 MergeTree

    5.1.1 数据TTL

    TTL即Time To Live,顾名思义,它表示数据的存活时间。在MergeTree中,可以为某个列字段或整张表设置TTL。当时间到达时,如果是列字段级别的TTL,则会删除这一列的数据;如果是表级别的TTL,则会删除整张表的数据;如果同时设置了列级别和表级别的TTL,则会以先到期的那个为主。

    1.列级别TTL

    如果想要设置列级别的TTL,则需要在定义表字段的时候,为它们声明TTL表达式,主键字段不能被声明TTL。以下面的语句为例:

    CREATE TABLE ttl_table_v1(
        id String,
        create_time DateTime,
        code String TTL create_time + interval 10 SECOND,
        type UInt8 TTL create_time + interval 10 SECOND
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(create_time)
    ORDER BY id
    

    其中,create_time是日期类型,列字段code与type均被设置了TTL,它们的存活时间是在create_time的取值基础之上向后延续10秒。

    INSERT INTO TABLE ttl_table_v1 VALUES('A000',now(),'C1',1),
    ('A000',now() + INTERVAL 10 MINUTE,'C1',1)
    SELECT * FROM ttl_table_v1
    ┌─id───┬─────create_time──┬─code─┬─type─┐
    │ A000  │ 2019-06-12 22:49:00    │ C1    │     1 │
    │ A000  │ 2019-06-12 22:59:00    │ C1    │     1 │
    └────┴───────────────┴────┴─────┘
    
    optimize TABLE ttl_table_v1 FINAL  --接着心中默数10秒,然后执行optimize命令强制触发TTL清理:
    

    再次查询ttl_table_v1则能够看到,由于第一行数据满足TTL过期条件(当前系统时间>=create_time+10秒),它们的code和type列会被还原为数据类型的默认值:

    ┌─id───┬───────create_time─┬─code─┬─type─┐
    │ A000  │ 2019-06-12 22:49:00    │       │     0 │
    │ A000  │ 2019-06-12 22:59:00    │ C1    │     1 │
    └─────┴───────────────┴─────┴─────┘
    

    如果想要修改列字段的TTL,或是为已有字段添加TTL,则可以使用ALTER语句,示例如下:

    ALTER TABLE ttl_table_v1 MODIFY COLUMN code String TTL create_time + INTERVAL 1 DAY
    

    列级别TTL目前也没有取消的方法。

    2.表级别TTL

    如果想要为整张数据表设置TTL,需要在MergeTree的表参数中增加TTL表达式,例如下面的语句:

    CREATE TABLE ttl_table_v2(
        id String,
        create_time DateTime,
        code String TTL create_time + INTERVAL 1 MINUTE,
        type UInt8
    )ENGINE = MergeTree
    PARTITION BY toYYYYMM(create_time)
    ORDER BY create_time
    TTL create_time + INTERVAL 1 DAY
    

    ttl_table_v2整张表被设置了TTL,当触发TTL清理时,那些满足过期时间的数据行将会被整行删除。同样,表级别的TTL也支持修改,修改的方法如下:

    ALTER TABLE ttl_table_v2 MODIFY TTL create_time + INTERVAL 3 DAY
    

    表级别TTL目前也没有取消的方法。

    5.2 ReplacingMergeTree

    在一定程度上解决了重复数据的问题

    创建一张ReplacingMergeTree表的方法与创建普通MergeTree表无异,只需要替换Engine:

    ENGINE = ReplacingMergeTree(ver)
    

    其中,ver是选填参数,会指定一个UInt*、Date或者DateTime类型的字段作为版本号。这个参数决定了数据去重时所使用的算法。键ORDER BY所声明的表达式是后续作为判断数据是否重复的依据

    CREATE TABLE replace_table(
        id String,
        code String,
        create_time DateTime
    )ENGINE = ReplacingMergeTree()
    PARTITION BY toYYYYMM(create_time)
    ORDER BY (id,code)
    PRIMARY KEY id
    
    ┌─id───┬─code─┬───────create_time─┐
    │ A001  │ C1    │ 2019-05-10 17:00:00    │
    │ A001  │ C1    │ 2019-05-11 17:00:00    │
    │ A001  │ C100  │ 2019-05-12 17:00:00    │
    │ A001  │ C200  │ 2019-05-13 17:00:00    │
    │ A002  │ C2    │ 2019-05-14 17:00:00    │
    │ A003  │ C3    │ 2019-05-15 17:00:00    │
    └─────┴─────┴───────────────┘
    
    optimize TABLE replace_table FINAL  --optimize强制触发合并后 ,保留分组内的最后一条
    
    ┌─id───┬─code─┬──────create_time─┐  --将其余重复的数据删除:
    │ A001  │ C1    │ 2019-05-11 17:00:00   │
    │ A001  │ C100  │ 2019-05-12 17:00:00   │
    │ A001  │ C200  │ 2019-05-13 17:00:00   │
    │ A002  │ C2    │ 2019-05-14 17:00:00   │
    │ A003  │ C3    │ 2019-05-15 17:00:00   │
    └────┴────┴──────────────┘
    
    INSERT INTO TABLE replace_table VALUES('A001','C1','2019-08-10 17:00:00')
    
    ┌─id───┬─code─┬─────────create_time─┐
    │ A001   │ C1   │ 2019-08-22 17:00:00        │
    └─────┴────┴─────────────────┘
    ┌─id──┬─code─┬─────────create_time─┐
    │ A001  │ C1    │ 2019-05-11 17:00:00        │
    │ A001  │ C100  │ 2019-05-12 17:00:00        │
    │ A001  │ C200  │ 2019-05-13 17:00:00        │
    │ A002  │ C2    │ 2019-05-14 17:00:00        │
    │ A003  │ C3    │ 2019-05-15 17:00:00        │
    └────┴──────┴─────────────────┘
    

    可以看到A001:C1依然出现了重复, 是因为ReplacingMergeTree是以分区为单位删除重复数据的。只有在相同的数据分区内重复的数据才可以被删除,而不同数据分区之间的重复数据依然不能被剔除。这就是上面说ReplacingMergeTree只是在一定程度上解决了重复数据问题的原因

    现在接着说明ReplacingMergeTree版本号的用法。以下面的语句为例

    CREATE TABLE replace_table_v(
        id String,
        code String,
        create_time DateTime
    )ENGINE = ReplacingMergeTree(create_time)
    PARTITION BY toYYYYMM(create_time)
    ORDER BY id
    
    ┌─id──┬─code──┬───────────create_time─┐  --replace_table_v基于id字段去重,并且使用create_time字段作为版本号
    │ A001  │ C1     │ 2019-05-10 17:00:00           │
    │ A001  │ C1     │ 2019-05-25 17:00:00           │
    │ A001  │ C1     │ 2019-05-13 17:00:00           │
    └────┴─────┴───────────────────┘
    
    ┌─id────┬─code─┬──────────create_time─┐  --会保留同一组数据内create_time时间最长的那一行
    │ A001     │ C1   │ 2019-05-25 17:00:00          │
    └──────┴────┴──────────────────┘
    
    • (1)使用ORBER BY排序键作为判断重复数据的唯一键。
    • (2)只有在合并分区的时候才会触发删除重复数据的逻辑。
    • (3)以数据分区为单位删除重复数据。当分区合并时,同一分区内的重复数据会被删除;不同分区之间的重复数据不会被删除。
    • (4)在进行数据去重时,因为分区内的数据已经基于ORBER BY进行了排序,所以能够找到那些相邻的重复数据。
    • (5)数据去重策略有两种:
      ·如果没有设置ver版本号,则保留同一组重复数据中的最后一行。
      ·如果设置了ver版本号,则保留同一组重复数据中ver字段取值最大的那一行。

    5.3 SummingMergeTree

    假设有这样一种查询需求:终端用户只需要查询数据的汇总结果,不关心明细数据,并且数据的汇总条件是预先明确的(GROUP BY条件明确,且不会随意改变)。

    SummingMergeTree就是为了应对这类查询场景而生的。顾名思义,它能够在合并分区的时候按照预先定义的条件聚合汇总数据,将同一分组下的多行数据汇总合并成一行,这样既减少了数据行,又降低了后续汇总查询的开销。

    ENGINE = SummingMergeTree((col1,col2,…))  --ENGINE = SummingMergeTree((col1,col2,…))
    

    其中,col1、col2为columns参数值,这是一个选填参数,用于设置除主键外的其他数值类型字段,以指定被SUM汇总的列字段。如若不填写此参数,则会将所有非主键的数值类型字段进行SUM汇总。接来下用一组示例说明它的使用方法:

    CREATE TABLE summing_table(
        id String,
        city String,
        v1 UInt32,
        v2 Float64,
        create_time DateTime
    )ENGINE = SummingMergeTree()
    PARTITION BY toYYYYMM(create_time)
    ORDER BY (id, city)
    PRIMARY KEY id
    
    ┌─id──┬─city───┬─v1─┬─v2─┬────────create_time─┐
    │ A001  │ wuhan    │ 10  │ 20 │ 2019-08-10 17:00:00      │
    │ A001  │ wuhan    │ 20  │ 30 │ 2019-08-20 17:00:00      │
    │ A001  │ zhuhai   │ 20  │ 30 │ 2019-08-10 17:00:00      │
    └─────┴───────┴───┴───┴────────────────┘
    ┌─id──┬─city───┬─v1─┬─v2─┬────────create_time─┐
    │ A001  │ wuhan    │ 10 │ 20  │ 2019-02-10 09:00:00     │
    └─────┴───────┴───┴───┴───────────────┘
    ┌─id──┬─city───┬─v1─┬─v2─┬────────create_time─┐
    │ A002  │ wuhan    │ 60  │ 50  │ 2019-10-10 17:00:00     │
    └────┴──────┴───┴───┴───────────────┘
    
    optimize TABLE summing_table FINAL  --执行optimize强制进行触发和合并操作:
    
    ┌─id──┬─city───┬─v1─┬─v2─┬─────────create_time─┐
    │ A001  │ wuhan    │  30  │  50  │ 2019-08-10 17:00:00       │
    │ A001  │ zhuhai   │  20  │  30  │ 2019-08-10 17:00:00       │
    └─────┴──────┴────┴────┴─────────────────┘
    ┌─id──┬─city───┬─v1─┬─v2─┬─────────create_time─┐
    │ A001  │ wuhan    │  10  │  20 │ 2019-02-10 09:00:00       │
    └────┴──────┴────┴────┴─────────────────┘
    ┌─id──┬─city───┬─v1─┬─v2─┬─────────create_time─┐
    │ A002  │ wuhan    │  60  │  50  │ 2019-10-10 17:00:00       │
    └────┴──────┴────┴────┴─────────────────┘
    

    至此能够看到,在第一个分区内,同为A001:wuhan的两条数据汇总成了一行。其中,v1和v2被SUM汇总,不在汇总字段之列的create_time则选取了同组内第一行数据的取值。而不同分区之间,数据没有被汇总合并

    • (1)用ORBER BY排序键作为聚合数据的条件Key。
    • (2)只有在合并分区的时候才会触发汇总的逻辑。
    • (3)以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合Key相同的数据会被合并汇总,而不同分区之间的数据则不会被汇总。
    • (4)如果在定义引擎时指定了columns汇总列(非主键的数值类型字段),则SUM汇总这些列字段;如果未指定,则聚合所有非主键的数值类型字段。
    • (5)在进行数据汇总时,因为分区内的数据已经基于ORBER BY排序,所以能够找到相邻且拥有相同聚合Key的数据。
    • (6)在汇总数据时,同一分区内,相同聚合Key的多行数据会合并成一行。其中,汇总字段会进行SUM计算;对于那些非汇总字段,则会使用第一行数据的取值。

    5.4 CollapsingMergeTree

    CollapsingMergeTree就是一种通过以增代删的思路,支持行级数据修改和删除的表引擎。它通过定义一个sign标记位字段,记录数据行的状态。如果sign标记为1,则表示这是一行有效的数据;如果sign标记为-1,则表示这行数据需要被删除。当CollapsingMergeTree分区合并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。这种1和-1相互抵消的操作,犹如将一张瓦楞纸折叠了一般。这种直观的比喻,想必也正是折叠合并树(CollapsingMergeTree)名称的由来

    声明CollapsingMergeTree的方式如下:

    ENGINE = CollapsingMergeTree(sign)
    

    其中,sign用于指定一个Int8类型的标志位字段。一个完整的使用示例如下所示:

    CREATE TABLE collpase_table(
        id String,
        code Int32,
        create_time DateTime,
        sign Int8
    )ENGINE = CollapsingMergeTree(sign)
    PARTITION BY toYYYYMM(create_time)
    ORDER BY id
    

    与其他的MergeTree变种引擎一样,CollapsingMergeTree同样是以ORDER BY排序键作为后续判断数据唯一性的依据。按照之前的介绍,对于上述collpase_table数据表而言,除了常规的新增数据操作之外,还能够支持两种操作。

    其一,修改一行数据:

    --修改前的源数据, 它需要被修改
    INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1)
     
    --镜像数据, ORDER BY字段与源数据相同(其他字段可以不同),sign取反为-1,它会和源数据折叠
    INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1)
     
    --修改后的数据 ,sign为1
    INSERT INTO TABLE collpase_table VALUES('A000',120,'2019-02-20 00:00:00',1)
    

    其二,删除一行数据:

    --修改前的源数据, 它需要被删除
    INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1)
     
    --镜像数据, ORDER BY字段与源数据相同, sign取反为-1, 它会和源数据折叠
    INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1)
    

    CollapsingMergeTree在折叠数据时,遵循以下规则。

    1. ·如果sign=1比sign=-1的数据多一行,则保留最后一行sign=1的数据。
    2. ·如果sign=-1比sign=1的数据多一行,则保留第一行sign=-1的数据。
    3. ·如果sign=1和sign=-1的数据行一样多,并且最后一行是sign=1,则保留第一行sign=-1和最后一行sign=1的数据。
    4. ·如果sign=1和sign=-1的数据行一样多,并且最后一行是sign=-1,则什么也不保留。

    在使用CollapsingMergeTree的时候,还有几点需要注意。

    (1)折叠数据并不是实时触发的,和所有其他的MergeTree变种表引擎一样,这项特性也只有在分区合并的时候才会体现。所以在分区合并之前,用户还是会看到旧的数据。解决这个问题的方式有两种。

    • ·在查询数据之前,使用optimize TABLE table_name FINAL命令强制分区合并,但是这种方法效率极低,在实际生产环境中慎用。

    • ·需要改变我们的查询方式。以collpase_table举例,如果原始的SQL如下所示:

      SELECT id,SUM(code * sign),COUNT(code * sign),AVG(code * sign),uniq(code * sign)
          FROM collpase_table
          GROUP BY id
          HAVING SUM(sign) > 0
      
    --先写入sign=1   先写入sign=1,再写入sign=-1,则能够正常折叠:
    INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1)
    --再写入sign=-1
    INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1)
    
    --先写入sign=-1  先写入sign=-1,再写入sign=1,则不能够折叠:
    INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1)
    --再写入sign=1
    INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1)
    

    5.5 VersionedCollapsingMergeTree

    VersionedCollapsingMergeTree表引擎的作用与CollapsingMergeTree完全相同,它们的不同之处在于,VersionedCollapsingMergeTree对数据的写入顺序没有要求,在同一个分区内,任意顺序的数据都能够完成折叠操作

    在定义VersionedCollapsingMergeTree的时候,除了需要指定sign标记字段以外,还需要指定一个UInt8类型的ver版本号字段:

    ENGINE = VersionedCollapsingMergeTree(sign,ver)
    
    CREATE TABLE ver_collpase_table(
        id String,
        code Int32,
        create_time DateTime,
        sign Int8,
        ver UInt8
    )ENGINE = VersionedCollapsingMergeTree(sign,ver)
    PARTITION BY toYYYYMM(create_time)
    ORDER BY id
    

    VersionedCollapsingMergeTree是如何使用版本号字段的呢?其实很简单,在定义ver字段之后,VersionedCollapsingMergeTree会自动将ver作为排序条件并增加到ORDER BY的末端。以上面的ver_collpase_table表为例,在每个数据分区内,数据会按照ORDER BY id,ver DESC排序。所以无论写入时数据的顺序如何,在折叠处理时,都能回到正确的顺序。

    第6章 其他常见类型表引擎

    6.1 外部存储类型

    6.1.1 HDFS

    首先需要关闭HDFS的Kerberos认证(因为HDFS表引擎目前还不支持Kerberos);接着在HDFS上创建用于存放文件的目录:

    hadoop fs -mkdir /clickhouse
    

    最后,在HDFS上给ClickHouse用户授权。例如,为默认用户clickhouse授权的方法如下:

    hadoop fs -chown -R clickhouse:clickhouse /clickhouse
    

    HDFS表引擎的定义方法如下:

    ENGINE = HDFS(hdfs_uri,format)
    
    • ·hdfs_uri表示HDFS的文件存储路径;
    • ·format表示文件格式(指ClickHouse支持的文件格式,常见的有CSV、TSV和JSON等)。
    CREATE TABLE hdfs_table1(
        id UInt32,
        code String,
        name String
    )ENGINE = HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table1','CSV')
    
    INSERT INTO hdfs_table1 SELECT number,concat('code',toString(number)),
    concat('n',toString(number)) FROM numbers(5)
    
    SELECT * FROM hdfs_table1
    ┌─id─┬─code─┬─name─┐
    │  0  │ code0  │ n0    │
    │  1  │ code1  │ n1    │
    │  2  │ code2  │ n2    │
    │  3  │ code3  │ n3    │
    │  4  │ code4  │ n4    │
    └───┴─────┴────┘
    

    接着再看看在HDFS上发生了什么变化。执行hadoop fs -cat查看文件:

    $ hadoop fs -cat /clickhouse/hdfs_table1
    0,"code0","n0"
    1,"code1","n1"
    2,"code2","n2"
    3,"code3","n3"
    4,"code4","n4"
    

    可以发现,通过HDFS表引擎,ClickHouse在HDFS的指定目录下创建了一个名为hdfs_table1的文件,并且按照CSV格式写入了数据。不过目前ClickHouse并没有提供删除HDFS文件的方法,即便将数据表hdfs_table1删除:

    DROP Table hdfs_table1
    

    在HDFS上文件依然存在:

    $ hadoop fs -ls /clickhouse
    Found 1 items
    -rwxrwxrwx   3 clickhouse clickhouse        /clickhouse/hdfs_table1
    

    接下来,介绍第二种形式的使用方法,这种形式类似Hive的外挂表,由其他系统直接将文件写入HDFS。通过HDFS表引擎的hdfs_uri和format参数分别与HDFS的文件路径、文件格式建立映射。其中,hdfs_uri支持以下几种常见的配置方法:

    1. ·绝对路径:会读取指定路径的单个文件,例如/clickhouse/hdfs_table1。
    2. ·通配符:匹配所有字符,例如路径为/clickhouse/hdfs_table/,则会读取/click-house/hdfs_table路径下的所有文件。
    3. ·?通配符:匹配单个字符,例如路径为/clickhouse/hdfs_table/organization_?.csv,则会读取/clickhouse/hdfs_table路径下与organization_?.csv匹配的文件,其中?代表任意一个合法字符。
    4. ·{M..N}数字区间:匹配指定数字的文件,例如路径为/clickhouse/hdfs_table/organization_{1..3}.csv,则会读取/clickhouse/hdfs_table/路径下的文件organization_1.csv、organization_2.csv和organization_3.csv。

    现在用一个具体示例验证表引擎的效果。首先,将事先准备好的3个CSV测试文件上传至HDFS的/clickhouse/hdfs_table2路径(用于测试的CSV文件,可以在本书的github仓库获取):

    --上传文件至HDFS
    $ hadoop fs -put /chbase/demo-data/ /clickhouse/hdfs_table2
    --查询路径
    $ hadoop fs -ls /clickhouse/hdfs_table2
    Found 3 items
    -rw-r--r--   3 hdfs clickhouse  /clickhouse/hdfs_table2/organization_1.csv
    -rw-r--r--   3 hdfs clickhouse  /clickhouse/hdfs_table2/organization_2.csv
    -rw-r--r--   3 hdfs clickhouse  /clickhouse/hdfs_table2/organization_3.csv
    

    接着,创建HDFS测试表:

    CREATE TABLE hdfs_table2(
        id UInt32,
        code String,
        name String
    ) ENGINE = HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/*','CSV')
    
    HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/*','CSV')   --*通配符:
    HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/organization_?.csv','CSV')  --?通配符:
    HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/organization_{1..3}.csv','CSV')  --{M..N}数字区间:
    

    6.1.2 MySQL

    MySQL表引擎可以与MySQL数据库中的数据表建立映射,并通过SQL向其发起远程查询,包括SELECT和INSERT,它的声明方式如下:

    ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause'])
    

    其中各参数的含义分别如下:

    1. ·host:port表示MySQL的地址和端口。
    2. ·database表示数据库的名称。
    3. ·table表示需要映射的表名称。
    4. ·user表示MySQL的用户名。
    5. ·password表示MySQL的密码。
    6. ·replace_query默认为0,对应MySQL的REPLACE INTO语法。如果将它设置为1,则会用REPLACE INTO代替INSERT INTO。
    7. ·on_duplicate_clause默认为0,对应MySQL的ON DUPLICATE KEY语法。如果需要使用该设置,则必须将replace_query设置成0。

    现在用一个具体的示例说明MySQL表引擎的用法。假设MySQL数据库已准备就绪,则使用MySQL表引擎与其建立映射:

    CREATE TABLE dolphin_scheduler_table(
        id UInt32,
        name String
    )ENGINE = MySQL('10.37.129.2:3306', 'escheduler', 't_escheduler_process_definition', 'root', '')
    

    创建成功之后,就可以通过这张数据表代为查询MySQL中的数据了,例如:

    SELECT * FROM dolphin_scheduler_table
    ┌─id─┬─name───┐
    │  1 │ 流程1      │
    │  2 │ 流程2      │
    │  3 │ 流程3      │
    └──┴────────┘
    
    INSERT INTO TABLE dolphin_scheduler_table VALUES (4,'流程4')  --接着,尝试写入数据:
    

    不过比较遗憾的是,目前MySQL表引擎不支持任何UPDATE和DELETE操作,如果有数据更新方面的诉求,可以考虑使用CollapsingMergeTree作为视图的表引擎。

    6.1.4 Kafka

    Kafka是大数据领域非常流行的一款分布式消息系统。Kafka表引擎能够直接与Kafka系统对接,进而订阅Kafka中的主题并实时接收消息数据。

    Kafka表引擎的声明方式如下所示:

    ENGINE = Kafka()
    SETTINGS
        kafka_broker_list = 'host:port,... ',
        kafka_topic_list = 'topic1,topic2,...',
        kafka_group_name = 'group_name',
        kafka_format = 'data_format'[,]
        [kafka_row_delimiter = 'delimiter_symbol']
        [kafka_schema = '']
        [kafka_num_consumers = N]
        [kafka_skip_broken_messages = N]
        [kafka_commit_every_batch = N]
    

    其中,带有方括号的参数表示选填项,现在依次介绍这些参数的作用。首先是必填参数:

    1. ·kafka_broker_list:表示Broker服务的地址列表,多个地址之间使用逗号分隔,例如'hdp1.nauu.com:6667,hdp2.nauu.com:6667'。
    2. ·kafka_topic_list:表示订阅消息主题的名称列表,多个主题之间使用逗号分隔,例如'topic1,topic2'。多个主题中的数据均会被消费。
    3. ·kafka_group_name:表示消费组的名称,表引擎会依据此名称创建Kafka的消费组。
    4. ·kafka_format:表示用于解析消息的数据格式,在消息的发送端,必须按照此格式发送消息。数据格式必须是ClickHouse提供的格式之一,例如TSV、JSONEachRow和CSV等。
    5. ·kafka_row_delimiter:表示判定一行数据的结束符,默认值为'\0'。
    6. ·kafka_schema:对应Kafka的schema参数。
    7. ·kafka_num_consumers:表示消费者的数量,默认值为1。表引擎会依据此参数在消费组中开启相应数量的消费者线程。在Kafka的主题中,一个Partition分区只能使用一个消费者。
    8. ·kafka_skip_broken_messages:当表引擎按照预定格式解析数据出现错误时,允许跳过失败的数据行数,默认值为0,即不允许任何格式错误的情形发生。在此种情形下,只要Kafka主题中存在无法解析的数据,数据表都将不会接收任何数据。如果将其设置为非0正整数,例如kafka_skip_broken_messages=10,表示只要Kafka主题中存在无法解析的数据的总数小于10,数据表就能正常接收消息数据,而解析错误的数据会被自动跳过。
    9. ·kafka_commit_every_batch:表示执行Kafka commit的频率,默认值为0,即当一整个Block数据块完全写入数据表后才执行Kafka commit。如果将其设置为1,则每写完一个Batch批次的数据就会执行一次Kafka commit(一次Block写入操作,由多次Batch写入操作组成)。

    除此之外,还有一些配置参数可以调整表引擎的行为。在默认情况下,Kafka表引擎每间隔500毫秒会拉取一次数据,时间由stream_poll_timeout_ms参数控制(默认500毫秒)。数据首先会被放入缓存,在时机成熟的时候,缓存数据会被刷新到数据表。

    触发Kafka表引擎刷新缓存的条件有两个,当满足其中的任意一个时,便会触发刷新动作:

    1. ·当一个数据块完成写入的时候(一个数据块的大小由kafka_max_block_size参数控制,默认情况下kafka_max_block_size=max_block_size=65536)。
    2. ·等待间隔超过7500毫秒,由stream_flush_interval_ms参数控制(默认7500 ms)。

    6.1.5 File

    File表引擎的声明方式如下所示:

    ENGINE = File(format)
    

    其中,format表示文件中的数据格式,其类型必须是ClickHouse支持的数据格式,例如TSV、CSV和JSONEachRow等。可以发现,在File表引擎的定义参数中,并没有包含文件路径这一项。所以,File表引擎的数据文件只能保存在config.xml配置中由path指定的路径下。

    每张File数据表均由目录和文件组成,其中目录以表的名称命名,而数据文件则固定以data.format命名,例如:

    <ch-path>/data/default/test_file_table/data.CSV
    

    创建File表目录和文件的方式有自动和手动两种。首先介绍自动创建的方式,即由File表引擎全权负责表目录和数据文件的创建:

    CREATE TABLE file_table (
        name String, 
        value UInt32
    ) ENGINE = File("CSV")
    

    当执行完上面的语句后,在/data/default路径下便会创建一个名为file_table的目录。此时在该目录下还没有数据文件,接着写入数据:

    INSERT INTO file_table VALUES ('one', 1), ('two', 2), ('three', 3)
    

    在数据写入之后,file_table目录下便会生成一个名为data.CSV的数据文件:

    # pwd
    /chbase/data/default/file_table
    # cat ./data.CSV 
    "one",1
    "two",2
    "three",3
    

    接下来介绍手动创建的形式,即表目录和数据文件由ClickHouse之外的其他系统创建,例如使用shell创建:

    //切换到clickhouse用户,以确保ClickHouse有权限读取目录和文件
    # su clickhouse
    //创建表目录
    # mkdir /chbase/data/default/file_table1
     
    //创建数据文件
    # mv /chbase/data/default/file_table/data.CSV /chbase/data/default/file_table1
    

    在表目录和数据文件准备妥当之后,挂载这张数据表:

    ATTACH TABLE file_table1(
        name String, 
        value UInt32
    )ENGINE = File(CSV)
    

    查询file_table1内的数据:

    SELECT * FROM file_table1
    ┌─name──┬─value─┐
    │ one    │     1   │
    │ two    │     2   │
    │ three  │     3   │
    └─────┴─────┘
    
    INSERT INTO file_table1 VALUES ('four', 4), ('five', 5)  --即便是手动创建的表目录和数据文件,仍然可以对数据表插入数据,例如:
    

    6.2 内存类型

    6.2.1 Memory

    Memory表的创建方法如下所示:

    CREATE TABLE memory_1 (
        id UInt64
    )ENGINE = Memory()
    

    6.2.2 Set

    Set表引擎是拥有物理存储的,数据首先会被写至内存,然后被同步到磁盘文件中。所以当服务重启时,它的数据不会丢失,当数据表被重新装载时,文件数据会再次被全量加载至内存。众所周知,在Set数据结构中,所有元素都是唯一的。Set表引擎具有去重的能力,在数据写入的过程中,重复的数据会被自动忽略。然而Set表引擎的使用场景既特殊又有限,它虽然支持正常的INSERT写入,但并不能直接使用SELECT对其进行查询,Set表引擎只能间接作为IN查询的右侧条件被查询使用。

    CREATE TABLE set_1 (
        id UInt8
    )ENGINE = Set()
    

    6.2.3 Join

    Join表引擎可以说是为JOIN查询而生的,它等同于将JOIN查询进行了一层简单封装。在Join表引擎的底层实现中,它与Set表引擎共用了大部分的处理逻辑,所以Join和Set表引擎拥有许多相似之处。例如,Join表引擎的存储也由[num].bin数据文件和tmp临时目录两部分组成;数据首先会被写至内存,然后被同步到磁盘文件。但是相比Set表引擎,Join表引擎有着更加广泛的应用场景,它既能够作为JOIN查询的连接表,也能够被直接查询使用。

    ENGINE = Join(join_strictness, join_type, key1[, key2, ...])
    

    其中,各参数的含义分别如下:

    1. ·join_strictness:连接精度,它决定了JOIN查询在连接数据时所使用的策略,目前支持ALL、ANY和ASOF三种类型。
    2. ·join_type:连接类型,它决定了JOIN查询组合左右两个数据集合的策略,它们所形成的结果是交集、并集、笛卡儿积或其他形式,目前支持INNER、OUTER和CROSS三种类型。当join_type被设置为ANY时,在数据写入时,join_key重复的数据会被自动忽略。
    3. ·join_key:连接键,它决定了使用哪个列字段进行关联。

    6.2.4 Buffer

    Buffer表引擎完全使用内存装载数据,不支持文件的持久化存储,所以当服务重启之后,表内的数据会被清空。Buffer表引擎不是为了面向查询场景而设计的,它的作用是充当缓冲区的角色。假设有这样一种场景,我们需要将数据写入目标MergeTree表A,由于写入的并发数很高,这可能会导致MergeTree表A的合并速度慢于写入速度(因为每一次INSERT都会生成一个新的分区目录)。此时,可以引入Buffer表来缓解这类问题,将Buffer表作为数据写入的缓冲区。数据首先被写入Buffer表,当满足预设条件时,Buffer表会自动将数据刷新到目标表。

    Buffer表引擎的声明方式如下所示:

    ENGINE = Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)
    

    其中,参数可以分成基础参数和条件参数两类,首先说明基础参数的作用:

    1. database:目标表的数据库。
    2. ·table:目标表的名称,Buffer表内的数据会自动刷新到目标表。
    3. ·num_layers:可以理解成线程数,Buffer表会按照num_layers的数量开启线程,以并行的方式将数据刷新到目标表,官方建议设为16。
    4. ·min_time和max_time:时间条件的最小和最大值,单位为秒,从第一次向表内写入数据的时候开始计算;
    5. 假设一张Buffer表的max_bytes=100000000(约100 MB),num_layers=16,那么这张Buffer表能够同时处理的最大数据量约是1.6 GB。

    6.3 接口类型

    有这么一类表引擎,它们自身并不存储任何数据,而是像黏合剂一样可以整合其他的数据表。在使用这类表引擎的时候,不用担心底层的复杂性,它们就像接口一样,为用户提供了统一的访问界面,所以我将它们归为接口类表引擎。

    6.3.1 Merge

    Merge表引擎就如同一层使用了门面模式的代理,它本身不存储任何数据,也不支持数据写入。它的作用就如其名,即负责合并多个查询的结果集。Merge表引擎可以代理查询任意数量的数据表,这些查询会异步且并行执行,并最终合成一个结果集返回。

    ENGINE = Merge(database, table_name) --其中:database表示数据库名称;table_name表示数据表的名称,它支持使用正则表达式,例如^test表示合并查询所有以test为前缀的数据表。
    
    CREATE TABLE test_table_all as test_table_2018 
    ENGINE = Merge(currentDatabase(), '^test_table_')
    

    6.3.2 Dictionary

    Dictionary表引擎是数据字典的一层代理封装,它可以取代字典函数,让用户通过数据表查询字典。字典内的数据被加载后,会全部保存到内存中,所以使用Dictionary表对字典性能不会有任何影响。声明Dictionary表的方式如下所示:

    ENGINE = Dictionary(dict_name)
    
    CREATE TABLE tb_test_flat_dict (  --其中,dict_name对应一个已被加载的字典名称,例如下面的例子:
        id UInt64, 
        code String,
        name String
    )Engine = Dictionary(test_flat_dict);
    
    SELECT * FROM tb_test_flat_dict  --tb_test_flat_dict等同于数据字典test_flat_dict的代理表,现在对它使用SELECT语句进行查询:
    ┌─id─┬─code──┬─name─┐
    │  1  │  a0001  │ 研发部 │
    │  2  │  a0002  │ 产品部 │
    │  3  │  a0003  │ 数据部 │
    │  4  │  a0004  │ 测试部 │
    └───┴─────┴────┘
    

    第7章 副本与分片

    10.4 数据分片

    10.4.1 集群的配置方式

    1.不包含副本的分片

    如果直接使用node标签定义分片节点,那么该集群将只包含分片,不包含副本。以下面的配置为例:

    <yandex>
        <!--自定义配置名,与config.xml配置的incl属性对应即可 -->
        <clickhouse_remote_servers>
                <shard_2><!--自定义集群名称-->
                    <node><!--定义ClickHouse节点-->
                        <host>ch5.nauu.com</host>
                        <port>9000</port>
                    <!--选填参数
                    <weight>1</weight>
                    <user></user>
                    <password></password>
                    <secure></secure>
                    <compression></compression>
                    -->
                </node>
                <node>
                        <host>ch6.nauu.com</host>
                        <port>9000</port>
                </node>
                </shard_2>
            ……
        </clickhouse_remote_servers>
    

    该配置定义了一个名为shard_2的集群,其包含了2个分片节点,它们分别指向了是CH5和CH6服务器。现在分别对配置项进行说明:

    1. ·shard_2表示自定义的集群名称,全局唯一,是后续引用集群配置的唯一标识。在一个配置文件内,可以定义任意组集群。
    2. ·node用于定义分片节点,不包含副本。
    3. ·host指定部署了ClickHouse节点的服务器地址。
    4. ·port指定ClickHouse服务的TCP端口。
    5. ·weight分片权重默认为1,在后续小节中会对其详细介绍。
    6. ·user为ClickHouse用户,默认为default。
    7. ·password为ClickHouse的用户密码,默认为空字符串。
    8. ·secure为SSL连接的端口,默认为9440。
    9. ·compression表示是否开启数据压缩功能,默认为true。
    2.自定义分片与副本
    1)不包含副本的分片
    <!-- 2个分片、0个副本 -->
    <sharding_simple> <!-- 自定义集群名称 -->
        <shard> <!-- 分片 -->
            <replica> <!-- 副本 -->
                <host>ch5.nauu.com</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <replica>
                <host>ch6.nauu.com</host>
                <port>9000</port>
            </replica>
        </shard>
    </sharding_simple>
    
    2)N个分片和N个副本
    <!-- 1个分片 1个副本-->
    <sharding_simple_1>
        <shard>
            <replica>
                <host>ch5.nauu.com</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>ch6.nauu.com</host>
                <port>9000</port>
            </replica>
        </shard>
    </sharding_simple_1>
    
    3)下面所示集群sharding_ha拥有2个分片,而每个分片拥有1个副本:
    <sharding_ha>
        <shard>
            <replica>
                <host>ch5.nauu.com</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>ch6.nauu.com</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <replica>
                <host>ch7.nauu.com</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>ch8.nauu.com</host>
                <port>9000</port>
            </replica>
        </shard>
    </sharding_ha>
    

    在完成上述配置之后,可以查询系统表验证集群配置是否已被加载:

    SELECT cluster, host_name FROM system.clusters
    ┌─cluster────────┬─host_name──┐
    │ shard_2              │ ch5.nauu.com  │
    │ shard_2              │ ch6.nauu.com  │
    │ sharding_simple      │ ch5.nauu.com  │
    │ sharding_simple      │ ch6.nauu.com  │
    │ sharding_simple_1    │ ch5.nauu.com  │
    │ sharding_simple_1    │ ch6.nauu.com  │
    └─────────────┴─────────┘
    

    10.4.2 基于集群实现分布式DDL

    在加入集群配置后,就可以使用新的语法实现分布式DDL执行了,其语法形式如下:

    CREATE/DROP/RENAME/ALTER TABLE  ON CLUSTER cluster_name
    

    其中,cluster_name对应了配置文件中的集群名称,ClickHouse会根据集群的配置信息顺藤摸瓜,分别去各个节点执行DDL语句。

    CREATE TABLE test_1_local ON CLUSTER shard_2(
        id UInt64
    --这里可以使用任意其他表引擎,
    )ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/test_1', '{replica}')
    ORDER BY id
    ┌─host──────┬─port─┬─status─┬─error─┬─num_hosts_active─┐
    │ ch6.nauu.com   │ 9000  │      0   │        │               0   │
    │ ch5.nauu.com   │ 9000  │      0   │        │               0   │
    └─────────┴────┴──────┴─────┴───────────┘
    

    如果要删除test_1_local,则执行下面的分布式DROP:

    DROP TABLE test_1_local ON CLUSTER shard_2
    ┌─host──────┬─port─┬─status─┬─error─┬─num_hosts_active─┐
    │ ch6.nauu.com   │ 9000  │      0   │        │               0   │
    │ ch5.nauu.com   │ 9000  │      0   │        │               0   │
    └─────────┴────┴──────┴─────┴───────────┘
    

    在CH5节点的config.xml配置中预先定义了分区01的宏变量:

    <macros>
        <shard>01</shard>
        <replica>ch5.nauu.com</replica>
    </macros>
    

    在CH6节点的config.xml配置中预先定义了分区02的宏变量:

    <macros>
        <shard>02</shard>
        <replica>ch6.nauu.com</replica>
    </macros>
    
    1.数据结构
    1)ZooKeeper内的节点结构

    在默认情况下,分布式DDL在ZooKeeper内使用的根路径为:

    /clickhouse/task_queue/ddl
    

    该路径由config.xml内的distributed_ddl配置指定:

    <distributed_ddl>
        <!-- Path in ZooKeeper to queue with DDL queries -->
        <path>/clickhouse/task_queue/ddl</path>
    </distributed_ddl>
    
    2)DDLLogEntry日志对象的数据结构

    在/query-[seq]下记录的日志信息由DDLLogEntry承载,它拥有如下几个核心属性:

    (1)query记录了DDL查询的执行语句,例如:

    query: DROP TABLE default.test_1_local ON CLUSTER shard_2
    

    (2)hosts记录了指定集群的hosts主机列表,集群由分布式DDL语句中的ON CLUSTER指定,例如:

    hosts: ['ch5.nauu.com:9000','ch6.nauu.com:9000']
    

    (3)initiator记录初始化host主机的名称,hosts主机列表的取值来自于初始化host节点上的集群,例如:

    initiator: ch5.nauu.com:9000
    
    2.分布式DDL的核心执行流程

    10.5 Distributed原理解析

    Distributed表引擎是分布式表的代名词,它自身不存储任何数据,而是作为数据分片的透明代理,能够自动路由数据至集群中的各个节点,所以Distributed表引擎需要和其他数据表引擎一起协同工作。

    从实体表层面来看,一张分片表由两部分组成:

    1. ·本地表:通常以_local为后缀进行命名。本地表是承接数据的载体,可以使用非Distributed的任意表引擎,一张本地表对应了一个数据分片。
    2. ·分布式表:通常以_all为后缀进行命名。分布式表只能使用Distributed表引擎,它与本地表形成一对多的映射关系,日后将通过分布式表代理操作多张本地表。

    10.5.1 定义形式

    Distributed表引擎的定义形式如下所示:

    ENGINE = Distributed(cluster, database, table [,sharding_key])
    
    CREATE TABLE test_shard_2_all ON CLUSTER sharding_simple (
        id UInt64
    )ENGINE = Distributed(sharding_simple, default, test_shard_2_local,rand())
    

    其中,各个参数的含义分别如下:

    1. ·cluster:集群名称,与集群配置中的自定义名称相对应。在对分布式表执行写入和查询的过程中,它会使用集群的配置信息来找到相应的host节点。
    2. ·database和table:分别对应数据库和表的名称,分布式表使用这组配置映射到本地表。
    3. ·sharding_key:分片键,选填参数。在数据写入的过程中,分布式表会依据分片键的规则,将数据分布到各个host节点的本地表。

    10.5.3 分片规则

    关于分片的规则这里将做进一步的展开说明。分片键要求返回一个整型类型的取值,包括Int系列和UInt系列。例如分片键可以是一个具体的整型列字段:

    --按照用户id的余数划分
    Distributed(cluster, database, table ,userid)
    --按照随机数划分
    Distributed(cluster, database, table ,rand())
    --按照用户id的散列值划分
    Distributed(cluster, database, table , intHash64(userid))
    

    10.5.4 分布式写入的核心流程

    在向集群内的分片写入数据时,通常有两种思路:一种是借助外部计算系统,事先将数据均匀分片,再借由计算系统直接将数据写入ClickHouse集群的各个本地表,如图10-15所示。

    第二种思路是通过Distributed表引擎代理写入分片数据的,接下来开始介绍数据写入的核心流程。

    为了便于理解整个过程,这里会将分片写入、副本复制拆分成两个部分进行讲解。在讲解过程中,会使用两个特殊的集群分别进行演示:第一个集群拥有2个分片和0个副本,通过这个示例向大家讲解分片写入的核心流程;第二个集群拥有1个分片和1个副本,通过这个示例向大家讲解副本复制的核心流程。

    1.将数据写入分片的核心流程

    10.5.5 分布式查询的核心流程

    1.多副本的路由规则

    在查询数据的时候,如果集群中的一个shard,拥有多个replica,那么Distributed表引擎需要面临副本选择的问题。它会使用负载均衡算法从众多replica中选择一个,而具体使用何种负载均衡算法,则由load_balancing参数控制:

    load_balancing = random/nearest_hostname/in_order/first_or_random
    
    1)random

    random是默认的负载均衡算法,正如前文所述,在ClickHouse的服务节点中,拥有一个全局计数器errors_count,当服务发生任何异常时,该计数累积加1。而random算法会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则在它们之中随机选择一个。

    2)nearest_hostname

    nearest_hostname可以看作random算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则选择集群配置中host名称与当前host最相似的一个。而相似的规则是以当前host名称为基准按字节逐位比较,找出不同字节数最少的一个,例如CH5-1-1和CH5-1-2.nauu.com有一个字节不同:

    3)in_order

    in_order同样可以看作random算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则按照集群配置中replica的定义顺序逐个选择。

    4)first_or_random

    first_or_random可以看作in_order算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,它首先会选择集群配置中第一个定义的replica,如果该replica不可用,则进一步随机选择一个其他的replica。

    第11章 管理与运维

    11.1 用户配置

    11.1.3 用户定义

    1.username

    username用于指定登录用户名,这是全局唯一属性。该属性比较简单,这里就不展开介绍了。

    2.password

    (1)明文密码:在使用明文密码的时候,直接通过password标签定义,例如下面的代码。

    <password>123</password>
    <password></password>   !--如果password为空,则表示免密码登录--
    

    (2)SHA256加密:在使用SHA256加密算法的时候,需要通过password_sha256_hex标签定义密码,例如下面的代码。

    <password_sha256_hex>a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3</password_sha256_hex>
    

    可以执行下面的命令获得密码的加密串,例如对明文密码123进行加密:

    # echo -n 123 | openssl dgst -sha256
    (stdin)= a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
    

    (3)double_sha1加密:在使用double_sha1加密算法的时候,则需要通过password_double_sha1_hex标签定义密码,例如下面的代码。

    <password_double_sha1_hex>23ae809ddacaf96af0fd78ed04b6a265e05aa257</password_double_sha1_hex>
    

    可以执行下面的命令获得密码的加密串,例如对明文密码123进行加密:

    # echo -n 123 | openssl dgst -sha1 -binary | openssl dgst -sha1
    (stdin)= 23ae809ddacaf96af0fd78ed04b6a265e05aa257
    
    
    3.networks
    4.profile

    用户所使用的profile配置,直接引用相应的名称即可,例如:

    <default>
        <profile>default</profile>
    </default>
    
    5.quota

    quota用于设置该用户能够使用的资源限额,可以理解成一种熔断机制。关于这方面的介绍将会在11.3节展开。

    <yandex>
        <profiles>
            ……
        </profiles>
        <users>
                <default><!—默认用户 -->
                ……
                </default>
            <user_plaintext>
                    <password>123</password>
                    <networks>
                        <ip>::/0</ip>
                    </networks>
                    <profile>normal_1</profile>
                    <quota>default</quota>
            </user_plaintext>
    

    由于配置了密码,所以在登录的时候需要附带密码参数:

    # clickhouse-client -h 10.37.129.10 -u user_plaintext --password 123
    Connecting to 10.37.129.10:9000 as user user_plaintext.
    

    11.2 权限管理

    11.2.1 访问权限

    1.网络访问权限

    (1)IP地址:直接使用IP地址进行设置。

    <ip>127.0.0.1</ip>
    

    (2)host主机名称:通过host主机名称设置。

    <host>ch5.nauu.com</host>
    

    (3)正则匹配:通过表达式来匹配host名称。

    <host>^ch\d.nauu.com$</host>
    
    2.数据库与字典访问权限

    在客户端连入服务之后,可以进一步限制某个用户数据库和字典的访问权限,它们分别通过allow_databases和allow_dictionaries标签进行设置。如果不进行任何定义,则表示不进行限制。现在继续在用户user_normal的定义中增加权限配置:

    <user_normal>
        ……
        <allow_databases>
            <database>default</database>
            <database>test_dictionaries</database>
        </allow_databases>
        <allow_dictionaries>
            <dictionary>test_flat_dict</dictionary>
        </allow_dictionaries>
    </user_normal>
    

    11.2.2 查询权限

    查询权限是整个权限体系的第二层防护,它决定了一个用户能够执行的查询语句。查询权限可以分成以下四类:

    1. ·读权限:包括SELECT、EXISTS、SHOW和DESCRIBE查询。
    2. ·写权限:包括INSERT和OPTIMIZE查询。
    3. ·设置权限:包括SET查询。
    4. ·DDL权限:包括CREATE、DROP、ALTER、RENAME、ATTACH、DETACH和TRUNCATE查询。

    上述这四类权限,通过以下两项配置标签控制:

    (1)readonly:读权限、写权限和设置权限均由此标签控制,它有三种取值。

    1. ·当取值为0时,不进行任何限制(默认值)。
    2. ·当取值为1时,只拥有读权限(只能执行SELECT、EXISTS、SHOW和DESCRIBE)。
    3. ·当取值为2时,拥有读权限和设置权限(在读权限基础上,增加了SET查询)。

    (2)allow_ddl:DDL权限由此标签控制,它有两种取值。

    1. ·当取值为0时,不允许DDL查询。
    2. ·当取值为1时,允许DDL查询(默认值)。

    现在继续用一个示例说明。与刚才的配置项不同,readonly和allow_ddl需要定义在用户profiles中,例如:

    <profiles>        
        <normal> <!-- 只有read读权限-->
            <readonly>1</readonly>
            <allow_ddl>0</allow_ddl>
        </normal>
        <normal_1> <!-- 有读和设置参数权限-->
            <readonly>2</readonly>
            <allow_ddl>0</allow_ddl>
        </normal_1>
    

    11.2.3 数据行级权限

    数据权限是整个权限体系中的第三层防护,它决定了一个用户能够看到什么数据。数据权限使用databases标签定义,它是用户定义中的一项选填设置。database通过定义用户级别的查询过滤器来实现数据的行级粒度权限,它的定义规则如下所示:

    <databases>
            <database_name><!--数据库名称-->
                <table_name><!--表名称-->
                    <filter> id < 10</filter><!--数据过滤条件-->
                </table_name>
        </database_name>
    

    其中,database_name表示数据库名称;table_name表示表名称;而filter则是权限过滤的关键所在,它等同于定义了一条WHERE条件子句,与WHERE子句类似,它支持组合条件。现在用一个示例说明。这里还是用user_normal,为它追加databases定义:

    <user_normal>
        ……
        <databases>
                <default><!--默认数据库-->
                        <test_row_level><!—表名称-->
                            <filter>id < 10</filter>
                        </test_row_level>
     
                    <!—支持组合条件 
                    <test_query_all>
                        <filter>id <= 100 or repo >= 100</filter>
                    </test_query_all> -->
                </default>
            </databases>
    

    11.3 熔断机制

    1.根据时间周期的累积用量熔断

    在这种方式下,系统资源的用量是按照时间周期累积统计的,当累积量达到阈值,则直到下个计算周期开始之前,该用户将无法继续进行操作。这种方式通过users.xml内的quotas标签来定义资源配额。以下面的配置为例:

    <quotas>
        <default> <!-- 自定义名称 -->
            <interval>
                <duration>3600</duration><!-- 时间周期 单位:秒 -->
                <queries>0</queries>
                <errors>0</errors>
                <result_rows>0</result_rows>
                <read_rows>0</read_rows>
                <execution_time>0</execution_time>
            </interval>
        </default>
    </quotas>
    

    其中,各配置项的含义如下:

    1. ·default:表示自定义名称,全局唯一。
    2. ·duration:表示累积的时间周期,单位是秒。
    3. ·queries:表示在周期内允许执行的查询次数,0表示不限制。
    4. ·errors:表示在周期内允许发生异常的次数,0表示不限制。
    5. ·result_row:表示在周期内允许查询返回的结果行数,0表示不限制。
    6. ·read_rows:表示在周期内在分布式查询中,允许远端节点读取的数据行数,0表示不限制。
    7. ·execution_time:表示周期内允许执行的查询时间,单位是秒,0表示不限制。
    2.根据单次查询的用量熔断

    首先介绍一组针对普通查询的熔断配置。

    (1)max_memory_usage:在单个ClickHouse服务进程中,运行一次查询限制使用的最大内存量,默认值为10 GB,其配置形式如下。

    <max_memory_usage>10000000000</max_memory_usage>
    

    (2)max_memory_usage_for_user:在单个ClickHouse服务进程中,以用户为单位进行统计,单个用户在运行查询时限制使用的最大内存量,默认值为0,即不做限制。

    (3)max_memory_usage_for_all_queries:在单个ClickHouse服务进程中,所有运行的查询累加在一起所限制使用的最大内存量,默认为0,即不做限制。

    (4)max_partitions_per_insert_block:在单次INSERT写入的时候,限制创建的最大分区个数,默认值为100个。如果超出这个阈值,将会出现如下异常:

    (5)max_rows_to_group_by:在执行GROUP BY聚合查询的时候,限制去重后聚合

    (6)group_by_overflow_mode:当max_rows_to_group_by熔断规则触发时,group_by_overflow_mode将会提供三种处理方式。

    (7)max_bytes_before_external_group_by:在执行GROUP BY聚合查询的时候,限制使用的最大内存量,默认值为0,即不做限制。当超过阈值时,聚合查询将会进一步借用本地磁盘。

    11.4 数据备份

    11.4.1 导出文件备份

    如果数据的体量较小,可以通过dump的形式将数据导出为本地文件。例如执行下面的语句将test_backup的数据导出:

    #clickhouse-client --query="SELECT * FROM test_backup" > /chbase/test_backup.tsv
    # cat /chbase/test_backup.tsv | clickhouse-client --query "INSERT INTO test_backup FORMAT TSV"   --将备份数据再次导入,则可以执行下面的语句:
    

    上述这种dump形式的优势在于,可以利用SELECT查询并筛选数据,然后按需备份。如果是备份整个表的数据,也可以直接复制它的整个目录文件,例如:

    # mkdir -p /chbase/backup/default/ & cp -r /chbase/data/default/test_backup /chbase/backup/default/
    

    11.4.2 通过快照表备份

    快照表实质上就是普通的数据表,它通常按照业务规定的备份频率创建,例如按天或者按周创建。所以首先需要建立一张与原表结构相同的数据表,然后再使用INSERT INTO SELECT句式,点对点地将数据从原表写入备份表。假设数据表test_backup需要按日进行备份,现在为它创建当天的备份表:

    CREATE TABLE test_backup_0206 AS test_backup
    

    有了备份表之后,就可以点对点地备份数据了,例如:

    INSERT INTO TABLE test_backup_0206 SELECT * FROM test_backup
    

    如果考虑到容灾问题,也可以将备份表放置在不同的ClickHouse节点上,此时需要将上述SQL语句改成远程查询的形式:

    INSERT INTO TABLE test_backup_0206 SELECT * FROM remote('ch5.nauu.com:9000', 'default', 'test_backup', 'default')
    

    11.4.3 按分区备份

    1.使用FREEZE备份

    FREEZE的完整语法如下所示:

    ALTER TABLE tb_name FREEZE PARTITION partition_expr
    

    分区在被备份之后,会被统一保存到ClickHouse根路径/shadow/N子目录下。其中,N是一个自增长的整数,它的含义是备份的次数(FREEZE执行过多少次),具体次数由shadow子目录下的increment.txt文件记录。而分区备份实质上是对原始目录文件进行硬链接操作,所以并不会导致额外的存储空间。整个备份的目录会一直向上追溯至data根路径的整个链路:

    /data/[database]/[table]/[partition_folder]
    

    例如执行下面的语句,会对数据表partition_v2的201908分区进行备份:

    :) ALTER TABLE partition_v2 FREEZE PARTITION 201908
    

    进入shadow子目录,即能够看到刚才备份的分区目录:

    # pwd
    /chbase/data/shadow/1/data/default/partition_v2
    # ll
    total 4
    drwxr-x---. 2 clickhouse clickhouse 4096 Sep  1 00:22 201908_5_5_0
    

    对于备份分区的还原操作,则需要借助ATTACH装载分区的方式来实现。这意味着如果要还原数据,首先需要主动将shadow子目录下的分区文件复制到相应数据表的detached目录下,然后再使用ATTACH语句装载。

    11.5 服务监控

    11.5.1 系统表

    1.metrics

    metrics表用于统计ClickHouse服务在运行时,当前正在执行的高层次的概要信息,包括正在执行的查询总次数、正在发生的合并操作总次数等。该系统表的查询方法如下所示:

    SELECT * FROM system.metrics LIMIT 5
    ┌─metric──────┬─value─┬─description─────────────────────┐
    │ Query           │     1  │ Number of executing queries                     │
    │ Merge           │     0  │ Number of executing background merges           │
    │ PartMutation    │     0  │ Number of mutations (ALTER DELETE/UPDATE)       │
    │ ReplicatedFetch │     0  │ Number of data parts being fetched from replica │
    │ ReplicatedSend  │     0  │ Number of data parts being sent to replicas     │
    └──────────┴─────┴─────────────────────────────┘
    
    2.events

    events用于统计ClickHouse服务在运行过程中已经执行过的高层次的累积概要信息,包括总的查询次数、总的SELECT查询次数等,该系统表的查询方法如下所示:

    SELECT event, value FROM system.events LIMIT 5
    ┌─event─────────────────────┬─value─┐
    │ Query                                   │   165  │
    │ SelectQuery                             │    92  │
    │ InsertQuery                             │    14  │
    │ FileOpen                                │  3525  │
    │ ReadBufferFromFileDescriptorRead        │  6311  │
    └─────────────────────────┴─────┘
    
    3.asynchronous_metrics

    asynchronous_metrics用于统计ClickHouse服务运行过程时,当前正在后台异步运行的高层次的概要信息,包括当前分配的内存、执行队列中的任务数量等。该系统表的查询方法如下所示:

    SELECT * FROM system.asynchronous_metrics LIMIT 5
    ┌─metric───────────────────────┬─────value─┐
    │ jemalloc.background_thread.run_interval      │            0   │
    │ jemalloc.background_thread.num_runs          │            0   │
    │ jemalloc.background_thread.num_threads       │            0   │
    │ jemalloc.retained                            │     79454208   │
    │ jemalloc.mapped                              │    531341312   │
    └────────────────────────────┴──────────┘
    

    11.5.2 查询日志

    查询日志目前主要有6种类型,它们分别从不同角度记录了ClickHouse的操作行为。所有查询日志在默认配置下都是关闭状态,需要在config.xml配置中进行更改,接下来分别介绍它们的开启方法。在配置被开启之后,ClickHouse会为每种类型的查询日志自动生成相应的系统表以供查询。

    1.query_log

    query_log是最常用的查询日志,它记录了ClickHouse服务中所有已经执行的查询记录,它的全局定义方式如下所示:

    <query_log>
        <database>system</database>
        <table>query_log</table>
        <partition_by>toYYYYMM(event_date)</partition_by>
        <!—刷新周期-->
        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
    </query_log>
    

    如果只需要为某些用户单独开启query_log,也可以在user.xml的profile配置中按照下面的方式定义:

    <log_queries> 1</log_queries>
    

    query_log开启后,即可以通过相应的系统表对记录进行查询:

    SELECT type,concat(substr(query,1,20),'...')query,read_rows, 
    query_duration_ms AS duration FROM system.query_log LIMIT 6
    ┌─type──────────┬─query───────────┬─read_rows─┬─duration─┐
    │ QueryStart           │ SELECT DISTINCT arra... │         0  │        0  │
    │ QueryFinish          │ SELECT DISTINCT arra... │      2432  │       11  │
    │ QueryStart           │ SHOW DATABASES...       │         0  │        0  │
    │ QueryFinish          │ SHOW DATABASES...       │         3  │        1  │
    │ ExceptionBeforeStart │ SELECT * FROM test_f... │         0  │        0  │
    │ ExceptionBeforeStart │ SELECT * FROM test_f... │         0  │        0  │
    └─────────────┴───────────────┴───────┴───────┘
    
    2.query_thread_log

    query_thread_log记录了所有线程的执行查询的信息,它的全局定义方式如下所示:

    <query_thread_log>
        <database>system</database>
        <table>query_thread_log</table>
        <partition_by>toYYYYMM(event_date)</partition_by>
        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
    </query_thread_log>
    

    同样,如果只需要为某些用户单独开启该功能,可以在user.xml的profile配置中按照下面的方式定义:

    <log_query_threads> 1</log_query_threads>
    

    query_thread_log开启后,即可以通过相应的系统表对记录进行查询:

    SELECT thread_name,concat(substr(query,1,20),'...')query,query_duration_ms AS duration,memory_usage AS memory FROM system.query_thread_log LIMIT 6
    ┌─thread_name───┬─query───────────┬─duration─┬─memory─┐
    │ ParalInputsProc │ SELECT DISTINCT arra... │        2   │ 210888  │
    │ ParalInputsProc │ SELECT DISTINCT arra... │        3   │ 252648  │
    │ AsyncBlockInput │ SELECT DISTINCT arra... │        3   │ 449544  │
    │ TCPHandler      │ SELECT DISTINCT arra... │       11   │      0  │
    │ TCPHandler      │ SHOW DATABASES...       │        2   │      0  │
    └──────────┴───────────────┴───────┴──────┘
    
    3.part_log

    part_log日志记录了MergeTree系列表引擎的分区操作日志,其全局定义方式如下所示:

    <part_log>
        <database>system</database>
        <table>part_log</table>
        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
    </part_log>
    

    part_log开启后,即可以通过相应的系统表对记录进行查询:

    SELECT event_type AS type,table,partition_id,event_date FROM system.part_log
    ┌─type────┬─table─────────────┬─partition_id─┬─event_date─┐
    │ NewPart    │ summing_table_nested_v1    │ 201908        │ 2020-01-29  │
    │ NewPart    │ summing_table_nested_v1    │ 201908        │ 2020-01-29  │
    │ MergeParts │ summing_table_nested_v1    │ 201908        │ 2020-01-29  │
    │ RemovePart │ ttl_table_v1               │ 201505        │ 2020-01-29  │
    │ RemovePart │ summing_table_nested_v1    │ 201908        │ 2020-01-29  │
    │ RemovePart │ summing_table_nested_v1    │ 201908        │ 2020-01-29  │
    └───────┴─────────────────┴─────────┴────────┘
    
    4.text_log

    text_log日志记录了ClickHouse运行过程中产生的一系列打印日志,包括INFO、DEBUG和Trace,它的全局定义方式如下所示:

    <text_log>
        <database>system</database>
        <table>text_log</table>
        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
    </text_log>
    

    text_log开启后,即可以通过相应的系统表对记录进行查询:

    SELECT thread_name,
    concat(substr(logger_name,1,20),'...')logger_name,
    concat(substr(message,1,20),'...')message 
    FROM system.text_log LIMIT 5
    ┌─thread_name──┬─logger_name───────┬─message────────────┐
    │ SystemLogFlush │ SystemLog (system.me... │ Flushing system log...     │
    │ SystemLogFlush │ SystemLog (system.te... │ Flushing system log...     │
    │ SystemLogFlush │ SystemLog (system.te... │ Creating new table s...    │
    │ SystemLogFlush │ system.text_log...      │ Loading data parts...      │
    │ SystemLogFlush │ system.text_log...      │ Loaded data parts (0...    │
    └──────────┴───────────────┴─────────────────┘
    
    5.metric_log

    metric_log日志用于将system.metrics和system.events中的数据汇聚到一起,它的全局定义方式如下所示:

    <metric_log>
            <database>system</database>
            <table>metric_log</table>
            <flush_interval_milliseconds>7500</flush_interval_milliseconds>
            <collect_interval_milliseconds>1000</collect_interval_milliseconds>
        </metric_log>
    

    其中,collect_interval_milliseconds表示收集metrics和events数据的时间周期。metric_log开启后,即可以通过相应的系统表对记录进行查询。

  • 相关阅读:
    Python中的生成器
    API测试的五大好处
    Python可变参数*args和**kwargs
    Python中的迭代器
    docker下php安装imagick扩展
    docker端口映射不生效
    mysql学习笔记(一)一条sql查询语句是如何执行的
    lavavel学习笔记(一)安装
    mysql学习笔记(四)索引
    mysql学习笔记(二)一条sql更新语句是如何执行的
  • 原文地址:https://www.cnblogs.com/yoyo1216/p/15538516.html
Copyright © 2020-2023  润新知