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

news2025/4/15 10:44:25

       笔者在近一两年接触了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的问题,且这里的性能也是比较快的

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python基础3——流程控制

文章目录 一、操作符1.1 比较操作符1.2 逻辑操作符1.3 成员操作符1.4 身份操作符 二、流程控制2.1 条件判断2.2 循环语句2.2.1 for循环2.2.2 while循环 2.3 continue与break语句2.4 文件操作函数 三、函数3.1 定义函数3.2 作用域3.3 闭包3.4 函数装饰器3.5 内建函数 一、操作符…

一文看懂 D类数字功放原理及应用

他V hezkz17进数字音频系统研究开发交流答疑群(课题组) D类放大器工作在开关状态&#xff0c;作为控制元件的晶体管本身消耗功率较低&#xff0c;功放的效 率就高&#xff0c;可达 90%以上&#xff0c;因此能较大地降低能源损耗&#xff0c;减小放大器体积。D 类音 频功率放大器…

面试热题(最长上升子序列)

给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 输入&#xff1…

Docker容器监控(Cadvisor +Prometheus+Grafana)

环境部署&#xff0c;接着上一篇文章Docker容器部署&#xff08;Cadvisor InfluxDBGrafana&#xff09;开始 目录 1、先清理一下容器 2、部署Cadvisor 3、访问Cadvisor页面 4、部署Prometheus 5、准备配置 6、运行prometheus容器 7、访问prometheus页面 8、部署Grafan…

录屏怎么录声音?很简单,轻松操作!

“录屏的时候怎么把声音录进去呀&#xff1f;今天用手机录了一段视频&#xff0c;录完后打开却发现没有声音&#xff0c;真的服了&#xff0c;录了好久了&#xff0c;有没有人知道怎么才能录制声音呀&#xff1f;” 录屏是一种非常实用的技术&#xff0c;可以帮助我们记录屏幕…

PHPstudy配置伪静态步骤,tp5.1的框架

搜索mod_rewrite.so&#xff0c;然后去掉前面的#&#xff08;即放开注释&#xff09; 2.找到index.php 同级文件.htaccess&#xff08;没有就新建&#xff09; 这些是tp5.1自带的内容&#xff0c;把它注释掉&#xff0c;是错误的内容&#xff0c;添加下面的这段配置 #<If…

SIT1042AQ5V 供电,IO 口兼容 3.3V,±58V 总线耐压,CAN FD 待机模式总线收发器

等领域&#xff0c;支持 5Mbps 灵活数据速率 CAN FD &#xff0c;具有在总线与 CAN 协议控制器之间进行差分信号传 输的能力。 SIT1042AQ 为 SIT1042Q 芯片的升级版本&#xff0c;改善了总线信号的对称性&#xff0c;拥有更低的电磁辐射性能。 另外&#xff0c;SIT104…

ffplay数据结构分析(一)

本文为相关课程的学习记录&#xff0c;相关分析均来源于课程的讲解&#xff0c;主要学习音视频相关的操作&#xff0c;对字幕的处理不做分析 下面我们对ffplay的相关数据结构进行分析&#xff0c;本章主要是对PacketQueue的讲解 struct MyAVPacketList和PacketQueue队列 ffp…

线程池-手写线程池C++11版本(生产者-消费者模型)

本项目是基于C11的线程池。使用了许多C的新特性&#xff0c;包含不限于模板函数泛型编程、std::future、std::packaged_task、std::bind、std::forward完美转发、std::make_shared智能指针、decltype类型推断、std::unique_lock锁等C11新特性功能。 本项目有一定的上手难度。推…

浏览器是如何渲染页面的?

浏览器是如何渲染页面的&#xff1f; 当浏览器的网络线程收到 HTML 文档后&#xff0c;会产生一个渲染任务&#xff0c;并将其传递给渲染主线程的消息队列。 在事件循环机制的作用下&#xff0c;渲染主线程取出消息队列中的渲染任务&#xff0c;开启渲染流程。 整个渲染流程分…

对员工画饼out了,领导力之我的地盘TA做主

