Clickhouse学习系列——一条SQL完成gourp by分组与不分组数值计算

       笔者在近一两年接触了Clickhouse数据库,在项目中也进行了一些实践,但一直都没有一些技术文章的沉淀,近期打算做个系列,通过一些具体的场景将Clickhouse的用法进行沉淀和分享,供大家参考。

      首先我们假设一个Clickhouse数据表:

CREATE TABLE Test_Table (
  page_id String,  /* 页面ID */
  user_id String,  /* 用户ID */
  is_slow String,  /* 请求是否慢 */
  url String,      /* 请求URL */
) ENGINE = MergeTree()
ORDER BY (page_id, device_id);

这个表格的字段含义如注释,该表主要存储的数据是:

每个用户(user_id)在某个页面(page_id)发起的请求(url)是否耗时较长(is_slow),耗时较长我们简称“慢请求”

其中,一个用户可能在一个或多个页面发起一个或多个请求,每个请求可能是慢请求,也可能不是

is_slow的内容是枚举值,即:true 和 false

      数据表格有了,我们还要设定一个业务需求

我们期望以页面的纬度,查看每个页面中有多少用户遇到了慢请求,并算出在这个页面慢请求用户在所有页面发起请求的总用户数(无论是否发起了慢请求)的百分比

       从业务角度上比较好理解,这个结果其实就是想知道这个页面的慢影响了多少人,占比是多少

       从研发者的角度,一个页面一般是一个固定的团队成员开发,甚至就是一个团队成员开发,所以,这个角度主要面向问题的解决和责任的划分,同时也让项目的管理者,技术的管理者知道目前用户体验的痛点在哪里

       场景有了,业务需求也有了,那么如何实现呢?

       首先,我们要获取的内容有两个,一个是“每个页面中有多少用户遇到了慢请求

       这个比较简单:

    SELECT page_id, count(DISTINCT device_id) AS slow_user_count
    FROM Test_Table
    WHERE
      and is_slow = 'true'
    group by page_id

    另一个要获取的是“在所有页面发起请求的总用户数

 SELECT count(DISTINCT device_id) AS total_user_count
    FROM Test_Table

    最后,我们需要获得“在这个页面慢请求用户占在所有页面发起请求的总用户数(无论是否发起了慢请求)的百分比

     这里一般来说,比较常见的方法是使用With来进行拼接:

WITH slow_users AS (
    SELECT page_id, count(DISTINCT device_id) AS slow_user_count
    FROM Test_Table
    WHERE 
      is_slow = 'true'
    group by page_id
),
total_users AS (
    SELECT count(DISTINCT device_id) AS total_user_count
    FROM Test_Table
)
SELECT
  slow_users.page_id,
  slow_users.slow_user_count,
  total_users.total_user_count,
  slow_user_count * 100.0 / total_users.total_user_count AS slow_user_percentage
FROM
  slow_users,
  total_users
ORDER BY
  slow_user_percentage DESC;

  嗯,这个需求做完了,是不是很简单,貌似不熟悉ClickHouse的同学也能写

  但既然笔者来写这篇文章,肯定不是想用,这么简单的方案,更何况,这个SQL本身还是有问题

  第一个问题是:去重函数的性能问题

   首先在Clickhouse里面有多个去重计数的函数,主要包含两类:

   1.非精确去重函数:uniq、uniqHLL12、uniqCombined

   2.精确去重函数:uniqExact、groupBitmap

 从官网资料上来看:

 在非精确去重函数中:

   uniq函数使用自适应采样算法,

   uniqHLL12函数使用的是HyperLogLog 算法

   uniqCombined函数使用三种算法的组合:数组、哈希表和包含错误修正表的HyperLogLog算法

  官方推荐:uniq和uniqCombined函数,不推荐uniqCombined函数

  同时对于uniq和uniqCombined的区别上,官方给出的建议是:

 在精确去重函数中:

 uniqExact函数是uniq系列方法中的一个,比 uniq 使用更多的内存,因为状态的大小随着不同值的数量的增加而无界增长。参数可以是TupleArrayDateDateTimeString,或数字类型。


groupBitmap函数比较特殊,参数得是一个无符号整数列,算法主要用的是“位图或聚合计算”

从这篇文章中查看了两个函数的源码:

/ count(distinct)
// HashSetTable
void merge(const Self & rhs)
    {
        if (!this->hasZero() && rhs.hasZero())
        {
            this->setHasZero();
            ++this->m_size;
        }

        for (size_t i = 0; i < rhs.grower.bufSize(); ++i)
            if (!rhs.buf[i].isZero(*this))
                this->insert(rhs.buf[i].getValue());
    }
