Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ClickHouse分布式IN & JOIN 查询的避坑指南

ClickHouse分布式IN & JOIN 查询的避坑指南

作者头像
Nauu
修改于 2020-04-28 16:28:50
修改于 2020-04-28 16:28:50
10.1K0
举报

当数据表包含多个分片的时候,我们需要将普通的本地查询转换为分布式查询。当然,这个转换动作是不需要用户自己进行的,在ClickHouse里面会由Distributed表引擎代劳。

Distributed表引擎的定位就好比是一个分表的中间件,它本身并不存储数据,而是分片的代理,能自动的将SQL查询路由到每个分片。

对于分片概念、定义方式以及相关表引擎作用等内容,这里就不再赘述了,我在ClickHouse这本书中对它们都有过详细的论述。

总而言之,分布式查询是面向Distributed表引擎的,而Distributed与分片表的关系如下图所示:

一种约定俗成的命名方式,是将Distributed表附带_all后缀;本地分片附带_local后缀,以示区分。

当我们面对Distributed表引擎查询的时候,它主要为我们做了3件事情:

  1. 发起远程调用,根据集群的配置信息,从当前节点向远端分片发起Remote远程查询调用
  2. 分布式表转本地表,在发送远程查询时,将SQL内的 _all表 转成 _local表
  3. 合并结果集,合并由多个分片返回的数据

假设Distributed表test_all映射了两个分片,它们分布在CH5和CH6两个节点,那么在CH5节点执查询SELECT * FROM test_all 的执行计划会是下面的这个样子:

其中,Remote远程查询 和 One本地查询是并行的,所以图中归为了一个步骤。可以看到,面向Distributed表引擎查询,就自动的完成了整个分布式查询的过程。

是不是这样就高枕无忧了呢?

显然不是,铺垫了1000字,现在终于要进入正文了,哦也。

在大多数时候,面向Distributed表的SQL写法与本地查询没有多大区别。但当我们执行 IN 或者 JOIN 查询的时候,一不小心就容易掉到坑里,因为这些查询子句会面对多张数据表。

为了便于演示,我们简化一下场景,用一个自查询的IN子句来解释说明,假设一张表的数据如下:

代码语言:sql
AI代码解释
复制
SELECT * FROM test_query_local
┌─id─┬─repo─┐
│  1100 │
│  2100 │
│  3100 │
│  3200 │
│  4200 │
└────┴──────┘

现在有一个统计的需求,找到同时拥有repo = 100repo = 200的个数,那么它的查询SQL可能是下面这个样子

代码语言:sql
AI代码解释
复制
SELECT uniq(id) FROM test_query_local WHERE repo = 100 
AND id IN (SELECT id FROM test_query_local WHERE repo = 200)

这条语句目前在单机执行是没有问题的,id为3的数据同时拥有2个repo:

代码语言:sql
AI代码解释
复制
┌─uniq(id)─┐
│        1 │
└──────────┘

现在模拟分布式的场景,把这张表进行分片操作,将它们分布到CH5和CH6两个节点,且每个节点的数据数据如下:

代码语言:sql
AI代码解释
复制
CH5节点 test_query_local
┌─id─┬─repo─┐
│  1100 │
│  2100 │
│  3100 │
└────┴──────┘
CH6节点 test_query_local
┌─id─┬─repo─┐
│  3200 │
│  4200 │
└────┴──────┘

接着使用 分布式表 test_query_all 映射这2个分片。

那么,刚才的那条SQL应该怎么改?

  • 第一种改法

将本地表 test_query_local 改成 分布式表 test_query_all

代码语言:sql
AI代码解释
复制
ch5.nauu.com :) SELECT uniq(id) FROM test_query_all WHERE repo = 100 AND id IN (SELECT id FROM test_query_local WHERE repo = 200)

SELECT uniq(id)
FROM test_query_all
WHERE (repo = 100) 
AND (
 id IN 
   (    
    SELECT id    
    FROM test_query_local    
    WHERE repo = 200
   )
)
┌─uniq(id)─┐
│        0 │
└──────────┘
1 rows in set. Elapsed: 0.009 sec.

你会发现返回的数据不对,进一步检查,原因是由 IN 子句引起的,因为它还在使用本地表 test_query_local

这是什么原理呢?我们看下面这张图就明白了

分布式查询将 _all 表转 _local之后,在两个分片最终执行的语句是这样的:

代码语言:sql
AI代码解释
复制
SELECT uniq(id) FROM test_query_local WHERE repo = 100 
AND id IN (SELECT id FROM test_query_local WHERE repo = 200)

由于分片的数据分布是不同的,所以数据没有查全。

  • 第二种改法

在有了刚才的经验之后,现在把 IN 子句也替换成 _all 分布式表:

代码语言:sql
AI代码解释
复制
ch5.nauu.com :) SELECT uniq(id) FROM test_query_all WHERE repo = 100 AND id IN (SELECT id FROM test_query_all WHERE repo = 200)