90后的员工为什么爱离职? 我想了一年&#xff0c;才嗅到一丝隐晦的内味。因为不结婚、不生娃、不买房、不买车&#xff0c;没压力&#xff0c;骂TA还敢顶嘴。 员工动力不足&#xff0c;没有干劲&#xff0c;宁愿熬夜打游戏&#xff0c;也不愿意多加班&#xff0c;到底是工资…

Springboot03--restful、swagger+orm/mybatis,mybatis-plus

参考这个方法配置&#xff0c;主要是我的springboot和swagger的版本号的问题 SpringBoot2.7.14集成Swagger3.0 (liqinglin0314.com) 常用的一些注解 放在controller里面 2. mybatisplus <!-- MyBatisPlus依赖--><dependency><groupId>com.baomidou</gr…

FL Studio低版本怎么免费升级:FL Studio升级要钱吗?

为了更好的服务国内FL Studio用户&#xff0c;FL Studio 官网提供了跨版本升级的服务&#xff0c;用户可以通过缴纳一定的费用&#xff0c;将自己已购买的入门版或其他非完整版的版本&#xff0c;升级为更高的版本&#xff0c;解锁更多的插件&#xff0c;而无需重新购买整套版本…

嵌入式开发学习(STC51-4-蜂鸣器)

内容 控制蜂鸣器发出声音&#xff0c;一段时间后关闭 蜂鸣器简介 蜂鸣器是一种一体化结构的电子讯响器&#xff0c;采用直流电压供电&#xff0c;广泛应用于计算机、打印机、复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件&#xff1b; 蜂…

算法通关村第六关——如何使用中序和后序来恢复一颗二叉树

1 树的基础知识 1.1 树的定义 树(Tree)&#xff1a;表现得是一种层次关系&#xff0c;为 n &#xff08; n ≥ 0 &#xff09; n&#xff08;n≥0&#xff09; n&#xff08;n≥0&#xff09;个节点构成的有限集合&#xff0c;当n0时&#xff0c;称为空树&#xff0c;对于任一…

锁定Mac的内置键盘,防止外接键盘时的误触

场景&#xff1a;把你的外接键盘放在mac上&#xff0c;然后打字时&#xff0c;发现外接键盘误触mac键盘&#xff0c;导致使用体验极差 解决方案&#xff1a;下载Karabiner-Elements这款软件&#xff0c;并给它开启相关权限。 地址&#xff1a;https://github.com/pqrs-org/Ka…

并网逆变器学习笔记6---三电平SVPWM下的连续和不连续调制

之前在学习中总结过一次DPWM策略选择&#xff1a;并网逆变器学习笔记5---三电平DPWM 但是对于三电平逆变器而言&#xff0c;如何从连续调制切换到不连续调制&#xff0c;存在一些疑惑点&#xff0c;下午闲来无事&#xff0c;把SVPWM下的连续调制和不连续调制的开关状态选择&am…

C++实现一个链栈

C实现一个链栈 什么是链栈如何实现链栈链栈的实现开发环境代码实现运行结果 什么是链栈 链栈不名思意&#xff0c;就是既具有链表的特性&#xff0c;又具有栈的特性。 即&#xff1a; 链栈中的元素由指针域和数据域组成&#xff0c;通过指针指向下一个元素&#xff1b;2.链栈同…

软件测试学习:师傅领进门修行看个人

本课程学习导图 2-1 软件测试阶段 1、单元测试 概念&#xff1a; 对软件中的 最小可测试单元 进行检查和验证。 原则&#xff1a; &#xff08;1&#xff09;尽可能测试用例相互独立 &#xff08;2&#xff09;一般由代码开发人员实施 好处&#xff1a;&#xff08;…

影响亚马逊Listing转化率的14大因素你知道吗?

我们都知道亚马逊listing转化率对于链接的推新和维稳来说有多么重要&#xff0c;只要转化率的比值无法达到整体市场平均比值的及格线&#xff0c;你就很可能会慢慢被亚马逊的飞轮算法所淘汰。 那么&#xff0c;具体是哪些因素在影响着你的listing转化率呢?这里我们可以分为显…