2024년 8월 3차 베타 성공 이후, PostgreSQL 開発그룹은 9월 26일에 GA 버전을 发布しました. 最近, 저는 PostgreSQL 17에서 볼 수 있는 一些의 주요 논리 副本 기능에 대해 블로그를 기록했습니다. 이 블로그에서는 Postgres 17에서 찾을 수 있는 一些의 새로운 성능 기능을 descript하고, 이전 系列 블로그에서 盖过한 다른 重要한 논리 副本 기능을 소개하고자 합니다.
PostgreSQL은 수년 동안 Remarkably 成长しました, 그리고 각 주요 发布에 따라, 临床上 重要한 或者 临床上 重要하지 않은 enterprise 응용 프로그램에 대해 더 robust, reliable, 및 responsive database가 되었습니다. 글로벌 且 활発한 PostgreSQL community가 PostgreSQL의 성공에 기여하고 있습니다. 모든 변경사항을 caution하며 项目 source code에 추가되る 전에 精ly review하고 있습니다. Microsoft, Google, Apple 같은 대기업이 Postgres에 투자하고 내부 expertise를 開発하고 open-source community에 기여하는 것을 기쁩니다.
논리 副本의 改善은 분산 PostgreSQL 지원을 기본 기능에 추가하는 길을 마련하고 있습니다. 분산 PostgreSQL는 PostgreSQL를 분산 아키텍처에 적용하는 것을 말합니다. 이를 통해 다양한 노드에 걸쳐 향상된 スケールability, 容错性, 및 성능을 달성할 수 있습니다.
더 이상 이야기를 -. 让我们来讨论一些 PostgreSQL 17 的性能特性。
Materialized CTEs를 사용한 쿼리 성능 改善
일반 테이블 표현(CTEs)는 PostgreSQL에서 SELECT
, INSERT
, UPDATE
, DELETE
문안에서 参照 가능한 일시적인 result set입니다. 복잡한 query의 читабель성과 조직화를 향상시키며, 재귀적으로 구성할 수 있기 때문에 阶层적인 데이터를 처리하는 용도로 특히 유용합니다. CTE 쿼리의 기본 구문은 다음과 같습니다:
WITH cte_names AS
(– QUERY here )
Select * from cte_names;
CTE를 생성하려면 쿼리에 WITH
关键字을 포함하고, CTE 이름 다음에 AS
CLAUSE를 이용하여 親 쿼리(result set을 정의하는 쿼리)를 続けます. CTE를 정의하고 나면, CTE의 이름을 사용하여 CTE의 result set을 参照하고 같은 query내에서 이를 통해 추가적인 操作을 수행할 수 있습니다.
PostgreSQL 17은 CTEs를 centric한 성능과 기능을 进一步提升시켰습니다. 이전 버전의 Postgres는 CTEs를 Optimization Fence로 처리하여, 기획자가 predicates를 그들로 down push하지 못했습니다. 然而, PostgreSQL 12 이후로는 더 효율적인 실행 계획을 정의할 수 있습니다. 성능이 critical할 때는 항상 쿼리를 분석하고 실행 계획을 고려해야 합니다.
パフォーマンスのヒント: 同じ結果セットを複数回参照する場合は、MATERIALIZED
キーワードを使用してCTEを作成します。マテリアルイジズied CTEを作成すると、Postgresは親クエリの結果を計算して保管します。その後、CTEを複数回参照する場合、後続のクエリは再び複雑な計算を行う必要はなくなります。
CTEから列統計を抽出する方法、Postgres 17はマテリアルイジズied CTEを改善しました
マテリアルイジズied CTEは基本的に最適化フェンスとして機能します。つまり、最外のクエリは子クエリの計画を選んだ後、その計画に影響を与えません。最外のクエリはCTEの結果セットの推定幅と行数に見えていますので、子クエリの列統計情報を最外のクエリの計画に伝搬することが合理的です。最外のクエリは利用可能なすべての情報を利用し、列統計情報を最外のクエリ計画に伝搬しますが、CTE計画には下がりません。
このバグはコミュニティに報告されました。このバグには、この改善の影響とクエリ計画器の動作を示す単純なテストケースが含まれています。
例: Postgres 16の動作とPostgres 17の比較
まず、Postgres 16でワークスペースを作成し、それにANALYZE
を実行します。そして、2つのテーブルとインデックスを対象にします。
postgres=# create table t1(a int);
CREATE TABLE
postgres=# create table t2(b int);
CREATE TABLE
postgres=# create index my_index on t1 using btree (a);
CREATE INDEX
postgres=# insert into t1 select generate_series(1, 100000) from generate_series(1, 3);
INSERT 0 300000
postgres=# insert into t2 select generate_series(1, 100) from generate_series(1, 10);
INSERT 0 1000
postgres=# analyze t1;
ANALYZE
postgres=# analyze t2;
ANALYZE
Then, we create our materialized CTE:
postgres=# explain analyze with my_cte as materialized (select b from t2) select *
from t1 where t1.a in (select b from my_cte);
The query plan from our Postgres 16 code sample contains:
QUERY PLAN
----------------------------------------------------------------------
Nested Loop (cost=37.92..856.50 rows=2966 width=4) (actual time=0.574..0.722 rows=300 loops=1)
CTE my_cte
-> Seq Scan on t2 (cost=0.00..15.00 rows=1000 width=4) (actual time=0.038..0.161 rows=1000 loops=1)
-> HashAggregate (cost=22.50..24.50 rows=200 width=4) (actual time=0.449..0.461 rows=100 loops=1)
Group Key: my_cte.b
Batches: 1 Memory Usage: 40kB
-> CTE Scan on my_cte (cost=0.00..20.00 rows=1000 width=4) (actual time=0.046..0.322 rows=1000 loops=1)
-> Index Only Scan using my_index on t1 (cost=0.42..4.06 rows=3 width=4) (actual time=0.002..0.002 rows=3 loops=1
00)
Index Cond: (a = my_cte.b)
Heap Fetches: 0
Planning Time: 1.242 ms
Execution Time: 1.051 ms
(12 rows)
쿼리 계획에서 볼 수 있듯이, 서브쿼리의 200개 행에 대한 컬럼 통계가 잘못되어 전체 계획에 영향을 미치고 있습니다.
-> HashAggregate (cost=22.50..24.50 rows=200 width=4) (actual time=0.449..0.461 rows=100 loops=1)
Group Key: my_cte.b
그런 다음, 동일한 설정과 쿼리를 PostgreSQL 17에 대해 테스트해보겠습니다:
postgres=# explain analyze with my_cte as materialized (select b from t2) select *
from t1 where t1.a in (select b from my_cte);
QUERY PLAN
-------------------------------------------------------------------------------------------------
---------------------------------
Merge Join (cost=42.25..54.29 rows=302 width=4) (actual time=0.627..0.712 rows=300 loops=1)
Merge Cond: (t1.a = my_cte.b)
CTE my_cte
-> Seq Scan on t2 (cost=0.00..15.00 rows=1000 width=4) (actual time=0.031..0.134 rows=1000
loops=1)
-> Index Only Scan using my_index on t1 (cost=0.42..7800.42 rows=300000 width=4) (actual tim
e=0.027..0.049 rows=301 loops=1)
Heap Fetches: 0
-> Sort (cost=26.82..27.07 rows=100 width=4) (actual time=0.598..0.604 rows=100 loops=1)
Sort Key: my_cte.b
Sort Method: quicksort Memory: 25kB
-> HashAggregate (cost=22.50..23.50 rows=100 width=4) (actual time=0.484..0.494 rows=1
00 loops=1)
Group Key: my_cte.b
Batches: 1 Memory Usage: 24kB
-> CTE Scan on my_cte (cost=0.00..20.00 rows=1000 width=4) (actual time=0.033..0
.324 rows=1000 loops=1)
Planning Time: 1.066 ms
Execution Time: 0.946 ms
(15 rows)
PostgreSQL 17의 쿼리 계획에서 볼 수 있듯이, 서브쿼리의 컬럼 통계가 외부 쿼리의 상위 플래너로 올바르게 전파되고 있습니다. 이는 PostgreSQL이 쿼리의 실행 시간을 개선하는 더 나은 계획을 선택하는 데 도움이 됩니다.
이는 간단한 쿼리이지만, 더 크고 복잡한 쿼리에서는 이러한 변경이 큰 성능 차이를 가져올 수 있습니다.
CTE에서 외부 쿼리로 Pathkeys 전파
PostgreSQL 17의 CTE 기능에 대한 또 다른 흥미로운 개선점은 서브쿼리에서 외부 쿼리로의 pathkeys 전파입니다. PostgreSQL에서 pathkeys는 주로 ORDER BY
절이 있는 쿼리나 병합 조인과 같은 다른 작업에 정렬이 필요한 경우와 같이 정렬된 결과가 필요한 쿼리에서 행을 정렬하고 순서를 지정하는 데 사용되는 쿼리 실행 계획 프로세스의 일부입니다.
PostgreSQL 17 이전에는 구체화된 CTE 서브쿼리의 정렬 순서가 인덱스 스캔 노드나 정렬 노드에 의해 보장되더라도 외부 쿼리와 공유되지 않았습니다. 보장된 정렬 순서가 없으면 PostgreSQL 플래너가 덜 최적화된 계획을 선택할 수 있지만, 보장된 정렬 순서가 있으면 최적화된 계획을 선택할 가능성이 더 높아집니다.
PostgreSQL 17에서, CTE(Common Table Expression)가 물리적으로 materialized되어 있으며 정렬 순서가 지정되어 있다면, planners는 이러한 정보를 外的 쿼리에서 다시 사용할 수 있게 되어, 중복된 정렬을 避免하거나 더 効果적인 联姻 方法을 가능하게 하는 것으로 性能 개선이 발생합니다. 이를 주목하는 것에 Tom Lane의 커밋 코멘트에 의하면:
“regular
RTE_SUBQUERY
subqueries의 경우 外 query로 pathkeys를 hoisting하는 코드는 이미 存在하지만, CTE에 대해 それ을 사용하지 않고 있었으며, CTE와 外的 query사이의 개선 분界线을 유지하기 위해서 也许”
Postgres 소스 코드에 대한 이러한 간단한 수정은, 특히 sorting 또는 merge join이 CTE 결과의 내적 order를 이용하여 개선될 수 있는 경우에서 복잡한 CTE를 사용하는 쿼리에 대한 性能 개선이 나타납니다.
PostgreSQL 통합 testers에서 데이터를 사용하여 다음과 같은 예를 들어보겠습니다:
postgres=# CREATE TABLE tenk1 (
postgres(# unique1 int4,
postgres(# unique2 int4,
postgres(# two int4,
postgres(# four int4,
postgres(# ten int4,
postgres(# twenty int4,
postgres(# hundred int4,
postgres(# thousand int4,
postgres(# twothousand int4,
postgres(# fivethous int4,
postgres(# tenthous int4,
postgres(# odd int4,
postgres(# even int4,
postgres(# stringu1 name,
postgres(# stringu2 name,
postgres(# string4 name
postgres(# );
CREATE TABLE
postgres=# CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
CREATE INDEX
postgres=# \copy tenk1 FROM '~/projects/postgres/src/test/regress/data/tenk.data';
COPY 10000
postgres=# VACUUM ANALYZE tenk1;
VACUUM
Postgres 16 코드 샘플의 쿼리 계획은 다음과 같습니다:
postgres=# explain analyze with x as materialized (select unique1 from tenk1 b order by unique1)
select count(*) from tenk1 a
where unique1 in (select * from x);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
---
Aggregate (cost=764.29..764.30 rows=1 width=8) (actual time=21.592..21.593 rows=1 loops=1)
CTE x
-> Index Only Scan using tenk1_unique1 on tenk1 b (cost=0.29..306.29 rows=10000 width=4) (actual time=0.046..1.415 rows=10000 loops=
1)
Heap Fetches: 0
-> Nested Loop (cost=225.28..445.50 rows=5000 width=0) (actual time=7.545..20.911 rows=10000 loops=1)
-> HashAggregate (cost=225.00..227.00 rows=200 width=4) (actual time=7.535..9.051 rows=10000 loops=1)
Group Key: x.unique1
Batches: 1 Memory Usage: 929kB
-> CTE Scan on x (cost=0.00..200.00 rows=10000 width=4) (actual time=0.070..3.933 rows=10000 loops=1)
-> Index Only Scan using tenk1_unique1 on tenk1 a (cost=0.29..1.08 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=10000)
Index Cond: (unique1 = x.unique1)
Heap Fetches: 0
Planning Time: 0.806 ms
Execution Time: 21.890 ms
(14 rows)
Postgres 17 코드 샘플의 쿼리 계획은 다음과 같습니다:
postgres=# explain analyze with x as materialized (select unique1 from tenk1 b order by unique1)
select count(*) from tenk1 a
where unique1 in (select * from x);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------- Aggregate (cost=987.55..987.56 rows=1 width=8) (actual time=8.777..8.778 rows=1 loops=1)
CTE x
-> Index Only Scan using tenk1_unique1 on tenk1 b (cost=0.29..306.29 rows=10000 width=4) (actual time=0.010..1.095 rows=100
00 loops=1)
Heap Fetches: 0
-> Merge Semi Join (cost=0.31..656.26 rows=10000 width=0) (actual time=0.037..8.024 rows=10000 loops=1)
Merge Cond: (a.unique1 = x.unique1)
-> Index Only Scan using tenk1_unique1 on tenk1 a (cost=0.29..306.29 rows=10000 width=4) (actual time=0.013..1.262 rows
=10000 loops=1)
Heap Fetches: 0
-> CTE Scan on x (cost=0.00..200.00 rows=10000 width=4) (actual time=0.016..3.678 rows=10000 loops=1)
Planning Time: 0.800 ms
Execution Time: 8.899 ms
(11 rows)
Postgres 16과 Postgres 17의 쿼리 плаans은 이 버전 17 개선 사항 때문에 상당히 다릅니다. 이것은 작은 예시입니다; larger queries에서 성능 이득이 значитель할 것입니다. ORDER BY
CLAUSE가 CTE 서브쿼리에 있어야만 이 개선이 유효하다는 것을 기억하십시오.
Scalar Array에 대한 Fast B-Tree Index Scans
PostgreSQL에서, ScalarArrayOpExpr
는 실행 пла안에서 배열 또는 값 목록을 处理하는 쿼리가 涉及하는 操作 like IN
or ANY
를 处理하는 노드 유형입니다. 특히 열을 값 집합과 比较하는 쿼리에 유용합니다. 예를 들어: SELECT * FROM table WHERE column = ANY(ARRAY[1, 2, 3]);
.
ScalarArrayOpExpr
는 PostgreSQL가 여러 比较操作에 사용되는 IN
or ANY
를 Optimize하는 쿼리를 가능하게 합니다. PostgreSQL 17는 이러한 操作을 더 빨라지게 하는 새로운 성능 개선을 도입했습니다.
PostgreSQL 17에서는 B-tree index scans에 대한 중요한 개선이 되었습니다. 이러한 개선은 특히 대량의 IN
목록 또는 ANY
조건의 쿼리에 적용되어 성능을 개선합니다. 이러한 개선은 시스템이 수행하는 index scans의 수를 줄이고 CPU와 버퍼 페이지 contention을 줄여, 쿼리 실행을 빨라줍니다.
주요 개선 사항 중 하나는 스칼라 벡터 OPERATION 표현式(SAOP)에 대한 처리입니다. 이것은 B-树 인덱스를 더 효율적으로 값 탐색하는 것을 지원하며, 특히 다차원 쿼리에 유용합니다. 例如, 여러 인덱스 열(각각 자신의 IN
목록을 갖는)가 존재하는 경우, PostgreSQL 17는 이전 버전의 여러 스캔을 대신 한 인덱스 스캔으로 이러한 操作을 더 effiiciently 처리할 수 있습니다. 이는 이전에 页面 Acces를 瓶颈로 하는 CPU-bound 작업 負荷에서 20-30%의 성능 향상을 가져올 수 있습니다.
또한, PostgreSQL 17는 내부적인 잠금을 更好地 관리하는 것을 도입하여, 特别是 B-树 인덱스 내에서 다양한 维 scan 를 수행하는 것에 대해 高性能 로 loaded를 더 향상시킵니다.
이를 简单하게 示例 하는 것을 통해 이를 보여 드릴 수 있습니다. 우리는 이전의 예제에서 사용했던 同样 tenk1
表과 데이터를 이용하겠습니다.
우리의 예제는 Postgres 16에서 처음으로 실행되었습니다:
CREATE TABLE tenk1 (
postgres(# unique1 int4,
postgres(# unique2 int4,
postgres(# two int4,
postgres(# four int4,
postgres(# ten int4,
postgres(# twenty int4,
postgres(# hundred int4,
postgres(# thousand int4,
postgres(# twothousand int4,
postgres(# fivethous int4,
postgres(# tenthous int4,
postgres(# odd int4,
postgres(# even int4,
postgres(# stringu1 name,
postgres(# stringu2 name,
postgres(# string4 name
postgres(# );
CREATE TABLE
postgres=# CREATE INDEX tenk1_unique1 ON tenk1 USING btree(unique1 int4_ops);
CREATE INDEX
postgres=# \copy tenk1 FROM '~/projects/postgres/src/test/regress/data/tenk.data';
COPY 10000
postgres=# EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 IN (1, 2, 3);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=14.20..330.12 rows=176 width=244) (actual time=0.138..0.153 rows=3 loops=1)
Recheck Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Heap Blocks: exact=3
Buffers: shared hit=9
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..14.16 rows=176 width=0) (actual time=0.102..0.102 rows=3 loops=1)
Index Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Buffers: shared hit=6
Planning:
Buffers: shared hit=2
Planning Time: 0.900 ms
Execution Time: 0.242 ms
(11 rows)
postgres=# SELECT idx_scan, idx_tup_fetch FROM pg_stat_user_tables WHERE relname = 'tenk1';
idx_scan | idx_tup_fetch
----------+---------------
3 | 3
(1 row)
이전 쿼리에서 보면 IN
쿼리의 shared buffer hit가 9이고, 인덱스 스캔 3회를 통해 인덱스 스캔 results를 얻었다는 것을 볼 수 있습니다. PostgreSQL에서는 shared hit이란 특정 종류의 캐시 命中과 관련이 있는 버퍼 관리에 대한 命中을 말합니다. shared hit은 PostgreSQL가 디스크에서 대신 공유 버퍼 풀에서 데이터 ブロック나 页面을 액세스하는 것을 의미하며 쿼리 성능을 改善시키는 것입니다.
이번에는 Postgres 17에서 실행한 同样 예제로 보겠습니다.
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 IN (1, 2, 3);
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=12.88..24.08 rows=3 width=244) (actual time=0.043..0.054 rows=3 loops=1)
Recheck Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Heap Blocks: exact=3
Buffers: shared hit=5
-> Bitmap Index Scan on tenk1_unique1 (cost=0.00..12.88 rows=3 width=0) (actual time=0.026..0.026 rows=3 loops=1)
Index Cond: (unique1 = ANY ('{1,2,3}'::integer[]))
Buffers: shared hit=2
Planning:
Buffers: shared hit=59
Planning Time: 0.479 ms
Execution Time: 0.116 ms
(11 rows)
postgres=# SELECT idx_scan, idx_tup_fetch FROM pg_stat_user_tables WHERE relname = 'tenk1';
idx_scan | idx_tup_fetch
----------+---------------
1 | 3
(1 row)
正如你所见,在 Postgres 17 中,共享缓冲区命中率降低到了5,最重要的是,它只进行了一次索引扫描(与 Postgres 16 的情况相比减少了3次扫描)。随着 Postgres 17 的这一改进,标量数组操作的性能得到了大幅提高,Postgres 可以从中选择更好的优化查询计划。
在升级过程中保留逻辑复制槽和订阅
在 PostgreSQL 17 的主要升级过程中,保留逻辑复制槽和迁移订阅依赖是另一个添加到 PostgreSQL 17 的逻辑复制功能。请注意,此功能只有在从 PostgreSQL 17 升级到后续版本时才有效,对于在 Postgres 17 之前的升级不支持。在构建逻辑复制环境时生成复制槽和复制源。然而,为了记录复制状态、应用状态和 WAL 传输状态,这些信息是特定于节点的,所以它们不会作为升级过程的一部分升级。一旦发布的节点升级完成,用户需要手动构建这些对象。
在 PostgreSQL 17 中,pg_upgrade 过程得到了改进,以引用并重建这些内部对象;这一功能使得在升级具有逻辑复制的节点时,复制可以自动恢复。以前,在执行主要版本升级时,用户必须删除逻辑复制槽,迫使他们在升级后与订阅者重新同步数据。这增加了升级过程中的复杂性并延长了停机时间。
리 publisher クラスタ를 업그레이드 하는 것을 방지하기 위해서는 다음 단계를 따라야 합니다.
- 任何时候에も publisher に 구독 정보가 存在하는지 확인하고, 存在하면
ALTER SUBSCRIPTION….DISABLE
명령을 실행하여 이를 일시적으로 無効化하십시오. upgrade 과정이 완료되면 이러한 구독을 다시 활성화합니다. - 새로운 クラス터의
wal_level
을logical
로 설정합니다. - 새로운 クラ스터의
max_replication_slots
의 값은 旧クラス터의 替換slot 이상과 같은 값을 가져야 합니다. - 替換slot 에 사용되는 출력 플러그인은 새로운 クラ스터에 설치되어 있어야 합니다.
- 旧クラ스터의 모든 変更이 升级 전에 대상 クラ스터에 替換되었음을 확인합니다.
- 旧クラ스터의 모든 slot 은 사용 가능하게 있어야 합니다. 이를 확인하기 위해 pg_replication_slots 视图에서 충돌 열을 检查하십시오. 旧クラ스터의 모든 slot 에 대해 충돌은 false 로 되어 있어야 합니다.
- 新区間の slot に
false
の値を持っている 것은 없어야 합니다.pg_replication_slots
视图のTemporary
列에서. 新クラス터에는 永続的な logical 替換slot が 存在하지 않아야 합니다.
替換slot 을 upgrade 하는 pg_upgrade
과정에서는 이전의 사전 요구 사항을 만족하지 않으면 에러가 발생합니다.
결론
PostgreSQL 17 버전에서, 开源 communities 는 PostgreSQL을 更高的性能, 可扩展性, 安全性, 그리고 企业 준비도를 갖추기 위한 일에 继续保持 관심을 投注합니다. Postgres 17은 개발자 경험을 改善하기 위해 호환성을 改善하기 위한 새로운 기능을 추가하고 기존 기능을 더 強力하고 健全한 것으로 改善하였습니다.
버전 17를 벗어나 더 나중에, PostgreSQL은 기업 应用에 더 強力한 데이터베이스를 필요로하는 것에 의해 계속적으로 成长, 改善, 그리고 性能을 향상시키게 됩니다. 年来, 확장性(수평과 수직)이 改善되었지만, PostgreSQL에 sharding 기능을 추가하여 수평 수능을 더 改善하는 것에 대해 의미가 있습니다. 我們은 DDL 복제 또는 누락 오브젝트(如火sequences)의 복제를 改善하는 것을 보며, 노드 관리를 更好的 것을 기대합니다. communities는 PostgreSQL을 더 호환性 있게 만들기 위한 需要을 인지하고 있으며, 이에 의해 Postgres 17에서 MERGE
명령어의 改善이 되었고, Postgres 17 이후에 더 많은 호환성 기능을 예정이며 있습니다.
Source:
https://dzone.com/articles/postgresql-17-a-major-step-forward-in-performance