理解 postgresql.conf 的work_mem 参数配置

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: 主要是通过具体的实验来理解 work_mem

今天我们着重来了解 postgresql.conf 中的 work_mem 参数

官方文档描述如下:


指定在写入临时文件之前内部排序操作和散列表使用的内存量。该值默认为1MB。请注意, 对于复杂查询,可能会同时运行多个
排序和散列操作;在开始向临时文件中写数据之前, 每一个操作将会被允许使用尽可能多的内存。 而且, 多个运行会话(session)
可以同时进行这样的操作。因此总的内存使用是work_mem的很多倍,在对 work_mem 做参数调整时, 要考虑上述事实。 排序操
做用于 ORDER BY, DISTINCT 和mergejoin。 散列表用于hash join, 基于散列的聚集操作, 基于散列的 IN 子查询。
进行这样的操作


上面的描述非常恰当,让我们来看一些具体的例子:
正如上面所介绍的, work_mem 是用于限制操作的内存使用的, 这关系到每个操作的 排序和哈希。

从排序开始, 让我们来看它是如何工作的。

建立测试表

CREATE TABLE test (id serial primary key, random_text text);

--- 使用脚本生成测试数据

$perl -e '@c=("a".."z", "A".."Z", 0..9); print join("", map{$c[rand@c]}10..20+rand(40))."n" for 1..1000000' > \
/home/whatcat/test/random_strings

-- 复制数据到数据库的表中

copy test (random_text) from '/home/postgres/test/random_strings'

查看导入数据的情况

select * from test limit 5;

idrandom_text1pktMlgRik7d4zhaOqv5dmP3CsjzE2T5uGSsXl8tgN9dh1c0E7ZE3G7TIhV5TpKHMNjpitcbsrga9rPM5mk6GVeEgyXoxmUACaIh2D84hds5sxUGAFM9YVy7SLbLN8WO96XKiZQI9IZ5ZwNHzmi8C5x5xyhPE6v5nUek5jSQYlGr8ZQ6WW1E8D6osRsXCr7AQYtBAbD44yDCxYcazTaDx1bbEe7VASl3E51iPNEgWvP1BkbBmT9BT5kLC4k2Wa7EdP6H8ts7yOgM4qXMld1fajvxDX4KQSpTc89NZUAklMSJsfXVbNfjOPVtkQEtQ110xlTBIF49zJv5SsGygSpGKQDZMGoJ4q3GqvTsVaJBTwK0Z

(10 rows)

Time: 0.620 ms

因为测试表具有 serial 数据类型的 id 字段, 所以对于我们使用排序操作去限制输出前 10, 100, 1000 或者更多记录,
同时 random_text 字段没有索引, 将会执行非常大的排序。
开始第一个测试

show work_mem

work_mem

4MB
(1 row)

explain analyze select * from test where id <= 10 order by random_text asc;

                                                      QUERY PLAN          
                                            

Sort (cost=60559.43..61430.65 rows=348488 width=36) (actual time=0.037..0.037
rows=10 loops=1)
Sort Key: random_text
Sort Method: quicksort Memory: 25kB
-> Bitmap Heap Scan on test (cost=6361.21..18949.31 rows=348488 width=36) (
actual time=0.018..0.019 rows=10 loops=1)

    Recheck Cond: (id <= 10)
    Heap Blocks: exact=1
    ->  Bitmap Index Scan on test_pkey  (cost=0.00..6274.09 rows=348488 wid

th=0) (actual time=0.012..0.012 rows=10 loops=1)

          Index Cond: (id <= 10)

Planning time: 0.084 ms
Execution time: 0.067 ms
(10 rows)

Time: 0.503 ms

从上述执行计划我们可以看到, 这个查询使用的内存是 25KB, work_mem 的限制大小为 4MB 。

所以, 让我们来看当数据足增大, work_mem 依旧是 4MB. 每一个查询都运行三次,来看下面查询数量下的
实验情况。
rows Sort method sort mem use
10 q 0.503 ms 25 KB
100 q 0.670 ms 33 KB
1000 q 1.189 ms 113 KB
10000 q 10.857 ms 1266 KB
100000 em 163.738 ms 4432 KB
1000000 em 1668.335 ms
44464 KB