SELECT uniq(id)
FROM test_query_all
WHERE (repo = 100) 
AND (
id IN (    
    SELECT id    
    FROM test_query_all    
    WHERE repo = 200
    )
)
┌─uniq(id)─┐
│        1 │
└──────────┘

从返回结果来看,这次好像没问题了。

为什么这样能返回正确的结果呢? 如下图所示:

站在CH5节点的视角,在SQL语句 _all 转 _local后,在CH5本地会执行下面的语句:

代码语言:sql
AI代码解释
复制
SELECT uniq(id) FROM test_query_local WHERE repo = 100 
AND id IN (SELECT id FROM test_query_all WHERE repo = 200)

注意,IN 子句此时是分布式表 test_query_all,所以它又转成了下面的形式,分别在CH5本地和CH6远端执行:

代码语言:sql
AI代码解释
复制
SELECT id FROM test_query_local WHERE repo = 200

讲到这里就应该很清楚了,因为 IN子句 单独发起了一次分布式查询,所以数据查不全的问题被解决了。

还有什么"坑" 吗? 当然有啦 !!

现在站在CH6节点的视角,SQL在CH5被 _all 转 _local后,会向CH6节点发起远程查询调用。在CH6本地将同样会执行下面的语句:

代码语言:sql
AI代码解释
复制
SELECT uniq(id) FROM test_query_local WHERE repo = 100 
AND id IN (SELECT id FROM test_query_all WHERE repo = 200)

注意 IN 子查询,由于它是 分布式表 test_query_all,所以它又会向集群内其他分片发起分布式查询,如下图所示:

这就是分布式查询的放大问题,放大次数是 N的平方(N = 分片数量)。所以说,如果一张表有10个分片,那么一次分布式 IN 查询的背后会涉及100次查询,这显然是不可接受的。

  • 第三种改法

查询放大怎么解决呢? ClickHouse为我们提供了解决方案,继续改造刚才的语句,增加 GLOBAL修饰符:

代码语言:sql
AI代码解释
复制
SELECT uniq(id) FROM test_query_all WHERE repo = 100 
AND id GLOBAL IN (SELECT id FROM test_query_all WHERE repo = 200)

增加了 GLOBAL 之后查询会有什么变化呢?

在使用了 GLOBAL 之后,整个分布式查询的流程又发生了变化,我们看下面这张图:

首先,GLOBAL 修饰的子句,单独进行了一次分布式查询;

接着,将子句的结果汇总后,用内存临时表保存;

最后,直接将临时表分发至每个分片节点,从而避免了查询放大的问题。

  • 关于JOIN查询

对于分布式JOIN查询而言,其执行逻辑和 IN查询是一样的,它们唯一的区别是分发的语句不同,例如:

当执行 IN子句的时候,是将IN子句提取,发起分布式查询:

代码语言:sql
AI代码解释
复制
GLOBAL IN (SELECT id FROM test_query_all WHERE repo = 200)

IN子句 _all 转 _local,分发到每个分片执行,再汇总:

代码语言:sql
AI代码解释
复制
#分布式执行
SELECT id FROM test_query_local WHERE repo = 200

当执行JOIN子句的时候,是将右表提取,发起分布式查询:

代码语言:sql
AI代码解释
复制
SELECT * FROM test_query_all AS t1 GLOBAL JOIN test_query_all AS t2 ON t1.id = t2.id

右表 _all 转 _local,分发到每个分片执行,再汇总:

代码语言:sql
AI代码解释
复制
#分布式执行
SELECT id, repo FROM default.test_query_local

所以分布式JOIN查询我就不再演示图例了,参照IN子句的即可。

好了,现在总结一下,当执行分布式JOIN 或者IN 查询的时候,会碰到几种问题:

  1. 查询不全,由于分片的数据不均,会出现查询数据不全的问题,所以JOIN表IN子句 也要使用 _all 分布式表;
  2. 查询放大,由于JOIN表 IN子句 也是 _all 分布式表,所以每个分片又会向其他远端的分片发起分布式查询,最终的查询次数是 N 的平方(N=分片数量);
  3. 解决思路,使用 GLOBAL IN GLOBAL JOIN 可以避免查询放大的问题。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ClickHouse的秘密基地 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
01.线程状态/创建/启动
多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的。 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程
Java帮帮
2018/03/15
7870
01.线程状态/创建/启动
【Java】多线程初探
 参考书籍:《Java核心技术 卷Ⅰ 》 Java的线程状态 从操作系统的角度看,线程有5种状态:创建, 就绪, 运行, 阻塞, 终止(结束)。如下图所示 而Java定义的线程状态有: 创建(New)