// groupBitmap
// RoaringBitmapWithSmallSet
void merge(const RoaringBitmapWithSmallSet & r1)
    {
        if (r1.isLarge())
        {
            if (isSmall())
                toLarge();

            *rb |= *r1.rb;
        }
        else
        {
            for (const auto & x : r1.small)
                add(x.getValue());
        }
    }

uniqExact函数使用了HashSetTable数据结构来解决,这里是比较费内存的,所以耗时也比较长

groupBitmap函数使用了RoaringBitmap,一个低内存去重方案,具体的算法参考

大数据分析常用去重算法分析『Bitmap篇』

 从行业测试的结果上来看:

   从这篇文章来看,这几个方法的效果如下:

 可以看到精确去重函数的耗时是比较长的,非精确去重函数的误差在0.5%以内,而在实际的也场景中,很多数据分析平台更多的是需要一个数量级的概念,而不需要一个精确的数据,比如一个产品的UV为2600万,这个2600万就是一个概略数字,且随着变动越大,故可以用非精确去重函数

在上面的SQL中DISTINCT方法实际上是在用uniqExact,也就是最耗时的精确去重函数,在这个场景下,我们假设用户数据量比较多,请求数据量也比较多,我们更关注哪个页面问题多,问题大,而不是有精确的多少个慢请求数,符合非精确去重函数的场景,

那这里进行修改:    

/* 精确但耗时的方法 */
COUNT(DISTINCT device_id) FILTER (WHERE is_slow = 'true')  AS slow_user_count

/* 不精确但快速的方法*/
uniqIf(device_id, is_slow = 'true') AS slow_user_count

知识点:上图中,除了将DISTINCT修改uniq外,还增加了如果增加了IF判断应该怎么写的语法

第二个问题是:With用法的性能问题

   在Clickhouse,说起来,With有两种用法,

一种是通用SQL常见的用法 :with alias as (…),这个叫CTE,common table expression,是SQL定义中的一部份,按照这篇文章来看:

The common table expression (CTE) is a powerful construct in SQL that helps simplify a query. CTEs work as virtual tables (with records and columns), created during the execution of a query, used by the query, and eliminated after query execution. CTEs often act as a bridge to transform the data in source tables to the format expected by the query.

common table expression, or CTE, is a temporary named result set created from a simple SELECT statement that can be used in a subsequent SELECT statement. Each SQL CTE is like a named query, whose result is stored in a virtual table (a CTE) to be referenced later in the main query.

就是建立一个虚拟表,来存储中间数据,然后进行使用,值得一提的是,子查询和CTE嵌套的性能理论上是一样的,但后者的可读性更好,不过在某些关系型数据库的引擎上略有区别,但本质上区别不大

比如上一章节根据业务输出的带With的SQL可以转换成以下嵌套子查询

/*不带with版本*/
SELECT page_id,
       COUNT(DISTINCT device_id) FILTER (WHERE is_slow = 'true') AS slow_user_count,
       (SELECT COUNT(DISTINCT device_id)
        FROM Test_Table
       ) AS total_user_count,
       slow_user_count * 100.0 / total_user_count  AS slow_user_percentage
FROM Test_Table
WHERE is_slow = 'true'
group by page_id
ORDER BY slow_user_percentage DESC;

但意义不大,因为性能没啥变化,两种方式都是二次查询(读两次盘)

另一种是with (…) as alias,这个是ClickHouse的宏展开一样的能力,是Clickhouse独有的语法

根据官方的文档:主要有四种用法

1.使用常量作为"变量"

WITH '2019-08-01 15:23:00' as ts_upper_bound
SELECT *
FROM hits
WHERE
    EventDate = toDate(ts_upper_bound) AND
    EventTime <= ts_upper_bound;

2.封装表达式

WITH sum(bytes) as s
SELECT
    formatReadableSize(s),
    table
FROM system.parts
GROUP BY table
ORDER BY s;

3.使用标量子查询的结果(这个和前面的with有点像,但不能用.xxx的形式获取值,且Select只能一个值)

/* this example would return TOP 10 of most huge tables */
WITH
    (
        SELECT sum(bytes)
        FROM system.parts
        WHERE active
    ) AS total_disk_usage
SELECT
    (sum(bytes) / total_disk_usage) * 100 AS table_disk_usage,
    table
FROM system.parts
GROUP BY table
ORDER BY table_disk_usage DESC
LIMIT 10;

