greenplum,teradata,presto,clickhouse四种分布式数据库的对比

目录

 

1. 四种数据库的比较

2. Greenplum数据库

2.1 Greenplum架构

2.1.1 采用MMP架构

2.2.2 Hadoop与MPP的应用区别

2.2 greenplum 的高可用性

2.2.1 master冗余

2.2.2 segment冗余

2.3 greenplum的并行查询

2.4 greenplum的多版本控制(MVCC)

3 Teradata数据库

3.1 Teradata 数据库架构

3.1.1 连接层

3.1.2 PE层(parsing engine)

3.1.3 MPL层(message passing layer)

3.1.4 AMP层

3.1.5 VDisk层

3.2 Teradata的并行处理能力

3.3 Teradata做的优化

3.3.1 大数据量与小数据量访问矛盾

3.3.2用户管理

3.3.3 索引

3.3.4 加速处理

3.3.5 索引

3.3.6 锁机制

4 Presto数据库

4.1 Presto架构:

4.2 Presto数据模式

4.3 Presto查询

4.4 Presto内存管理

4.5 Presto实现低延时查询的原理

传统 sql 优化

完全基于内存的并行计算

流水线

本地化计算

小心使用内存和数据结构

GC控制

5 ClickHouse数据库

5.1 ClickHouse 架构

5.1.1 clickHouse 存储架构

5.1.2 ClickHouse部署

5.2 ClickHouse数据库系统特点

5.3 ClickHouse运行快的原因

6 OLAP相关知识

6.1 OLTP与OLAP

6.2 OLAP场景的关键特征

7 数据库技术中常见名词与技术

7.1 数据共享的方式

7.2 MPP大规模并行处理架构

7.2 MPPDB特征

7.3 列存储技术(达梦商用数据库为例)

7.4 RAID方案

7.5 LSM-Tree (long structured merge tree)


1. 四种数据库的比较

数据库  描述
Greenplum     开源大规模并行数据分析引擎。借助MPP架构,在大型数据集上执行复杂SQL分析的速度比很多解决方案都要快。应用广泛。
Teradata     大型数据仓库系统,产品成熟,价格昂贵。用于证券系统。
Presto     分布式SQL查询引擎, 专门进行高速、实时的数据分析。本身不存储数据,但是可以接入多种数据源。擅长对海量数据进行复杂的分析。用于大数据量分析。
Clickhouse      用于在线数据分析。支持功能简单。CPU 利用率高,速度极快。用于行为统计分析。

2. Greenplum数据库

2.1 Greenplum架构

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

2.1.1 采用MMP架构

master host:负责与客户端对接, 不包含任何用户数据,使用postgres数据库内核,保存有元数据,与segment 由局域网通讯。

segment:存放数据,监听master的连接,用户只能通过master访问,是独立的PostgreSQL数据库。一个节点可运行多个segment实例

Interconnect:协调进程,由局域网互连,支持TCP or UDP

2.2.2 Hadoop与MPP的应用区别

Hadoop     MPP
非结构化数据,半结构化数据 关系数据
海量数据存储查询、批量数据ETL、日志分析、文本分析 多维度数据自助分析、数据集市

可将两种架构混合使用(MPPDB+Hadoop):用MPP处理PB级别的、高质量的结构化数据,同时为应用提供丰富的SQL和事物支持能力;用Hadoop实现半结构化、非结构化数据处理。这样可以同时满足结构化、半结构化和非结构化数据的高效处理需求。

2.2 greenplum 的高可用性

2.2.1 master冗余

  • 设立Standby 节点复制master的系统目录表日志( catalog tables )
  • master坏掉时需由管理员触发激活standby成为新master。
  • 使用基于预读日志(WAL)的流复制来保持primary master和standby master服务器同步。

2.2.2 segment冗余

  • 主segment接收来自master的请求以更改主segment的数据库,然后将这些更改复制到相应的mirror segment上。
  • 可选择对主机mirror(group mirroring)或对segment分散mirror(spread mirroring)
  • group mirroring:一台机器出现问题时,另一台机器将有两倍负荷
  • spread mirroring:可负载均衡
  • 当segment实例或主机发生故障,master将记录实例的down状态,并激活与其对应的segment实例。
  • Mirror segment采用物理文件复制的方案,而对于堆表,日会先同步志,当主segment的块需要写回磁盘时再同步mirror的文件,primary segment失败时,mirror自动成为primary,且状态为 Change Tracking。mirror失败时,primary会将状态改为 Change Tracking
  • AO表(Append-optimized)不使用内存缓存机制。对AO表的块所做的更改会立即复制到mirror segment上。