我们可以看到在测试数据为 100K 条记录时, PostgreSQL 选择从内存中执行 quicksort 切换到外部合并的方法 (参看查询计划)。有意思的是--处理每一条记录的时间并没有显著增加,但是这可能是因为我们处理的数据集依旧很小的原因, 如果我们
的机器具有相当多的内存, 因此内核会缓存大量的临时文件数据。

不过, 为什么 PostgreSQL 会放弃内存的排序 qsort 去选择disk的外排序, 这个原因很简单, 当work_mem 不够用时, 就会去使
用磁盘, 所以意味着它已经被填满了。 所以,按“磁盘 排序意味着或多或少使用了整个 work_mem 加上 4432KB 的磁盘。

如何纯内存的工作呢?

set work_mem = '8MB';

explain analyze select * from test where id <= 1000000 order by random_text asc;

                                                  QUERY PLAN                
                                  

Sort (cost=147735.34..150235.34 rows=1000000 width=35) (actual time=852.386..1
419.290 rows=1000000 loops=1)
Sort Key: random_text
Sort Method: external merge Disk: 44384kB
-> Seq Scan on test (cost=0.00..20732.00 rows=1000000 width=35) (actual tim
e=0.018..203.833 rows=1000000 loops=1)

    Filter: (id <= 1000000)

Planning time: 0.513 ms
Execution time: 1512.669 ms
(7 rows)

Time: 1514.233 ms

从上面的查询计划我们可以看到, 实际的内存使用情况。
对于hash操作,哈希算子不会溢出到磁盘上(最起码目前没有提及), 而是通过增加 “batches” 的数量实现。

hash 操作的实验

explain analyze select * from test a join test b using (random_text) where a.id < 10;

                                                      QUERY PLAN            
                                          

Hash Join (cost=8.66..21990.75 rows=8 width=39) (actual time=0.093..321.716 ro
ws=9 loops=1)
Hash Cond: (b.random_text = a.random_text)
-> Seq Scan on test b (cost=0.00..18232.00 rows=1000000 width=35) (actual t
ime=0.023..143.559 rows=1000000 loops=1)
-> Hash (cost=8.56..8.56 rows=8 width=35) (actual time=0.016..0.016 rows=9
loops=1)

    Buckets: 1024  Batches: 1  Memory Usage: 9kB
    ->  Index Scan using test_pkey on test a  (cost=0.42..8.56 rows=8 width

=35) (actual time=0.005..0.008 rows=9 loops=1)

          Index Cond: (id < 10)

Planning time: 33.548 ms
Execution time: 321.775 ms
(9 rows)

Time: 396.186 ms

数据量增大

explain analyze select * from test a join test b using (random_text) where a.id < 100000;

                                                            QUERY PLAN      
                                                      

Hash Join (cost=5604.65..44961.38 rows=98174 width=39) (actual time=66.299..61
1.290 rows=99999 loops=1)
Hash Cond: (b.random_text = a.random_text)
-> Seq Scan on test b (cost=0.00..18232.00 rows=1000000 width=35) (actual t
ime=0.013..150.562 rows=1000000 loops=1)
-> Hash (cost=3610.47..3610.47 rows=98174 width=35) (actual time=65.906..65
.906 rows=99999 loops=1)

    Buckets: 65536  Batches: 2  Memory Usage: 3880kB
    ->  Index Scan using test_pkey on test a  (cost=0.42..3610.47 rows=9817

4 width=35) (actual time=0.018..33.366 rows=99999 loops=1)

          Index Cond: (id < 100000)

Planning time: 0.168 ms
Execution time: 620.401 ms
(9 rows)

Time: 620.979 ms

或者如下数据量

explain analyze select * from test a join test b using (random_text) where a.id < 1000000;

                                                      QUERY PLAN            
                                          

Hash Join (cost=38545.00..96466.00 rows=1000000 width=39) (actual time=445.161
..2030.208 rows=999999 loops=1)
Hash Cond: (a.random_text = b.random_text)
-> Seq Scan on test a (cost=0.00..20732.00 rows=1000000 width=35) (actual t
ime=0.013..245.331 rows=999999 loops=1)

    Filter: (id < 1000000)
    Rows Removed by Filter: 1