4.在子查询中重用表达式

WITH test1 AS (SELECT i + 1, j + 1 FROM test1)
SELECT * FROM test1;

其中第一个、第二个相当于直接替换,没有啥影响,而第三种、第四种和CTE的作用差不多,都会逐个去执行SQL,也就意味着二次查询(读两次盘)

所以看起来使用With无法避免二次读盘的问题

那这里,有没有一次读盘就可以解决这里的问题呢?看起来group by分组前后的数据做数值计算也是一个经典场景

那这里就得用到Clickhouse经典的窗口函数和物化视图了

窗口函数这篇文章有比较详细的介绍

物化试图这篇文章有比较详细的介绍

先看结果SQL

/* 一条sql的版本*/
SELECT page_id,
       uniqIf(device_id, is_slow = 'true') AS slow_user_count,
       uniq(device_id) AS page_user_count,
       uniqMerge(uniqState(device_id)) OVER () as total_user_count
FROM Test_Table
group by page_id

前两个一个是某个页面慢请求的用户数,一个该页面所欲请求的用户数

第三个需要拆开来看, uniqState是一个物化视图的方法,可以理解成一个AggregateFunction类型的数据的中间状态,这里可以理解基于每个页面都生成了一个数组,存储对应的用户名单

uniqMerge可以将多个AggregateFunction类型的中间状态组合计算为最终的聚合结果,比如以下两个SQL是等价的:

SELECT uniq(UserID) FROM table

SELECT uniqMerge(state) FROM (SELECT uniqState(UserID) AS state FROM table GROUP BY RegionID)

当然在这里用到的是uniqState 和 uniqMerge

这里可以换成任何以-State和-Merge为后缀的方法

回到这个SQL,这里uniqMerge(uniqState(device_id)) OVER () 相当于合并了基于每个页面都生成的数组,每个数组存储对应的用户名单”,即访问所有页面的所有用户数

这样就比较优雅的实现了不用with的问题,且这里的性能也是比较快的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/71403.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

EXPLAIN使用分析

系列文章目录 文章目录 系列文章目录一、type说明二、MySQL中使用Show Profile1.查看当前profiling配置2.在会话级别修改profiling配置3.查看profile记录4.要深入查看某条查询执行时间的分布 一、type说明 我们只需要注意一个最重要的type 的信息很明显的提现是否用到索引&…

单参数构造函数的隐式类型转化

单参数构造函数的隐式类型转化 如果你不想发生隐式类型的转化&#xff0c;可以在默认构造函数前加上关键字&#xff1a;explicit 多参数的玩法和单参数的是不一样的 c98 不支持多参数隐式类型的转化 c11 支持多参数隐式类型的转化 举个例子&#xff1a; 多参数可以这样写&…

考虑分布式电源的配电网无功优化问题研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

计算机视觉中的Transformer

几十年来&#xff0c;理论物理学家一直在努力提出一个宏大的统一理论。通过统一&#xff0c;指的是将被认为是完全不同的两个或多个想法结合起来&#xff0c;将它们的不同方面证明为同一基础现象。一个例子是在19世纪之前&#xff0c;电和磁被看作是无关的现象&#xff0c;但电…

SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系

SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 1 SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 1 SolidWorks不能使用选择如允许此选择将生成有冲突的前后关系 https://www.swrjzxw.com/1556.html SolidWorks装配体时 显示 不能使用选择如允许此选…

CSS 中的优先级规则是怎样的?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐内联样式&#xff08;Inline Styles&#xff09;⭐ID 选择器&#xff08;ID Selectors&#xff09;⭐类选择器、属性选择器和伪类选择器&#xff08;Class, Attribute, and Pseudo-class Selectors&#xff09;⭐元素选择器和伪元素选择器…

2023.8.12号论文阅读

文章目录 TriFormer: A Multi-modal Transformer Framework For Mild Cognitive Impairment Conversion Prediction摘要本文方法实验结果 SwIPE: Efficient and Robust Medical Image Segmentation with Implicit Patch Embeddings摘要本文方法实验结果 TriFormer: A Multi-mod…

服务器安装Tomcat

下载Tomcat 下载地址在这&#xff1a; Tomcat官网 下载完成以后把压缩包上传到服务器中&#xff08;我传到了www/java&#xff09;,进行解压(解压到)&#xff0c;如果没有进行指定解压到哪里&#xff0c;默认是到root文件夹中 tar -zxvf /www/java/apache-tomcat-9.0.103.tar.…

