程序員必備技能之ClickHouse

 

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------mysql

ClickHouse是近年來備受關注的開源列式數據庫,主要用於數據分析(OLAP)領域。目前國內社區火熱,各個大廠紛紛跟進大規模使用:linux

今日頭條 內部用ClickHouse來作用戶行爲分析,內部一共幾千個ClickHouse節點,單集羣最大1200節點,總數據量幾十PB,日增原始數據300TB左右。git

騰訊 內部用ClickHouse作遊戲數據分析,而且爲之創建了一整套監控運維體系。github

攜程 內部從18年7月份開始接入試用,目前80%的業務都跑在ClickHouse上。天天數據增量十多億,近百萬次查詢請求。算法

快手 內部也在使用ClickHouse,存儲總量大約10PB, 天天新增200TB, 90%查詢小於3S。sql

在國外,Yandex內部有數百節點用於作用戶點擊行爲分析,CloudFlare、Spotify等頭部公司也在使用。數據庫

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------express

在社區方面,github star數目增速驚人。json

image-20191217212708825.png

在DB-engines排名上,以下圖中紅色曲線所示。ClickHouse開源時間雖短,可是增勢迅猛。 數組

image.png

爲什麼ClickHouse得到了如此普遍的關注,獲得了社區的青睞,也獲得了諸多大廠的應用呢????

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1 簡介

Clickhouse是一款分佈式實時分析型列式數據庫服務。高性能,開箱即用,企業特性支持。應用於流量分析,廣告營銷分析,行爲分析,人羣劃分,客戶畫像,敏捷BI,數據集市,網絡監控,分佈式服務和鏈路監控等業務場景。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

我相信您必定會關注以下內容:

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1 極致性能

充分發揮多核並行優點(SIMD高效指令集、向量化執行引擎)並藉助分佈式技術,加速計算提供實時分析能力。開源公開benchmark顯示比傳統方法快100~1000倍,提供50MB~200MB/s的高吞吐實時導入能力。

2 簡單靈活

提供完善SQL支持,上手十分簡單;提供json、map、array等靈活數據類型適配業務快速變化;同時支持近似計算、機率數據結構等應對海量數據處理。

3 低成本

藉助於精心設計的列存、高效的數據壓縮算法,提供高達10倍的壓縮比,大幅提高單機數據存儲和計算能力,大幅下降使用成本,是構建海量數據倉庫的絕佳方案。

4 架構靈活

支持單節點、單副本、多節點、多副本多種架構

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

簡單總結一下clickhouse的特色,方便你們快速瞭解clickhouse

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Ø  1.真正的面向列的DBMS

Ø  2.數據高效壓縮

Ø  3.磁盤存儲的數據

Ø  4.多核並行處理

Ø  5.在多個服務器上分佈式處理

Ø  6.SQL語法支持

Ø  7.向量化引擎

Ø  8.實時數據更新

Ø  9.索引

Ø  10.適合在線查詢

Ø  11.支持近似預估計算

Ø  12.支持嵌套的數據結構

Ø  13支持數組做爲數據類型

Ø  14.支持限制查詢複雜性以及配額

Ø  15.複製數據複製和對數據完整性的支持

---------------------------------------------------

Ø  1.不支持事物。

Ø  2.不支持Update/Delete操做。

Ø  3.支持有限操做系統。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2 再深刻一點

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2.1 存儲層

ClickHouse從OLAP場景需求出發,定製開發了一套全新的高效列式存儲引擎,而且實現了數據有序存儲、主鍵索引、稀疏索引、數據Sharding、數據Partitioning、TTL、主備複製等豐富功能。以上功能共同爲ClickHouse極速的分析性能奠基了基礎。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

列式存儲

與行存將每一行的數據連續存儲不一樣,列存將每一列的數據連續存儲。示例圖以下:

image.png

相比於行式存儲,列式存儲在分析場景下有着許多優良的特性。

1)如前所述,分析場景中每每須要讀大量行可是少數幾個列。在行存模式下,數據按行連續存儲,全部列的數據都存儲在一個block中,不參與計算的列在IO時也要所有讀出,讀取操做被嚴重放大。而列存模式下,只須要讀取參與計算的列便可,極大的減低了IO cost,加速了查詢。

2)同一列中的數據屬於同一類型,壓縮效果顯著。列存每每有着高達十倍甚至更高的壓縮比,節省了大量的存儲空間,下降了存儲成本。

3)更高的壓縮比意味着更小的data size,從磁盤中讀取相應數據耗時更短。

4)自由的壓縮算法選擇。不一樣列的數據具備不一樣的數據類型,適用的壓縮算法也就不盡相同。能夠針對不一樣列類型,選擇最合適的壓縮算法。

5)高壓縮比,意味着同等大小的內存可以存放更多數據,系統cache效果更好。

官方數據顯示,經過使用列存,在某些分析場景下,可以得到100倍甚至更高的加速效應

數據有序存儲

ClickHouse支持在建表時,指定將數據按照某些列進行sort by。

排序後,保證了相同sort key的數據在磁盤上連續存儲,且有序擺放。在進行等值、範圍查詢時,where條件命中的數據都緊密存儲在一個或若干個連續的Block中,而不是分散的存儲在任意多個Block, 大幅減小須要IO的block數量。另外,連續IO也可以充分利用操做系統page cache的預取能力,減小page fault。

主鍵索引

ClickHouse支持主鍵索引,它將每列數據按照index granularity(默認8192行)進行劃分,每一個index granularity的開頭第一行被稱爲一個mark行。主鍵索引存儲該mark行對應的primary key的值。

對於where條件中含有primary key的查詢,經過對主鍵索引進行二分查找,可以直接定位到對應的index granularity,避免了全表掃描從而加速查詢。

可是值得注意的是:ClickHouse的主鍵索引與MySQL等數據庫不一樣,它並不用於去重,即使primary key相同的行,也能夠同時存在於數據庫中。要想實現去重效果,須要結合具體的表引擎ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree實現,咱們會在將來的文章系列中再進行詳細解讀。

稀疏索引

ClickHouse支持對任意列建立任意數量的稀疏索引。其中被索引的value能夠是任意的合法SQL Expression,並不只僅侷限於對column value自己進行索引。之因此叫稀疏索引,是由於它本質上是對一個完整index granularity(默認8192行)的統計信息,並不會具體記錄每一行在文件中的位置。目前支持的稀疏索引類型包括:

  • minmax: 以index granularity爲單位,存儲指定表達式計算後的min、max值;在等值和範圍查詢中可以幫助快速跳過不知足要求的塊,減小IO。
  • set(max_rows):以index granularity爲單位,存儲指定表達式的distinct value集合,用於快速判斷等值查詢是否命中該塊,減小IO。
  • ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed):將string進行ngram分詞後,構建bloom filter,可以優化等值、like、in等查詢條件。
  • tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed): 與ngrambf_v1相似,區別是不使用ngram進行分詞,而是經過標點符號進行詞語分割。
  • bloom_filter([false_positive]):對指定列構建bloom filter,用於加速等值、like、in等查詢條件的執行。