-> Hash (cost=18232.00..18232.00 rows=1000000 width=35) (actual time=444.69
2..444.692 rows=1000000 loops=1)

    Buckets: 65536  Batches: 32  Memory Usage: 2586kB
    ->  Seq Scan on test b  (cost=0.00..18232.00 rows=1000000 width=35) (ac

tual time=0.004..153.395 rows=1000000 loops=1)
Planning time: 0.251 ms
Execution time: 2116.081 ms
(10 rows)

Time: 2116.709 ms

那么将 work_mem 调高是否会有帮助呢? 这个想法是通过调高 work_mem 使得hash使用更少的 "batches",散列更大, 从而
使得哈希操作更快。为什么是这个想法呢? 接下来这么做

set work_mem = '100MB';

explain analyze select * from test a join test b using (random_text) where a.id < 1000000;

                                                      QUERY PLAN            
                                          

Hash Join (cost=30732.00..65214.00 rows=1000000 width=39) (actual time=495.487
..1359.246 rows=999999 loops=1)
Hash Cond: (a.random_text = b.random_text)
-> Seq Scan on test a (cost=0.00..20732.00 rows=1000000 width=35) (actual t
ime=0.014..197.752 rows=999999 loops=1)

    Filter: (id < 1000000)
    Rows Removed by Filter: 1

-> Hash (cost=18232.00..18232.00 rows=1000000 width=35) (actual time=492.33
5..492.335 rows=1000000 loops=1)

    Buckets: 1048576  Batches: 1  Memory Usage: 74107kB
    ->  Seq Scan on test b  (cost=0.00..18232.00 rows=1000000 width=35) (ac

tual time=0.006..154.439 rows=1000000 loops=1)
Planning time: 0.202 ms
Execution time: 1452.360 ms
(10 rows)

Time: 1452.967 ms

我们可以看到 hash 使用了的内存为 ,速度上变化不大。

我们将 work_mem 调整到很低的参数时, 是否会获得一定的性能差异显示呢?

set work_mem='1MB';

explain analyze select * from test a join test b using (random_text) where a.id < 1000000;

                                                      QUERY PLAN            
                                          

Hash Join (cost=38545.00..96466.00 rows=1000000 width=39) (actual time=467.893
..1960.566 rows=999999 loops=1)
Hash Cond: (a.random_text = b.random_text)
-> Seq Scan on test a (cost=0.00..20732.00 rows=1000000 width=35) (actual t
ime=0.032..255.996 rows=999999 loops=1)

    Filter: (id < 1000000)
    Rows Removed by Filter: 1

-> Hash (cost=18232.00..18232.00 rows=1000000 width=35) (actual time=467.16
4..467.164 rows=1000000 loops=1)

    Buckets: 16384  Batches: 128  Memory Usage: 651kB
    ->  Seq Scan on test b  (cost=0.00..18232.00 rows=1000000 width=35) (ac

tual time=0.004..162.964 rows=1000000 loops=1)
Planning time: 0.193 ms
Execution time: 2046.817 ms
(10 rows)

Time: 2047.374 ms

坦率的讲, 上述实验三个结果并没有办法很好的解释,因为调整work_mem 可能会使得时间减少, 但是work_mem 减少, 耗时
变化也没有太明显。

正如之前的实验显示, 随着work_mem 参数的增加, 对于 排序有提升 (在内存中完成, 没有溢出到磁盘)。正是由于排序的提升,
对于依靠排序操作的 (聚集操作, 去重操作, merge join) 都会加快。
如果我们将 work_mem 设置成 1GB, 也会如上述结论所预测的那样吗?

事实上,这个问题是对于执行程序使用的work_mem,是有上限设置的, 而不是一味的增加。
例如, 我们的执行操作就像上面的实验所说的, 我们启动101个排序或者hash 操作, 那么我们实际需要的内存是 101*work_mem,
对于单数据库连接。