掌握Python的X篇_32_使用python编辑pdf文件_pdfrw

本篇介绍利用python操作pdf文件&#xff0c;我们平时也会有合并和拆分pdf的需求&#xff0c;此时我们就可以使用本节内容。 文章目录 1. pdfrw的安装2. 切分pdf文件3. pdfrw官网及实现一版四面的实例 1. pdfrw的安装 pip install pdfrw官网地址&#xff1a;https://github.co…

QT之UDP通信

QT之UDP通信 UDP不分客户端口服务器,只需要使用一个类QUdpSocket QT += core gui networkgreaterThan(QT_MAJOR_VERSION, 4): QT += widgetsTARGET = udp TEMPLATE = app# The following define makes your compiler emit warnings if you use # any feature of Qt …

使用Openoffice或LibreOffice实现World、Excel、PPTX在线预览

使用Openoffice或LibreOffice实现World、Excel、PPTX在线预览 预览方案使用第三方服务使用前端库转换格式 jodconverterjodconverter概述主要特性OpenOfficeLibreOffice jodconverter的基本使用添加依赖配置创建DocumentConverter实例上传与转换预览启动上传与预览World 与Spri…

C#应用处理传入参数 - 开源研究系列文章

今天介绍关于C#的程序传入参数的处理例子。 程序的传入参数应用比较普遍&#xff0c;特别是一个随操作系统启动的程序&#xff0c;需要设置程序启动的时候不显示主窗体&#xff0c;而是在后台运行&#xff0c;于是就有了传入参数问题&#xff0c;比如传入/h或者/min等等。所以此…

配置vscode

配置vscode 设置相关 网址&#xff1a;https://code.visualstudio.com/ 搜索不要用百度用这个&#xff1a;cn.bing.com 1.安装中文包 Chinese (Simplified) (简体中文) 2.安装 open in browser 3.安装主题 Atom One Dark Theme 4. 安装图标样式 VSCode Great Icons 5.安装 L…

25. K 个一组翻转链表

25. K 个一组翻转链表 题目-困难难度示例1. 链表转列表 -> 计算 -> 列表转链表2. 反转合并 题目-困难难度 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果…

Ansible Playbook快速部署一主多从MySQL集群

部署目标&#xff1a; 1、快速部署一套一主两从的mysql集群 2、部署过程中支持交互式定义安装目录及监听端口号 部署清单目录结构&#xff1a; rootmaster:/opt/mysql# tree . . ├── group_vars │ └── all.yml ├── hosts ├── mysql.yml └── roles└── mys…

WebRTC本地视频通话使用ossrs服务搭建

iOS开发-ossrs服务WebRTC本地视频通话服务搭建 之前开发中使用到了ossrs&#xff0c;这里记录一下ossrs支持的WebRTC本地服务搭建。 一、ossrs是什么&#xff1f; ossrs是什么呢&#xff1f; SRS(Simple Realtime Server)是一个简单高效的实时视频服务器&#xff0c;支持RTM…

计算机网络—TCP

这里写目录标题 TCP头格式有哪些为什么需要TCP&#xff0c;TCP工作在哪什么是TCP什么是TCP连接如何确定一个TCP连接TCP和UDP的区别&#xff0c;以及场景TCP和UDP能共用一个端口&#xff1f;TCP的建立TCP三次握手过程为什么是三次握手、不是两次、四次why每次建立连接&#xff0…

【电机绘图】:插补算法(一)—直线插补—逐点比较法

今日介绍学习一种使用电机作画、绘图、加工零件时需要使用的算法 &#xff1a; 插补算法 本文提供直线插补的概念基础&#xff0c;基本思路分析&#xff0c;C语言实现等&#xff0c;代码会直接贴出&#xff01; 插补算法是指在数值计算或数据处理中&#xff0c;根据已有的数据…

C++——vector介绍及其简要模拟实现

vector的介绍 此主题介绍转载自(https://cplusplus.com/reference/vector/vector/) 1.vector是一个表示可变大小数组的序列容器 2.vector同数组一样&#xff0c;采用连续存储空间来存储元素&#xff0c;这样可以用下标来对vector中的元素进行访问&#xff0c;但是vector的大…

AAAI论文阅读

文章目录 Open-Vocabulary Multi-Label Classifcation via Multi-Modal Knowledge Transfer——知识蒸馏的范畴Med-EASi: Finely Annotated Dataset and Models for Controllable Simplifcation of Medical Texts——医学领域数据集构建“Nothing Abnormal”: Disambiguating M…