數據Sharding

ClickHouse支持單機模式,也支持分佈式集羣模式。在分佈式模式下,ClickHouse會將數據分爲多個分片,而且分佈到不一樣節點上。不一樣的分片策略在應對不一樣的SQL Pattern時,各有優點。ClickHouse提供了豐富的sharding策略,讓業務能夠根據實際需求選用。

1) random隨機分片:寫入數據會被隨機分發到分佈式集羣中的某個節點上。

2) constant固定分片:寫入數據會被分發到固定一個節點上。

3)column value分片:按照某一列的值進行hash分片。

4)自定義表達式分片:指定任意合法表達式,根據表達式被計算後的值進行hash分片。

數據分片,讓ClickHouse能夠充分利用整個集羣的大規模並行計算能力,快速返回查詢結果。
更重要的是,多樣化的分片功能,爲業務優化打開了想象空間。好比在hash sharding的狀況下,JOIN計算可以避免數據shuffle,直接在本地進行local join; 支持自定義sharding,能夠爲不一樣業務和SQL Pattern定製最適合的分片策略;利用自定義sharding功能,經過設置合理的sharding expression能夠解決分片間數據傾斜問題等。

另外,sharding機制使得ClickHouse能夠橫向線性拓展,構建大規模分佈式集羣,從而具有處理海量數據的能力。

數據Partitioning

ClickHouse支持PARTITION BY子句,在建表時能夠指定按照任意合法表達式進行數據分區操做,好比經過toYYYYMM()將數據按月進行分區、toMonday()將數據按照周幾進行分區、對Enum類型的列直接每種取值做爲一個分區等。

數據Partition在ClickHouse中主要有兩方面應用:

  • 在partition key上進行分區裁剪,只查詢必要的數據。靈活的partition expression設置,使得能夠根據SQL Pattern進行分區設置,最大化的貼合業務特色。
  • 對partition進行TTL管理,淘汰過時的分區數據。

數據TTL

在分析場景中,數據的價值隨着時間流逝而不斷下降,多數業務出於成本考慮只會保留最近幾個月的數據,ClickHouse經過TTL提供了數據生命週期管理的能力。

ClickHouse支持幾種不一樣粒度的TTL:

1) 列級別TTL:當一列中的部分數據過時後,會被替換成默認值;當全列數據都過時後,會刪除該列。

2)行級別TTL:當某一行過時後,會直接刪除該行。

3)分區級別TTL:當分區過時後,會直接刪除該分區。

高吞吐寫入能力

ClickHouse採用類LSM Tree的結構,數據寫入後按期在後臺Compaction。經過類LSM tree的結構,ClickHouse在數據導入時所有是順序append寫,寫入後數據段不可更改,在後臺compaction時也是多個段merge sort後順序寫回磁盤。順序寫的特性,充分利用了磁盤的吞吐能力,即使在HDD上也有着優異的寫入性能。

官方公開benchmark測試顯示可以達到50MB-200MB/s的寫入吞吐能力,按照每行100Byte估算,大約至關於50W-200W條/s的寫入速度。

有限支持delete、update

在分析場景中,刪除、更新操做並非核心需求。ClickHouse沒有直接支持delete、update操做,而是變相支持了mutation操做,語法爲alter table delete where filter_expr, alter table update col=val where filter_expr

目前主要限制爲刪除、更新操做爲異步操做,須要後臺compation以後才能生效。

主備同步

ClickHouse經過主備複製提供了高可用能力,主備架構下支持無縫升級等運維操做。並且相比於其餘系統它的實現有着本身的特點:

1)默認配置下,任何副本都處於active模式,能夠對外提供查詢服務;

2)能夠任意配置副本個數,副本數量能夠從0個到任意多個;

3)不一樣shard能夠配置不提供副本個數,用於解決單個shard的查詢熱點問題;

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2.2 計算層

ClickHouse在計算層作了很是細緻的工做,竭盡所能榨乾硬件能力,提高查詢速度。它實現了單機多核並行、分佈式計算、向量化執行與SIMD指令、代碼生成等多種重要技術。

多核並行

ClickHouse將數據劃分爲多個partition,每一個partition再進一步劃分爲多個index granularity,而後經過多個CPU核心分別處理其中的一部分來實現並行數據處理。

在這種設計下,單條Query就能利用整機全部CPU。極致的並行處理能力,極大的下降了查詢延時。

分佈式計算

除了優秀的單機並行處理能力,ClickHouse還提供了可線性拓展的分佈式計算能力。ClickHouse會自動將查詢拆解爲多個task下發到集羣中,而後進行多機並行處理,最後把結果匯聚到一塊兒。

在存在多副本的狀況下,ClickHouse提供了多種query下發策略:

  • 隨機下發:在多個replica中隨機選擇一個;
  • 最近hostname原則:選擇與當前下發機器最相近的hostname節點,進行query下發。在特定的網絡拓撲下,能夠下降網絡延時。並且可以確保query下發到固定的replica機器,充分利用系統cache。
  • in order:按照特定順序逐個嘗試下發,當前一個replica不可用時,順延到下一個replica。
  • first or random:在In Order模式下,當第一個replica不可用時,全部workload都會積壓到第二個Replica,致使負載不均衡。first or random解決了這個問題:當第一個replica不可用時,隨機選擇一個其餘replica,從而保證其他replica間負載均衡。另外在跨region複製場景下,經過設置第一個replica爲本region內的副本,能夠顯著下降網絡延時。

向量化執行與SIMD

ClickHouse不只將數據按列存儲,並且按列進行計算。傳統OLTP數據庫一般採用按行計算,緣由是事務處理中以點查爲主,SQL計算量小,實現這些技術的收益不夠明顯。可是在分析場景下,單個SQL所涉及計算量可能極大,將每行做爲一個基本單元進行處理會帶來嚴重的性能損耗:

1)對每一行數據都要調用相應的函數,函數調用開銷佔比高;

2)存儲層按列存儲數據,在內存中也按列組織,可是計算層按行處理,沒法充分利用CPU cache的預讀能力,形成CPU Cache miss嚴重;

3)按行處理,沒法利用高效的SIMD指令;

ClickHouse實現了向量執行引擎(Vectorized execution engine),對內存中的列式數據,一個batch調用一次SIMD指令(而非每一行調用一次),不只減小了函數調用次數、下降了cache miss,並且能夠充分發揮SIMD指令的並行能力,大幅縮短了計算耗時。向量執行引擎,一般可以帶來數倍的性能提高。