啦啦啦321
2018/03/30
7260
【Java】多线程初探
Java多线程与并发
答:进程是资源分配的最小单位,线程是CPU调度的最小单位。   1)、进程是资源分配的基本单位,所有与进行相关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或者正在使用它们。   2)、进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。进程拥有一个完整的虚拟内存地址空间,当进程发生调度的时候,不同的进程拥有不同的虚拟地址空间,而同一进程内不同线程共享同一地址空间,与进程相对应。线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程里面的资源。   3)、线程只由堆栈、寄存器、程序计数器和线程计数表TCB组成。
别先生
2020/04/10
1.1K0
Java多线程与并发
一篇文章弄懂Java多线程基础和Java内存模型
写在前面:提起多线程大部门同学可能都会皱起眉头不知道多线程到底是什么、什么时候可以用到、用的时候是不是有共享变量问题等等一大堆问题。本篇文章将分为两部分第一部分是讲解多线程基础、第二部分讲解Java内存模型。
全栈程序员站长
2022/08/31
2550
一篇文章弄懂Java多线程基础和Java内存模型
Java 多线程系列Ⅰ
首先,所有的创建线程的方式都是基于Thread类来实现,每个线程都必须通过 Thread 类的构造方法创建,并实现 run() 方法来执行线程的任务。
终有救赎
2023/12/26
2010
Java 多线程系列Ⅰ
多线程基础知识(全面):创建线程、线程状态如何变化、wait()、notify()、sleep()、停止线程
一直想着抽时间 学习多线程相关知识,目前整理了多线程的基础部分,特在此记录下,并发安全、线程池等后续再补充。
寻求出路的程序媛
2024/05/01
2720
多线程基础知识(全面):创建线程、线程状态如何变化、wait()、notify()、sleep()、停止线程
java基础第十六篇之多线程
1:线程的概念 进程(任务):一个正在运行的程序 进程的调度:CPU来决定什么时候该运行哪个进程 (时间片轮流法) 线程在一个应用程序中,同时,有多个不同的执行路径,是进程中的实际运作单位。 好处是提高程序效率。
海仔
2019/08/05
2950
Java多线程
例如打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,Chrome等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。
用户10358987
2024/04/23
1210
Java多线程
Java 多线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合 即指一 段静态的代码,静态对象。
Java_慈祥
2024/08/06
1210
Java 多线程
Java多线程一:基础知识与线程创建的几种方式
进程是系统资源分配的基本单位,线程是CPU调度的基本单位,一个进程可以包含多个线程,同一个进程下面的资源共享很容易,但是进程之间的资源共享相对较难。
全栈学习笔记
2022/04/24
2460
【Java】创建多线程的四种方式
每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
CODER-V
2023/03/04
1.4K0
【Java】创建多线程的四种方式
Java进程和线程
1. 进程和线程 进程:进程表示一个运行的程序,程序的代码段,数据段这些都是存放在磁盘中的,在运行时加载到内存中。
翎野君
2023/05/12
7420
Java进程和线程
多线程详解(一)
线程首先得说到进程, 进程:正在执行的应用程序。是系统进行资源分配和调用的独立单元。每一个进程都有他自己的内存空间和系统资源,简单说就是程序进入内存运行变成一个进程,具有一定独立功能。 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。
smallmayi
2022/05/12
2130
☀️苏州程序大白一文解析Java多线程☀️《❤️记得收藏❤️》
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
苏州程序大白
2021/10/13
3650
Java多线程与并发
进程是资源分配的基本单位,所有与进程有关的资源都记录在进程控制块PCB中,以表示进程拥有这些资源或者正在使用它们,进程也是抢占处理机的调度单位,它拥有完整的虚拟内存地址空间,当进程发生调度时,不同的进程拥有不同的地址空间,而同一进程内的不同线程共享同一地址空间。与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程共享进程的资源。
ha_lydms
2023/08/10
2250
Java多线程与并发
1.11 手把手教你从多线程到线程池
单CPU系统中: 每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。 多CPU系统中: 则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序。
ha_lydms
2023/08/09
2500
1.11 手把手教你从多线程到线程池
夯实Java基础系列17:一文搞懂Java多线程使用方式、实现原理以及常见面试题
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
Java技术江湖
2019/10/08
1.2K0
【多线程实践】一、为何使用多线程&三种线程创建方式利弊分析
在平常的业务场景中,多线程无疑是比较常用的,而且熟练的使用多线程是开发高并发系统的基础,今天呢,我们就来根据在实际开发中是如何使用多线程的来探讨一下多线程的相关技术,少讲理论多谈实践,以实际开发的角度去总结一下。
灰小猿
2022/10/08
4810
【多线程实践】一、为何使用多线程&三种线程创建方式利弊分析
Java多线程详解
每个运行的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个进程。
二十三年蝉
2018/08/01
8841
Java多线程详解
java 多线程
就绪,当线程调用了strat()方法的时候,线程就绪,会为其创建方法调用栈和程序计数器。
mySoul
2018/11/19
7930
推荐阅读
相关推荐
01.线程状态/创建/启动
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档