[译]以PostgreSQL为例,谈join计算的代价
join计算的代价很高吗?
看情况
join的代价依赖于join的条件,索引是什么样,依赖于表有多大,相关信息是否已经cache住了,使用的什么硬件,配置参数的信息,统计信息是否已经更新,同时是否还有其他运行的计算……
晕了?别急!在以下情景下,我们依然可以找到一些规律来分析判断:
- 随着join的表的数量增加
- 随着这些表的行数的增加
- 有没有索引
此类情况,在工作中经常会碰到,比如:如果有一张产品表 product,但业务上需要加入一个产品的状态,包括Active、Discontinued、Recalled等。此时,我们会有3种不同的做法:
- 在产品表 product 中,增加一列状态号 status_id,同时增加一个新的状态表 status。
- 在产品表 product 中,增加一列状态号 status_id,同时让应用来定义每个状态号 status_id 对应的含义及显示。
- 在产品表 product 中,增加一列文本列,用来描述状态信息。
通常,我们会选择第一个做法。关于后两种的做法,通常的质疑在两个方面:join的性能和开发人员的工程化能力。后者通常与个人喜好有关,姑且不谈,咱们来一起讨论一下join的性能问题。
为便于讨论,选用PostgreSQL测试数据来讨论。以等值连接为例,让我们看看执行上面的join时,性能会有什么变化?我们担心的性能变慢,那具体会变成多慢。
以下是用来生成测试用的建表语句。
DROP FUNCTION IF EXISTS create_tables(integer, integer, boolean);
CREATE FUNCTION create_tables(num_tables integer, num_rows integer, create_indexes boolean) RETURNS void AS $function_text$
BEGIN-- There's no table before the first one, so this one's a little different. Create it here instead of in our loop.
DROP TABLE IF EXISTS table_1 CASCADE;
CREATE TABLE table_1 (id serial primary key
);-- Populate the first table
INSERT INTO table_1 (id)
SELECTnextval('table_1_id_seq')
FROMgenerate_series(1, num_rows);-- Create and populate all the other tables
FOR i IN 2..num_tables LOOPEXECUTE 'DROP TABLE IF EXISTS table_' || i || ' CASCADE;';EXECUTE format($$CREATE TABLE table_%1$s (id serial primary key,table_%2$s_id integer references table_%2$s (id));INSERT INTO table_%1$s (table_%2$s_id)SELECTidFROMtable_%2$sORDER BYrandom();$$, i, i-1);IF create_indexes THENEXECUTE 'CREATE INDEX ON table_' || i || ' (table_' || i - 1 || '_id);';END IF;
END LOOP;
END;
$function_text$ LANGUAGE plpgsql;-- We'll want to make sure PostgreSQL has an idea of what's in these tables
DROP FUNCTION IF EXISTS analyze_tables(integer);
CREATE FUNCTION analyze_tables(num_tables integer) RETURNS void AS $function_text$
BEGINFOR i IN 1..num_tables LOOPEXECUTE 'ANALYZE table_' || i || ';';
END LOOP;
END;
$function_text$ LANGUAGE plpgsql;
执行建表函数……
SELECT create_tables(10, 10000, False);SELECT * from table_1 limit 10;id
----12345678910
(10 rows)SELECT * from table_2 limit 10;id | table_1_id
----+------------1 | 8242 | 9733 | 8594 | 7895 | 9016 | 1127 | 1628 | 2129 | 33310 | 577
(10 rows)
OK,现在我们可以任意创建所需要的表了。
我们还需要方法来查询,以测试join的性能。有一些不错的长查询,但我们不希望手工来编写,于是我们创建了另一个函数来生成它们。只需要告诉它有多少表参与join,以及where子句中最后一张表的最大的id,它就可以执行了。
DROP FUNCTION IF EXISTS get_query(integer, integer);
CREATE FUNCTION get_query(num_tables integer, max_id integer) RETURNS text AS $function_text$
DECLAREfirst_part text;second_part text;third_part text;where_clause text;
BEGINfirst_part := $query$SELECTcount(*)FROMtable_1 AS t1 INNER JOIN$query$;second_part := '';FOR i IN 2..num_tables-1 LOOPsecond_part := second_part || format($query$table_%1$s AS t%1$s ONt%2$s.id = t%1$s.table_%2$s_id INNER JOIN$query$, i, i-1);
END LOOP;third_part := format($query$table_%1$s AS t%1$s ONt%2$s.id = t%1$s.table_%2$s_idWHEREt1.id <= %3$s$query$, num_tables, num_tables-1, max_id);RETURN first_part || second_part || third_part || ';';
END;
$function_text$ LANGUAGE plpgsql;
下面是一个生成查询的示例。
SELECT get_query(5, 10);get_query
--------------------------------------------------+SELECT +count(*) +FROM +table_1 AS t1 INNER JOIN +table_2 AS t2 ON +t1.id = t2.table_1_id INNER JOIN+table_3 AS t3 ON +t2.id = t3.table_2_id INNER JOIN+table_4 AS t4 ON +t3.id = t4.table_3_id INNER JOIN+table_5 AS t5 ON +t4.id = t5.table_4_id +WHERE +t1.id <= 10;
(1 row)Time: 1.404 ms
OK,让我们花一些时间来思考一下,当我们运行这条查询时,我们实际让Postgres做了哪些事情。在这条SQL中,我们在询问表 table_5 中的 table_4_id 列有多少在表 table_4中,而且表 table_4 中 table_3_id 列有多少在表 table_2 中,而且表 table_2 中 table_1_id 列有多少在表 table_1 中,而且 table_1_id 小于等于10。
我们继续运行……
SELECTcount(*)
FROMtable_1 AS t1 INNER JOINtable_2 AS t2 ONt1.id = t2.table_1_id INNER JOINtable_3 AS t3 ONt2.id = t3.table_2_id INNER JOINtable_4 AS t4 ONt3.id = t4.table_3_id INNER JOINtable_5 AS t5 ONt4.id = t5.table_4_id
WHEREt1.id <= 10;count
-------10
(1 row)Time: 40.494 ms
我们可以通过抛出 EXPLAIN ANALYZE 来查看进展。
EXPLAIN ANALYZE
SELECTcount(*)
FROMtable_1 AS t1 INNER JOINtable_2 AS t2 ONt1.id = t2.table_1_id INNER JOINtable_3 AS t3 ONt2.id = t3.table_2_id INNER JOINtable_4 AS t4 ONt3.id = t4.table_3_id INNER JOINtable_5 AS t5 ONt4.id = t5.table_4_id
WHEREt1.id <= 10;QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------Aggregate (cost=827.93..827.94 rows=1 width=8) (actual time=43.392..43.392 rows=1 loops=1)-> Hash Join (cost=645.31..827.90 rows=9 width=0) (actual time=35.221..43.353 rows=10 loops=1)Hash Cond: (t5.table_4_id = t4.id)-> Seq Scan on table_5 t5 (cost=0.00..145.00 rows=10000 width=4) (actual time=0.024..3.984 rows=10000 loops=1)-> Hash (cost=645.20..645.20 rows=9 width=4) (actual time=34.421..34.421 rows=10 loops=1)Buckets: 1024 Batches: 1 Memory Usage: 9kB-> Hash Join (cost=462.61..645.20 rows=9 width=4) (actual time=25.281..34.357 rows=10 loops=1)Hash Cond: (t4.table_3_id = t3.id)-> Seq Scan on table_4 t4 (cost=0.00..145.00 rows=10000 width=8) (actual time=0.022..4.828 rows=10000 loops=1)-> Hash (cost=462.50..462.50 rows=9 width=4) (actual time=23.519..23.519 rows=10 loops=1)Buckets: 1024 Batches: 1 Memory Usage: 9kB-> Hash Join (cost=279.91..462.50 rows=9 width=4) (actual time=12.617..23.453 rows=10 loops=1)Hash Cond: (t3.table_2_id = t2.id)-> Seq Scan on table_3 t3 (cost=0.00..145.00 rows=10000 width=8) (actual time=0.017..5.065 rows=10000 loops=1)-> Hash (cost=279.80..279.80 rows=9 width=4) (actual time=12.221..12.221 rows=10 loops=1)Buckets: 1024 Batches: 1 Memory Usage: 9kB-> Hash Join (cost=8.55..279.80 rows=9 width=4) (actual time=0.293..12.177 rows=10 loops=1)Hash Cond: (t2.table_1_id = t1.id)-> Seq Scan on table_2 t2 (cost=0.00..145.00 rows=10000 width=8) (actual time=0.017..5.407 rows=10000 loops=1)-> Hash (cost=8.44..8.44 rows=9 width=4) (actual time=0.054..0.054 rows=10 loops=1)Buckets: 1024 Batches: 1 Memory Usage: 9kB-> Index Only Scan using table_1_pkey on table_1 t1 (cost=0.29..8.44 rows=9 width=4) (actual time=0.024..0.035 rows=10 loops=1)Index Cond: (id <= 10)Heap Fetches: 10Planning time: 1.659 msExecution time: 43.585 ms
(26 rows)
我们可以看到,除了使用 表table_1的主键索引外,都是顺序扫描。它还可以怎么做呢?因为我们没有建任何索引来优化它。
如果我们重新做这个实验,告诉* create_tables()*去创建索引……
SELECT create_tables(10, 10000, True);
重新运行后,我们得到不同的查询计划。
EXPLAIN ANALYZE
SELECTcount(*)
FROMtable_1 AS t1 INNER JOINtable_2 AS t2 ONt1.id = t2.table_1_id INNER JOINtable_3 AS t3 ONt2.id = t3.table_2_id INNER JOINtable_4 AS t4 ONt3.id = t4.table_3_id INNER JOINtable_5 AS t5 ONt4.id = t5.table_4_id
WHEREt1.id <= 10;QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------Aggregate (cost=88.52..88.53 rows=1 width=8) (actual time=0.411..0.411 rows=1 loops=1)-> Nested Loop (cost=1.43..88.50 rows=9 width=0) (actual time=0.067..0.399 rows=10 loops=1)-> Nested Loop (cost=1.14..85.42 rows=9 width=4) (actual time=0.054..0.304 rows=10 loops=1)-> Nested Loop (cost=0.86..82.34 rows=9 width=4) (actual time=0.043..0.214 rows=10 loops=1)-> Nested Loop (cost=0.57..79.25 rows=9 width=4) (actual time=0.032..0.113 rows=10 loops=1)-> Index Only Scan using table_1_pkey on table_1 t1 (cost=0.29..8.44 rows=9 width=4) (actual time=0.015..0.023 rows=10 loops=1)Index Cond: (id <= 10)Heap Fetches: 10-> Index Scan using table_2_table_1_id_idx on table_2 t2 (cost=0.29..7.86 rows=1 width=8) (actual time=0.007..0.007 rows=1 loops=10)Index Cond: (table_1_id = t1.id)-> Index Scan using table_3_table_2_id_idx on table_3 t3 (cost=0.29..0.33 rows=1 width=8) (actual time=0.008..0.008 rows=1 loops=10)Index Cond: (table_2_id = t2.id)-> Index Scan using table_4_table_3_id_idx on table_4 t4 (cost=0.29..0.33 rows=1 width=8) (actual time=0.007..0.008 rows=1 loops=10)Index Cond: (table_3_id = t3.id)-> Index Only Scan using table_5_table_4_id_idx on table_5 t5 (cost=0.29..0.33 rows=1 width=4) (actual time=0.007..0.008 rows=1 loops=10)Index Cond: (table_4_id = t4.id)Heap Fetches: 10Planning time: 2.287 msExecution time: 0.546 ms
(19 rows)
结果是使用来索引,速度快了很多。而且这与我们预期的一致。我们现在准备将这些混到一起,看看随着表和列数量的增加时会有什么变化?
需要说明一下,这些测试是运行在AWS RDS db.m4.large实例上。这是最便宜的实例,而且也不会在性能上自动扩容,所以可以作为基准。我们共运行10次,取平均值。
最初的查询,join涉及的表数量从2到200,包含3种行设置,而且没有索引。
![](https://yqfile.alicdn.com/img_2c422b52ed8619b5cb40732ce773485d.png)
性能还是不错的。更为重要的是,我们可以计算每个增加的join的成本!对于100行的表,下面的数据显示了每次增加表所带来的执行时间的增加:
表的数量 | 平均增加量 | 时间(ms) |
---|---|---|
2-50 | (0.012738 - 0.000327) / (50 - 2) = | 0.259 |
50-100 | (0.0353395 - 0.012738) / (100 - 50) = | 0.452 |
100-150 | (0.0762056 - 0.0353395) / (150 - 100) = | 0.817 |
150-200 | (0.1211591 - 0.0762056) / (200 - 150) = | 0.899 |
甚至在参与join的表数量已经接近200时,每增加一个表只增加少于1ms的执行时间。
在讨论增加一个表用来存储产品的状态时,表增加至1000行时就过载了。所以,不考虑索引的情况下,增加一个引用的表并不会对性能产生实质性影响。
由于之前所做的基准测试的数据量不大,如果碰到大表呢?我们重新做了相同的测试,但这次每张表都包含一百万行。这次只增加到50张表。为什么?运行它需要一段时间,我只有有限都预算和耐心
![](https://yqfile.alicdn.com/img_121c81e313f9d03f7747554619b5febe.png)
这次运行结果曲线就没有重叠了。请看最大的1百万行的表,每增加1张join的表,它需要多运行93ms。
表的数量 | 平均增加量 | 时间(ms) |
---|---|---|
2-10 | (0.8428495 - 0.0924571) / (10 - 2) = | 93.799 |
10-20 | (1.781959 - 0.8428495) / (20 - 10) = | 93.911 |
20-30 | (2.708342 - 1.781959) / (30 - 20) = | 92.638 |
30-40 | (3.649164 - 2.708342) / (40 - 30) = | 94.082 |
40-50 | (4.565644 - 3.649164) / (50 - 40) = | 91.648 |
此次都是顺序扫描,因此我们增加索引,看看会有什么变化?
![](https://yqfile.alicdn.com/img_f1797689ed8e7044b967afb263eef71b.png)
增加索引后,性能影响很显著。当能使用到索引时,不管表中有多少行,测试结果都差不多。针对10万行的表的查询一般最慢,但并总是最慢。
表的数量 | 平均增加量 | 时间(ms) |
---|---|---|
2-50 | (0.0119917 - 0.000265) / (50 - 2) = | 0.244 |
50-100 | (0.035345 - 0.0119917) / (100 - 50) = | 0.467 |
100-150 | (0.0759236 - 0.035345) / (150 - 100) = 92.638 | 0.811 |
150-200 | (0.1378461 - 0.0759236) / (200 - 150) = | 1.238 |
即使查询已经涉及到150张表,此时增加1一张表只会增加1.2ms。
最后一个测试场景,由于历时太长,等不及生成200个1百万行的表及建立索引,但又想观察性能的变化,于是选择测试50张表时的结果……
![](https://yqfile.alicdn.com/img_cdc41f9f832aae48dc693ce38adb2c15.png)
表的数量 | 平均增加量 | 时间(ms) |
---|---|---|
2-10 | (0.0016811 - 0.000276) / (10 - 2) = | 0.176 |
10-20 | (0.003771 - 0.0016811) / (20 - 10) = | 0.209 |
20-30 | (0.0062328 - 0.003771) / (30 - 20) = | 0.246 |
30-40 | (0.0088621 - 0.0062328) / (40 - 30) = | 0.263 |
40-50 | (0.0120818 - 0.0088621) / (50 - 40) = | 0.322 |
基于之前增加索引带来的性能改进结果,这次并没有带来太多的性能惊喜。50张1百万行的表做join,只需要12ms。Cool!
也许这些额外的join操作的成本比我们预想的要低一些,但有件事需要我们去考虑,虽然每个增加的join运算所占用的时间很小,但越多的表意味着越多的查询计划需要考虑,这很可能会导致很难找到最佳的查询计划。例如,当join的数量超过 geqo_threshold 时(默认为12),postgres 会停止参考所有可能的查询计划,改为使用通用算法。这会改变查询计划,引起对性能的负面影响。
由于每个系统的业务千差万别,一定要基于你的数据来测试你的查询。虽然我们看到增加join的成本很低,但仍然非常有必要去规范你的数据。
[原文] Cost of a Join
非直译,仅为增加乐趣,向作者的严谨性致敬。
[译]以PostgreSQL为例,谈join计算的代价相关推荐
- 浅析数据库多表连接:KaiwuDB 的分布式 join 计算
Join 是 SQL 中的常用操作.在实际的数据库应用中,我们经常需要从多个数据表中读取数据,这时我们就可以使用 SQL 语句中的连接(join),在两个或多个数据表中查询数据. 常用 Join 算法 ...
- java 连接 postgresql_java如何连接数据库并对其操作(以PostgreSQL为例)
nblogs-markdown"> java如何连接数据库并对其操作(以PostgreSQL为例)相关概念 JDBC(Java Data Base Connectivity)是一种用于 ...
- 小学计算机教学教师培训,例谈小学信息技术课堂的有效教学
例谈小学信息技术课堂的有效教学 在社会的各个领域,大家都不可避免地会接触到论文吧,论文可以推广经验,交流认识.为了让您在写论文时更加简单方便,以下是小编整理的例谈小学信息技术课堂的有效教学的论文相关内 ...
- web应用服务器计算资源核算,浅谈网络计算与应用.doc
浅谈网络计算与应用.doc 浅谈网络计算与应用 摘要:作为一种新型的分布计算技术,网格计算将地理上分布的.异构的资源 用高速网络连接在一起,集成一台高速的超级计算机.分析了网格计算的意义. 体系结构. ...
- 4个优化方法,让你能了解join计算过程更透彻
摘要:现如今, 跨源计算的场景越来越多, 数据计算不再单纯局限于单方,而可能来自不同的数据合作方进行联合计算. 本文分享自华为云社区<如何高可靠.高性能地优化join计算过程?4个优化让你掌握其 ...
- 计量经济学及stata应用思维导图_陈怡丨 例谈“思维导图”在小学英语读写课中的应用...
培养阅读能力 提升思维品质 --例谈"思维导图"在小学英语读写课中的应用 桐乡市凤鸣天女中心小学 陈怡[摘要]Read and write 板块是PEP 教材中阅读教学的呈现载体. ...
- 万丈高楼平地起 ——浅谈网格计算基础
万丈高楼平地起 --浅谈网格计算基础 网格技术的产生.发展必须具备以下三个基本条件:计算资源的广域分布.网络技术(特别是Internet)以及不断增长的对资源共享的需求.在计算器技术发展的早期阶段,只 ...
- 例3.2 计算存款利息。有1000元,想存一年。有3种方法可选:。。。
C程序设计(第四版) 谭浩强 个人设计 例3.2 计算存款利息.有1000元,想存一年.有3种方法可选:(1)活期,年利率为人:(2)一年期定期,年利率为r2:(3)存两次半年定期,年利率为r3.请分 ...
- 计算机的词块英语,考前说 | 例谈英语写作的关键(附常见词块32组)
原标题:考前说 | 例谈英语写作的关键(附常见词块32组) 写作文的绞尽脑汁写漂亮句子,但改卷子老师是不会把你的句子一个个拿出来分析的,而是一眼看过去讲究一个感觉:舒服.做到舒服就要牢记以下要点. 一 ...
最新文章
- 20160127:开始学VBA:(三)、判断语句
- 安卓手机兼容_重磅:鸿蒙OS2.0手机开发者Beta版发布,能兼容安卓
- vi/vim命令怎么在Linux系统中使用
- 《南溪的目标检测学习笔记》——DCN(DCNv2)的学习笔记
- linux login 安装桌面,Linux_Ubuntu Linux下安装配置fluxbox桌面环境,安装 基本系统Ubuntu 7.10 G - phpStudy...
- html5实现拖拽上传图片,JS HTML5拖拽上传图片预览
- 前端项目总结与分享(PPT整理)
- ggplot做双曲线阈值火山图
- python爬股票历史价格_【Python】利用ricequant获取上证指数以及所有股票历史价格数据...
- 在业务规则中使用OR有何不妥?
- 华为Mate40/华为Mate40Pro忘记密码怎么解锁激活手机设备已锁定恢复出厂无法解锁账户ID屏幕锁解除刷机方法教程
- 洛谷P3987 我永远喜欢珂朵莉~(set 树状数组)
- React打包出现:The project was built assuming it is hosted at ./.
- 【Java基础[数组及对象数组取子数组]】
- python之json扩展
- 价值连城 图灵奖得主杰弗里·欣顿(Geoffrey·Hinton)的采访 给AI从业者的建议
- android弱网模拟路由器,Mac 下使用命令行模拟弱网环境
- Ajax 改造,第 1 部分: 使用 Ajax 和 jQuery 改进现有站点
- “鸡”不可失,驱动人生助力开启“绝地求生”
- 成才之路(9):结束语
热门文章
- Apache ZooKeeper - 使用Apache Curator操作ZK
- 爬虫学习笔记(十)—— Scrapy框架(五):下载中间件、用户/IP代理池、settings文件
- Java中的nextInt()和next()与nextLine()区别详解
- pyqt5教程10:Widgets2组件
- 2021-03-20 数据挖掘算法—SVM算法 python
- c语言容斥原理,容斥原理 | 易学教程
- 数据结构实验之二叉树五:层序遍历(STL和模拟队列两种方法)
- 【Linux】3.dpkg、apt安装卸载软件
- 【深度学习】单位高斯化
- 走近人脸检测:从VJ到深度学习(下)