動態代碼生成Runtime Codegen

在經典的數據庫實現中,一般對錶達式計算採用火山模型,也即將查詢轉換成一個個operator,好比HashJoin、Scan、IndexScan、Aggregation等。爲了鏈接不一樣算子,operator之間採用統一的接口,好比open/next/close。在每一個算子內部都實現了父類的這些虛函數,在分析場景中單條SQL要處理數據一般高達數億行,虛函數的調用開銷再也不能夠忽略不計。另外,在每一個算子內部都要考慮多種變量,好比列類型、列的size、列的個數等,存在着大量的if-else分支判斷致使CPU分支預測失效。

ClickHouse實現了Expression級別的runtime codegen,動態地根據當前SQL直接生成代碼,而後編譯執行。以下圖例子所示,對於Expression直接生成代碼,不只消除了大量的虛函數調用(即圖中多個function pointer的調用),並且因爲在運行時表達式的參數類型、個數等都是已知的,也消除了沒必要要的if-else分支判斷。

image-20191216205308005.png

近似計算

近似計算以損失必定結果精度爲代價,極大地提高查詢性能。在海量數據處理中,近似計算價值更加明顯。

ClickHouse實現了多種近似計算功能:

  • 近似估算distinct values、中位數,分位數等多種聚合函數;
  • 建表DDL支持SAMPLE BY子句,支持對於數據進行抽樣處理;

複雜數據類型支持

ClickHouse還提供了array、json、tuple、set等複合數據類型,支持業務schema的靈活變動。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3 再近一點

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.1 安裝

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

clickhouse架構靈活,能夠單節點安裝,也能夠多節點安裝 !操做十分便捷, 多節點安裝,在多個機器上分別安裝Clickhouse,而後安裝按成之後作集羣配置便可

在虛擬機Linux01 上安裝:

1)安裝curl工具
yum install -y curl
2)添加clickhouse的yum鏡像
curl -s https://packagecloud.io/install/repositories/altinity/clickhouse/script.rpm.sh | sudo bash
3)檢查鏡像狀況
[root@doit04 yum.repos.d]# yum list | grep clickhouse                        
clickhouse-client.x86_64                    20.5.4.40-1.el7            @Altinity_clickhouse
clickhouse-common-static.x86_64             20.5.4.40-1.el7            @Altinity_clickhouse
4)安裝clickhouse的服務端和客戶端
yum install -y clickhouse-server clickhouse-client
5)啓動服務端
service clickhouse-server start 
6)啓動交互式客戶端
clickhouse-client  -m

 在虛擬機Linux02 上安裝:

1)安裝curl工具
yum install -y curl
2)添加clickhouse的yum鏡像
curl -s https://packagecloud.io/install/repositories/altinity/clickhouse/script.rpm.sh | sudo bash
3)檢查鏡像狀況
[root@doit04 yum.repos.d]# yum list | grep clickhouse                        
clickhouse-client.x86_64                    20.5.4.40-1.el7            @Altinity_clickhouse
clickhouse-common-static.x86_64             20.5.4.40-1.el7            @Altinity_clickhouse
4)安裝clickhouse的服務端和客戶端
yum install -y clickhouse-server clickhouse-client
5)啓動服務端
service clickhouse-server start 
6)啓動交互式客戶端
clickhouse-client  -m

......以此類推......

 

[root@linux01 flink-1.11.2]# clickhouse-client  -m
ClickHouse client version 20.8.3.18.
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 20.8.3 revision 54438.

linux01 :) 在此處操做clickhouse
SHOW DATABASES

┌─name───────────────────────────┐
│ _temporary_and_external_tables │
│ db_ch_mysql                    │
│ db_doit19                      │
│ default                        │
│ system                         │
└────────────────────────────────┘
CREATE DATABASE db1
Ok.
0 rows in set. Elapsed: 0.007 sec. 
USE  db1 ;

 create  table tb_t1 ... ;

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.2 引擎

表引擎(table engine)。若是對MySQL熟悉的話,或許你應該據說過InnoDB和MyISAM存儲引擎。不一樣的存儲引擎提供不一樣的存儲機制、索引方式、鎖定水平等功能,也能夠稱之爲表類型。ClickHouse提供了豐富的表引擎,這些不一樣的表引擎也表明着不一樣的表類型。好比數據表擁有何種特性、數據以何種形式被存儲以及如何被加載

做用

  • 決定表存儲在哪裏以及以何種方式存儲
  • 支持哪些查詢以及如何支持
  • 併發數據訪問
  • 索引的使用
  • 是否能夠執行多線程請求
  • 數據複製參數
引擎分類 引擎名稱
MergeTree系列 MergeTree 、ReplacingMergeTree 、SummingMergeTree 、 AggregatingMergeTree CollapsingMergeTree 、 VersionedCollapsingMergeTree 、GraphiteMergeTree
Log系列 TinyLog 、StripeLog 、Log
Integration Engines Kafka 、MySQL、ODBC 、JDBC、HDFS
Special Engines Distributed 、MaterializedView、 Dictionary 、Merge 、File、Null 、Set 、Join 、 URL View、Memory 、 Buffer

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

clickhouse提供了大量的引擎,下面咱們列舉兩個表明性的引擎示例 :

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.2.1 MergeTree引擎

MergeTree系列的表引擎是ClickHouse數據存儲功能的核心。它們提供了用於彈性和高性能數據檢索的大多數功能:列存儲,自定義分區,稀疏的主索引,輔助數據跳過索引等。

基本MergeTree表引擎能夠被認爲是單節點ClickHouse實例的默認表引擎,由於它在各類用例中通用且實用。

對於生產用途,ReplicatedMergeTree是必經之路,由於它爲常規MergeTree引擎的全部功能增長了高可用性。一個額外的好處是在數據提取時自動進行重複數據刪除,所以若是插入過程當中出現網絡問題,該軟件能夠安全地重試。

MergeTree系列的全部其餘引擎爲某些特定用例添加了額外的功能。一般,它是做爲後臺的其餘數據操做實現的。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster](
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2) 
ENGINE = MergeTree()ORDER BY expr[PARTITION BY expr][PRIMARY KEY expr][SAMPLE BY expr][TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...][SETTINGS name=value, ...]

VersionedCollapsingMergeTree 

這種引擎是以uid爲主鍵,以uid爲排序字段,咱們能夠保留或者刪除指定版本的數據

CREATE TABLE tb_vscmt
(
    uid UInt64,
    name String,
    age UInt8,
    sign Int8,
    version UInt8
)
ENGINE = VersionedCollapsingMergeTree(sign, version)
ORDER BY uid;