为了说明一些观点, 假设你的查询语句需要 4-10 个 sort/hash 操作, 你的最大连接限制参数 max_connections = 100, 那么这就意
味着,查询在100个连接下, 内存消耗为 100 * work_mem。

正如,我们上面得到的结论, 增加 work_mem 加快查询, 但是这么大的并发量, 很容易超出实际的物理内存, 引起 OOM(out of
memory) 问题。在PostgreSQL 官方论坛的邮件中, 一些用户将work_mem设置为 512MB 或者更大而引起OOM问题。简单的说就是你运行的复杂查询不要引起 OOM 问题.

简单的说, 最好能为每一个 session 的查询设置自己的 work_mem.最好在 postgresql.conf 的值设置的很低 (1-10MB), 然后根据
那些查询会使用的 work_mem 超过设置的值, 在对该查询的 session 设置对应的值。

set work_mem ='100MB'

reset work_mem;


这里就有一个想淡然的问题, 我们怎么知道一个查询到底需要多少内存呢?
这非常简单-- 只需要修改 logging 的参数-- 设置 log_temp_files = 0 (所有的 log 都是临时文件), 每隔一段时间
检查一次, 检查最慢的查询, 它们的执行计划, 如果他们是依赖磁盘排序的。

对于 work_mem 的值。 有一个不成文的规定就是 work_mem 限制在1GB。 即使, 你的物理内存足够大, 你设置为 10GB , PostgreSQL 也不会使用那么多的。

explain analyze select * from lima where mike < 9000000 order by tango;

sort mothod: quick memory : 1304617 KB


这里比较推荐一个网址, 用来解析查询计划 explain.depesz.com

接下来介绍使用 GIN 索引情况下, work_mem 的使用。


在启用FASTUPDATE, 在一系列的GIN插入操作。这会引起内存使用增长,当增长后大于work_mem, 系统会清楚暂挂
条目列表。为了避免影响我们监控响应时间, 我们启动后台自动清理。可以通过增加 work_mem 或使用 autovacuum 更具
有倾略性的来清理。 但是需要提一点, work_mem的增大也意味着发生清理时, 耗时的增加。

GIN 索引的使用并不是很常见。当对含有 GIN 索引表执行插入是, 可以将数据导入到 tsvector 上有gin 索引的表执行全局搜索。
通过增加 work_mem 可以加速响应效率。

接下来举一个具体的例子
省测绘数据的perl 代码

!/usr/bin/perl

use strict;
use warnings;
use autodie;