2.3 greenplum的并行查询

一个表的数据按hash映射分布在不同segment节点上,每次操作产生一个slice,slice之间通过 gather、broadcast、redistribution 方式传播

gather     broadcast     redistribution
每个节点数据发至master节点   表数据分布在各节点上,需每个节点发数据至每个节点,使每个节点都拥有表的完整数据,适用于小表 join与group by时,广播代价大时可按键重新将各节点数据打散至各节点再对每个节点操作,适用于大表

greenplum支持有三种存储方式: 行存储、列存储、外部表

行存储 列存储 外部表
多列更新快 一次只访问几个字段,压缩比高 数据库只保留元数据信息

可以对数据进行hash分区或范围分区

2.4 greenplum的多版本控制(MVCC)

事务型数据库用锁机制控制并发访问,greenplum用多版本控制保证数据一致性。最大的好处是读写不冲突,读的是snapshot。

3 Teradata数据库

3.1 Teradata 数据库架构

每个节点物理上是一个SMP处理单元(多CPU计算机),节点硬件包括CPU、内存、磁盘、网卡、BYNET端口

网卡:与IBM MainFrame连接的Channel Adapter;局域网网卡。 一个节点上只会使用一种网卡,但会有多块网卡,分别用于不同的连接和冗余。

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

3.1.1 连接层

CLI( call level interface ):请求响应、创建session、缓冲区分配、信息打包

TDP( teradata director program ): 运行在客户端系统:session初始化终止、登陆、验证、恢复重起、现有人员传递至PE的 session 队列

MTDP( micro TDP ):与TDP的区别是不负责session在PE间分配。

MOSI( micro operating system interface ):实现不同数据库平台上运行的隔离层

3.1.2 PE层(parsing engine)

session control:主控logon、logoff

**parser:**解析sql,判断语法语义正确性,查询字典确认请求对象是否存在,用户是否有权限

**optimizer:**评估执行计划,转为AMP可执行步骤

**dispatcher:**分配AMP 所选任务,返回用户结果

3.1.3 MPL层(message passing layer)

MPL 层负责 PE 与 AMP之间传送信息、合成的返回结果集传 PE,由 PDE 与 Bynet 软硬件组成

PDE (parallel database extension):

直接架构在操作系统之上的一个接口层 , 提供并行环境, 执行虚拟处理器、进行Teradata并行任务调度、进行操作系统内核和Teradata数据库的运行时故障处理。
bynet软件、硬件:

用于节点之间的双向广播(bidirectional broadcast)、多路传递(multicast)和点对点通信(point-to-point communication),
BYNET还实现SQL查询过程中的合并功能(每个节点或AMP,均匀分布表中一部分数据,当查询的时候每个节点并行查询,结果汇总到某个节点反馈给查询者,提高查询速度。
一般典型的teradata有两个BYNET同时工作 , BYNET自动均衡,避免某一个负载太多
PE能支持120个session处理,每个session可管理16个请求与相关结果,但每个时间点只有一个请求活动

3.1.4 AMP层

AMP(access module process)

ShareNothing架构的核心。

一个AMP最多控制 64个物理磁盘 (对商用OLTP来说,主要由DBA控制记录在磁盘的分布)

hash算法类似矩阵映射,修改AMP数时只需变动hashmap,速度非常快。

每个AMP可并行处理80个任务

AMP功能:

排序、聚合、格式化、转换操作
可能将数据传给其它 AMP
Lock 数据库或表
返回结果给dispatcher
空间使用控制和空间分配
输出数据的编码转换,与PE相反的工作

3.1.5 VDisk层

存储数据根据哈希算法被均匀分散存储到磁盘阵列中的不同的磁盘上。RAID0 与 RAID5 为主。因采用混合均匀存储而不存在数据库重组问题。

3.2 Teradata的并行处理能力

查询并行:每个AMP作为一个虚拟进程,独立处理一部分数据(如查询一个表)

步内并行:每个运算步骤都由多个进程并行处理(如借助于pipline的 join操作)

多步并行:优化器分解sql请求原则是尽可能使各步独立(如两个表的查询同时进行)

传统 OLTP 对于提出的新的问题,DBA 会建立一条新的索引,可能使数据库占用磁盘空间过大。而 teradata 采用将相同执行步骤的执行结果暂存于系统缓冲区的方法, 减少数据库本身的大小。

teradata 提供的并行 OLAP 操作有:排序、累计、移动平均、移动和、移动养分、采样、分位、限定
 

3.3 Teradata做的优化

3.3.1 大数据量与小数据量访问矛盾

teradata 在解决 hash 函数范围查询需要访问所有数据的问题时引入用户索引(分区主索引PPI),将数据按索引进行分区greenplum,teradata,presto,clickhouse四种分布式数据库的对比

3.3.2用户管理

引入角色来分配权限

引入用户参数为用户配置持久空间(分配的最大存储容量)、spool空间(存放中间过程与最后结果 )、临时空间(存放global temporary table被实例化的数据)限制、缺省数据库设定、用户密码设定

3.3.3 索引

AMP分组:对一个操作只使用部分amp单元进行处理,提高数据吞吐量

利用稀疏索引来去除大量空值索引

3.3.4 加速处理

解析器与分派器合为一

增强插入更新能力

引入合并更新

3.3.5 索引

对于unique key 元组在各磁盘分布

对于none-unique key 元组按key分布

次索引一般没有必要,taradata本身性能很高

3.3.6 锁机制

排它锁(create table):拒绝任意锁

写锁(update):拒绝read、write、exclusive

读锁(select):拒绝write、exclusive

访问锁:只拒绝排它锁(用于积累大量行数据,但结果可能不是最新结果)

4 Presto数据库

presto是facebook为了改进之前基于map-reduce的hive框架查询数据仓库时间过长而开发的数据分析工具。Presto本身不是数据库,而是分布式查询引擎。Presto可以访问HDFS,及其他数据源,数据库。但不能处理在线事务。

Presto被设计为数据仓库和数据分析产品:数据分析、大规模数据聚集和生成报表。这些工作经常通常被认为是线上分析处理操作 。

4.1 Presto架构:

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

采用 master - slave 模型:

**coordinator:**master,负责meta管理,worker管理,query解析与调度

worker: 计算与读写

discovery server: 通常内嵌于coordinator节点中,也可以单独部署,用于节点心跳。

4.2 Presto数据模式

catalog:一类数据源,如mysql,hive

schema:数据库

table:表

存储单元包括 page,block:

page:多行数据集合,包含多列,但只提供逻辑行,实际以列存储

block:一列数据

array类型 int 、long、double

valueisnull[] 一行是否有值
value[] 一行具体值
可变宽 string

slice 所有行拼接的字符串
offset 每一行的偏移位置
valueisnull[] 某行是否有值
固定宽string

字典(distinct值较少)

任意类型
ids[] 每一行数据对应字典中编号


4.3 Presto查询


**sql 查询:**http POST 请求

**抽象语法树:**以 query 为单位,分层表示子查询

**逻辑计划:**将抽象语法树转为最简单操作

优化器:

把聚合节点写成map-reduce形式(partial node 和 final node)
在map-reduce节点插入exchange节点
将提前加速节点下扒
能合并的合并
**调度器:**以exchange节点划分的段为单位进行调度

每类fragment 由哪些机器执行
source类型任务:根据meta决定读取多少split,分配一个split到一台机器, 在配置中指定了network-topology=flat,则尽量选择split所在的机器。 每个结果会向每个上层fragment的机器发送
fixed类型与single类型(由 query.initial-hash-partitions参数配置,默认是8 ):为某个fragment分配几台机器,中间结果分配多台机器,只最后结果分配一台机器
下游机器多台的(group by)按 groupby计算hash,按hash选择一个下游机器输出, 对于非group by的计算,会随机选择或者round robin。
物理执行计划:

fragment发送到机器上后,由结点树形式转写成operator list,根据逻辑代码动态编译生成字节码。动态生成字节码,主要是利用编译原理:
展开循环
根据数据列的类型,直接调用对用的函数,以减少分支跳转语句。
source类型的operator ,每一次调用都会获取一份新的数据;对于Aggregate的operator,只有之前所有的operator都finish之后,才能获取输出结果。

聚合计算有两类

AggregationOperator:对每行操作,每次输出一个结果
HashAggregationOperator:对列用hash计算key,key相同才把结果存在一起,用于group by类计算
聚合计算均提供四个接口用于在map-reduce之间加一层计算,接受中间结果输入输出。

接受原始数据的输入

接受中间结果的输入

输出中间结果

输出最终结果。

函数分两类:

Scaler函数:数据的转换处理,不保存状态,一个输入产生一个输出。

Aggregate函数:数据的聚合处理,利用已有状态+输入,产生新的状态。

4.4 Presto内存管理

内存池:

system pool:40% 留给系统
reserve pool:10% 留给最大query
general pool:用于一般查询
为了防止大任务分配到每台机器后一部分(在该机器是最大内存的任务)在reserve pool执行完成,另一部分(在该机器不是最大内存任务)在general pool等待执行,造成死锁,presto将最大query分配给reserve pool的任务交给coordinator去做(虽然这会造成一部分不执行这个query的机器reserve pool 浪费),而不是让每台机器把最大query任务在reserve pool执行。

cordinator计算query的每个task的内存大小,同时有一个线程定期轮训每台机器内存状态,汇总query内存与机器内存后, coordinator会挑选出一个内存使用最大的query,分配给Reserved Pool。

4.5 Presto实现低延时查询的原理

传统 sql 优化

完全基于内存的并行计算

源数据的并行读取:每个 Source节点 调用 HDFS InputSplit API , 然后每个InputSplit分配一个Worker节点去执行
分布式hash聚合:partial任务的结果按group by的hash值分配不同计算节点

流水线

每个worker从优先级队列中取 PrioritizedSplitRunner 对象,周期检查完成情况并删除完成的任务,未完成的放回队列
每个节点的exchange操作为每个向上一个Stage的Worker节点拉数据,数据的最小单位是一个Page对象,取到数据后放入Pages队列中
任务执行时每个operator从上一个operator取page,数据存在则执行(page是由列存储的block组成的几行数据)

本地化计算

优先选择数据在的位置节点作为worker
动态编译执行计划(如循环展开优化): 使用Google Guava提供的LoadingCache缓存生成的Byte Code。

小心使用内存和数据结构

使用Slice进行内存操作,Slice使用Unsafe#copyMemory实现了高效的内存拷贝,

类BlinkDB的近似查询

为了加快avg、count distinct、percentile等聚合函数的查询速度, 引入了一些近似查询函数approx_avg、approx_distinct、approx_percentile。approx_distinct使用HyperLogLog Counting算法实现。

HyperLogLog 算法:key值hash转为64位bit,取低6位作为实验轮数(桶号),取60位中第一个出现1的位置的序号,转为2进制bit,存入桶中。统计每个桶中最大的序号,由下公式得到元素的部数量

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

GC控制

Presto团队在使用hotspot java7时发现了一个JIT的BUG,当代码缓存快要达到上限时,JIT可能会停止工作,从而无法将使用频率高的代码动态编译为native代码。
Presto团队使用了一个比较Hack的方法去解决这个问题,增加一个线程在代码缓存达到70%以上时进行显式GC,使得已经加载的Class从perm中移除,避免JIT无法正常工作的BUG。

5 ClickHouse数据库

5.1 ClickHouse 架构

5.1.1 clickHouse 存储架构

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

5.1.2 ClickHouse部署

扩展性
clickhouse本身不扩展,可借助Distributed引擎(本身不存储数据)

写:

  • 指定哪些数据写入到哪些节点里
  • 通过一个节点写,数据分到不同的节点里,通过指定分片的key来分发数据。
  • 数据写入是异步的,对于插入数据到集群的其他节点,数据先是写入到本地磁盘,然后在后台发送到其他分片服务器,如果想查看数据有没有同步完,可以检查下面的目录:/var/lib/clickhouse/data/database/table

读:

  • 由一个节点收集所有节点数据返回客户端
  • 可靠性
  • 依赖于zookeeper,使用物理复制

greenplum,teradata,presto,clickhouse四种分布式数据库的对比

5.2 ClickHouse数据库系统特点

在内存数据库领域号称是最快的。
数据始终以列存储,包括矢量执行过程。

有两种不同的加速查询处理的方法: 矢量化查询执行和运行时代码生成

clickhouse是列存储数据库,使用向量查询技术,select,orderby,limit操作都不改原列向量,而是新建。适合少修改的数据。

clickhouse使用抽象渗漏提高速度,使列的数据与数据类型、数据块处理分离。

查询流水线每一步中间数据都保存,临时数据要适合CPU缓存。

对于分布式数据不完全支持join这样的复杂查询。

ClickHouse 使用稀疏索引,不适合高负载的点状查询。

ClickHouse 不依赖Hadoop 生态

不支持事物,不支持Update/Delete操作

向量化引擎:数据不仅按列存储,而且由矢量 - 列的部分进行处理。这使我们能够实现高CPU性能。

允许在运行时创建表和数据库,加载数据和运行查询,而无需重新配置和重新启动服务器。

5.3 ClickHouse运行快的原因

它的数据剪枝能力比较强,分区剪枝在执行层,而存储格式用局部数据表示,就可以更细粒度地做一些数据的剪枝。它的引擎在实际使用中应用了一种现在比较流行的 LSM 方式。

它对整个资源的垂直整合能力做得比较好,并发 MPP+ SMP 这种执行方式可以很充分地利用机器的集成资源。它的实现又做了很多性能相关的优化,它的一个简单的汇聚操作有很多不同的版本,会根据不同 Key 的组合方式有不同的实现。对于高级的计算指令,数据解压时,它也有少量使用。

ClickHouse 是一套完全由 C++ 模板 Code 写出来的实现

不支持Transaction

使用了矢量化查询执行

当进行查询时,操作被转发到数组上,而不是在特定的值上
向量化查询执行实用性并不那么高,如果临时数据并不适合L2缓存,读缓存将有问题。但是向量化查询执行更容易利用CPU的SIMD能力。
提供了有限的运行时动态代码生成(group by的内部循环第一阶段)。

运行时代码生成可以更好地将多个操作融合在一起,从而充分利用 CPU 执行单元和流水线。矢量化查询执行不是特别实用,因为它涉及必须写到缓存并读回的临时向量。如果 L2 缓存容纳不下临时数据,那么这将成为一个问题。但矢量化查询执行更容易利用 CPU 的 SIMD 功能。

6 OLAP相关知识

6.1 OLTP与OLAP

**联机事务处理( OLTP)**由业务数据库所支持,数据固定,操作负载少,强调数据库一致性与可恢复性。

**在线分析处理(OLAP )**由数据仓库支持,反应历史数据、多来源数据,通常多维建模,典型的OLAP操作有:上钻(聚合)、下钻(展开)、切割(选择和投影)、轴转(多维视图)。

OLAP的查询问题有不确定性。

OLAP数据库最关键的两个因素是:并行处理与可扩展性

ROLAP(关系型OLAP):在标准的或扩展的关系DBMS 上,支持扩展SQL和特殊访问

MOLAP(多维OLAP ):直接把多维数据存储在特定的数据结构

6.2 OLAP场景的关键特征

大多数是读请求
数据总是以相当大的批(> 1000 rows)进行写入
不修改已添加的数据
每次查询都从数据库中读取大量的行,但是同时又仅需要少量的列
宽表,即每个表包含着大量的列
较少的查询(通常每台服务器每秒数百个查询或更少)
对于简单查询,允许延迟大约50毫秒
列中的数据相对较小: 数字和短字符串(例如,每个URL 60个字节)
处理单个查询时需要高吞吐量(每个服务器每秒高达数十亿行)
事务不是必须的
对数据一致性要求低
每一个查询除了一个大表外都很小
查询结果明显小于源数据,换句话说,数据被过滤或聚合后能够被盛放在单台服务器的内存中

7 数据库技术中常见名词与技术

7.1 数据共享的方式

shared everything(SMP):网络与磁盘是瓶颈(SQLServer)

shared disk(NUMA): 存储器接口达到饱和的时候,增加节点并不能获得更高的性能 (Oracle Rac)

shared nothing(MPP): 各自处理自己的数据,可扩展,成本低

7.2 MPP大规模并行处理架构

● 任务并行执行;

● 数据分布式存储(本地化);

● 分布式计算;

● 私有资源;

● 横向扩展;

● Shared Nothing架构。

7.2 MPPDB特征

(1)无master 高平结构greenplum,teradata,presto,clickhouse四种分布式数据库的对比

(2)可处理 PB 级数据,采用 hash 分布、random 策略存储,采用先进压缩算法

(3)高扩展,支持集群扩容缩容

(4)高并发:读写不互斥,可边加载边查询

(5)行列混合存储

7.3 列存储技术(达梦商用数据库为例)

数据组织实现:

  • 不同于普通段、簇、页管理,采用HTS(huge tablespace)相当于文件系统。
  • HTS -> SCH模式目录 -> TAB表目录 -> COL列(.dta文件,默认64M)-> 区(存储行数在一开始就指定,开始位置4k对齐)
  • 辅助表:管理HTS中数据,每条记录对应一个区,查询在辅助表进行
  • 智能索引:
  • 一定程度上可以替代BTree索引,最大最小值可起到过滤作用
  • 自适应压缩:
  • 字典编码
  • 异常值( 少数不同时, 异常值使用<行号+值>的方式存储 )
  • RLE编码(数据大量相同,每个值个数较均匀)
  • 序列编码(存在代数关系时,可只存与共用基础数值的差值,)
  • 自适应的步骤:
  1. 如果该列为自增列或序列,则直接使用序列编码;
  2. 获得区数据统计信息:不同值个数n_dist、每个不同值个数、不同值数据指针、每个值连续出现的次数、整型数的最大值;
  3. 根据获得的区统计信息,确定使用常量编码、RLE编码还是字典编码;这三种编码的使用顺序为:优先使用常量编码,其次使用RLE编码,最后才使用字典编码。
  4. 如果编码后的总长度超过了原始长度,则不编码,直接返回;
  5. 注:解压引起的性能损失远远小于磁盘IO等待的开销。

7.4 RAID方案

以 RAID1 与 RAID5 最常见

JBOD 磁盘直接串连
JBOD 磁盘直接串连而成的磁盘柜
RAID0 无冗余分布存储
RAID1 镜像冗余
RAID2 海明码校验,4个磁盘存数据分布存储,3个磁盘校验,实际很少用
RAID3 至少三数据盘分布存储(按位分布或按字节分布),一磁盘校验,适用于大容量分散顺序访问。RAID3在有坏盘时性能下降,常由RAID5代替
RAID4 同RAID3,但按块分布,读性能好,写性能差,实际少见
RAID5 同RAID4,校验数据分布在各磁盘,不存在RAID4的写瓶颈,目前最佳方案
RAID6 上面只保护单个磁盘失效,RAID6为双重校验(可在两个磁盘用两种算法存不同的校验数据),写性能差
RAID00 双重RAID0
RAID01 先镜像1再条带化0,保证数据安全性的同时又提高了性能
RAID10 先条带化0再镜像1,保证数据安全性的同时又提高了性能
RAID30/50/60 提高性能
RAID7 一套事件驱动的操作系统,采用非同步访问减轻写瓶颈提高IO,自动读写优化
RAID-DP 采用NVRAM存储写数据,掉电也不丢失,集中写,采用RAID6,性能比RAID4下降小于2%,固件实时更新时也不中断
RAID5E 提供冗余盘,在一块磁盘损坏时自动降级至RAID5,时间较长

7.5 LSM-Tree (long structured merge tree)

  1. 最大的特点就是写入速度快,主要利用了磁盘的顺序写
  2. LSM-tree 主要针对的场景是写密集、少量查询的场景。 被用于各种键值数据库
  3.          写:一次写操作会先写入内存(f0层), 数据达到一定大小,就使用归并排序来合并,并写入磁盘(f1层),当磁盘f1层达到一定大小会继续合并为c2层。
  4.          读:查询时是一层一层向下查
  5. lsm-tree 的一种实现 LevelDB:文件分三种,内存中有memtable,immutable,磁盘中有SStable(sorted string table)写会写入memtable,当达到阈值,就把immutable合并入磁盘并把memtable转为immutable。

 

 

上一篇:mysql中有group_concat()可以合并多行


下一篇:inner join 与 left join 之间的区别