3.2.2 分佈式引擎

使用ReplicatedMergeTree和Distributed引擎構建咱們的分佈式表

-- 每一個機器都須要創建各自的replica table,也須要建Distributed table當作路由

create table dm.delphi_membership_properties_replica
(
  membership_id  int,  -- comment '會員id',
  membership_uid String, -- comment '會員uid',
  business_group_id int, -- comment '商戶id',
  business_group_uid String , --comment '商戶uid',
  business_group_name String, -- comment '商戶名',
  business_id Nullable(int), -- comment '門店id',
  business_uid Nullable(String), -- comment '門店uid',
  business_name Nullable(String), -- comment '門店name',
  membership_source String, -- comment '會員入會來源',
  created_at DateTime,
  calendar_date Date,
  last_visited_date Date, -- comment '最近一次訪問時間',
  membership_level int, -- comment '會員等級',
  customer_type String, -- comment '會員類型:新會員/忠誠會員/常來會員/淡忘會員/流失會員,根據最後一次訪問時間和商戶配置計算而來',
  visit_count int, -- comment '到訪次數',
  consumptions_count Nullable(int), -- comment '消費次數',
  consumptions_original_amount Nullable(Decimal128(2)), -- comment '消費總金額:原始金額',
  consumptions_amount Nullable(Decimal128(2)), -- comment '消費總金額:實付金額',
  average_consume Nullable(Decimal128(2)), -- comment '平均消費金額:原始金額/消費次數',
  account_id int, -- comment '用戶id',
  account_uid String, -- comment '用戶uid',
  account_phone String, -- comment '用戶手機',
  age Nullable(int), -- comment '年齡',
  birthday Nullable(String), -- comment '生日',
  birthday_month Nullable(int), -- comment '生日月份',
  birthday_day Nullable(int), -- comment '生日天',
  birthday_year Nullable(int), -- comment '生日年',
  zodiac String, -- comment '星座',
  name Nullable(String), -- comment '姓名',
  gender int, -- comment '性別',
  profession Nullable(String), -- comment '職業',
  country Nullable(String), -- comment '國家',
  province Nullable(String), -- comment '省份',
  city Nullable(String), -- comment '城市',
  region Nullable(String), -- comment '商圈',
  head_img_url Nullable(String), -- comment '頭像',
  wechat_name Nullable(String), -- comment '微信名',
  wechat_city Nullable(String), -- comment '微信城市',
  wechat_country Nullable(String), -- comment '微信國家',
  wechat_province Nullable(String), -- comment '微信省份',
  wechat_head_img_url Nullable(String), -- comment '微信頭像',
  wechat_groupid int, -- comment '微信組',
  wechat_remark Nullable(String), -- comment '微信備註'
  insert_time DateTime DEFAULT now(), -- 數據插入時間
  insert_date Date DEFAULT toDate(now()) -- 數據插入日期
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/delphi_membership_properties_replica', '{replica}')
order by (business_group_uid, calendar_date, created_at, membership_uid);

create table dm.delphi_membership_properties as dm.delphi_membership_properties_replica
ENGINE = Distributed(ck_cluster, dm, delphi_membership_properties_replica, rand());

delphi_membership_properties_replica是各個機器上的本地表,delphi_membership_properties是分佈式表,比對下兩個表的建立engine的區別。

ReplicatedMergeTree

在表引擎名稱上加上 Replicated 前綴,就表示是一種複製表。ReplicatedMergeTree 參數

  • zoo_path — ZooKeeper 中該表的路徑。
  • replica_name — ZooKeeper 中的該表所在的副本名稱。
 
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/delphi_membership_properties_replica', '{replica}')
order by (business_group_uid, calendar_date, created_at, membership_uid);

這些參數能夠包含宏替換的佔位符,即大括號的部分。它們會被替換爲配置文件裏 ‘macros’ 那部分配置的值

 
<yandex>
    <macros>
        <replica>172.31.59.118</replica>
        <shard>01</shard>
        <layer>01</layer>
    </macros>
</yandex>

「ZooKeeper 中該表的路徑」對每一個可複製表都要是惟一的。不一樣分片上的表要有不一樣的路徑。 這種狀況下,路徑包含下面這些部分:

  • /clickhouse/tables/ 是公共前綴,官方推薦。

  • {layer}-{shard} 是分片標識部分

  • table_name 是該表在 ZooKeeper 中的名稱。使其與 ClickHouse 中的表名相同比較好。 這裏它被明肯定義,跟 ClickHouse 表名不同,它並不會被 RENAME 語句修改

注意點:

  1. 副本是表級別的,不是整個服務器級的。因此,服務器裏能夠同時有複製表和非複製表。
  2. DDL語句只會在單個服務器上執行,不會被複制

 

Distributed

Distributed(logs, default, hits[, sharding_key])

分佈式引擎參數:服務器配置文件中的集羣名,遠程數據庫名,遠程表名,數據分片鍵(可選)。數據分片鍵的概念就是數據插入時是根據什麼原則分配到具體分片上的。

在上面的表結構中:

ENGINE = Distributed(ck_cluster, dm, delphi_membership_properties_replica, rand());

表示將會從ck_cluster集羣中dm.delphi_membership_properties_replica中讀取數據。

集羣的名稱是在集羣搭建時的metrika.xml文件中配置的,具體的能夠看集羣搭建部分的配置。能夠在配置中配置任意數量的集羣。

要查看集羣,可以使用「system.clusters」表。

clickhouse_system_clusters

經過分佈式引擎能夠像使用本地服務器同樣使用集羣。可是,集羣不是自動擴展的:必須編寫集羣配置到服務器配置文件中。

這樣咱們就可使用clickhouse建立分佈式表 ,實現分佈式數據庫功能, 而且表中的數據也支持數據備份

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.3 集成

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

clickhouse中實現不少集成引擎和表函數 ,clickhouse和其餘數據源集成十分方便

Function Description
file Creates a File-engine table.
merge Creates a Merge-engine table.
numbers Creates a table with a single column filled with integer numbers.
remote Allows you to access remote servers without creating a Distributed-engine table.
url Creates a Url-engine table.
mysql Creates a MySQL-engine table.
jdbc Creates a JDBC-engine table.
odbc Creates a ODBC-engine table.
hdfs Creates a HDFS-engine table.

以集成mysql爲例 ; 使用以下建表語句指定MySQL引擎,ck就能夠讀取mysql中數據

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
) ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause']);
CREATE TABLE hdfs_engine_table (name String, value UInt32) ENGINE=HDFS('hdfs://hdfs1:9000/other_storage', 'TSV')

這個例子是CK直接加載HDFS 上的結構化數據

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.4 函數