open my $fh, '<', '/usr/share/dict/american-english-insane';
my @dict;
while (my $1 = <$fh>){

$1 =~s/\s+//;;
push @dict, $1 is $1 =~/\s/;

close $fh;

for my $size (100, 1_000, 10_000, 100_1000, 1_000_000, 10_000_000){

open my $out '>','/home/whatcat/test/' . $size . '.list';

for my $i ( 1.. $size){
    my $count = int(20 + rand 150);
    my @words = map {$dict[rand @dict]} 1..$count;
    print $out join(' ', @words) . "\n";
}
close $out;

}
exit;

接下来,建表,创建索引, 导入数据, 删除表
设置的 work_mem 为
1MB
10MB
100MB
1GB

Script, in case you're interested:

!/bin/bash

for data_input_file in /home/whatcat/test/100.list /home/whatcat/test/1000.list /home/whatcat/test/10000.list /home/whatcat/test/1000000.list
do

for work_mem in 1MB 10Mb 100MB 1GB
do
    echo "working on $data_input_file with $workj_mem work_mem."
    for i in 1 2 3
    do 
        (
            echo "set work_mem = '$work_mem'; "
            echo "create table gin_test (body text);"
            echo "create index gin_test_body_gin_idx on gin_test using gin (to_tsvector('english', body));"
        ) | psql -qAtX
        /usr/bin/time -f "- %e" psql -c "\\copy gion_test from '$data_input_file' "
        psql -qAtX -c "drop table gin_test"
    done
done

done

尽管上面的代码很长, 但是实验结果还是很好容易理解的
file work_mem (sec)
1 MB 10 MB 100 MB 1GB
100.list 0.05 0.05 0.05 0.05
1000.list 0.72 0.74 0.81 0.76
10000.list 9.90 10.07 10.01 9.92
100000.list 673.33 672.68 679.72 680.76

根据的时间的差异,认为这是不相关的。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
1月前
|
DataWorks Java 关系型数据库
DataWorks常见问题之将预警信息发送至邮箱
DataWorks是阿里云提供的一站式大数据开发与管理平台,支持数据集成、数据开发、数据治理等功能;在本汇总中,我们梳理了DataWorks产品在使用过程中经常遇到的问题及解答,以助用户在数据处理和分析工作中提高效率,降低难度。
35 1
|
6月前
|
存储 关系型数据库 数据库
用Patroni配置PostgreSQL高可用集群
Patroni是Zalando开发的数据库高可用管理软件,用于编排和自动化PostgreSQL集群的管理过程。Patroni 需要一系列其他组件的支持,通过利用第三方分布式一致性软件,组建并实现数据库高可用方案。
509 4
用Patroni配置PostgreSQL高可用集群
|
6月前
|
关系型数据库 MySQL Nacos
nacos数据库使用PostgreSQL及集群配置
从Nacos2.2版本开始,Nacos提供了数据源扩展插件,以便让需要进行其他数据库适配的用户自己编写插件来保存数据。
923 2
|
5月前
|
缓存 关系型数据库 数据库
PostgreSQL技术大讲堂 - 第32讲:数据库参数调整
从零开始学PostgreSQL技术大讲堂 - 第32讲:数据库参数调整
447 2
|
4月前
|
SQL 关系型数据库 数据库
postgresql数据库修改参数的方式
在PostgreSQL数据库中,你可以通过多种方式修改数据库参数,以更改其行为。以下是一些常见的修改数据库参数的方式: 1. **通过配置文件修改(postgresql.conf):** PostgreSQL的配置文件是 `postgresql.conf`。你可以直接编辑该文件,找到要修改的参数,修改其值,然后重新启动PostgreSQL服务以使更改生效。 通常,`postgresql.conf` 文件位于 PostgreSQL 数据目录下。修改完毕后,确保重新启动 PostgreSQL 服务。 2. **使用 ALTER SYSTEM 命令:** PostgreSQL
105 1
|
6月前
|
安全 关系型数据库 Go
远程连接PostgreSQL:配置指南与安全建议
远程连接PostgreSQL:配置指南与安全建议
320 0
|
2月前
|
关系型数据库 Java 分布式数据库
PolarDB for PostgreSQL参数问题之参数删除失败如何解决
PolarDB for PostgreSQL是基于PostgreSQL开发的一款云原生关系型数据库服务,它提供了高性能、高可用性和弹性扩展的特性;本合集将围绕PolarDB(pg)的部署、管理和优化提供指导,以及常见问题的排查和解决办法。
|
3月前
|
关系型数据库 网络安全 数据安全/隐私保护
你会开启Postgresql 的SSL单向认证 配置?
你会开启Postgresql 的SSL单向认证 配置?
你会开启Postgresql 的SSL单向认证 配置?
|
3月前
|
关系型数据库 PostgreSQL
PostgreSQL 的哪些参数不能通过ALTER SYSTEM SET 修改
在 PostgreSQL 中,有一些参数是不能通过 `ALTER SYSTEM SET` 语句进行动态修改的,这些参数通常需要在 PostgreSQL 的配置文件中进行手动修改。以下是一些不能通过 `ALTER SYSTEM SET` 修改的常见参数: 1. **track_activities** 2. **track_counts** 3. **track_io_timing** 4. **track_functions** 5. **track_activity_query_size** 6. **track_commit_timestamp** 7. **shared_preload
|
4月前
|
关系型数据库 Linux 数据安全/隐私保护
PostgreSQL【部署 02】在线安装PostgreSQL(Some psql features might not work 问题处理+角色密码设置+配置远程访问)
PostgreSQL【部署 02】在线安装PostgreSQL(Some psql features might not work 问题处理+角色密码设置+配置远程访问)
36 0
PostgreSQL【部署 02】在线安装PostgreSQL(Some psql features might not work 问题处理+角色密码设置+配置远程访问)