clickhouse中提供了大量的函數,而且還支持表函數 ,高階函數,鏈路函數,集合函數......爲咱們使用CK數據分析帶來了極大的便利!

3.4.1日期類函數

函數 用途 舉例 結果
toYear() 取日期或時間日期的年份 toYear(toDateTime(‘2018-12-11 11:12:13’)) toYear(toDate(‘2018-12-11’)) 返回 2018 返回 2018
toMonth() 取日期或時間日期的月份 toMonth(toDateTime(‘2018-12-11 11:12:13’)) toMonth(toDate(‘2018-12-11’)) 返回 12返回 12
toDayOfMonth() 取日期或時間日期的天(1-31) toMonth(toDayOfMonth(‘2018-12-11 11:12:13’)) toMonth(toDayOfMonth(‘2018-12-11’)) 返回 11返回 11
toDayOfWeek() 取日期或時間日期的星期(星期一爲1,星期日爲7)。 toDayOfWeek(toDateTime(‘2018-12-11 11:12:13’)) toDayOfWeek(toDate(‘2018-12-11’)) 返回 2返回 2
toHour() 取時間日期的小時 toHour(toDateTime(‘2018-12-11 11:12:13’)) 返回 11
toMinute() 取時間日期的分鐘 toMinute(toDateTime(‘2018-12-11 11:12:13’)) 返回 12
toSecond() 取時間日期的秒 toSecond(toDateTime(‘2018-12-11 11:12:13’)) 返回 13
toMonday() 取時間日期最近的週一(返回日期) toMonday(toDate(‘2018-12-11’)) toMonday(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-10返回 2018-12-10
toTime() 將時間日期的日期固定到某一天,保留原始時間 toTime(toDateTime(‘2018-12-11 11:12:13’)) 返回 1970-01-02 11:12:13

 

函數 用途 舉例 結果
toStartOfMonth() 取日期或時間日期的月份的第一天,返回日期 toStartOfMonth(toDateTime(‘2018-12-11 11:12:13’))toStartOfMonth(toDate(‘2018-12-11’)) 返回 2018-12-01返回 2018-12-01
toStartOfQuarter() 取日期或時間日期的季度的第一天,返回日期 toStartOfQuarter(toDateTime(‘2018-12-11 11:12:13’))toStartOfQuarter(toDate(‘2018-12-11’)) 返回 2018-10-01返回 2018-10-01
toStartOfYear() 取日期或時間日期的年份的第一天,返回日期 toStartOfYear(toDateTime(‘2018-12-11 11:12:13’))toStartOfYear(toDate(‘2018-12-11’)) 返回 2018-01-01返回 2018-01-01
toStartOfMinute() 截取時間日期到分鐘(以後歸零),返回日期 toStartOfMinute(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-11 11:12:00
toStartOfFiveMinute() 截取時間日期到最近的5的倍數分鐘(以後歸零),返回日期 toStartOfFiveMinute(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-11 11:10:00
toStartOfFifteenMinutes() 截取時間日期到最近的15的倍數分鐘(以後歸零),返回日期 toStartOfFifteenMinutes(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-11 11:00:00
toStartOfHour() 截取時間日期到小時(以後歸零),返回日期 toStartOfHour(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-11 11:00:00
toStartOfDay() 截取時間日期到天(以後歸零),返回日期 toStartOfDay(toDateTime(‘2018-12-11 11:12:13’)) 返回 2018-12-11 00:00:00
timeSlot() 將時間日期中,分鐘大於等於30的歸於30,分鐘數小於30的歸爲00 timeSlot(toDateTime(‘2018-12-11 11:33:13’))timeSlot(toDateTime(‘2018-12-11 11:33:13’)) 返回 2018-12-11 11:00:00返回 2018-12-11 11:30:00

3.4.2日期或時間日期生成函數

函數 用途 舉例 結果
now() 生成當前時間日期 now() 返回 2018-12-13 10:10:12
today() 生成今天的日期 today() 返回 2018-12-13
yesterday() 生成昨天的日期 yesterday() 返回 2018-12-12

3.4.3類型轉化類函數

 

函數 用途 舉例 結果
toDecimal32(‘whdwjfew’,8) 將數值型或者含有非數字的字符串進行精度保留 toDecimal32(23.12291, 3)toDecimal32(’_23.12291’, 3) 返回 23.122返回 0.000
toDecimal64(‘whdwjfew’,8) 將數值型或者含有非數字的字符串進行精度保留 toDecimal64(23.12291, 3)toDecimal64(’_23.12291’, 3) 返回 23.122返回 0.000
toDecimal128(‘whdwjfew’,8) 將數值型或者含有非數字的字符串進行精度保留 toDecimal128(23.12291, 3)toDecimal128(’_23.12291’, 3) 返回 23.122返回 0.000

 

函數 用途 舉例 結果
toUInt8OrZero() 將無符號整數字符型轉化爲整數型,不然返回0 toUInt8OrZero(‘123’)toUInt8OrZero(‘123.12’) 返回 123返回 0
toInt8OrZero() 將整數字符型轉化爲整數型,不然返回0 toInt8OrZero(‘123’)toInt8OrZero(’-123’) 返回 123返回 -123
toFloat32OrZero() 將數值字符串型轉化爲數值型,注意:從toFloat32OrZero開始,丟32的沒有對應的函數 toFloat32OrZero(‘-123’)toFloat32OrZero(‘123.123’) 返回 -123返回 123.123

 

函數 用途 舉例 結果
toDate() 將字符型日期轉化爲日期型 toDate(‘2018-12-24’) 返回 2018-12-24
toDateTime() 將字符型時間日期轉化爲時間日期型 toDateTime(‘2018-12-24 10:10:00’) 返回 2018-12-24 10:10:00

 

函數 用途 舉例 結果
toString() 將數值型、字符型、日期等轉化爲字符型 toString(‘2018-12-24’)toString(‘123’) 返回 2018-12-24返回 123

 

函數 用途 舉例 結果
toTypeName() 返回數據的類型 toTypeName(toString(‘123’))toTypeName(toDate(‘2018-12-24’)) 返回 String返回 Date

3.4.4 字符串操做

 

函數 用途 舉例 結果
empty() 判斷字符串是空爲1,不然爲0 empty(’’)empty(‘123a’) 返回 1返回 0
notEmpty() 判斷字符串是非空爲1,不然爲0 notEmpty(’’)notEmpty(‘123a’) 返回 0返回 1
length() 返回字符串的長度 length(’’)length(‘123a’) 返回 0返回 4
lower() 將字符串轉爲小寫 lower(‘aBc’) 返回 abc
upper() 將字符串轉爲大寫 upper(‘aBc’) 返回 ABC
reverse() 將字符串反轉 reverse(‘abc’) 返回 cba
substring(s, offset, length) 字符串截取 substring(‘123abcABC’, 2, 3) 返回 23a
appendTrailingCharIfAbsent(s, c) 若是字符串s非空,則將s後追加一個字符c(s最後一個字符與c不一樣),不然不處理 appendTrailingCharIfAbsent(‘123abc’, ‘b’)appendTrailingCharIfAbsent(‘123abc’, ‘c’) 返回 123abcb返回 123abc

 

函數 用途 舉例 結果
match(haystack,pattern) 字符串正則匹配,返回0或1 match(‘avhsca’,'vh’) 返回 1
extract(haystack,pattern) 返回匹配到的第一個子串 extract(‘iioomAj12123124OOBJB’, ‘\d+’) 返回 12123124
extractAll(haystack,pattern) 返回匹配到的全部子串,輸出列表 extractAll(‘iioomAj12123124OOBJ123B’, ‘\d+’) 返回 [12123124,123]
like(haystack,pattern) 匹配到的數據返回1,不然返回0 like(‘avhsca’,’%vh%’)like(‘avhsca’,’%vabjh%’) 返回 1返回 0
notLike(haystack, pattern) 與like()函數相反 notLike(‘avhsca’,’%vh%’)notLike(‘avhsca’,’%vabjh%’) 返回 0返回 1

 

函數 用途 舉例 結果
replaceOne(haystack,pattern,replacement) 替換第一個匹配到的pattern replaceOne(‘asd123cbbj464sd’, ‘sd’, ‘-’) 返回 a-123cbbj464sd
replaceAll(haystack,pattern,replacement) 替換全部匹配到的pattern replaceOne(‘asd123cbbj464sd’, ‘sd’, ‘-’) 返回 a-123cbbj464-
replaceRegexpOne(haystack, pattern, replacement) 正則匹配替換第一個匹配到的pattern replaceRegexpOne(‘Hello, World!’, ‘o’, '- ') 返回 Hell- , World!
replaceRegexpAll(haystack,pattern,replacement) 正則匹配替換全部匹配到的pattern replaceRegexpAll(‘Hello, World!’, ‘^’, 'here: ')replaceRegexpAll(‘Hello, World!’, ‘o’, '-- ') 返回 here: Hello, World!返回 Hell-- , W-- rld!

 

函數 用途 舉例 結果
splitByChar(separator, s) 以單個字符分割字符串 splitByChar(’-’, ‘qw-asaf-asfqw-2312-asd’) 返回 [‘qw’,‘asaf’,‘asfqw’,‘2312’,‘asd’]
splitByString(separator, s) 以單個或多個字符分割字符串 splitByString(’-’, ‘qw-asaf-asfqw-2312-asd’)splitByString(’-a’, ‘qw-asaf-asfqw-2312-asd’) 返回 [‘qw’,‘asaf’,‘asfqw’,‘2312’,‘asd’]返回 [‘qw’,‘saf’,‘sfqw-2312’,‘sd’]

 

函數 用途 舉例 結果
concat(s1,s2,…) 將字符串拼接 concat(‘123’, ‘abc’, ‘ABC’) 返回 123abcABC

3.4.5 條件語句 

函數 用途 舉例 結果
if(cond,then,else) 條件輸出 if(1 > 2, ‘正確’, ‘錯誤’) 返回 錯誤
multiIf(cond_1, then_1, cond_2, then_2…else) 多條件輸出 multiIf(1 > 2, ‘正確’, 2 < 0, ‘正確’, ‘錯誤’) 返回 錯誤

3.4.6 數學函數

函數 用途 舉例 結果
e() 返回e的值 e() 返回 2.718281828459045
pi() 返回pi的值 pi() 返回 3.141592653589793
exp(x) 返回e的x次方 exp(1) 返回 2.718281828459045
exp2(x) 返回2的x次方 exp2(2) 返回 4
exp10(x) 返回10的x次方 exp10(1) 返回 10
log(x) 返回log以e爲底的對數值 log(e()) 返回 1
log2(x) 返回log以2爲底的對數值 log2(2) 返回 1
log10(x) 返回log以10爲底的對數值 log10(100) 返回 2
sqrt(x) 對x開平方 sqrt(4) 返回 2
cbrt(x) 對x開立方 cbrt(8) 返回 2
pow(x, y) 返回x的y次方 pow(2, 3) 返回 8

3.4.7舍入函數

函數 用途 舉例 結果
floor(x[, N]) 向下取數 floor(123.883, 1)floor(123.883, -1) 返回 123.8返回 120
ceil(x[, N]) 向上取數 ceil(123.883, 1)ceil(123.883, -1) 返回 123.9返回 130
round(x[, N]) 四捨五入 round(123.883, 1)round(123.883, -1) 返回 123.9返回 120

3.4.8 URL操做函數

函數 用途 舉例 結果
protocol() 返回URL的協議類型 protocol(‘http://www.baidu.com.cn’) 返回 http
domain() 返回URL的域名 domain(‘http://www.baidu.com.cn’) 返回 www.baidu.com.cn
domainWithoutWWW() 返回URL不帶www的域名 domainWithoutWWW(‘http://www.baidu.com.cn’) 返回 baidu.com.cn
topLevelDomain() 返回頂級域名 topLevelDomain(‘http://www.baidu.com.cn’) 返回 cn
firstSignificantSubdomain() Returns the 「first significant subdomain」. firstSignificantSubdomain(‘http://www.baidu.com.cn’) 返回 baidu
cutToFirstSignificantSubdomain() Returns the part of the domain that includes top-level subdomains up to the 「first significant subdomain」 (see the explanation above). cutToFirstSignificantSubdomain(‘http://www.baidu.com.cn’) 返回 baidu.com.cn
path() 返回URL的路徑 path(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split’) 返回 /s
pathFull() 返回URL的完整路徑 pathFull(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split’) 返回 /s?wd=SQL%E4%B8%AD%E7%9A%84split
queryString() 返回URL的參數(查詢字符串) queryString(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split’) 返回 wd=SQL%E4%B8%AD%E7%9A%84split
extractURLParameters() 以列表的形式返回URL的參數 extractURLParameters(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split&ur=qwguq’) 返回 [‘wd=SQL%E4%B8%AD%E7%9A%84split’,‘ur=qwguq’]
extractURLParameterNames() 以列表的形式返回URL的參數名 extractURLParameterNames(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split&ur=qwguq’) 返回 [‘wd’,‘ur’]
cutQueryString() 返回URL?(參數)前面的內容 cutQueryString(‘https://www.baidu.com/s?wd=SQL%E4%B8%AD%E7%9A%84split&ur=qwguq’) 返回 https://www.baidu.com/s

3.4.9 IP操做函數

函數 用途 舉例 結果
IPv4StringToNum(s) 將IPV4轉爲數值,非IPV4的轉化爲0 IPv4StringToNum(‘23.217.198.69’)IPv4StringToNum(‘adwh.124.qwfqw’) 返回 400148037返回 0
IPv4NumToString(num) 將數值轉爲IPV4 IPv4NumToString(400148037) 返回 23.217.198.69
IPv4NumToStringClassC(num) 將數值轉爲IPV4,且最後的段位用xxx代替 IPv4NumToStringClassC(400148037) 返回 23.217.198.xxx

3.4.10 表操做

函數 用途 舉例 結果
INNER JOIN 內鏈接 A表 INNER JOIN B表 A表與B表的公共部分
LEFT OUTER JOIN 左外鏈接 A 表 LEFT OUTER JOIN B表 A表不在B表中的部分
RIGHT OUTER JOIN 右外鏈接 A 表 RIGHT OUTER JOIN B表 B表不在A表中的部分
FULL OUTER JOIN 全外鏈接 A 表 FULL OUTER JOIN B表 A與B表所有,沒有爲NULL

 

函數 用途 舉例 結果
LIMIT N 查詢N條數據,通常跟ORDER BY 連用 ORDER BY hit DESC LIMIT 10 按照hit列降排取前10
LIMIT N BY Clause 按照Clause列查詢N條數據,通常跟ORDER BY 連用 SELECT date, domain, count(1) AS hit from db.tb where…GROUP BY date, domain,ORDER BY hit DESCLIMIT 10 BY date 取天天TOP10的域名

3.4.11 字典操做

函數 用途 舉例 結果
dictGetString() 字典映射 dictGetString(‘ck_abc_dic’, ‘ck_value’, tuple(_abc))

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

4 用戶行爲分析

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ClickHouse爲用戶提供了豐富的多參聚合函數(parametric aggregate function)和基於數組+Lambda表達式的高階函數(higher-order function),將它們靈活使用能夠達到魔法般的效果。在咱們的體系中,ClickHouse定位點擊流數倉,因此下面舉幾個用它來作用戶行爲(路徑)分析的實戰例子,包括:

  • 路徑匹配
  • 智能路徑檢測
  • 有序漏斗轉化
  • 用戶留存
  • Session統計

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

4.1 路徑匹配

CK默認提供了sequenceMatch函數檢查是否有事件鏈知足輸入的模式,sequenceCount函數則統計知足輸入模式的事件鏈的數量。示例:

SELECT
  site_id,
  sequenceMatch('(?1)(?t<=15)(?2).*(?3)')(
    ts_date_time,
    event_type = 'shtLogon',
    event_type = 'shtKkclick' AND column_type = 'homePage',
    event_type = 'shtAddCart'
  ) AS is_match
FROM ods.analytics_access_log_all
WHERE ts_date >= '2020-07-01'
AND site_id IN (10266,10022,10339,10030)
GROUP BY site_id;

 

┌─site_id─┬─is_match─┐
│   10030 │        1 │
│   10339 │        1 │
│   10266 │        1 │
│   10022 │        1 │
└─────────┴──────────
SELECT
  site_id,
  sequenceCount('(?1)(?t<=15)(?2).*(?3)')(
    ts_date_time,
    event_type = 'shtLogon',
    event_type = 'shtKkclick' AND column_type = 'homePage',
    event_type = 'shtAddCart'
  ) AS seq_count
FROM ods.analytics_access_log_all
WHERE ts_date >= '2020-07-01'
AND site_id IN (10266,10022,10339,10030)
GROUP BY site_id;
┌─site_id─┬─seq_count─┐
│   10030 │     33611 │
│   10339 │     14045 │
│   10266 │     74542 │
│   10022 │     31534 │
└─────────┴───────────┘

4.2 智能路徑檢測

CK內置的sequenceMatch和sequenceCount函數只能知足部分需求,現有一個更復雜的需求:

給按期望的路徑終點、途經點和最大事件時間間隔,查詢出符合條件的路徑詳情及符合路徑的用戶數(按用戶數降序排列)。

目前並無現成的函數能夠直接出結果,可是咱們能夠曲線救國,用數組和高階函數的組合間接實現。完整SQL語句以下,

SELECT
  result_chain,
  uniqCombined(user_id) AS user_count
FROM (
  WITH
    toUInt32(maxIf(ts_date_time, event_type = 'shtOrderDone')) AS end_event_maxt,
    arrayCompact(arraySort(
      x -> x.1,
      arrayFilter(
        x -> x.1 <= end_event_maxt,
        groupArray((toUInt32(ts_date_time), (event_type, column_type)))
      )
    )) AS sorted_events,
    arrayEnumerate(sorted_events) AS event_idxs,
    arrayFilter(
      (x, y, z) -> z.1 <= end_event_maxt AND (z.2.1 = 'shtOrderDone' OR y > 600),
      event_idxs,
      arrayDifference(sorted_events.1),
      sorted_events
    ) AS gap_idxs,
    arrayMap(x -> x + 1, gap_idxs) AS gap_idxs_,
    arrayMap(x -> if(has(gap_idxs_, x), 1, 0), event_idxs) AS gap_masks,
    arraySplit((x, y) -> y, sorted_events, gap_masks) AS split_events
  SELECT
    user_id,
    arrayJoin(split_events) AS event_chain_,
    arrayCompact(event_chain_.2) AS event_chain,
    hasAll(event_chain, [('shtKkClick', 'homePage')]) AS has_midway_hit,
    arrayStringConcat(arrayMap(
      x -> concat(x.1, '#', x.2),
      event_chain
    ), ' -> ') AS result_chain
  FROM (
    SELECT ts_date,ts_date_time,event_type,column_type,user_id
    FROM ods.analytics_access_log_all
    WHERE ts_date >= '2020-06-30' AND ts_date <= '2020-07-02'
    AND site_id IN (10266,10022,10339,10030)
  )
  GROUP BY user_id
  HAVING length(event_chain) > 1
)
WHERE event_chain[length(event_chain)].1 = 'shtOrderDone'
AND has_midway_hit = 1
GROUP BY result_chain
ORDER BY user_count DESC LIMIT 20;

主要思路

  • 將用戶的行爲用groupArray函數整理成<時間, <事件名, 頁面名>>的元組,並用arraySort函數按時間升序排序;
  • 利用arrayEnumerate函數獲取原始行爲鏈的下標數組;
  • 利用arrayFilter和arrayDifference函數,過濾出原始行爲鏈中的分界點下標。分界點的條件是路徑終點或者時間差大於最大間隔;
  • 利用arrayMap和has函數獲取下標數組的掩碼(由0和1組成的序列),用於最終切分,1表示分界點;
  • 調用arraySplit函數將原始行爲鏈按分界點切分紅單次訪問的行爲鏈。注意該函數會將分界點做爲新鏈的起始點,因此前面要將分界點的下標加1;
  • 調用arrayJoin和arrayCompact函數將事件鏈的數組打平成多行單列,並去除相鄰重複項。
  • 調用hasAll函數肯定是否所有存在指定的途經點。若是要求有任意一個途經點存在便可,就換用hasAny函數。固然,也能夠修改WHERE謂詞來排除指定的途經點。
  • 將最終結果整理成可讀的字符串,按行爲鏈統計用戶基數,完成。

4.3  有序漏斗轉化

CK提供了windowFunnel函數實現漏斗,以指定時長(單位爲秒)滑動窗口按序匹配事件鏈,並返回在窗口內轉化到的步數。若有多種匹配,以步數最大(轉換最深)的爲準。

以自定義漏斗模型爲例:

@Mapper
public interface FunnelMapper {
    @Select("select\n" +
            "total ,\n" +
            "(o1+o2+o3+o4) as v1 ,\n" +
            "(o2+o3+o4) as v2 ,\n" +
            "(o3+o4) as v3 ,\n" +
            "(o4) as v4 \n" +
            "from\n" +
            "(select\n" +
            "count(1) as total ,\n" +
            "sum(if(cc=4 , 1 , 0)) as o4 ,\n" +
            "sum(if(cc=3 , 1 , 0)) as o3 ,\n" +
            "sum(if(cc=2 , 1 , 0)) as o2 ,\n" +
            "sum(if(cc=1 , 1 , 0)) as o1 \n" +
            "from\n" +
            "(select\n" +
            "deviceId ,\n" +
            "windowFunnel(3600)(\n" +
            "toDateTime(ctime) ,\n" +
            "eventId='adShow'  ,\n" +
            "eventId='adClick',\n" +
            "eventId='productView' ,\n" +
            "eventId='submitOrder'\n" +
            ") as cc\n" +
            "from\n" +
            "tb_log \n" +
            "group by deviceId))")
    public EventCountBean getStaticFunnel() ;

    @Insert("insert into tb_funnel values(#{id},#{name},#{eventChannel})")
    public  void  addFunnel(TbFunnel tbFunnel) ;

    @Select("select * from tb_funnel")
    public List<TbFunnel> getAllFunnel() ;

    /**
     * mybatis的SQL輸入
     *     #{}   建議使用
     *     ${}  like   ${}
     * @param name
     * @return
     */
    @Select("select * from tb_funnel where  name = #{name}")
    public  TbFunnel getFunnelByName(String name) ;

    @SelectProvider(type = MySQlProvider.class ,method = "getdynamicSQL")
    public EventCountBean getStaticFunnelByEventChannel(String  eventChannel) ;

    class MySQlProvider{
        public String  getdynamicSQL(String eventChannel){
            // 拼接 事件鏈條屬性
            String[] split = eventChannel.split("-");
            StringBuilder sb = new StringBuilder();
            StringBuilder sb2 =  new StringBuilder();
            StringBuilder sb3 =  new StringBuilder();
            for(int i = 0 ; i<split.length ; i++)  {
                sb.append("eventId=").append("'"+split[i]+"'").append(",");
                sb2.append("sum(if(cc=").append(split.length-i).append(",1,0)) as o").append(split.length-i+",") ;
                for(int j = split.length ; j > i ; j--){
                    sb3.append("o").append(j).append("+") ;
                }
                sb3.deleteCharAt(sb3.length()-1).append(" as v").append(i+1).append(",");
            }
            String s1 = sb.deleteCharAt(sb.length() - 1).toString();
            String s2 = sb2.deleteCharAt(sb2.length() - 1).toString();
            String s3 = sb3.deleteCharAt(sb3.length() - 1).toString();
            String sql = "select\n" +
                    "total ,\n" +
                    s3+
                    " from\n" +
                    "(select\n" +
                    "count(1) as total ,\n" +
                    s2+
                    " from\n" +
                    "(select\n" +
                    "deviceId ,\n" +
                    "windowFunnel(3600)(\n" +
                    "toDateTime(ctime) ,\n" +
                    s1+
                    ") as cc\n" +
                    "from\n" +
                    "tb_log \n" +
                    "group by deviceId));";
            return  sql ;
        }

    }


}

 

 

4.4  用戶留存

 

retention函數能夠方便地計算留存狀況。該函數接受多個條件,以第一個條件的結果爲基準,觀察後面的各個條件是否也知足,若知足則置1,不知足則置0,最終返回0和1的數組。經過統計1的數量,便可計算出留存率。

SELECT
  sum(ret[1]) AS original,
  sum(ret[2]) AS next_day_ret,
  round(next_day_ret / original * 100, 3) AS next_day_ratio,
  sum(ret[3]) AS seven_day_ret,
  round(seven_day_ret / original * 100, 3) AS seven_day_ratio
FROM (
  WITH toDate('2020-06-24') AS first_date
  SELECT
    user_id,
    retention(
      ts_date = first_date,
      ts_date = first_date + INTERVAL 1 DAY,
      ts_date = first_date + INTERVAL 7 DAY
    ) AS ret
  FROM ods.ms_order_done_all
  WHERE ts_date >= first_date AND ts_date <= first_date + INTERVAL 7 DAY
  GROUP BY user_id
);

 

SELECT 
    sum(r[1]) AS r1,
    sum(r[2]) AS r2,
    sum(r[3]) AS r3,
    sum(r[4]) AS r4
FROM 
(
    SELECT 
        uid,
        retention(date = '2020-01-01', date = '2020-01-02', date = '2020-01-03', date = '2020-01-04') AS r
    FROM retention_test
    WHERE date IN ('2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04')
    GROUP BY uid
)

┌─r1─┬─r2─┬─r3─┬─r4─┐
│  5 │  3 │  3 │  1 │
└────┴────┴────┴────┘

4.5 session統計

SELECT
  ts_date,
  sum(length(session_gaps)) AS session_cnt
FROM (
  WITH
    arraySort(groupArray(toUInt32(ts_date_time))) AS times,
    arrayDifference(times) AS times_diff
  SELECT
    ts_date,
    arrayFilter(x -> x > 1800, times_diff) AS session_gaps
  FROM ods.analytics_access_log_all
  WHERE ts_date >= '2020-06-30'
  GROUP BY ts_date,user_id
)
GROUP BY ts_date;

 

                                                                                                              友情推薦不錯的視頻教程

dd

相關文章
相關標籤/搜索