├── Ad-hoc monitoring.md ├── Autovacuum queue and progress.md ├── EXPLAIN ANALYZE or EXPLAIN (ANALYZE, BUFFERS).md ├── Find-or-insert using a single query.md ├── How many tuples can be inserted in a page.md ├── How to NOT get screwed as a DBA (DBRE).md ├── How to add a CHECK constraint without downtime.md ├── How to add a column.md ├── How to add a foreign key.md ├── How to analyze heavyweight locks, part 1.md ├── How to analyze heavyweight locks, part 2 Lock trees (a.k.a. lock queues, wait queues, blocking chains).md ├── How to analyze heavyweight locks, part 3. Persistent monitoring.md ├── How to benchmark.md ├── How to break a database, Part 1 How to corrupt.md ├── How to break a database, Part 2- Simulate infamous transaction ID wraparound.md ├── How to break a database, Part 3 Harmful workloads.md ├── How to change a Postgres parameter.md ├── How to change ownership of all objects in a database.md ├── How to check btree indexes for corruption (pg_amcheck).md ├── How to check btree indexes for corruption.md ├── How to compile Postgres on Ubuntu 22.04.md ├── How to convert a physical replica to logical.md ├── How to create an index, part 1.md ├── How to create an index, part 2.md ├── How to deal with bloat.md ├── How to deal with long-running transactions (OLTP).md ├── How to decide when a query is too slow and needs optimization.md ├── How to determine the replication lag.md ├── How to draw frost patterns using SQL ❄.md ├── How to drop a column.md ├── How to enable data checksums without downtime.md ├── How to estimate the YoY growth of a very large table using row creation timestamps and the planner statistics.md ├── How to find int4 PKs with out-of-range risks in a large database.md ├── How to find query examples for problematic pg_stat_statements records.md ├── How to find redundant indexes.md ├── How to find the best order of columns to save on storage (Column Tetris).md ├── How to find unused indexes.md ├── How to flush caches (OS page cache and Postgres buffer pool).md ├── How to format SQL (SQL style guide).md ├── How to format text output in psql scripts.md ├── How to generate fake data.md ├── How to get into trouble using some Postgres features.md ├── How to help others.md ├── How to import CSV to Postgres.md ├── How to install Postgres 16 with plpython3u Recipes for macOS, Ubuntu, Debian, CentOS, Docker.md ├── How to make e work in psql on a new machine (editornanovi not found).md ├── How to make the non-production Postgres planner behave like in production.md ├── How to monitor CREATE INDEX : REINDEX progress in Postgres 12+.md ├── How to monitor transaction ID wraparound risks.md ├── How to monitor xmin horizon to prevent XIDMultiXID wraparound and high bloat.md ├── How to perform initial rough Postgres tuning.md ├── How to plot graphs right in psql on macOS (iTerm2).md ├── How to quickly check data type and storage size of a value.md ├── How to quit from psql.md ├── How to rebuild many indexes using many backends avoiding deadlocks.md ├── How to redefine a PK without downtime.md ├── How to reduce WAL generation rates.md ├── How to remove a foreign key.md ├── How to set application_name without extra queries.md ├── How to speed up bulk load.md ├── How to speed up pg_dump when dumping large databases.md ├── How to troubleshoot Postgres performance using FlameGraphs and eBPF (or perf).md ├── How to troubleshoot a growing pg_wal directory.md ├── How to troubleshoot and speed up Postgres stop and restart attempts.md ├── How to troubleshoot long Postgres startup.md ├── How to troubleshoot streaming replication lag.md ├── How to tune Linux parameters for OLTP Postgres.md ├── How to tune work_mem.md ├── How to understand LSN values and WAL filenames.md ├── How to understand what's blocking DDL.md ├── How to use Docker to run Postgres.md ├── How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat.md ├── How to use UUID.md ├── How to use lib_pgquery in shell to normalize and match queries from various sources.md ├── How to use pg_restore.md ├── How to use subtransactions in Postgres.md ├── How to use variables in psql scripts.md ├── How to work with arrays, part 1.md ├── How to work with arrays, part 2.md ├── How to work with metadata.md ├── How to work with pg_stat_statements, part 1.md ├── How to work with pg_stat_statements, part 2.md ├── How to work with pg_stat_statements, part 3.md ├── Index maintenance.md ├── Learn how to work with schema metadata by spying after psql.md ├── Over-indexing.md ├── Postgres major upgrade without any downtime for a very large cluster running under heavy load.md ├── Pre- and post-steps for benchmark iterations.md ├── README.md ├── Rough configuration tuning (8020 rule; OLTP).md ├── UUID v7 and partitioning (TimescaleDB).md ├── Understanding how sparsely tuples are stored in a table.md ├── psql shortcuts.md └── psql tuning.md /Ad-hoc monitoring.md: -------------------------------------------------------------------------------- 1 | # Ad-hoc monitoring 2 | 3 | // 我每天发布一篇新的 PostgreSQL "howto" 文章。和我一起踏上这段旅程 — 订阅、提供反馈、分享! 4 | 5 | 在某些情况下,我们需要观察 Postgres 或其运行环境中的一些值 (操作系统、文件系统等),然而: 6 | 7 | - 我们没有好的监控工具,或者 8 | - 现有的监控缺少我们所需的某些功能,或者 9 | - 我们不完全信任现有的监控 (例如,你是刚开始接手某个系统的工程师)。 10 | 11 | 能够根据特定的指标组织一个"临时"的观察是一项重要的技能。此处我们将描述一些技巧和方法,可能会对你有帮助。 12 | 13 | 在某些情况下,你可以快速安装类似 [Netdata](https://www.netdata.cloud/) 的工具 (一个非常强大的现代监控工具,能快速安装,并且有Postgres plugin),或者使用一些临时的控制台工具,如 [pgCenter](https://github.com/lesovsky/pgcenter) 或 [pg_top](https://pg_top.gitlab.io/)。但你可能仍然希望手动监控某些特定的方面。 14 | 15 | 以下假设我们使用的是 Linux,但大多数方法也可以应用于 macOS 或 BSD 系统。 16 | 17 | ## 关键原则 18 | 19 | - 观察不应依赖于你的互联网连接。 20 | - 保存结果以便长期存储和后续分析。 21 | - 记录时间戳。 22 | - 防止程序因意外中断 (如按下Ctrl-C) 而终止。 23 | - 在 Postgres 重启后仍能保持观察。 24 | - 既要记录正常信息,也要记录错误信息。 25 | - 优先以有助于编程处理的形式收集数据 (例如 CSV)。 26 | 27 | ## 示例 28 | 29 | 假设我们需要收集 `pg_stat_activity` (pgsa) 的样本,以研究运行超过 1 分钟的长事务。 30 | 31 | 以下是操作步骤 — 我们将逐步详细讨论。 32 | 33 | 1. 启动一个 `tmux` 会话。这是一个幂等的代码片段,可以用来连接到一个名为 "observe" 的现有会话,或者如果找不到该会话,则创建一个: 34 | 35 | ```bash 36 | export TMUX_NAME=observe 37 | tmux a -t $TMUX_NAME || tmux new -s $TMUX_NAME 38 | ``` 39 | 40 | 2. 每秒收集一次 `pgsa` 样本,并无限次地记录日志 (直到手动中断): 41 | 42 | ```bash 43 | while sleep 1; do 44 | psql -XAtc " 45 | copy ( 46 | with samples as ( 47 | select 48 | clock_timestamp(), 49 | clock_timestamp() - xact_start as xact_duration, 50 | * 51 | from pg_stat_activity 52 | ) 53 | select * 54 | from samples 55 | where xact_duration > interval '1 minute' 56 | order by xact_duration desc 57 | ) to stdout delimiter ',' csv 58 | " 2>&1 \ 59 | | tee -a long_tx_$(date +%Y%m%d).log.csv 60 | done 61 | ``` 62 | 63 | 各种技巧和提示详解 64 | 65 | ## 使用 tmux (或 screen) 66 | 67 | 好处显而易见:如果你遇到网络连接问题而断开连接,只要 tmux 会话在服务器上运行,工作不会丢失。遵循预定义的会话命名约定在团队协作时也很有帮助。 68 | 69 | ## 如何使用循环/批处理 70 | 71 | 某些程序支持批量报告 (例如:`iostat -x 5`、`top -b -n 100 -d 5`),而有些不支持。在后者的情况下,可以使用 `while` 循环。我倾向于使用无限循环,如 `while sleep 5; do ... ; done`。它有一个小缺点 — 开始时会先等待一会儿,然后再执行有用的工作 — 但它的好处是,大多数时候你可以使用 `Ctrl-C` 中断。 72 | 73 | ## 使用 psql 选项:-X, -A, -t 74 | 75 | 用于观测相关采样的 `psql` 最佳实践 (以及通常的工作自动化): 76 | 77 | - 始终使用 `-X` — 这在某一天会对你非常有用,尤其当你在一个服务器上工作时,意外的`~/.psqlrc`文件可能包含影响输出的不确定内容(例如,一个简单的 `\timing on`)。`-X` 选项告诉psql忽略 `~/.psqlrc `文件。 78 | - 选项 `-A` 和 `-t` — 提供无对齐且无标题的输出。这有助于生成更容易解析和处理的大量结果。 79 | 80 | ## 使用 CSV 作为输出 81 | 82 | 有几种方式可以生成 CSV: 83 | 84 | - 使用 psql 的命令 `\copy` — 这种情况下,结果将保存在客户端。 85 | - `copy (...) to '/path/to/file'` — 这种方式会将结果保存在服务器上 (如果路径对于 Postgres 运行的操作系统用户不可写,可能会遇到权限问题)。 86 | - `psql --csv -F,` — 生成 CSV (但可能存在字段分隔符与转义值冲突的问题)。 87 | - `copy (...) to stdout` — 这种方式可能最适合采样和记录日志。 88 | 89 | ## 日志技巧:不要丢失 STDERR,记录时间戳,追加而非覆盖 90 | 91 | 为了后续分析 (谁知道我们几个小时后会检查什么?),最好将所有内容保存到文件中。 92 | 93 | 但同样重要的是不要丢失错误信息 — 通常,它们被打印到 `STDERR`,因此我们需要将它们写入单独的文件。我们还希望不要丢失文件中的现有内容,所以我们不想简单地覆盖 (`>`),而是要追加 (`>>`): 94 | 95 | ```bash 96 | command 2>>error.log >>messages.log 97 | ``` 98 | 99 | 或者将所有内容都重定向到一个文件中: 100 | 101 | ```bash 102 | command &>>everything.log 103 | ``` 104 | 105 | 如果你想同时查看和记录所有内容,使用 `tee` — 或者在追加模式下使用 `tee -a` (这里,`2>&1 `将 `STDERR` 重定向到 `STDOUT`,之后 `tee` 获取从 `STDOUT` 输出的所有内容): 106 | 107 | ``` 108 | command 2>&1 | tee -a everything.log 109 | ``` 110 | 111 | 如果输出缺少时间戳 (在我们上面的 psql 代码片段中则不需要),可以使用 `ts` 为每一行前添加时间戳: 112 | 113 | ```bash 114 | command 2>&1 \ 115 | ts \ 116 | tee -a everything.log 117 | ``` 118 | 119 | 最后,通常明智的做法是使用当前日期 (甚至是日期+时间,取决于情况) 为结果文件命名: 120 | 121 | ```bash 122 | command 2>&1 \ 123 | | ts \ 124 | | tee -a observing_our_command_$(date +%Y%m%d).log 125 | ``` 126 | 127 | 使用 `tee` 的一个缺点是,有时你可能会意外中断它 (例如,按错了 `tmux` 窗口/窗格中的 `Ctrl-C`)。因此,有些人更喜欢使用 `nohup ... &` 将观测任务放在后台运行,并使用 `tail -f` 查看结果。 128 | 129 | ## 结果处理 130 | 131 | 如果结果是 CSV 格式,后续处理非常方便 — Postgres 对其处理非常好。我们只需要记住使用的列集,就可以创建一个表并加载数据: 132 | 133 | ```sql 134 | nik=# create table log_loaded as select clock_timestamp(), clock_timestamp() - xact_start as xact_duration, * from pg_stat_activity limit 0; 135 | SELECT 0 136 | nik=# copy log_loaded from '/Users/nik/long_tx_20231006.log.csv' csv delimiter ','; 137 | COPY 4 138 | ``` 139 | 140 | 然后你可以用 SQL 进行分析。或者你也可以将 CSV 加载到某些电子表格工具中,然后分析数据并创建一些图表。 141 | 142 | ## 另一种采样方式——直接在 psql 中操作 143 | 144 | 在 psql 中,你可以使用 `\o | tee -a logfile` 和`\watch` 来同时查看数据和记录日志 — 不过注意,这不会捕捉到文件中的错误。示例: 145 | 146 | ```sql 147 | nik=# \o | tee -a alternative.log 148 | 149 | nik=# \t 150 | Tuples only is on. 151 | nik=# \a 152 | Output format is unaligned. 153 | nik=# \f ┃ 154 | Field separator is "┃". 155 | nik=# select clock_timestamp(), 'test' as c1 \watch 3 156 | 2023-10-06 21:01:05.33987-07┃test 157 | 2023-10-06 21:01:08.342017-07┃test 158 | 2023-10-06 21:01:11.339183-07┃test 159 | ^C 160 | nik=# \! cat alternative.log 161 | 2023-10-06 21:01:05.33987-07┃test 162 | 2023-10-06 21:01:08.342017-07┃test 163 | 2023-10-06 21:01:11.339183-07┃test 164 | nik=# 165 | ``` -------------------------------------------------------------------------------- /EXPLAIN ANALYZE or EXPLAIN (ANALYZE, BUFFERS).md: -------------------------------------------------------------------------------- 1 | # EXPLAIN ANALYZE 还是 EXPLAIN (ANALYZE, BUFFERS)? 2 | 3 | 在分析 PostgreSQL 查询执行计划时,我总是推荐使用 `BUFFERS` 选项: 4 | 5 | ```sql 6 | explain (analyze, buffers) ; 7 | ``` 8 | 9 | ## 示例 10 | 11 | ```sql 12 | test=# explain (analyze, buffers) select * from t1 where num > 10000 order by num limit 1000; 13 | QUERY PLAN 14 | ---------------------------------------------------------- 15 | Limit (cost=312472.59..312589.27 rows=1000 width=16) (actual time=314.798..316.400 rows=1000 loops=1) 16 | Buffers: shared hit=54173 17 | ... 18 | Rows Removed by Filter: 333161 19 | Buffers: shared hit=54055 20 | Planning Time: 0.212 ms 21 | Execution Time: 316.461 ms 22 | (18 rows) 23 | ``` 24 | 25 | 如果 `EXPLAIN ANALYZE` 未使用 `BUFFERS`,那么执行计划将缺少缓冲池 IO 的相关信息。 26 | 27 | ## 推荐使用 `EXPLAIN (ANALYZE, BUFFERS)` 而不仅仅是 `EXPLAIN ANALYZE` 的原因 28 | 29 | 1. 每个计划节点的缓冲池 IO 操作都可见。 30 | 2. 可以了解涉及的数据量 (注意:buffer hits 可能涉及多次"命中"同一个缓冲区)。 31 | 3. 如果分析重点是 IO 数字,那么即使在较弱的硬件上 (较少的内存,较慢的磁盘),仍然可以获得可靠的查询优化数据。 32 | 33 | 为了更好地理解,建议将缓冲区数量转换为字节。在大多数系统中,1 个缓冲区为 8 KiB。因此,10 次缓冲区读取即为 80 KiB。 34 | 35 | 然而,要注意可能的混淆:需要记住的是,`EXPLAIN (ANALYZE, BUFFERS)` 提供的数字并不是数据量,而是 **IO 次数**——即完成的 IO 工作量。例如,针对内存中的单个缓冲区,可能会有 10 次命中——在这种情况下,我们的缓冲池中并没有 80 KiB 的数据,而是处理了 80 KiB,多次处理了同一个缓冲区。实际上,命名不太准确:它显示为 `Buffers: shared hit=5`,更准确地说是表示 `buffer hits` (缓冲区命中次数),而不是 `buffers hit` (命中的缓冲区数量)。也就是说,这个数字指的是操作的次数,而不是数据的大小。 36 | 37 | ## 总结 38 | 39 | 始终使用 `EXPLAIN (ANALYZE, BUFFERS)`,而不仅仅是 `EXPLAIN ANALYZE`——这样你可以看到 Postgres 在执行查询时实际完成的 IO 工作量。 40 | 41 | 这有助于更好地了解所涉及的数据量。更好的是,你可以开始将缓冲区数量转换为字节——只需将它们乘以块大小 (大多数情况下是 8 KiB)。 42 | 43 | 在优化过程中不要过于关注时间数字——这可能听起来违反直觉,但这可以让你忘记环境之间的差异。而且这也允许你使用轻量克隆——看看 Database Lab Engine 以及其他公司在这方面做了什么。 44 | 45 | 最后,当你成功减少 BUFFERS 数字时,这意味着 Postgres 在执行查询时将需要更少的缓冲区,减少了 IO,降低了争用的风险,并为其他任务留出了更多的缓冲区空间。遵循这一方法,最终可能对数据库的整体性能产生积极影响。 46 | 47 | 关于这一主题的博客文章:[EXPLAIN (ANALYZE) needs BUFFERS to improve the Postgres query optimization process](https://postgres.ai/blog/20220106-explain-analyze-needs-buffers-to-improve-the-postgres-query-optimization-process) 48 | 49 | # 我感 50 | 51 | 为什么这篇文章里提到了不要过于关注时间数字呢?因为"计时"本身也是有一定消耗和误差的,在之前还看过一篇文章: 52 | 53 | | query | calls | total | mean | min | max | stddev | 54 | | :----------------------------------------------------------- | ----: | -----: | ----: | ----: | ----: | -----: | 55 | | explain analyze select sum(i1.i * i2.i) from i1 inner join i2 using (i) | 20 | 917.20 | 45.86 | 45.32 | 49.24 | 0.84 | 56 | | select sum(i1.i * i2.i) from i1 inner join i2 using (i) | 20 | 615.73 | 30.79 | 30.06 | 34.48 | 0.92 | 57 | 58 | 在官网上也有所提及 59 | 60 | >There are two significant ways in which run times measured by `EXPLAIN ANALYZE` can deviate from normal execution of the same query. First, since no output rows are delivered to the client, network transmission costs and I/O conversion costs are not included. Second, the measurement overhead added by `EXPLAIN ANALYZE` can be significant, especially on machines with slow `gettimeofday()` operating-system calls. You can use the [pg_test_timing](https://www.postgresql.org/docs/current/pgtesttiming.html) tool to measure the overhead of timing on your system. -------------------------------------------------------------------------------- /How many tuples can be inserted in a page.md: -------------------------------------------------------------------------------- 1 | # How many tuples can be inserted in a page 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在 Postgres 中,所有表都有隐藏的系统列,`ctid` 便是其中之一。通过读取 `ctid`,我们可以看到元组 (元组 = 行的物理版本) 的物理位置,包括页号以及页内偏移量: 6 | 7 | ```sql 8 | nik=# create table t0 as select 1 as id; 9 | SELECT 1 10 | 11 | nik=# select ctid, id from t0; 12 | ctid | id 13 | -------+---- 14 | (0,1) | 1 15 | (1 row) 16 | ``` 17 | 18 | 👉 第 0 页,1 位置处。 19 | 20 | 单个 PostgreSQL 页面默认为 8 KiB,可以通过查看 `block_size` 来确认: 21 | 22 | ```sql 23 | nik=# show block_size; 24 | block_size 25 | ------------ 26 | 8192 27 | (1 row) 28 | ``` 29 | 30 | 一个页面中可以容纳多少个元组?来看一下: 31 | 32 | ```sql 33 | nik=# create table t0 as select i 34 | from generate_series(1, 1000) as i; 35 | SELECT 1000 36 | 37 | nik=# select count(*) 38 | from t0 39 | where (ctid::text::point)[0] = 0; 40 | count 41 | ------- 42 | 226 43 | (1 row) 44 | 45 | nik=# select pg_column_size(i) from t0 limit 1; 46 | pg_column_size 47 | ---------------- 48 | 4 49 | (1 row) 50 | ``` 51 | 52 | 👉 如果我们使用 4 字节的数字,那么可以容纳 226 条元组。此处我使用 `(ctid::text::point)[0]` 将 `ctid` 的值转换为"point"来获取第一个组成部分,即页号。 53 | 54 | 即使使用 2 字节的数字或 1 字节的布尔值 (注意,布尔值需要 1 字节,而不是 1 比特),这个数量也是相同的: 55 | 56 | ```sql 57 | nik=# drop table t0; 58 | DROP TABLE 59 | 60 | nik=# create table t0 as select true 61 | from generate_series(1, 1000) as i; 62 | SELECT 1000 63 | 64 | nik=# select count(*) 65 | from t0 66 | where (ctid::text::point)[0] = 0; 67 | count 68 | ------- 69 | 226 70 | (1 row) 71 | ``` 72 | 73 | 为什么还是 226?事实上,值的大小在这里无关紧要,只要小于或等于 8 字节即可。对于每一行,对齐填充都会添加"零",因此每行始终有 8 个字节: 74 | $$ 75 | \frac{8192 - 24}{4 + 24 + 8} = 226 76 | $$ 77 | 👉 这里我们统计了以下内容: 78 | 79 | - 24 字节的页头 (`PageHeaderData`)。 80 | - 每个元组指针 — 每个 4 字节 (`ItemIdData`)。 81 | - 每个元组头 — 每个 23 字节,填充到 24 字节 (`HeapTupleHeaderData`)。 82 | - 每个元组值 — 如果 ≤ 8字节,则填充到 8 字节。 83 | 84 | 源码定义了这些结构 (for [PG16](https://github.com/postgres/postgres/blob/REL_16_STABLE/src/include/storage/bufpage.h))。 85 | 86 | **我们能容纳更多元组吗?** 87 | 88 | 答案是可以的。Postgres 允许创建没有列的表!在这种情况下,计算如下: 89 | $$ 90 | \frac{8192 - 24}{4 + 24} = 291 91 | $$ 92 | 让我们观察一下 (注意 `SELECT` 子句中的空列): 93 | 94 | ```sql 95 | nik=# create table t0 as select 96 | from generate_series(1, 1000) as i; 97 | SELECT 1000 98 | 99 | nik=# select count(*) 100 | from t0 101 | where (ctid::text::point)[0] = 0; 102 | count 103 | ------- 104 | 291 105 | (1 row) 106 | ``` 107 | 108 | ## 我见 109 | 110 | 以 0️⃣ 填充: 111 | 112 | image 113 | 114 | >Here we only need 40 bytes per row excluding the variable sized data and 24-byte tuple header. 8 bytes being saved may not sound like much, but for tables as large as the events table it does begin to matter. For example, when storing 80 000 000 rows this translates to a space saving of at least 610 MB, all by just changing the order of a few columns. 115 | -------------------------------------------------------------------------------- /How to NOT get screwed as a DBA (DBRE).md: -------------------------------------------------------------------------------- 1 | # How to NOT get screwed as a DBA (DBRE) 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 以下规则非常简单 (但由于组织原因,实施起来往往不那么简单)。 6 | 7 | 这对在快速成长的初创公司中负责数据库的人会有所帮助,其中一些建议也适用于大公司的 DBA/DBRE。 8 | 9 | ## 1) 确保备份系统可靠 10 | 11 | 1. 不要使用 `pg_dump` 作为备份工具 — 而是使用支持 PITR (例如 `pgBackRest`,`WAL-G`) 的系统。如果使用的是托管解决方案,请详细了解备份系统是如何组织的 — 不要盲目相信。 12 | 13 | 2. 了解 RPO 和 RTO,测量实际值并定义目标值。使用监控系统覆盖这些值 (例如,`archive_command` 滞后应被视为最高优先级的事件)。 14 | 3. 测试备份。这是最关键的部分。未经测试的备份是"薛定谔的备份" — 任何备份的状态在尝试恢复之前都是未知的。自动化测试。 15 | 4. 局部恢复的自动化和加速:在某些情况下,需要恢复手动删除的数据,而不是整个数据库。在这种情况下,考虑以下选项以加快恢复速度:(a) 特殊的延迟备库;(b) 频繁的云快照 + PITR;(c) 使用 DBLab,每小时快照 + 克隆上的 PITR。 16 | 5. 其他需要涵盖的主题:适当的加密选项、保留策略,将老的备份移动到"冷存储"以节省成本、次要存储位置,位于不同云中,并限制访问。 17 | 18 | 毫无疑问,备份是数据库管理中最重要的话题。在这个方面遇到麻烦是任何 DBA 的噩梦。请对备份保持高度关注,学习他人的经验教训,而不是自己的。 19 | 20 | 可靠的备份系统也许是一些组织选择托管 Postgres 服务的最大原因之一。但仍然:不要盲目相信 — 仔细研究所有细节,并自行测试。 21 | 22 | ## 2) 数据损坏控制 23 | 24 | 1. 启用[数据校验和](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0037_how_to_enable_data_checksums_without_downtime.md)。 25 | 2. 谨慎进行操作系统或 `glibc` 升级 — 避免索引损坏。 26 | 3. 使用 `amcheck` 来检测索引、堆和序列。 27 | 4. 设置警报,快速响应 Postgres 日志中的错误代码 `XX000`、`XX001`、`XX002` ([PostgreSQL Error Codes](https://postgresql.org/docs/current/errcodes-appendix.html)) 28 | 5. 数据损坏有很多种类型 — 覆盖基础部分,然后继续学习并实施更好的控制和预防措施。(关于该主题的一组好材料:[PostgreSQL Data Corruption and Bugs – Runbook](https://docs.google.com/spreadsheets/u/1/d/1zUH7IYOv46CVSmc-72CD7ROnMA6skJSQZjnm4yxvX9A/edit#gid=0)) 29 | 30 | ## 3) 高可用 31 | 32 | - 设置备节点。 33 | - 考虑使用 `synchronous_commit=remote_write` (或根据情况选择 `remote_apply`) 和 `synchronous_standby_names` ([Multiple Synchronous Standbys](https://postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION-MULTIPLE-STANDBYS))。 34 | - 如果是自托管,使用 **Patroni**。否则,研究你的提供商提供的所有高可用选项并使用它们。 35 | 36 | ## 4) 性能 37 | 38 | - 拥有良好的监控系统 ([my PGCon slide deck on monitoring](https://twitter.com/samokhvalov/status/1664686535562625034))。 39 | - 设置高级查询分析工具:`pg_stat_statements`、`pg_stat_kcache`、`pg_wait_sampling` 或 `pgsentinel`、`auto_explain`。 40 | - 构建可扩展的"实验室"环境和实验流程 — DBA 不应该成为工程师们实验的瓶颈 ([@Database_Lab](https://twitter.com/Database_Lab) 可以解决这个问题)。 41 | - 实施容量规划,确保有足够的增长空间,进行主动的基准测试。 42 | - 架构设计:微服务和分片都是很棒的想法,值得考虑。但主要问题是:何时开始?从初创公司成立之初,还是之后?答案是"视情况而定"。请记住,凭借当前 Postgres 的性能和任何云提供的硬件,你可以轻松扩展到几十 TB 或数百 TB 的数据量,以及每秒成千上万的 TPS。选择你的优先事项 — 有许多成功的案例可以证明任何方法都是可行的。 43 | - 不要犹豫寻求帮助 — 无论是社区帮助还是付费咨询。Postgres 支撑着大量的项目,有丰富的经验可以借鉴。 44 | 45 | ## 5) 学习他人的错误 46 | 47 | - Postgres 仍然使用 32 位事务ID。确保不要触发事务 ID (以及组事务 ID) 的回卷问题 — [Sentry](https://blog.sentry.io/transaction-id-wraparound-in-postgres/) 和[Mailchimp](https://mailchimp.com/what-we-learned-from-the-recent-mandrill-outage/) 的案例是很好的教训。 48 | - 子事务 — 我个人认为在负载较重的系统中使用它们是危险的,建议[避免](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0035_how_to_use_subtransactions_in_postgres.md)。 49 | - 调优 `autovacuum` — 不要让 Postgres 积累大量的死元组或膨胀 (是的,这两者是不同的)。关闭 `autovacuum` 是让你的服务器宕机的好方法。 50 | - 在 OLTP 环境中,避免长时间运行的事务和未使用/滞后的复制槽。 51 | - 学习如何无停机地部署架构变更。有用的文章: 52 | - [Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries) 53 | - [Common DB schema change mistakes](https://postgres.ai/blog/20220525-common-db-schema-change-mistakes) -------------------------------------------------------------------------------- /How to add a CHECK constraint without downtime.md: -------------------------------------------------------------------------------- 1 | # How to add a CHECK constraint without downtime 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 添加 `CHECK` 约束有益于: 6 | 7 | - 提升数据质量 8 | - 在 PG12+ 中,定义 `NOT NULL` 约束不需要停机 (更多信息参考[此处](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0060_how_to_add_a_column.md?ref_type=heads#not-null)) 9 | 10 | 要在不中断服务的情况下添加 `CHECK` 约束,我们需要: 11 | 12 | 1. 快速定义带有 `NOT VALID` 标志的约束 13 | 2. 在一个单独的事务中,"验证"现有行是否满足该约束 14 | 15 | ## 使用NOT VALID添加CHECK约束 16 | 17 | 示例: 18 | 19 | ```sql 20 | alter table t 21 | add constraint c_id_is_positive 22 | check (id > 0) not valid; 23 | ``` 24 | 25 | 这仅需要一个非常短暂的 `AccessExclusiveLock`,因此在负载较大的系统上,应设置较低的 `lock_timeout` 并进行重试 (参考:[Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries))。 26 | 27 | 🖋️ **重要**:当 `NOT VALID `约束添加之后,新的数据写入会立即进行检查 (而旧数据尚未验证,可能会违反约束): 28 | 29 | ```sql 30 | nik=# insert into t select -1; 31 | ERROR: new row for relation "t" violates check constraint "c_id_is_positive" 32 | DETAIL: Failing row contains (-1). 33 | ``` 34 | 35 | ## 验证 36 | 37 | 完成添加约束的过程后,我们需要验证旧数据: 38 | 39 | ```sql 40 | alter table t 41 | validate constraint c_id_is_positive; 42 | ``` 43 | 44 | 该操作会扫描整个表,因此对于大表可能需要较长时间 — 但此查询仅获取表上的 `ShareUpdateExclusiveLock`,不会阻塞运行 DML 查询的会话。但是,如果有 `autovacuum` 在预防事务 ID 回卷的模式下运行并处理该表,或有其他会话在该表上创建索引或执行其他 `ALTER` 操作,则锁请求会被阻塞。因此,在运行 `ALTER` 之前,应确保没有这些繁重的操作在执行,以避免过长的等待时间。 45 | 46 | ## 我感 47 | 48 | 这个技巧很不错,在创建分区表的时候也可以使用这个技巧,直接 alter table xxx add constraint 会全程 8 级锁,可以先 not valid,再慢慢校验,最后 attach。 -------------------------------------------------------------------------------- /How to add a column.md: -------------------------------------------------------------------------------- 1 | # How to add a column 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 添加列很简单: 6 | 7 | ```sql 8 | alter table t1 add column c1 int8; 9 | 10 | comment on column t1.c1 is 'My column'; 11 | ``` 12 | 13 | 然而,可能会有一些潜在的复杂情况。 14 | 15 | ## 锁定问题 16 | 17 | 添加列需要获取表级的 `AccessExclusiveLock`,这会阻塞对该表的所有查询,包括 `SELECT`。这会产生两个后果: 18 | 19 | 1. 我们不希望操作持续太久 (例如,扫描整个表,或者更糟的是重写整个表)。 20 | 2. 锁的获取应当尽量优雅地进行。 21 | 22 | 对于第二点,在 [Zero-downtime Postgres schema migrations need this: lock_timeout and retries ](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)中进行了详细分析。 23 | 24 | 以下是一个优雅获取锁的示例,使用较低的 `lock_timeout` 并进行重试: 25 | 26 | ```sql 27 | do $do$ 28 | declare 29 | lock_timeout constant text := '50ms'; 30 | max_attempts constant int := 1000; 31 | ddl_completed boolean := false; 32 | begin 33 | 34 | perform set_config('lock_timeout', lock_timeout, false); 35 | 36 | for i in 1..max_attempts loop 37 | begin 38 | execute 'alter table t1 add column c1 int8'; 39 | ddl_completed := true; 40 | exit; 41 | exception 42 | when lock_not_available then 43 | null; 44 | end; 45 | end loop; 46 | 47 | if ddl_completed then 48 | raise info 'DDL successfully executed'; 49 | else 50 | raise exception 'DDL execution failed'; 51 | end if; 52 | end $do$; 53 | ``` 54 | 55 | 请注意,此示例中隐式使用了子事务 (`BEGIN/EXCEPTION WHEN/END `块)。在 XID 增长率较高 (例如有许多写事务) 和长事务的情况下,在备库上这可能会触发 `SubtransSLRU` 的争用问题,详情参见:[PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful)。在这种情况下,应在事务级别实现重试逻辑。 56 | 57 | ## 默认值 58 | 59 | 从 Postgres 11 开始 (因此,在所有当前支持的大版本中,截至 2023 年 11 月),添加带有默认值的列不会导致整个表重写,因此我们通常不需要担心它。 60 | 61 | 在创建列时定义的默认值不会物理写入所有现有的行,相反,它是"虚拟的" — 存储在特殊系统目录 [pg_attrdef](https://postgresql.org/docs/current/catalog-pg-attrdef.html) 中。示例: 62 | 63 | ```sql 64 | nik=# alter table t1 add column c2 int8 default -10; 65 | ALTER TABLE 66 | 67 | nik=# select pg_get_expr(adbin, 't1'::regclass::oid) from pg_attrdef; 68 | pg_get_expr 69 | ---------------- 70 | '-10'::integer 71 | (1 row) 72 | ``` 73 | 74 | 而对于已存在的列,所定义的默认值存储在 `pg_attribute` 中: 75 | 76 | ```sql 77 | nik=# alter table t1 alter column c2 set default -20; 78 | ALTER TABLE 79 | 80 | nik=# select attmissingval from pg_attribute where attrelid = 't1'::regclass::oid and attname = 'c2'; 81 | attmissingval 82 | --------------- 83 | {-10} 84 | (1 row) 85 | ``` 86 | 87 | 这也意味着,在添加新列时,如果希望"虚拟"地为所有现有行回填值 A,但对未来的所有行使用值 B,可以: 88 | 89 | 1. 在列创建时使用一个默认值。 90 | 2. 创建后立即更改默认值。 91 | 92 | 如果使用的是较早的 Postgres 版本 (11 之前),建议使用回填 (backfill) 操作以避免长时间锁定。 93 | 94 | ## NOT NULL 95 | 96 | 添加 `NOT NULL` 约束 (这是 [重新] 定义主键所必需的),通常需要全表扫描,不支持两步添加法以避免长时间的锁定。 97 | 98 | > 译者注:此处原文是 there is no support of two-step addition to avoid long-lasting locking,即不支持像定义主键那样,通过唯一索引 + 检查约束 (not null) 以降低锁的粒度。 99 | 100 | 但是,当新列需要此约束时,我们可以使用此技巧 101 | 102 | 1. 在创建列时使用一些临时的默认值与 NOT NULL 相结合: 103 | 104 | ```sql 105 | alter table t1 106 | add column id_new int8 not null default -1; 107 | 108 | comment on column "t1"."id_new" is 'my future PK'; 109 | ``` 110 | 111 | 2. 注意此列中现有的值,以及新出现的值: 112 | 113 | - 添加触发器为新行自动填充值。 114 | - 批量回填现有行中的值。 115 | 116 | 3. 完成后,删除临时的默认值: 117 | 118 | ```sql 119 | alter table t1 alter column id_new drop default; 120 | ``` 121 | 122 | 4. 切换到该新列的常规用途,然后如果需要,进行清理 (删除触发器等) 123 | 124 | ## 回填 125 | 126 | 在某些情况下,单值 `DEFAULT` 不足以为所有现有的行定义新列中的值,我们仍然需要进行回填。为避免长时间锁定,建议按批次进行更新: 127 | 128 | 1. 对于 OLTP (如 Web 和移动应用),建议控制批量更新的时间在 1-2 秒内。 129 | 2. 为了能够高效地找到下一个批处理的范围,我们可以在新列和现有 PK 上创建一个索引 (此索引可能是临时的,以支持高效批处理),然后删除。此索引可以是部分索引。例如,如果我们的新列名为 `id_new`,并且在列创建时使用的默认值为 `-1`: 130 | 131 | - 创建支持索引 132 | 133 | ```sql 134 | create index concurrently i_t1_id_new on t1(id) where "id_new" = -1; 135 | ``` 136 | 137 | - 对于批处理,定义 UPDATE 的范围: 138 | 139 | 140 | ```sql 141 | update t1 set id_new = where id_new = -1 order by id limit ; 142 | ``` 143 | 144 | - 注意监控死元组数量和 `autovacuum` 的行为,避免死元组数量过高 (导致膨胀)。必要时限制更新频率或者/并且时不时手动执行一下 `VACUUM`。 145 | 146 | 147 | - 如果支持索引不再需要,可以删除: 148 | 149 | 150 | ```sql 151 | drop index concurrently i_t1_id_new; 152 | ``` 153 | 154 | ## 关于默认值存储的内部机制的修正 155 | 156 | - `pg_attrdef` 存储了所有当前的默认值。当我们更改已存在列的默认值时,该系统目录会更新以存储新值: 157 | 158 | ```sql 159 | nik=# alter table t1 alter column c2 set default -30; 160 | ALTER TABLE 161 | 162 | nik=# select pg_get_expr(adbin, 't1'::regclass::oid) from pg_attrdef; 163 | pg_get_expr 164 | ---------------- 165 | '-30'::integer 166 | (1 row) 167 | ``` 168 | 169 | 而存储在 `pg_attribute` 中的 `attmissingval` 值是在列创建之前,存在的行所使用的默认值: 170 | 171 | ```sql 172 | nik=# select attmissingval from pg_attribute where attrelid = 't1'::regclass::oid and attname = 'c2'; 173 | attmissingval 174 | --------------- 175 | {-10} 176 | (1 row) 177 | ``` 178 | -------------------------------------------------------------------------------- /How to add a foreign key.md: -------------------------------------------------------------------------------- 1 | # How to add a foreign key 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 添加外键 (FK) 很简单: 6 | 7 | ```sql 8 | alter table messages 9 | add constraint fk_messages_users 10 | foreign key (user_id) 11 | references users(id); 12 | ``` 13 | 14 | 然而,此操作需要锁定涉及的两个表: 15 | 16 | - 被引用表上的 `ShareRowExclusiveLock`,`RowShareLock` 和 `AccessShareLock` 锁,在本例中为 `users` (包括该表主键上的 `AccessShareLock`)。这会阻止对 `users` 表的任何数据修改 (`UPDATE`、`DELETE`、`INSERT`),以及 DDL 操作。 17 | - 引用表上的 `ShareRowExclusiveLock` 和 `AccessShareLock` ,在本例中为 `messages` (包括其主键的`AccessShareLock`)。同样,这也会阻止对该表的写入以及 DDL 操作。 18 | 19 | 为了确保现有数据不违反约束,需要对表进行全表扫描 — 因此,表中的数据越多,隐式扫描的时间越长。在此期间,锁会阻塞所有写入和 DDL 操作。 20 | 21 | 要避免停机,我们需要分三步创建 FK: 22 | 23 | 1. 快速定义带有 `NOT VALID` 标志的约束。 24 | 2. 对于现有数据,如果需要,修复会破坏 FK 的行。 25 | 3. 在单独的事务中,验证现有行是否满足约束。 26 | 27 | ## 第1步:使用NOT VALID添加FK 28 | 29 | ```sql 30 | alter table messages 31 | add constraint fk_messages_users 32 | foreign key (user_id) 33 | references users(id) 34 | not valid; 35 | ``` 36 | 37 | 此操作仅需短暂的 `ShareRowExclusiveLock` 和 `AccessShareLock`,所以在负载较大的系统上,建议设置较低的 `lock_timeout` 并进行重试 (参照 [Zero-downtime database schema migrations](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)),以避免锁队列阻塞对表的写入。 38 | 39 | 🖋️ **重要**:一旦带有 `NOT VALID` 的约束生效,新的写入会立即进行约束检查 (而旧数据尚未验证,可能会违反约束): 40 | 41 | ```sql 42 | nik=# \d messages 43 | Table "public.messages" 44 | Column | Type | Collation | Nullable | Default 45 | ---------+--------+-----------+----------+--------- 46 | id | bigint | | not null | 47 | user_id | bigint | | | 48 | Indexes: 49 | "messages_pkey" PRIMARY KEY, btree (id) 50 | Foreign-key constraints: 51 | "fk_messages_users" FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID 52 | 53 | nik=# insert into messages(id, user_id) select 1, -1; 54 | ERROR: insert or update on table `messages` violates foreign key constraint "fk_messages_users" 55 | DETAIL: Key (user_id)=(-1) is not present in table `users`. 56 | ``` 57 | 58 | ## 第2步:如有需要,修复现有数据 59 | 60 | 添加了 `NOT VALID` 标志的 FK 后,Postgres 已经根据新约束检查了所有新数据,但旧数据可能仍有部分行违反该约束。在进行下一步操作之前,有必要确保没有违反新 FK 的旧行。可以使用以下查询来完成: 61 | 62 | ```sql 63 | select id 64 | from messages 65 | where 66 | user_id not in ( 67 | select id from users 68 | ); 69 | ``` 70 | 71 | 该查询会扫描整个 `messages` 表,因此可能需要较长时间。确保 `users` 通过主键访问以提高性能 (这取决于数据量和规划器设置)。 72 | 73 | 找到的行将阻止下一步操作,因此需要删除或进行更改,以避免 FK 冲突。 74 | 75 | ## 第3步:验证 76 | 77 | 完成后,需要在单独的事务中验证旧行: 78 | 79 | ```sql 80 | alter table messages 81 | validate constraint fk_messages_users; 82 | ``` 83 | 84 | 如果表较大,此 `ALTER` 操作可能需要较长时间。然而,它仅需要获取引用表 (本例中为 `messages`) 上的 `ShareUpdateExclusiveLock` 和 `AccessShareLock`。 85 | 86 | 因此并不会阻塞 `UPDATE` / `DELETE` / `INSERT`,但会与 DDL 和 `VACUUM` 冲突。对于被引用的表 (本例中为 `users`),需要获取 `AccessShareLock` 和 `RowShareLock`。 87 | 88 | 与往常一样,如果 `autovacuum` 在预防事务 ID 回卷的模式下处理该表,它将不会"妥协"— 因此在运行此操作之前,请确保没有 `autovacuum` 在该模式下运行,也没有 DDL 操作在进行。 89 | 90 | ## 我见 91 | 92 | 📒 TODO:包括前一篇文章,都提到了在 93 | 94 | >processes this table in the transaction ID wraparound prevention mode 95 | 96 | 即使不在冻结,也需要获取 `ShareUpdateExclusiveLock`,尚不明白作者为何需要特别提及?待验证。 -------------------------------------------------------------------------------- /How to analyze heavyweight locks, part 3. Persistent monitoring.md: -------------------------------------------------------------------------------- 1 | # How to analyze heavyweight locks, part 3. Persistent monitoring 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如果活跃会话 (`pg_stat_activity`,`state='active'`) 的峰值伴随着独占锁 (`pg_locks`,`mode ~* 'exclusive'`) 的峰值发生,我们需要分析被阻塞和阻塞的会话,以了解根本原因。 6 | 7 | 之前讨论过临时的分析方法: 8 | 9 | - [Day 22, How to analyze heavyweight locks, part 1](https://xiongcccc.github.io/postgres-howtos/#/./docs/22) 10 | - [Day 42, How to analyze heavyweight locks, part 2: Lock trees (a.k.a. "lock queues", "wait queues", "blocking chains")](https://xiongcccc.github.io/postgres-howtos/#/./docs/42) 11 | - 特别是对于 DDL 被阻塞的情况: [Day 71, How to understand what's blocking DDL](https://xiongcccc.github.io/postgres-howtos/#/./docs/71) 12 | 13 | 然而,如果事件已经结束,临时的分析方法也无济于事。部分原因是,即使设置了 `log_lock_waits = 'on'`,Postgres 日志也只报告"受害者" (被阻塞的会话) 的查询文本 (日志条目的 `STATEMENT` 部分),关于阻塞会话,只能获取到 `PID` 的信息。 14 | 15 | 要对过去的事件进行排障并了解趋势,我们需要在监控中实现锁分析,提供关于被阻塞和阻塞会话的详细信息。 16 | 17 | 以下是 [@VKukharik](https://twitter.com/VKukharik) 为 [pgwatch2 - Postgres.ai Edition](https://hub.docker.com/r/postgresai/pgwatch2) 开发的查询。它没有使用函 数 `pg_blocking_pids()`,因为由于可能的观察者效应,它并不安全。 18 | 19 | ```sql 20 | with sa_snapshot as ( 21 | select * 22 | from pg_stat_activity 23 | where 24 | datname = current_database() 25 | and pid <> pg_backend_pid() 26 | and state <> 'idle' 27 | ) 28 | select 29 | (extract(epoch from now()) * 1e9)::bigint as epoch_ns, 30 | waiting.pid as waiting_pid, 31 | waiting_stm.usename::text as tag_waiting_user, 32 | waiting_stm.application_name::text as tag_waiting_appname, 33 | waiting.mode as waiting_mode, 34 | waiting.locktype as waiting_locktype, 35 | waiting.relation::regclass::text as tag_waiting_table, 36 | waiting_stm.query as waiting_query, 37 | (extract(epoch from (now() - waiting_stm.state_change)) * 1000)::bigint as waiting_ms, 38 | blocker.pid as blocker_pid, 39 | blocker_stm.usename::text as tag_blocker_user, 40 | blocker_stm.application_name::text as tag_blocker_appname, 41 | blocker.mode as blocker_mode, 42 | blocker.locktype as blocker_locktype, 43 | blocker.relation::regclass::text as tag_blocker_table, 44 | blocker_stm.query as blocker_query, 45 | (extract(epoch from (now() - blocker_stm.xact_start)) * 1000)::bigint as blocker_tx_ms 46 | from pg_catalog.pg_locks as waiting 47 | join sa_snapshot as waiting_stm on waiting_stm.pid = waiting.pid 48 | join pg_catalog.pg_locks as blocker on 49 | waiting.pid <> blocker.pid 50 | and blocker.granted 51 | and waiting.database = blocker.database 52 | and ( 53 | waiting.relation = blocker.relation 54 | or waiting.transactionid = blocker.transactionid 55 | ) 56 | join sa_snapshot as blocker_stm on blocker_stm.pid = blocker.pid 57 | where not waiting.granted; 58 | ``` 59 | 60 | 将此查询添加到监控中,当发生涉及锁争用峰值的事件时,可以观察到如下内容: 61 | 62 | ![img](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0073_01.jpg) 63 | 64 | 它提供了被阻塞和阻塞会话的所有详细信息,为排障和分析根本原因提供支持。 65 | 66 | ![img](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0073_02.jpg) 67 | 68 | ## 我见 69 | 70 | 维基百科:**观测者效应** (Observer effect),是指“观测”这种行为对被观测对象造成一定影响的效应。 71 | 72 | 不止一篇文中提到了观测者效应,pg_blocking_pids 便是其中之一。 73 | 74 | >Frequent calls to this function could have some impact on database performance, because it needs exclusive access to the lock manager's shared state for a short time. -------------------------------------------------------------------------------- /How to benchmark.md: -------------------------------------------------------------------------------- 1 | # How to benchmark 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 基准测试是一个很大的话题。此处,我们介绍了 Postgres 基准测试所需的最少步骤,使其信息丰富、有用且正确。 6 | 7 | 在本文中,我们假设遵循以下原则: 8 | 9 | 1. **独占环境**:整个机器只供我们独自使用 (没有其他人在使用),我们旨在研究 Postgres 的整体行为及其所有组件 (而非微观基准测试,如通过使用 `EXPLAIN` 研究特定查询或专注于底层组件如磁盘和文件系统的性能)。 10 | 2. **高质量**:我们追求诚实 (没有"营销式的基准测试"目标)、透明 (共享所有细节)、精确,并在发生错误时修正它们。每次基准测试运行应足够长,以考虑缓存的冷状态或各种波动等因素。通常,每种基准测试应该运行多次 (通常,进行 4-5 次),以确保测试的可重复性。学习现有经验是很有意义的:Brendan Gregg 的 ["System Performance"](https://brendangregg.com/blog/2020-07-15/systems-performance-2nd-edition.html) 书中有一章关于基准测试的章节;此外,从其他领域 (如物理学等) 学习[成功实验](https://en.wikipedia.org/wiki/Experiment)的原则和方法论也是有益的。 11 | 3. **准备学习**:我们有足够的专业知识来理解瓶颈和局限性,或准备利用他人的帮助,在必要时重新进行基准测试。 12 | 13 | ------ 14 | 15 | ### 基准测试结构 16 | 17 | 基准测试是一种数据库实验,一般情况下,我们使用多个 DBMS 会话,研究系统的整体行为以及它的所有或特定组件 (比如缓冲池、检查点、复制情况等)。 18 | 19 | 每个基准测试都应该有一个明确定义的结构。总的来说,它包含两大部分: 20 | 21 | - **输入**:我们在执行基准测试之前定义的一切 — 我们在何处运行基准测试、系统如何配置、使用什么数据库和工作负载、我们研究的变化是什么 (用于比较变化前后的行为)。 22 | - **输出**:各种可观测的数据,例如日志、观察到的错误、统计数据等等。 23 | 24 | ![Database Benchmark](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0013_db_benchmark.png) 25 | 26 | 每个部分都应有良好的文档记录,以便任何人都可以重现实验、理解主要指标 (延迟、吞吐量等),理解瓶颈,并在需要时进行额外的、修改后的实验。 27 | 28 | 数据库基准测试的所有方面的描述可能需要写一本书 — 在此,我仅提供基本建议,帮助你避免错误并提高基准测试的总体质量。 29 | 30 | 当然,某些内容可以根据需要省略。但通常建议,自动记录所有实验的文档和生成的文件,以便日后方便研究细节。你可以在[这里](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues)找到一些为特定目的进行的基准测试的优秀示例 (例如,研究病态的子事务行为或衡量启用`wal_compression`的好处)。 31 | 32 | ### 输入: 环境 33 | 34 | --- 35 | 36 | - 确保你使用的机器规格合适,不要在笔记本电脑上运行 (除非绝对必要)。AWS Spot 实例或 GCP 的Preemptible 实例在短时间内使用时价格非常便宜,非常适合实验。例如,具有 128 个 Intel 或 AMD 虚拟 CPU 和 256-1024 GiB 内存的 VM 的 Spot 实例每小时价格低至 5-10 美元 ([good comparison tool](https://instances.vantage.sh)),按秒计费 — 这使得在大机器上进行非常经济高效的实验成为可能。 37 | - 获取 VM 后,使用 fio、sysbench 等微基准测试工具快速检查 CPU、内存、磁盘和网络是否按预期工作。如果有疑问,可以与其他同类型 VM 进行比较并进行选择。 38 | - 记录 VM 的技术规格、磁盘和文件系统 (这两个对于数据库非常重要!)、操作系统的选择和非默认设置。 39 | - 记录使用的 Postgres 版本和任何额外的扩展 (即使只是安装了某些扩展,它们也可能会产生 "观察者效应")。 40 | - 记录所有非默认的 Postgres 设置。 41 | 42 | ------ 43 | 44 | ### 输入: 数据库 45 | 46 | 记录你使用的模式和数据。理想情况下,应该以完全可重现的形式 (SQL /转储文件) 记录。 47 | 48 | ------ 49 | 50 | ### 输入: 工作负载 51 | 52 | - 记录你所使用的工作负载的所有方面 — 理想情况下,也应以完全可重现的形式记录 (SQL、pgbench、sysbench 等细节)。 53 | - 理解你基准测试的类型以及你想进行的负载测试类型:是极限负载测试(压力测试),旨在"全速"运行,还是常规负载测试,模拟现实场景下的负载,例如 CPU 使用率通常远低于 100%。请注意,默认情况下,pgbench 倾向于为你进行"压力测试" (不限制 TPS 数量 — 要限制它,请使用`-R`选项)。 54 | 55 | ------ 56 | 57 | ### 输入: 差异 58 | 59 | 可能有各种类型的"差异" (定义测试运行之间差异的研究对象)。以下是一些示例: 60 | 61 | - 不同的 Postgres 次要或主要版本 62 | - 不同的操作系统版本 63 | - 不同的设置,如 `work_mem` 64 | - 不同的硬件 65 | - 规模变化:不同数量的客户端与数据库进行交互或不同的表大小 66 | - 不同的文件系统 67 | 68 | 不建议将 SQL 查询中更改的模式更改视为"差异",因为: 69 | 70 | - 此类工作负载变化通常发生得非常快 71 | - 完整的基准测试非常昂贵 72 | - 可以在共享环境中研究模式和查询的更改,专注于 IO 指标 (如BUFFERS!),以实现高效的时间和成本效益(参考 @Database_Lab) 73 | 74 | ------ 75 | 76 | ### 输出: 收集成果 77 | 78 | 收集各种成果,并确保它们不会丢失 (例如,将它们上传到对象存储中)。 79 | 80 | - 每次运行前,使用 `pg_stat_reset()`、`pg_stat_reset_shared(..)` ,其他标准的`pg_stat_reset_***()` 等函数 ([docs](https://postgresql.org/docs/current/monitoring-stats.html))、`pg_stat_statements_reset()`、`pg_stat_kcache_reset()` 等重置所有统计信息。 81 | - 每次运行后,将所有 `pg_stat_*** `视图以 CSV 格式导出。 82 | - 收集所有 Postgres 日志及任何相关日志 (如 pgBouncer、Patroni、系统日志)。虽然 Postgres、pgBouncer 或其他配置文件属于"输入",但创建所有实际观察到的配置值的快照 (例如,`select * from pg_settings;`),并将此数据也视为成果。 83 | - 收集查询分析信息:`pg_stat_statements`、`pg_stat_kcache`、`pg_wait_sampling` / [pgsentinel ](https://github.com/pgsentinel/pgsentinel)等的快照。提取关于错误的所有信息,来源于 (a)日志,(b) `pg_stat_database` (如`xact_rollback`) 等类似指标的 SQL,并将其视为分析的重要成果类型。可以考虑使用一个名为 [logerrors](https://github.com/munakoiso/logerrors) 的小扩展,它将注册所有错误代码并通过 SQL 暴露它们。 84 | - 如果使用了监控,收集监控图表。对于实验特别方便的是 [Netdata](https://netdata.cloud) ,因为它非常容易安装在新机器上,并且具有仪表盘导出/导入功能 (不幸的是,它们位于客户端一侧,因此始终需要手动操作;但在进行数据库实验时,我个人认为它们非常方便)。 85 | 86 | ------ 87 | 88 | ### 分析 89 | 90 | 以下是一些建议 (远不完整): 91 | 92 | - 始终检查错误。基准测试运行时,很常见的是得出某些结论,而后来才意识到错误计数过高,使得测试无效。 93 | - 理解瓶颈在哪里。我们常常在磁盘 IO 上饱和,认为我们观察的是数据库系统的行为,但实际上我们看到的是云磁盘限速或文件系统限制的表现。在这种情况下,我们需要思考如何调整输入,避免这种瓶颈,以进行有用的实验。 94 | - 在某些情况下,反之亦然,达到某种饱和状态是非常理想的 — 例如,如果我们研究 `pg_dump `或 `pg_restore `的速度,我们可能希望观察磁盘系统的饱和情况,并调整输入 (例如,我们如何进行 `pg_dump` — 使用多少个并行工作进程,是否涉及压缩,是否涉及网络等),以便确实达到预期的饱和状态,并展示它。 95 | - 理解你在测试运行之间要比较的主要指标 — 延迟、吞吐量、查询分析指标 (如 `pg_stat_statements` 中的指标,等待事件分析等)。 96 | - 开发一个良好的总结格式并遵循这个格式。它可以包括对各种输入部分的简短描述,包括工作负载差异和主要比较指标。以这种结构良好的形式保存所有运行的总结。 -------------------------------------------------------------------------------- /How to break a database, Part 1 How to corrupt.md: -------------------------------------------------------------------------------- 1 | # How to break a database, Part 1: How to corrupt 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 有时候,你可能想要破坏一个数据库 — 用于学习目的,模拟故障,学习如何处理这些故障,测试缓解措施等。 6 | 7 | 让我们讨论一些破坏的方法。 8 | 9 | ⚠️ 不要在生产环境中尝试,除非你是一名混沌工程师 ⚠️ 10 | 11 | ## 数据损坏 12 | 13 | 有很多类型的损坏,并且有非常简单的方法可以使数据库损坏,例如: 14 | 15 | 👉 **直接修改系统目录:** 16 | 17 | ```sql 18 | nik=# create table t1(id int8 primary key, val text); 19 | CREATE TABLE 20 | 21 | nik=# delete from pg_attribute where attrelid = 't1'::regclass and attname = 'val'; 22 | DELETE 1 23 | 24 | nik=# table t1; 25 | ERROR: pg_attribute catalog is missing 1 attribute(s) for relation OID 107006 26 | LINE 1: table t1; 27 | ^ 28 | ``` 29 | 30 | 更多方法可以在这篇文章中找到:[How to corrupt your PostgreSQL database](https://cybertec-postgresql.com/en/how-to-corrupt-your-postgresql-database/),其中一些有趣的方法包括: 31 | 32 | - 设置 `fsync=off` 然后对 Postgres 使用 `kill -9` (或者 `pg_ctl stop -m immediate`)。 33 | - 使用 `kill -9` + `pg_resetwal -f`。 34 | 35 | 一个有用的方法是使用 `dd` 直接写入数据文件。此方法可以用于模拟通过校验和验证检测到的损坏 ([Day 37: How to enable data checksums without downtime]())。在这篇文章中也有展示:[pg_healer: repairing Postgres problems automatically](https://endpointdev.com/blog/2016/09/pghealer-repairing-postgres-problems/). 36 | 37 | 首先,创建一个表并查看数据文件所在位置: 38 | 39 | ```sql 40 | nik=# show data_checksums; 41 | data_checksums 42 | ---------------- 43 | on 44 | (1 row) 45 | 46 | nik=# create table t1 as select i from generate_series(1, 10000) i; 47 | SELECT 10000 48 | 49 | nik=# select count(*) from t1; 50 | count 51 | ------- 52 | 10000 53 | (1 row) 54 | 55 | nik=# select format('%s/%s', 56 | current_setting('data_directory'), 57 | pg_relation_filepath('t1')); 58 | format 59 | --------------------------------------------------- 60 | /opt/homebrew/var/postgresql@15/base/16384/123388 61 | (1 row) 62 | ``` 63 | 64 | 现在,使用 `dd` 直接写一些垃圾数据至文件中 (请注意,这里我们使用的是 macOS 版本,其中 `dd` 有选项 `oseek` - 在 Linux 上,它是 `seek_bytes`),然后重启 Postgres 以确保表不再存在于缓冲池中: 65 | 66 | ```bash 67 | ❯ echo -n "BOOoo" \ 68 | | dd conv=notrunc bs=1 \ 69 | oseek=4000 count=1 \ 70 | of=/opt/homebrew/var/postgresql@15/base/16384/123388 71 | 1+0 records in 72 | 1+0 records out 73 | 1 bytes transferred in 0.000240 secs (4167 bytes/sec) 74 | 75 | ❯ brew services stop postgresql@15 76 | Stopping `postgresql@15`... (might take a while) 77 | ==> Successfully stopped `postgresql@15` (label: homebrew.mxcl.postgresql@15) 78 | 79 | ❯ brew services start postgresql@15 80 | ==> Successfully started `postgresql@15` (label: homebrew.mxcl.postgresql@15) 81 | ``` 82 | 83 | 成功损坏 — 数据校验和机制对此发出了警告: 84 | 85 | ```sql 86 | nik=# table t1; 87 | WARNING: page verification failed, calculated checksum 52379 but expected 35499 88 | ERROR: invalid page in block 0 of relation base/16384/123388 89 | ``` 90 | 91 | 🔜 未完待续 ... -------------------------------------------------------------------------------- /How to break a database, Part 3 Harmful workloads.md: -------------------------------------------------------------------------------- 1 | # How to break a database, Part 3: Harmful workloads 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 参照 6 | 7 | - [Part 1: How to Corrupt](). 8 | - [Part 2: Simulate infamous transaction ID wraparound](). 9 | 10 | ## 过多的连接 11 | 12 | 下面是一个简单的代码片段,通过 `psql` 和命名管道 (即 FIFO,在 macOS 和 Linux 中都适用) 创建 100 个空闲连接: 13 | 14 | ```bash 15 | mkfifo dummy 16 | 17 | for i in $(seq 100); do 18 | psql -Xf dummy >/dev/null 2>&1 & 19 | done 20 | 21 | ❯ psql -Xc 'select count(*) from pg_stat_activity' 22 | count 23 | ------- 24 | 106 25 | (1 row) 26 | ``` 27 | 28 | 要关闭这些连接,我们可以打开一个写入文件描述符到 FIFO 然后关闭它而不写入任何数据: 29 | 30 | ```bash 31 | exec 3>dummy && exec 3>&- 32 | ``` 33 | 34 | 现在,100 个额外的连接消失了: 35 | 36 | ```bash 37 | ❯ psql -Xc 'select count(*) from pg_stat_activity' 38 | count 39 | ------- 40 | 6 41 | (1 row) 42 | ``` 43 | 44 | 如果连接数达到了 `max_connections` 限制,当我们执行上述步骤时,尝试建立新连接时将看到以下错误: 45 | 46 | ```bash 47 | ❯ psql 48 | psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL: sorry, too many clients already 49 | ``` 50 | 51 | ## 闲置事务会话 52 | 53 | 这个方法我们在模拟事务 ID 回卷实验中使用过: 54 | 55 | ```bash 56 | mkfifo dummy 57 | 58 | psql -Xc " 59 | set idle_in_transaction_session_timeout = 0; 60 | begin; 61 | select pg_current_xact_id() 62 | " \ 63 | -f dummy & 64 | ``` 65 | 66 | 要释放会话: 67 | 68 | ```bash 69 | exec 3>dummy && exec 3>&- 70 | ``` 71 | 72 | ## 使用各种工具造成更多损害 73 | 74 | 这个工具可以帮助你模拟各种有害工作负载:[noisia – harmful workload generator for PostgreSQL](https://github.com/lesovsky/noisia) 75 | 76 | 截至 2023 年,它支持以下功能: 77 | 78 | - 空闲事务 — 在频繁写的表上处于活动状态但不执行任何操作的事务。 79 | - 回滚 — 伪造无效查询,生成错误并增加回滚计数。 80 | - 等待事务 — 锁定频繁写的表然后闲置的事务,导致其他事务被阻塞。 81 | - 死锁 — 同时进行的事务,每个事务持有其他事务所需的锁。 82 | - 临时文件 — 由于 `work_mem` 不足,查询产生的临时文件。 83 | - 终止后台进程 — 使用 `pg_terminate_backend()` 或 `pg_cancel_backend()` 终止随机的后台进程或查询。 84 | - 连接失败 — 耗尽所有可用连接 (其他客户端无法连接到 Postgres)。 85 | - fork 连接 — 在专用连接中执行单个、简短的查询 (导致 Postgres 后台进程的过度 fork)。 86 | 87 | 另一个工具 `pg_crash` 会定期崩溃你的数据库。 88 | 89 | 对于Aurora用户,还有一些有趣的函数:`aurora_inject_crash()`、`aurora_inject_replica_failure()`、`aurora_inject_disk_failure()`、`aurora_inject_disk_congestion()`:参见 [Testing Amazon Aurora PostgreSQL by using fault injection queries](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Managing.FaultInjectionQueries.html)。 90 | 91 | ## 总结 92 | 93 | 混沌工程是一个非常有趣的话题,我认为它在数据库领域有着巨大的潜力 — 用于测试恢复、故障转移,并实践各种事故情况。一些资源 (不仅限于数据库): 94 | 95 | - Wikipedia: [Chaos engineering](https://en.wikipedia.org/wiki/Chaos_engineering) 96 | - Netflix 的 [Chaos Monkey](https://github.com/Netflix/chaosmonkey) 是一款弹性工具,可帮助应用程序容忍随机实例故障 97 | 98 | 理想情况下,成熟的数据库管理流程,无论是在云端或本地,托管或自托管,都应该包括: 99 | 100 | - 定期在非生产环境中模拟事故,以实践并改进事故缓解的手册。 101 | - 定期在生产环境中发起事故,以查看自动化缓解措施的实际效果。例如:自动移除崩溃的副本、自动故障转移、针对长时间运行事务的告警以及团队的响应。 -------------------------------------------------------------------------------- /How to change a Postgres parameter.md: -------------------------------------------------------------------------------- 1 | # How to change a Postgres parameter 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如果你需要更改 Postgres 参数 (又称之为 GUC,Grand Unified Configuration) 以实现永久效果,请按照以下步骤操作。 6 | 7 | 参考文档:[设置参数](https://postgresql.org/docs/current/config-setting.html.) 8 | 9 | ## 1) 确认是否需要重启 10 | 11 | 快速检查是否需要重启的两种方法: 12 | 13 | - 使用 [postgresqlco.nf](https://postgresqlco.nf/) 并查看 `Restart: xxx` 字段。例如,对于 [max_wal_size](https://postgresqlco.nf/doc/en/param/max_wal_size/),`Restart: false`,而对于 [shared_buffers](https://postgresqlco.nf/doc/en/param/shared_buffers) 则为 `true`。 14 | - 检查 `pg_settings` 中的 `context` 字段 — 如果是 `postmaster`,则需要重启,否则不需要 (例外情况:`internal` — 这类参数无法更改)。例如: 15 | 16 | ```sql 17 | nik=# select name, context 18 | from pg_settings 19 | where name in ('max_wal_size', 'shared_buffers'); 20 | name | context 21 | ----------------+------------ 22 | max_wal_size | sighup 23 | shared_buffers | postmaster 24 | (2 rows) 25 | ``` 26 | 27 | ## 2) 进行更改 28 | 29 | 在 Postgres 配置文件中应用更改 (`postgresql.conf` 或其依赖项,如果使用了 `include` 命令的话)。除非必要,建议避免使用 `ALTER SYSTEM`,以免将来产生混淆 (它会写入 `postgresql.auto.conf`,且可能很容易被忽视;参见[相关讨论](https://postgresql.org/message-id/flat/CA%2BVUV5rEKt2%2BCdC_KUaPoihMu%2Bi5ChT4WVNTr4CD5-xXZUfuQw%40mail.gmail.com))。 30 | 31 | ## 3) 应用更改 32 | 33 | 如果需要重启,则重启 Postgres。如果忘记重启,稍后可以在 `pg_settings` 中查看 `pending_restart` 来检测未应用的更改。 34 | 35 | 如果不需要重启,在超级用户下执行: 36 | 37 | ```sql 38 | select pg_reload_conf(); 39 | ``` 40 | 41 | 或者使用以下方法之一: 42 | 43 | - `pg_ctl reload $PGDATA` 44 | 45 | - 发送 `SIGHUP` 信号给 postmaster 进程,例如: 46 | 47 | ~~~bash 48 | kill -HUP $(cat "${PGDATA}/postmaster.pid") 49 | ~~~ 50 | 51 | 当应用了无需重启的更改时,Postgres 日志中会显示类似以下内容: 52 | 53 | ```bash 54 | LOG: received SIGHUP, reloading configuration files 55 | LOG: parameter "max_wal_size" changed to "2GB" 56 | ``` 57 | 58 | ## 4) 验证更改 59 | 60 | 使用 `SHOW` 或 `current_setting(...)` 确认更改已生效,例如: 61 | 62 | ```sql 63 | nik=# show max_wal_size; 64 | max_wal_size 65 | -------------- 66 | 2GB 67 | (1 row) 68 | ``` 69 | 70 | 或者 71 | 72 | ```sql 73 | nik=# select current_setting('max_wal_size'); 74 | current_setting 75 | ----------------- 76 | 2GB 77 | (1 row) 78 | ``` 79 | 80 | ## 额外信息:数据库级、用户级和表级设置 81 | 82 | 如果 `pg_settings` 中的 `context` 为 `user` 或 `superuser`,那么可以在数据库或用户级别调整设置,例如: 83 | 84 | ```sql 85 | alter database verbosedb set log_statement = 'all'; 86 | alter user hero set statement_timeout = 0; 87 | ``` 88 | 89 | 这些结果可以在 `pg_db_role_setting` 中查看: 90 | 91 | ```sql 92 | nik=# select * from pg_db_role_setting; 93 | setdatabase | setrole | setconfig 94 | -------------+---------+----------------------- 95 | 24580 | 0 | {log_statement=all} 96 | 0 | 24581 | {statement_timeout=0} 97 | (2 rows) 98 | ``` 99 | 100 | 当使用 `CREATE TABLE `或 `ALTER TABLE` 时,某些设置还可以在表级别进行调整,参照 [CREATE TABLE / Storage Parameters](https://postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS)。请注意命名差异:`autovacuum_enabled` 用于启用或禁用特定表的 autovacuum 守护进程,而全局设置名称为 `autovacuum`。 101 | 102 | ## 我见 103 | 104 | 在 15 中,还支持了 `\dconfig` 命令,也很方便。 105 | 106 | >The \dconfig command can display the parameter settings of the instance. If no option is specified, a list of parameters that have changed from the default values is displayed. If a parameter name is specified, the specific parameter setting value can be checked. Wildcards can be used for parameter names. 107 | 108 | ~~~sql 109 | postgres=# \dconfig *mem* 110 | List of configuration parameters 111 | Parameter | Value 112 | ----------------------------------+------- 113 | autovacuum_work_mem | -1 114 | dynamic_shared_memory_type | posix 115 | enable_memoize | on 116 | hash_mem_multiplier | 2 117 | logical_decoding_work_mem | 64MB 118 | maintenance_work_mem | 64MB 119 | min_dynamic_shared_memory | 0 120 | multixact_member_buffers | 256kB 121 | shared_memory_size | 145MB 122 | shared_memory_size_in_huge_pages | 73 123 | shared_memory_type | mmap 124 | work_mem | 4MB 125 | (12 rows) 126 | ~~~ -------------------------------------------------------------------------------- /How to change ownership of all objects in a database.md: -------------------------------------------------------------------------------- 1 | # How to change ownership of all objects in a database 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如果需要更改当前数据库中所有对象的所有权,可以使用以下匿名 DO 块 (或从[此处](https://gitlab.com/postgres-ai/database-lab/-/snippets/2075222)粘贴): 6 | 7 | ~~~sql 8 | do $$ 9 | declare 10 | new_owner text := 'NEW_OWNER_ROLE_NAME'; 11 | object_type record; 12 | r record; 13 | sql text; 14 | begin 15 | -- New owner for all schemas 16 | for r in select * from pg_namespace loop 17 | sql := format( 18 | 'alter schema %I owner to %I;', 19 | r.nspname, 20 | new_owner 21 | ); 22 | 23 | raise debug 'Execute SQL: %', sql; 24 | 25 | execute sql; 26 | end loop; 27 | 28 | -- Various DB objects 29 | -- c: composite type 30 | -- p: partitioned table 31 | -- i: index 32 | -- r: table 33 | -- v: view 34 | -- m: materialized view 35 | -- S: sequence 36 | for object_type in 37 | select 38 | unnest('{type,table,table,view,materialized view,sequence}'::text[]) type_name, 39 | unnest('{c,p,r,v,m,S}'::text[]) code 40 | loop 41 | for r in 42 | execute format( 43 | $sql$ 44 | select n.nspname, c.relname 45 | from pg_class c 46 | join pg_namespace n on 47 | n.oid = c.relnamespace 48 | and not n.nspname in ('pg_catalog', 'information_schema') 49 | and c.relkind = %L 50 | order by c.relname 51 | $sql$, 52 | object_type.code 53 | ) 54 | loop 55 | sql := format( 56 | 'alter %s %I.%I owner to %I;', 57 | object_type.type_name, 58 | r.nspname, 59 | r.relname, 60 | new_owner 61 | ); 62 | 63 | raise debug 'Execute SQL: %', sql; 64 | 65 | execute sql; 66 | end loop; 67 | end loop; 68 | 69 | -- Functions, procedures 70 | for r in 71 | select 72 | p.proname, 73 | n.nspname, 74 | pg_catalog.pg_get_function_identity_arguments(p.oid) as args 75 | from pg_catalog.pg_namespace as n 76 | join pg_catalog.pg_proc as p on p.pronamespace = n.oid 77 | where 78 | not n.nspname in ('pg_catalog', 'information_schema') 79 | and p.proname not ilike 'dblink%' -- We do not want dblink to be involved (exclusion) 80 | loop 81 | sql := format( 82 | 'alter function %I.%I(%s) owner to %I', -- todo: check support CamelStyle r.args 83 | r.nspname, 84 | r.proname, 85 | r.args, 86 | new_owner 87 | ); 88 | 89 | raise debug 'Execute SQL: %', sql; 90 | 91 | execute sql; 92 | end loop; 93 | 94 | -- Full text search dictionary 95 | -- TODO: text search configuration 96 | for r in 97 | select * 98 | from pg_catalog.pg_namespace n 99 | join pg_catalog.pg_ts_dict d on d.dictnamespace = n.oid 100 | where not n.nspname in ('pg_catalog', 'information_schema') 101 | loop 102 | sql := format( 103 | 'alter text search dictionary %I.%I owner to %I', 104 | r.nspname, 105 | r.dictname, 106 | new_owner 107 | ); 108 | 109 | raise debug 'Execute SQL: %', sql; 110 | 111 | execute sql; 112 | end loop; 113 | 114 | -- Domains 115 | for r in 116 | select typname, nspname 117 | from pg_catalog.pg_type 118 | join pg_catalog.pg_namespace on pg_namespace.oid = pg_type.typnamespace 119 | where typtype = 'd' and not nspname in ('pg_catalog', 'information_schema') 120 | loop 121 | sql := format( 122 | 'alter domain %I.%I owner to %I', 123 | r.nspname, 124 | r.typname, 125 | new_owner 126 | ); 127 | 128 | raise debug 'Execute SQL: %', sql; 129 | 130 | execute sql; 131 | end loop; 132 | end 133 | $$; 134 | ~~~ 135 | 136 | 不要忘记更改 `new_owner` 的值。 137 | 138 | 该查询不包括模式 `pg_catalog` 和 `information_schema`,它涵盖:模式对象、表、视图、物化视图、函数、文本搜索字典和域。根据你的 PG 版本,可能还有其他对象需要处理。根据需要调整代码。 139 | 140 | 要查看调试消息,请在运行之前更改 `client_min_messages`: 141 | 142 | ~~~sql 143 | set client_min_messages to debug; 144 | ~~~ 145 | 146 | -------------------------------------------------------------------------------- /How to check btree indexes for corruption (pg_amcheck).md: -------------------------------------------------------------------------------- 1 | # How to check btree indexes for corruption (pg_amcheck) 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 🥳🤩🎂🎉 今天是我的生日,而且我们刚刚软启动了我们的 [postgres.ai bot](https://twitter.com/samokhvalov/status/1726177412755677283) — 所以请原谅这次文章并不完整。不过,我仍然继续发布 😅 6 | 7 | 有多种类型的数据损坏。某些类型的数据损坏可以通过扩展 `amcheck` 来识别 (参见:[Using amcheck Effectively](https://postgresql.org/docs/current/amcheck.html#AMCHECK-USING-AMCHECK-EFFECTIVELY)),该扩展包含在标准的"contrib 模块"包中 — 只需创建即可: 8 | 9 | ```sql 10 | create extension amcheck; 11 | ``` 12 | 13 | 对于 Postgres 14+,建议使用 CLI 工具 [pg_amcheck](https://postgresql.org/docs/current/app-pgamcheck.html),其优势之一是`-j `(`--jobs=NUM`) 选项 — 可以通过并行工作进程更快地检查多个索引。 14 | 15 | 以下是一个使用示例 (填入连接选项并调整并行工作进程数): 16 | 17 | ```bash 18 | pg_amcheck \ 19 | {{ connection options }} \ 20 | --install-missing \ 21 | --jobs=8 \ 22 | --verbose \ 23 | --progress \ 24 | --heapallindexed \ 25 | --parent-check \ 26 | 2>&1 \ 27 | | ts \ 28 | | tee -a pg_amcheck.$(date "+%F-%H-%M").log 29 | ``` 30 | 31 | **重要提示**:选项 `--heapallindexed` 和 `--parent-check` 会触发一个耗时但更高级的检查。 32 | `--parent-check` 选项会阻塞写操作 (如 `UPDATE` 等),因此不要在接收用户流量的生产节点上使用它。 33 | 选项 `--heapallindexed` 会增加检查的负载和持续时间,但可以小心地在生产环境中使用。 34 | 在没有这两个选项的情况下,执行的检查会较轻,可能无法发现某些问题 (参见:[Using amcheck Effectively](https://postgresql.org/docs/current/amcheck.html#AMCHECK-USING-AMCHECK-EFFECTIVELY))。 35 | 36 | 一旦上述命令执行完成,检查生成的日志中是否包含错误: 37 | 38 | ```bash 39 | egrep 'ERROR:|DETAIL:|LOCATION:' \ 40 | pg_amcheck.$(date "+%F-%H-%M").log 41 | ``` 42 | 43 | 如果检测到有损坏的索引,需要仔细分析,一旦问题得到确认,则需要重新创建索引 (使用`REINDEX CONCURRENTLY`)。 -------------------------------------------------------------------------------- /How to check btree indexes for corruption.md: -------------------------------------------------------------------------------- 1 | # How to check btree indexes for corruption 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 以下代码片段使用了 `$N` 个并行工作进程检查所有 Btree 索引 (一般推荐进行并行处理): 6 | 7 | ```bash 8 | pg_amcheck \ 9 | --install-missing \ 10 | --verbose \ 11 | --progress \ 12 | --jobs=$N \ 13 | --heapallindexed \ 14 | 2>&1 \ 15 | | ts \ 16 | | tee -a amcheck_$(date "+%F-%H-%M").log 17 | ``` 18 | 19 | 文档参考: 20 | 21 | - [amcheck (extension)](https://postgresql.org/docs/current/amcheck.html) 22 | - [pg_amcheck (CLI tool)](https://postgresql.org/docs/current/app-pgamcheck.html) 23 | 24 | 备注: 25 | 26 | 1. 此类检查只需获取索引的`AccessShareLock` — 因此 DML 操作不会被阻塞。不过,通常这种检查会产生较大的工作负载,因为它需要读取所有索引以及相应元组。 27 | 2. `--heapallindexed` 选项是可选的,但强烈推荐使用。如果不使用此选项,检查的时间会大幅缩短,但在这种情况下,仅进行"轻量级"索引检查 (不会涉及到表,不会跟随索引引用检查表中的元组)。 28 | 3. 这里还有一个很有用但没有使用的选项 `--parent-check`,它提供了更全面的索引结构检查 — 会检查索引中的父子关系。这对于索引的"深度"测试非常有帮助。不过,这个检查速度较慢,并且不幸的是,需要获取`ShareLock`锁。这意味着在这种模式下进行检查时,会阻塞修改操作(`INSERT`/`UPDATE`/`DELETE`)。因此,`--parent-check` 只能在不接收流量的克隆数据库或维护窗口期间使用。 29 | 4. 使用 `amcheck` 检查索引并不是启用数据校验和的替代方案。建议同时启用数据校验和并定期使用 `amcheck` 进行损坏检查 (例如,在创建副本之后、备份恢复验证时,或者任何时候怀疑有索引损坏)。 30 | 5. 如果发现错误,意味着有损坏 (除非 `amcheck` 有 bug)。如果没有发现错误,这并不意味着没有损坏 — 损坏问题的范围非常之广,没有任何单一工具可以检查所有可能的损坏类型。 31 | 6. 可能导致 Btree 索引损坏的情况: 32 | 1. Postgres 的 bug (例如,14.0-14.3 版本中的 `REINDEX CONCURRENTLY` 存在 bug,可能会导致 Btree 索引损坏)。 33 | 2. 将 `PGDATA` 从一个操作系统版本复制到另一个操作系统版本时,默默地切换到不同的 glibc 版本,导致排序规则发生变化。在这种情况下,PG15+ 会发出警告 (`WARNING: database XXX has a collation version mismatch`),而旧版本则不会发出警告,因此静默损坏的风险很大。始终建议在升级时测试并使用 amcheck 验证索引。 34 | 3. 执行重大升级时,使用`rsync --data-only`来升级副本,而未正确处理主服务器和副本停止的顺序。参考:[details](https://postgresql.org/message-id/flat/CAM527d8heqkjG5VrvjU3Xjsqxg41ufUyabD9QZccdAxnpbRH-Q@mail.gmail.com). 35 | 36 | 7. **GiST** 和 **GIN** 索引的损坏检测仍在开发中,虽然有一些[补丁](https://commitfest.postgresql.org/45/3733/)可以使用 (但需要注意,这些补丁尚未正式发布)。 37 | 38 | 检测唯一索引损坏的功能也在开发中。 39 | 40 | # 我感 41 | 42 | 细节可以参考:https://www.postgresql.org/docs/current/amcheck.html 43 | 44 | >`bt_index_check` does not verify invariants that span child/parent relationships, but will verify the presence of all heap tuples as index tuples within the index when *`heapallindexed`* is `true`. -------------------------------------------------------------------------------- /How to compile Postgres on Ubuntu 22.04.md: -------------------------------------------------------------------------------- 1 | # How to compile Postgres on Ubuntu 22.04 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 本文介绍了如何快速在 Ubuntu 22.04 上编译 Postgres。 6 | 7 | [官方文档](https://postgresql.org/docs/current/installation.html)提供了非常详细的信息,作为参考非常有用,但你不会找到针对特定操作系统版本的具体步骤,比如 Ubuntu。 8 | 9 | 本文提供的指南足够让你从 `master` 分支构建 Postgres 并开始使用。如果需要,可以根据实际需求扩展这些基本步骤。 10 | 11 | 几点说明: 12 | 13 | - 当前 (截至 2023 年,PG16) 文档中提到了一种新的 Postgres 构建方法 — [Meson](https://mesonbuild.com/) — 但它仍被认为是实验性的,因此本文不会使用。 14 | - 此处使用的二进制文件和 `PGDATA` 路径仅作为示例 (适用于"快速而粗糙"的设置来测试,例如,来自 `pgsql-hackers` 邮件列表的新补丁)。 15 | 16 | 1. 安装必需的软件 17 | 2. 获取源代码 18 | 3. 配置 19 | 4. 编译和安装 20 | 5. 创建集群并开始使用 21 | 22 | ### 1) 安装必需的软件 23 | 24 | ```bash 25 | sudo apt update 26 | 27 | sudo apt install -y \ 28 | build-essential libreadline-dev \ 29 | zlib1g-dev flex bison libxml2-dev \ 30 | libxslt-dev libssl-dev libxml2-utils \ 31 | xsltproc ccache 32 | ``` 33 | 34 | ### 2) 获取源代码 35 | 36 | ```bash 37 | git clone https://gitlab.com/postgres/postgres.git 38 | cd ./postgres 39 | ``` 40 | 41 | ### 3) 配置 42 | 43 | ```bash 44 | mkdir ~/pg16 # consider changing it! 45 | 46 | ./configure \ 47 | --prefix=$(echo ~/pg16) \ 48 | --with-ssl=openssl \ 49 | --with-python \ 50 | --enable-depend \ 51 | --enable-cassert 52 | ``` 53 | 54 | (查看可选项列表) 55 | 56 | ### 4) 编译和安装 57 | 58 | ```bash 59 | make -j$(grep -c processor /proc/cpuinfo) 60 | make install 61 | ``` 62 | 63 | ### 5) 创建集群并开始使用 64 | 65 | ```bash 66 | ~/pg16/bin/initdb \ 67 | -D ~/pgdata \ 68 | --data-checksums \ 69 | --locale-provider=icu \ 70 | --icu-locale=en 71 | 72 | ~/pg16/bin/pg_ctl \ 73 | -D ~/pgdata \ 74 | -l pg.log start 75 | ``` 76 | 77 | 现在,进行检查: 78 | 79 | ```bash 80 | $ ~/pg16/bin/psql postgres -c 'show server_version' 81 | server_version 82 | ---------------- 83 | 17devel 84 | (1 row) 85 | ``` -------------------------------------------------------------------------------- /How to convert a physical replica to logical.md: -------------------------------------------------------------------------------- 1 | # How to convert a physical replica to logical 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在某些情况下,将现有的异步物理副本转换为逻辑副本可能是有利的,或者先创建一个新的物理副本,然后将其转换为逻辑副本。 6 | 7 | 这种方式: 8 | 9 | - 一方面,消除了加载初始数据这一步骤的必要,这个步骤在处理大型、负载繁重的数据库时可能会很脆弱且压力很大。 10 | - 另一方面,使用这种方式创建的逻辑副本拥有源 Postgres 实例中的所有数据。 11 | 12 | 因此,这种方法非常适用于需要将源数据库中的所有数据呈现在新创建的逻辑副本中的情况,特别是在处理非常大且负载繁重的集群时极为有用。 13 | 14 | 下面的步骤十分简单。我们使用一个物理副本,通过流复制 `primary_conninfo` 和复制槽 (比如通过 Patroni 进行控制) 立即从主库复制数据,不涉及级联复制 (尽管也有可能实现)。 15 | 16 | ## 步骤1:选择一个用于转换的物理副本 17 | 18 | 选择一个物理副本进行转换,或者通过 `pg_basebackup`、从备份恢复或从云快照创建新的物理副本。 19 | 20 | 确保在转换期间没有用户使用该副本。 21 | 22 | ## 步骤2:确保满足要求 23 | 24 | 首先,确保为逻辑复制配置了相关设置,如[逻辑复制配置中](https://postgresql.org/docs/current/logical-replication-config.html)所述。 25 | 26 | 主库设置: 27 | 28 | - `wal_level = 'logical'` 29 | - `max_replication_slots > 0` 30 | - `max_wal_senders > max_replication_slots` 31 | 32 | 在我们将要转换的物理副本上: 33 | 34 | - `max_replication_slots > 0` 35 | - `max_logical_replication_workers > 0` 36 | - `max_worker_processes >= max_logical_replication_workers + 1` 37 | 38 | 此外: 39 | 40 | - 复制延迟较小; 41 | - 每个表都有主键或设置了 [REPLICA IDENTITY FULL](https://postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY); 42 | - 用于转换的副本没有设置 `restore_command` (如果有,临时将其值设为空字符串); 43 | - 临时增加主库上的 `wal_keep_size` (PG13+,在 PG12 或更早版本中为 `wal_keep_segments`),增加到对应于几个小时 WAL 生成的量。 44 | 45 | ## 步骤3:停止物理副本 46 | 47 | 关闭物理副本,并在下一步执行期间保持其关闭。这确保其位置比我们将在主库上创建的逻辑槽的位置早。 48 | 49 | ## 步骤4:创建发布、逻辑槽并记录其LSN 50 | 51 | 在主库上: 52 | 53 | 1. 手动执行一次 `CHECKPOINT`; 54 | 2. 创建发布; 55 | 3. 创建逻辑复制槽并记录其 LSN 位置。 56 | 57 | 示例: 58 | 59 | ```sql 60 | checkpoint; 61 | 62 | create publication my_pub for all tables; 63 | 64 | select lsn 65 | from pg_create_logical_replication_slot( 66 | 'my_slot', 67 | 'pgoutput' 68 | ); 69 | ``` 70 | 71 | 请务必记住最后一条命令返回的 LSN 值 — 稍后我们会使用到它。 72 | 73 | ## 步骤5:让物理副本追赶上主库 74 | 75 | 重新配置物理副本: 76 | 77 | - `recovery_target_lsn` — 将其设置为上一步获得的 LSN 值 78 | - `recovery_target_action = 'promote'` 79 | - `restore_command`,`recovery_target_timeline`,`recovery_target_xid`,`recovery_target_time`,`recovery_target_name` 为空或未设置。 80 | 81 | 现在启动物理副本,监控其延迟情况,直到副本追赶上我们需要的 LSN 并自动提升。这可能需要一些时间。完成之后进行检查: 82 | 83 | ```sql 84 | select pg_is_in_recovery(); 85 | ``` 86 | 87 | - 必须返回 `f`,这意味着该节点现在本身就是一个主节点 (一个克隆节点),其位置与源节点上的复制槽位置相对应。 88 | 89 | ## 步骤6:创建订阅并启动逻辑复制 90 | 91 | 在新创建的"克隆"节点上,创建逻辑订阅 `copy_data = false` 和 `create_slot = false`: 92 | 93 | ```sql 94 | create subscription 'my_sub' 95 | connection 'host=.. port=.. user=.. dbname=..' 96 | publication my_pub 97 | with ( 98 | copy_data = false, 99 | create_slot=false, 100 | slot_name = 'my_slot' 101 | ); 102 | ``` 103 | 104 | 确保复制处于运行中的状态 — 在源主库上检查: 105 | 106 | ```sql 107 | select * from pg_replication_slots; 108 | ``` 109 | 110 | 确保 `active` 字段为 `t`。 111 | 112 | ## 完成 113 | 114 | - 等待逻辑复制的延迟完全赶上 (偶尔出现的峰值是正常的)。 115 | - 将主库上的`wal_keep_size` (或 `wal_keep_segments`) 恢复到原始值。 116 | 117 | ## 额外说明 118 | 119 | 在这个方案中,我们使用了单个发布和逻辑复制槽。也可以使用多个槽,只需对过程进行一些调整。但是,如果选择这样做,请记住使用多个复制槽/发布的潜在复杂性,首先是这些: 120 | 121 | - 无法保证逻辑副本的引用完整性 (偶尔会暂时性地违反 FK), 122 | - 多个发布的创建更为脆弱 (创建 `FOR ALL TABLES` 的发布不需要表级锁,但是当我们使用多个发布,并为某些表创建发布时,需要表级锁 — 然而,这只是 `ShareUpdateExclusiveLock`,参考 [this comment on PostgreSQL source code](https://github.com/postgres/postgres/blob/1b6da28e0668eb977dcab6987d192ddedf32b752/src/backend/commands/publicationcmds.c#L1550)). 123 | 124 | 以及无论如何: 125 | 126 | - 确保已准备好处理对应版本的逻辑复制限制 (例如,[PG16](https://postgresql.org/docs/16/logical-replication-restrictions.html)); 127 | - 如果考虑使用此方法执行大版本升级,请避免在已转换的节点上运行 `pg_upgrade` — 这可能不安全 (参照:[pg_upgrade and logical replication](https://postgresql.org/message-id/flat/20230217075433.u5mjly4d5cr4hcfe@jrouhaud))。 128 | 129 | # 我见 130 | 131 | 看了一下操作流程,确实十分繁琐,17 版本中的 pg_createsubscriber 无疑是个巨大的福音,大大简化了将物理副本转化为逻辑副本的步骤。 132 | 133 | pg_createsubscriber to create a logical replica from a physical standby server 134 | 135 | - Speed up creation of logical subscriber 136 | - It can be used for upgrades 137 | 138 | 另外原文中所说的最后一点,在 17 版本看似也已解决。具体可以参照上面的邮件 139 | 140 | 以及如下🔗: 141 | 142 | - [PGConf.dev 2024 - New logical replication features in PostgreSQL 17](https://www.postgresql.fastware.com/blog/new-logical-replication-features-in-postgresql-17) 143 | - [What’s New In PostgreSQL 17](https://www.metisdata.io/blog/whats-new-in-postgresql-17) 144 | - https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=9a17be1e244a45a77de25ed2ada246fd34e4557d 145 | -------------------------------------------------------------------------------- /How to create an index, part 1.md: -------------------------------------------------------------------------------- 1 | # How to create an index, part 1 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 索引创建很简单: 6 | 7 | ```sql 8 | create index concurrently on t1(c1); 9 | ``` 10 | 11 | 一个更长的包含显式命名的格式: 12 | 13 | ```sql 14 | create index concurrently i_1 on t1(c1); 15 | ``` 16 | 17 | 甚至更长,显式包括索引名称和类型: 18 | 19 | ```sql 20 | create index concurrently i_1 21 | on t1 22 | using btree(c1); 23 | ``` 24 | 25 | 下面是一些"最佳实践"的考虑。 26 | 27 | ## 索引命名 28 | 29 | 创建索引时: 30 | 31 | - 使用显式命名利于更好的控制。 32 | - 建立并遵循某种命名规则。例如,可以在索引名称中包含表和列的名称:`i_table1_col1_col2`。其他可考虑包括的属性: 33 | - 是常规索引还是唯一索引? 34 | - 索引类型 35 | - 是否为部分索引? 36 | - 排序、使用的表达式 37 | - 使用的操作符类 38 | 39 | ## 设置 40 | 41 | 1. `statement_timeout`:如果全局设置了 `statement_timeout`,在创建索引的会话中将其取消: 42 | 43 | ~~~sql 44 | set statement_timeout to 0; 45 | ~~~ 46 | 47 | 或者,可以为创建索引创建一个专用的数据库用户,并调整其 `statement_timeout`: 48 | 49 | ~~~sql 50 | alter user index_creator set statement_timeout to 0; 51 | ~~~ 52 | 53 | 2. `maintenance_work_mem`:将此参数 ([docs](https://postgresqlco.nf/doc/en/param/maintenance_work_mem/)) 提高到几百 MiB 或几 GiB (在较大的系统上) 以更快地创建索引。 54 | 55 | ## CONCURRENTLY 56 | 57 | 除非以下情况,建议始终使用 `CONCURRENTLY` 选项: 58 | 59 | - 在一个尚未使用的表上创建索引 — 例如,一个刚创建的表 (此时可以避免使用 `CONCURRENTLY`,以便能够在事务块中一起创建索引)。 60 | - 在独自操作数据库。 61 | 62 | 👉 这是其在 16 版本中实现的[原理解读](https://github.com/postgres/postgres/blob/c136eb02981566d56e950f12ab7ee4a6ea51d698/src/backend/catalog/index.c#L1443-L1511)。 63 | 64 | 使用 `CONCURRENTLY` 会增加索引的创建时间,但会优雅地持有锁,不会长时间阻塞其他会话。使用此方法,可以在创建新索引时允许持续访问表、维护数据完整性和一致性,以及最大限度地减少对正常数据库操作的干扰之间取得平衡。 65 | 66 | 当使用此选项时,创建创建可能会因各种原因失败 — 例如,如果尝试同时创建两个索引,可能会看到类似如下的错误: 67 | 68 | ```sql 69 | nik=# create index concurrently i_3 on t1 using btree(c1); 70 | ERROR: deadlock detected 71 | DETAIL: Process 518 waits for ShareLock on virtual transaction 3/459; blocked by process 553. 72 | Process 553 waits for ShareUpdateExclusiveLock on relation 16402 of database 16401; blocked by process 518. 73 | HINT: See server log for query details. 74 | ``` 75 | 76 | 一般来说,Postgres 支持 DDL 事务,但对于 `CREATE INDEX CONCURRENTLY` 不适用: 77 | 78 | - 不能在事务块中包含 `CREATE INDEX CONCURRENTLY`。 79 | - 如果操作失败,会留下一个无效的索引,因此需要进行清理。 80 | 81 | ## 清理和重试 82 | 83 | 由于 `CREATE INDEX CONCURRENTLY `可能会失败,因此应准备手动或自动重试。在重试之前,需要清理无效的索引。此时,使用显式命名和某种命名约定的好处就体现出来了。清理时,同样使用 `CONCURRENTLY`: 84 | 85 | ```sql 86 | drop index concurrently i_1; 87 | ``` 88 | 89 | ## 进度监控 90 | 91 | 如何监控索引创建进度参照 [Day 15: How to monitor CREATE INDEX / REINDEX progress in Postgres 12+](). 92 | 93 | ## ANALYZE 94 | 95 | 通常,Postgres `autovacuum` 会维护最新的每列统计信息,并对内容发生足够变化的每个表运行 `ANALYZE`。 96 | 97 | 在新索引创建后,如果只是对列进行索引,通常无需重新构建统计信息。 98 | 99 | 但是,如果是对表达式创建索引,例如: 100 | 101 | ```sql 102 | create index i_t1_lower_email on t1 (lower(email)); 103 | ``` 104 | 105 | 那么你应该在表上运行 `ANALYZE`,以便 Postgres 收集表达式的统计信息: 106 | 107 | ```sql 108 | analyze verbose t1; 109 | ``` -------------------------------------------------------------------------------- /How to create an index, part 2.md: -------------------------------------------------------------------------------- 1 | # How to create an index, part 2 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 参照 [Part 1](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0061_how_to_create_an_index_part_1.md). 6 | 7 | 在第一部分中,我们讨论了创建索引的基础知识。今天,我们将探讨与索引创建相关的并行化和分区方面的内容。这两部分不涉及如何选择索引类型、何时使用部分索引、表达式索引或多列索引 — 这些将在单独的指南中讨论。 8 | 9 | ## 长时间索引创建及其加速方法 10 | 11 | 如前所述,索引创建时间过长,例如数小时 — 不仅不方便,还会阻止 `autovacuum` 处理表,并在整个操作期间持有 `xmin` 视界 (这意味着 `autovacuum` 无法删除数据库中所有表新生成的死元组)。 12 | 13 | 因此,值得优化单个索引的创建时间。以下是一些常见方法: 14 | 15 | 1. 配置调优 16 | 17 | - 提高 `maintenance_work_mem` ,此前已经描述过。 18 | 19 | > 🎯 **TODO**: 通过实验展示如何调整此参数。 20 | 21 | - **检查点调优**:临时提高 `max_wal_size` 和 `checkpoint_timeout` (无需重启) 以减少检查点频率,这可能会提高创建的时间。 22 | 23 | > 🎯 **TODO**: 进行实验来验证其效果。 24 | 25 | 2. 并行化 — 使用多个后台进程加快整个操作。 26 | 27 | 3. 分区 — 将表拆分为多个物理表可以减少创建单个索引所需的时间。 28 | 29 | ## 并行索引创建 30 | 31 | 选项 `max_parallel_maintenance_workers` (PG11+,参见[文档](https://postgresqlco.nf/doc/en/param/max_parallel_maintenance_workers/)) 定义了 `CREATE INDEX` 的最大并行工作进程数。目前 (截至 PG16),它仅适用于创建 B-tree 索引。 32 | 33 | 默认 `max_parallel_maintenance_workers` 为 `2`,可以增加,不需要重启,可以在会话中动态调整。其最大值取决于两个设置: 34 | 35 | - **`max_parallel_workers`**,默认为 8,同样可以在不重启的情况下更改。 36 | - **`max_worker_processes`**,默认为 8,更改需要重启。 37 | 38 | 增加 `max_parallel_maintenance_workers` 可以显著减少索引构建时间,但应在适当分析 CPU 和磁盘 IO 利用率后进行。 39 | 40 | > 🎯 **TODO**: 进行实验来验证效果。 41 | 42 | ## 分区表上的索引 43 | 44 | 如之前多个指南中讨论的那样,大表 (例如超过100 GiB;这不是一个硬性规定) 应当进行分区。如果没有分区,处理上 TB 的表时,索引创建时间会非常长,在此期间,`autovacuum` 无法处理该表。这会导致更高的膨胀度。 45 | 46 | 可以为各个分区创建索引。然而,考虑为所有分区使用统一的索引方法,并在分区表本身上定义索引是有意义的。 47 | 48 | 使用 `CONCURRENTLY` 选项无法在分区表 (父表) 上创建索引,只能在单个分区上使用。然而,这个问题可以通过以下方式解决 (PG11+): 49 | 50 | 1. 在所有分区上分别创建索引,使用 `CONCURRENTLY`: 51 | 52 | ```sql 53 | create index concurrently i_p_123 on partition_123 ...; 54 | ``` 55 | 56 | 2. 然后在分区表 (父表) 上创建索引,不使用 `CONCURRENTLY`,并使用关键字 `ONLY` — 这很快,因为物理上这不是一个大表,但在完全执行完下一步之前,它会被标记为 `INVALID`: 57 | 58 | ```sql 59 | create index i_p_main on only partitioned_table ...; 60 | ``` 61 | 62 | 3. 然后,将每个分区上的索引标记为"attached"到"主"索引,使用这种稍微奇怪的语法 (注意,这里使用的是索引名称,而不是表名): 63 | 64 | ```sql 65 | alter index i_p_main attach partition i_p_123; 66 | ``` 67 | 68 | 相关文档:[Partition Maintenance](https://postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-MAINTENANCE). -------------------------------------------------------------------------------- /How to deal with bloat.md: -------------------------------------------------------------------------------- 1 | # How to deal with bloat 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## 什么是膨胀? 6 | 7 | 膨胀是页面内部的空闲空间,在`autovacuum` 删除大量元组时产生。 8 | 9 | 当更新表中的一行时,Postgres 不会覆盖旧数据,而是将旧的行版本 (元组) 标记为"dead",并创建一个新的行版本。随着时间推移,越来越多的行被更新或删除,死元组占据的空间会积累起来。在某个时间点,`autovacuum` (或手动执行 `VACUUM`) 会删除死元组,留下页面内的空闲空间供重用。但是,如果死元组累积过多,可能会留下大量空闲空间 — 在最糟糕的情况下,它可能占据表或索引空间的 99% 或更多) 10 | 11 | 较低的膨胀率 (例如,低于 40%) 不应被视为问题,而较高的膨胀率会有问题,因为它们会导致严重的后果: 12 | 13 | 1. 磁盘使用量增加 14 | 2. 读写查询需要更多 IO 15 | 3. 缓存效率下降 (包括缓冲池和操作系统文件缓存) 16 | 4. 结果:查询性能变差 17 | 18 | ## 如何检查膨胀 19 | 20 | 应该定期检查索引和表的膨胀情况。注意,大多数常用的查询都是基于预估,容易出现误报 — 根据表的结构,有时会显示一些并不存在的膨胀 (我见过一些情况,即使是在刚创建的表中,也显示了高达 40% 的"幽灵膨胀")。但这些查询速度快,不需要安装额外的扩展。示例: 21 | 22 | - [Estimated table bloat](https://github.com/NikolayS/postgres_dba/blob/master/sql/b1_table_estimation.sql) 23 | - [Estimated btree index bloat](https://github.com/NikolayS/postgres_dba/blob/master/sql/b2_btree_estimation.sql) 24 | 25 | 监控系统中的建议: 26 | 27 | - 没有必要频繁运行这些查询 (例如每分钟一次),因为膨胀水平并不会迅速变化。 28 | - 应向用户提供警告,告知结果是预估值。 29 | - 对于大型数据库,查询执行时间可能较长,长达数秒,因此需要调整检查频率和 `statement_timeout`。 30 | 31 | 更精确确定膨胀水平的方式: 32 | 33 | - 基于 `pgstattuple` 的查询 (需要安装扩展)。 34 | - 在克隆的数据库上检查数据库对象大小,运行 `VACUUM FULL` (过重且会阻塞查询,不适用于生产) ,然后再次检查大小并进行前后对比。 35 | 36 | 定期检查膨胀水平是推荐的,以便在需要时及时做出反应。 37 | 38 | ## 缓解索引膨胀 (被动) 39 | 40 | 不幸的是,在经历大量 `UPDATE` 和 `DELETE` 的数据库中,索引健康状况会不可避免地会随着时间的推移而恶化。这意味着索引需要定期重建。 41 | 42 | 建议: 43 | 44 | - 使用 `REINDEX CONCURRENTLY` 以非阻塞方式重建膨胀的索引。 45 | - 请记住,`REINDEX CONCURRENTLY` 在运行时会持有 `xmin` 视界,这会影响 `autovacuum` 清理近期死元组的能力。这是使用分区的另一个原因;不要让表超过一定阈值 (例如,不超过 100 GiB)。 46 | - 你可以使用第 15 天的监控方法监控重建索引的进度: [Day 15: How to monitor CREATE INDEX / REINDEX](). 47 | - 优先使用 Postgres 14+ 版本,因为在 PG14 中,btree 索引经过了显著优化,在写入工作负载下性能下降得更慢。 48 | 49 | ## 缓解表膨胀 (被动) 50 | 51 | 某些水平的表膨胀可能不是坏事,因为它增加了优化 `UPDATE` 的机会 —`HOT` (Heap-Only Tuples) 更新。 52 | 53 | 但是,如果膨胀水平令人担忧,考虑使用 [pg_repack](https://github.com/reorg/pg_repack) 在不长时间持有排他锁的情况下重建表。`pg_repack` 的替代工具有:[pg_squeeze](https://github.com/cybertec-postgresql/pg_squeeze) 54 | 55 | 通常,这个过程不需要定期调度和完全自动化;通常只需在检测到高表膨胀时有控制地应用即可。 56 | 57 | ## 主动缓解膨胀 58 | 59 | - 调整 `autovacuum`。 60 | - 监控 `xmin` 视界,不要让其滞后太久 — [Day 45: How to monitor xmin horizon to prevent XID/MultiXID wraparound and high bloat](). 61 | - 不要允许不必要的长事务 (例如超过 1 小时),无论是在主节点还是在启用了 `hot_standby_feedback` 的备节点上。 62 | - 如果使用 Postgres 13 或更早版本,请考虑升级到 14+ 以从 btree 索引优化中受益。 63 | - 对大表 (100+ GiB) 进行分区。 64 | - 对具有队列类型工作负载的表使用分区,即使它们很小,并使用 `TRUNCATE` 或删除分区来处理旧数据,而不是使用 `DELETE`;在这种情况下,不需要 `VACUUM`,膨胀也不再是问题。 65 | - 不要使用大量的 `UPDATE` 和 `DELETE`,始终分批处理 (持续时间不超过 1-2 秒);确保 `autovacuum` 及时清理死元组,或者在需要进行大量数据更改时手动执行 `VACUUM`。 66 | 67 | ## 推荐阅读材料 68 | 69 | - Postgres 官方文档: [Routine Vacuuming](https://postgresql.org/docs/current/routine-vacuuming.html) 70 | - [When autovacuum does not vacuum](https://2ndquadrant.com/en/blog/when-`autovacuum`-does-not-vacuum/) 71 | - [How to Reduce Bloat in Large PostgreSQL Tables](https://timescale.com/learn/how-to-reduce-bloat-in-large-postgresql-tables/) -------------------------------------------------------------------------------- /How to decide when a query is too slow and needs optimization.md: -------------------------------------------------------------------------------- 1 | # How to decide when a query is too slow and needs optimization 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | "慢"是一个相对概念。在某些情况下,我们可能对查询延迟为 1 分钟并不会感到不满意 (或者不是?),而在其他场景下,即使是 1 毫秒可能也显得过于缓慢。 6 | 7 | 何时应用优化技术的决策对效率来说至关重要 — 正如 Donald Knuth 在《计算机程序设计艺术》中所说的那样: 8 | 9 | > 真正的问题在于,程序员花费了太多时间在错误的地方和错误的时间担心效率;在编程中,过早的优化是万恶之源 (或至少是大多数问题的根源)。 10 | 11 | 在下文中,我们假设我们正在处理 OLTP 或混合型工作负载,需要决定某个查询是否太慢并需要优化。 12 | 13 | ## 如何判断查询太慢 14 | 15 | 1. 你的用例是 OLTP 型的还是分析型的,又或者是混合型的?对于 OLTP 用例,要求更为严格,受到人类感知的影响 (参照: [What is a slow SQL query?](https://postgres.ai/blog/20210909-what-is-a-slow-sql-query)), 而对于分析需求,我们通常可以等待一两分钟 — 除非它也是面向用户的。如果是这样的话,可能我们会认为 1 分钟太慢了。在这种情况下,请考虑使用列存储数据库系统 (Postgres 生态系统中有一个新的产品 [@hydradatabase](https://twitter.com/hydradatabase))。对于 OLTP,大多数面向用户的查询应当低于 100 毫秒 — 理想情况下低于 10 毫秒 — 这样用户向后端发出的完整请求不会超过 100-200 毫秒 (每个请求可能会发出多个 SQL 查询,视具体情况而定)。当然,非面向用户的查询,例如来自后台任务、`pg_dump` 等,可以持续更长时间 — 前提是满足以下原则。 16 | 2. 在 OLTP 的情况下,第二个问题应当是:该查询是"只读"的吗,还是更改了数据 (无论是 DDL 还是简单的写入 DML — INSERT/UPDATE/DELETE)?在这种情况下,OLTP 中不应允许它运行超过一两秒,除非我们 100% 确定该查询不会长时间阻塞其他查询。对于大量写入操作,考虑将它们分批处理,以确保每个批次的持续时间不超过 1-2 秒。对于 DDL,务必小心锁的获取和锁链(阅读这些文章 [Common DB schema change mistakes](https://postgres.ai/blog/20220525-common-db-schema-change-mistakes#case-5-acquire-an-exclusive-lock--wait-in-transaction) 和 [Useful queries to analyze PostgreSQL lock trees (a.k.a. lock queues)](https://postgres.ai/blog/20211018-postgresql-lock-trees)). 17 | 3. 如果你处理的是只读查询,确保它不会运行太久 — 长时间运行的事务会导致 Postgres 长时间保留旧的死元组 ("xmin视界"未能前进),从而导致 autovacuum 无法删除在事务开始后成为死元组的条目。避免事务持续时间超过一两小时 (如果你确实需要这么长的事务,建议在活动较少的时间段运行,当 XID 推进较慢时进行,并且尽量不要频繁地运行它们)。 18 | 4. 最后,即使查询相对较快 — 例如 10 毫秒——如果其频率较高,仍可能被视为过慢。例如,10 毫秒的查询每秒运行 1,000 次 (可以通过 `pg_stat_statements.calls` 查看),则 Postgres 每秒需要花费 10 秒来处理这组查询。 在这种情况下,如果降低频率较难,则应将该查询视为慢查询,并尝试进行优化,以减少资源消耗 (目标是减少 `pg_stat_statements.total_exec_time `— 详见之前的帖子)。 19 | 20 | ## 总结 21 | 22 | 1. 所有超过 100-200 毫秒的查询,如果面向用户,应被视为慢查询。好的查询应该低于 10 毫秒。 23 | 2. 后台处理查询可以持续更长时间。如果它们修改了数据并可能阻塞面向用户的查询,则不应超过 1-2 秒。 24 | 3. DDL 应谨慎操作 — 确保它们不会导致大量写入 (如果会,则应分批处理),并使用较低的 `lock_timeout` 和重试机制,以避免形成阻塞链。 25 | 4. 不要允许长时间运行的事务。确保 xmin 视界在推进,autovacuum 可以及时删除死元组 — 避免事务持续时间过长 (>1-2 小时)。 26 | 5. 即使是快的查询 (<100 毫秒),如果 pg_stat_statements.calls 和 pg_stat_statements.total_exec_time 较高,也需要进行优化。 27 | 28 | ## 我见 29 | 30 | - 10 ms or less – good performance 31 | - 10-100 ms – optimization is recommended 32 | - more than 100 ms – poor performance, optimization is needed 33 | 34 | ![img](https://postgres.ai/assets/blog/20210909-slow-sql.png) 35 | 36 | 另外,在 pb 中,可以跟直观地看到 TPS/QPS,详见:https://postgres.ai/blog/20210909-what-is-a-slow-sql-query 37 | 38 | ~~~bash 39 | stats: 318 xacts/s, 443 queries/s, in 59883 B/s, out 122544 B/s, xact 1076 us, query 454 us, wait 548 us 40 | ~~~ 41 | 42 | -------------------------------------------------------------------------------- /How to determine the replication lag.md: -------------------------------------------------------------------------------- 1 | # How to determine the replication lag 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## 在主节点/备节点领导节点上 6 | 7 | 当连接到主节点 (或级联复制情况下的备节点的领导节点) 时,你可以使用 `pg_stat_replication`: 8 | 9 | ```sql 10 | nik=# \d pg_stat_replication 11 | View "pg_catalog.pg_stat_replication" 12 | Column | Type | Collation | Nullable | Default 13 | ------------------+--------------------------+-----------+----------+--------- 14 | pid | integer | | | 15 | usesysid | oid | | | 16 | usename | name | | | 17 | application_name | text | | | 18 | client_addr | inet | | | 19 | client_hostname | text | | | 20 | client_port | integer | | | 21 | backend_start | timestamp with time zone | | | 22 | backend_xmin | xid | | | 23 | state | text | | | 24 | sent_lsn | pg_lsn | | | 25 | write_lsn | pg_lsn | | | 26 | flush_lsn | pg_lsn | | | 27 | replay_lsn | pg_lsn | | | 28 | write_lag | interval | | | 29 | flush_lag | interval | | | 30 | replay_lag | interval | | | 31 | sync_priority | integer | | | 32 | sync_state | text | | | 33 | reply_time | timestamp with time zone | | | 34 | ``` 35 | 36 | 此视图包含物理复制 (仅限于使用流复制的情况,非 WAL 传输方式) 和逻辑复制的信息。延迟情况在此视图中可以通过字节 (lsn 相关列) 或时间间隔 (lag 相关列) 进行衡量,并且可以为每个复制流观察到多个步骤。 37 | 38 | 要分析此视图中的 LSN 值,我们需要将它们与所连接服务器上的当前 `LSN` 值进行比较: 39 | 40 | - 如果是主服务器 (`pg_is_in_recovery()` 返回 false ),则使用 `pg_current_wal_lsn()` 41 | - 否则 (备节点领导节点),使用 `pg_last_wal_replay_lsn()` 42 | 43 | 可以使用 `pg_wal_lsn_diff()` 函数或 `-` 操作符进行 LSN 的差值计算。 44 | 45 | 文档:[Monitoring pg_stat_replication view](https://postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-REPLICATION-VIEW). 46 | 47 | 以下是一些查询延迟的示例: 48 | 49 | - [Netdata](https://github.com/netdata/netdata/blob/9edcad92d83dba359f5eb6c06d0741b50030edcf/collectors/python.d.plugin/postgres/postgres.chart.py#L486) 50 | - [Postgres exporter for Prometheus](https://github.com/prometheus-community/postgres_exporter/blob/2a5692c0283fddf96e776cc73c2fc0d5caed1af6/cmd/postgres_exporter/queries.go#L46) 51 | 52 | ## 在物理备节点上 53 | 54 | 当连接到物理备节点时,获取其延迟时间间隔: 55 | 56 | ~~~sql 57 | select now() - pg_last_xact_replay_timestamp(); 58 | ~~~ 59 | 60 | 在某些情况下,`pg_last_xact_replay_timestamp()` 可能返回 `NULL`: 61 | 62 | - 如果备节点刚刚启动并且尚未重放任何事务; 63 | - 如果主节点上没有最近的事务。 64 | 65 | `pg_last_xact_replay_timestamp()` 的这种行为可能会导致错误的结论,认为备节点存在延迟,且复制不健康 — 这在低负载的环境 (例如非生产环境) 中并不罕见。 66 | 67 | 文档:[pg_last_xact_replay_timestamp](https://postgresql.org/docs/current/functions-admin.html#id-1.5.8.33.6.3.2.2.4.1.1.1). 68 | 69 | ------ 70 | 71 | ## 逻辑复制 72 | 73 | 在主节点 (在逻辑复制上下文中通常称为"发布端") 上可以观察到复制延迟。除了前面讨论的 `pg_stat_replication`,还可以使用 `pg_replication_slots`: 74 | 75 | ```sql 76 | select 77 | slot_name, 78 | pg_current_wal_lsn() - confirmed_flush_lsn as lag_bytes 79 | from pg_replication_slots; 80 | ``` 81 | 82 | ------ 83 | 84 | ## 混合情况:逻辑和物理复制 85 | 86 | 在某些情况下,你可能需要同时处理逻辑复制和物理复制。例如,考虑以下情况: 87 | 88 | - 集群 A:一个由 3 个节点 (主节点 + 2 个物理备节点) 组成的常规集群。 89 | - 集群 B:也是主节点 + 2 个物理备节点,且主节点通过逻辑复制连接到集群 A 的主节点。 90 | 91 | 这是一种典型的情况,通常在涉及逻辑复制的复杂更改 (例如大版本升级) 时会发生。在某些时候,你可能希望将部分只读流量从集群 A 的备节点重定向到集群 B 的备节点。 92 | 93 | 在这种情况下,如果我们需要了解每个节点的延迟情况,并且希望代码能很好地适用于所有节点,这可能会有些棘手。 94 | 95 | 以下是解决方法 (感谢来自 GitLab 的 [Dylan Griffith](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121621)),假设我们可以从我们正在分析的节点和集群 A 的主节点获取信息 (记住上面的所有评论): 96 | 97 | 首先,获取主节点的 LSN: 98 | 99 | ```sql 100 | select pg_current_wal_lsn() as primary_lsn; 101 | ``` 102 | 103 | 然后获取观察节点的 LSN 位置,并使用它来计算以字节为单位的延迟值: 104 | 105 | ```sql 106 | with current_node as ( 107 | select case 108 | when exists (select from pg_replication_origin_status) then ( 109 | select remote_lsn 110 | from pg_replication_origin_status 111 | ) 112 | when pg_is_in_recovery() then pg_last_wal_replay_lsn() 113 | else pg_current_wal_lsn() 114 | end as lsn 115 | ) 116 | select lsn – {{primary_lsn}} as lag_bytes 117 | from current_node; 118 | ``` -------------------------------------------------------------------------------- /How to draw frost patterns using SQL ❄.md: -------------------------------------------------------------------------------- 1 | # How to draw frost patterns using SQL ❄ 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 此创意和实现最初由Kirill Borovikov (kilor) 完成 — 我只是重新格式化并稍微调整了一下,扩展了字符集。 6 | 7 | 下面是查询代码: 8 | 9 | ~~~sql 10 | with recursive t as ( 11 | select 12 | 0 as x, 13 | 0 as y, 14 | '{"{0,0}"}'::text[] as c, 15 | 0 as i 16 | 17 | union all 18 | 19 | ( 20 | with z as ( 21 | select 22 | dn.x, 23 | dn.y, 24 | t.c, 25 | t.i 26 | from t, 27 | lateral ( 28 | select 29 | ((random() * 2 - 1) * 100)::integer as x, 30 | ((random() * 2 - 1) * 100)::integer as y 31 | ) as p, 32 | lateral ( 33 | select * 34 | from ( 35 | select 36 | (unnest::text[])[1]::integer as x, 37 | (unnest::text[])[2]::integer as y 38 | from unnest(t.c::text[]) 39 | ) as t 40 | order by sqrt((x - p.x) ^ 2 + (y - p.y) ^ 2) 41 | limit 1 42 | ) as n, 43 | lateral ( 44 | select 45 | n.x + dx as x, 46 | n.y + dy as y 47 | from 48 | generate_series(-1, 1) as dx, 49 | generate_series(-1, 1) as dy 50 | where (dx, dy) <> (0, 0) 51 | order by 52 | case 53 | when (p.x, p.y) = (n.x, n.y) then 0 54 | else abs( 55 | acos( 56 | ((p.x - n.x) * dx + (p.y - n.y) * dy) 57 | / sqrt((p.x - n.x) ^ 2 + (p.y - n.y) ^ 2) 58 | / sqrt(dx ^ 2 + dy ^ 2) 59 | ) 60 | ) 61 | end 62 | limit 1 63 | ) as dn 64 | ) 65 | select 66 | z.x, 67 | z.y, 68 | z.c || array[z.x, z.y]::text, 69 | z.i + 1 70 | from z 71 | where z.i < (1 << 10) 72 | ) 73 | ), 74 | map as ( 75 | select 76 | gx as x, 77 | gy as y, 78 | ( 79 | select sqrt((gx - T.x) ^ 2 + (gy - T.y) ^ 2) v 80 | from t 81 | order by v 82 | limit 1 83 | ) as v 84 | from 85 | generate_series(-40, 40) as gx, 86 | generate_series(-30, 30) as gy 87 | ), 88 | gr as ( 89 | select regexp_split_to_array('@%#*+=-:. ', '') as s 90 | ) 91 | select 92 | string_agg( 93 | coalesce(s[(v * (array_length(s, 1) - 1))::integer + 1], ' '), 94 | ' ' 95 | order by x 96 | ) as frozen 97 | from 98 | ( 99 | select 100 | x, 101 | y, 102 | v::double precision / max(v) over() as v 103 | from map 104 | ) as t, 105 | gr 106 | group by y 107 | order by y; 108 | ~~~ 109 | 110 | 每次它都会绘制一个新的霜花图案,此处是几个例子: 111 | 112 | ![frozen pattern 1](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0082_01.png) 113 | 114 | ![frozen pattern 2](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0082_02.png) 115 | 116 | ![frozen pattern 3](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0082_03.png) 117 | 118 | 节日快乐🎅 -------------------------------------------------------------------------------- /How to drop a column.md: -------------------------------------------------------------------------------- 1 | # How to drop a column 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 删除一列很简单: 6 | 7 | ```sql 8 | alter table t1 drop column c1; 9 | ``` 10 | 11 | 然而,需要注意在某些情况下可能出现的一些复杂情况。 12 | 13 | ## 风险1:应用代码未准备好 14 | 15 | 应用代码需要停止使用该列。这意味着代码需要先部署。 16 | 17 | ## 风险2:部分停机 18 | 19 | 在高负载下,如果没有设置较低的 `lock_timeout` 和重试机制,执行此类 `ALTER` 操作是一个糟糕的主意,因为该语句需要获取表的 `AccessExclusiveLock` 。如果尝试获取锁的时间较长 (例如,由于现有事务持有此表上的任何一个锁 — 可能是读取表中一行数据的事务,或者是为了防止事务 ID 回卷而处理该表的`autovacuum`),这将对当前所有查询造成阻塞,导致项目在高负载下出现部分停机。 20 | 21 | 解决方案:使用较低的 `lock_timeout` 和重试机制。以下是一个示例 (有关此问题的更多详细信息和更进阶的示例,请参照 [zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)): 22 | 23 | ```sql 24 | do $do$ 25 | declare 26 | lock_timeout constant text := '50ms'; 27 | max_attempts constant int := 1000; 28 | ddl_completed boolean := false; 29 | begin 30 | 31 | perform set_config('lock_timeout', lock_timeout, false); 32 | 33 | for i in 1..max_attempts loop 34 | begin 35 | execute 'alter table t1 drop column c1'; 36 | ddl_completed := true; 37 | exit; 38 | exception 39 | when lock_not_available then 40 | null; 41 | end; 42 | end loop; 43 | 44 | if ddl_completed then 45 | raise info 'DDL successfully executed'; 46 | else 47 | raise exception 'DDL execution failed'; 48 | end if; 49 | end $do$; 50 | ``` 51 | 52 | 请注意,在此示例中,子事务是隐式使用的 (`BEGIN/EXCEPTION WHEN/END`块)。在高 `XID` 增长率的情况下 (例如,有大量写入事务) 和长时间运行的事务中,这可能成为一个问题 — 可能会在从库上触发`SubtransSLRU` 的争用 (参照:[PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful))。在这种情况下,应在事务级别实现重试逻辑。 53 | 54 | ## 风险3:错误地预期数据会被删除 55 | 56 | 最后,在不同环境之间复制数据并删除敏感数据时,请记住 `ALTER TABLE ... DROP COLUMN ...` 并不安全,它不会真正删除数据。删除列 `c1` 后,元数据中仍然存在相关信息: 57 | 58 | ```sql 59 | nik=# select attname from pg_attribute where attrelid = 't1'::regclass::oid order by attnum; 60 | attname 61 | ------------------------------ 62 | tableoid 63 | cmax 64 | xmax 65 | cmin 66 | xmin 67 | ctid 68 | id 69 | ........pg.dropped.2........ 70 | (8 rows) 71 | ``` 72 | 73 | 超级用户可以轻松恢复该列: 74 | 75 | ```sql 76 | nik=# update pg_attribute 77 | set attname = 'c1', atttypid = 20, attisdropped = false 78 | where attname = '........pg.dropped.2........'; 79 | UPDATE 1 80 | nik=# \d t1 81 | Table "public.t1" 82 | Column | Type | Collation | Nullable | Default 83 | --------+---------+-----------+----------+--------- 84 | id | bigint | | | 85 | c1 | bigint | | | 86 | ``` 87 | 88 | 一些解决方法: 89 | 90 | - 在删除列后使用 `VACUUM FULL` 重建表。在这种情况下,虽然尝试恢复会成功,但数据将不会存在。 91 | - 考虑使用受限用户和列级别的权限,而不是删除列。列和数据仍然存在,但用户无法读取。当然,如果严格要求删除数据,此方法不适用。 92 | - 在删除列之后转储/恢复。 -------------------------------------------------------------------------------- /How to enable data checksums without downtime.md: -------------------------------------------------------------------------------- 1 | # How to enable data checksums without downtime 2 | 3 | ## 数据校验和,基础知识 4 | 5 | PostgreSQL 提供了[启用数据校验和](https://www.postgresql.org/docs/current/checksums.html)的功能,这是防止某些类型的数据损坏的有效手段 (但并不能防止所有损坏)。 6 | 7 | 需要注意的是,WAL 也有其自己的校验和,并且[始终开启](https://gitlab.com/postgres/postgres/blob/40d5e5981cc0fa81710dc2399b063a522c36fd68/src/backend/access/transam/xloginsert.c#L896)以验证 WAL 数据的完整性;在本文中,我们讨论的是针对表和索引页面的数据校验和。 8 | 9 | 数据校验和在默认情况下是禁用的,可以在集群初始化时通过执行 `initdb` 命令并带上 `--data-checksums` 选项来启用。 10 | 11 | ## 是否应该启用数据校验和? 12 | 13 | 根据[官方文档](https://www.postgresql.org/docs/current/app-initdb.html#APP-INITDB-DATA-CHECKSUMS:): 14 | 15 | > 启用校验和可能会导致显著的性能损耗。 16 | 17 | 然而,我强烈建议为所有集群启用数据校验和。如果担心性能损耗,建议进行测试。例如,一个[合成基准测试](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/44)显示,启用校验和只会使 CPU 负载增加约 2%。即使这个开销稍高一些,考虑到及时检测存储级别数据损坏的重要性,我认为仍然值得启用。 18 | 19 | ## 如何检查现有集群是否启用了数据校验和 20 | 21 | 有三种方法可以检查现有集群是否启用了数据校验和: 22 | 23 | 1. 使用 SQL (Postgres 需要在线): 24 | 25 | ```sql 26 | SHOW data_checksums; 27 | data_checksums 28 | ---------------- 29 | off 30 | ``` 31 | 32 | 2. 使用 `pg_controldata` (Postgres 是否在线无关紧要): 33 | 34 | ```bash 35 | pg_controldata -D /path/to/data_directory | grep checksum 36 | Data page checksum version: 0 37 | ``` 38 | 39 | `0` 表示未开启数据校验和。 40 | 41 | 3. 使用 `pg_checksums` (自 PostgreSQL 12 起提供;Postgres 必须关闭;注意,如果校验和已启用,该工具会扫描文件并检查校验和,因此你可能需要使用选项 `--progress` 来运行以查看进度): 42 | 43 | ```bash 44 | ❯ pg_checksums -D /opt/homebrew/var/postgresql@15 --check 45 | pg_checksums: error: data checksums are not enabled in cluster 46 | ``` 47 | 48 | ## 如何为现有集群启用数据校验和 49 | 50 | 不幸的是,无法为运行中的服务器启用数据校验和。启用数据校验和有两种通用方法: 51 | 52 | 1. 集群重新初始化 (转储/恢复,或逻辑复制) 53 | 1. `pg_checksums` (服务器必须关闭) 54 | 55 | 自 PostgreSQL 12 起,Postgres 自带了 [pg_checksums](https://postgresql.org/docs/current/app-pgchecksums.html) 工具。对于旧版本 (9.3-11),可以从[此处](https://github.com/credativ/pg_checksums)获取。 56 | 57 | 在执行 `pg_checksums` 时,如前所述,Postgres 必须关闭: 58 | 59 | ```bash 60 | ❯ brew services stop postgresql@15 61 | Stopping `postgresql@15`... (might take a while) 62 | ==> Successfully stopped `postgresql@15` (label: homebrew.mxcl.postgresql@15) 63 | 64 | ❯ time pg_checksums -D /opt/homebrew/var/postgresql@15 --enable --progress 65 | 31035/31035 MB (100%) computed 66 | Checksum operation completed 67 | Files scanned: 3060 68 | Blocks scanned: 3972581 69 | Files written: 1564 70 | Blocks written: 3711369 71 | pg_checksums: syncing data directory 72 | pg_checksums: updating control file 73 | Checksums enabled in cluster 74 | pg_checksums -D /opt/homebrew/var/postgresql@15 --enable --progress 5.19s user 14.23s system 56% cpu 34.293 total 75 | ``` 76 | 77 | 在这台 MacBook 上,启用数据校验和的速度约为 1 GiB/秒。请注意,`pg_checksums` 应在拥有数据目录的同一操作系统用户下执行。 78 | 79 | 检查结果: 80 | 81 | ```bash 82 | ❯ pg_controldata -D /opt/homebrew/var/postgresql@15 | grep checksum 83 | Data page checksum version: 1 84 | 85 | ❯ pg_checksums -D /opt/homebrew/var/postgresql@15 --check --progress 86 | 31035/31035 MB (100%) computed 87 | Checksum operation completed 88 | Files scanned: 3060 89 | Blocks scanned: 3972581 90 | Bad checksums: 0 91 | Data checksum version: 1 92 | ``` 93 | 94 | 一旦完成,我们便可以重新启动 PostgreSQL 并再次检查: 95 | 96 | ```bash 97 | ❯ brew services start postgresql@15 98 | ==> Successfully started `postgresql@15` (label: homebrew.mxcl.postgresql@15) 99 | 100 | ❯ psql -Xc 'show data_checksums' 101 | data_checksums 102 | ---------------- 103 | on 104 | (1 row) 105 | ``` 106 | 107 | 重要的是,要确保在 `pg_checksums --enable` 运行时 Postgres 不会启动。不幸的是,`pg_checksums` 在运行时不会检查 (它只在一开始时检查)。有一个很好的技巧可以避免意外启动 ([来源](https://www.crunchydata.com/blog/fun-with-pg_checksums)) — 暂时移动 Postgres 工作所需的一些核心文件或目录: 108 | 109 | ~~~bash 110 | mv $PGDATA/pg_twophase $PGDATA/pg_twophase.DO_NOT_START_THIS_DATABASE 111 | ~~~ 112 | 113 | ... 一旦 `pg_checksums` 工作完成,恢复: 114 | 115 | ~~~bash 116 | mv $PGDATA/pg_twophase.DO_NOT_START_THIS_DATABASE $PGDATA/pg_twophase 117 | ~~~ 118 | 119 | ## 如何在多节点集群中无停机启用数据校验和 120 | 121 | 幸运的是,如果集群中有副本,我们可以通过 switchover 以在不中断服务的情况下启用数据校验和。步骤如下: 122 | 123 | 1. 停止一个备库 (确保它在第 3 步之前不会启动!)。 124 | 2. 在备库上运行 `pg_checksums --enable`。 125 | 3. 重新启动备库并让其完全追上主库。 126 | 4. 对所有其他备库重复步骤 1-3。 127 | 5. 进行切换 (为了最小化停机时间,建议在切换之前执行一个显式 `CHECKPOINT`;如果使用了`pgBouncer`,建议使用它的 `PAUSE/RESUME` 功能实现**零停机**)。 128 | 6. 对前主库 (现在是备库) 执行步骤 1-3。 129 | 130 | 通过这种方法,可以成功地转换非常大的集群。执行此操作前,建议采取以下措施: 131 | 132 | - 先在克隆/低级别环境中测试 `pg_checksums --enable`,并预估生产环境的两个值:执行 `pg_checksums`的持续时间 (秒) 和期间的累积延迟情况 (字节)。 133 | - 选择低活动时间执行 (例如,夜间或周末,视工作负载类型而定),以使累积的延迟更小。 134 | 135 | 虽然截至 2023年 (PostgreSQL 16) 尚不支持并行处理,但未来版本很可能会实现此功能。 136 | 137 | 以 1GiB/秒的转换速度为例,对于 1TiB 的集群,需要约 17 分钟。在拥有更强磁盘性能的机器上,速度应更快。 -------------------------------------------------------------------------------- /How to estimate the YoY growth of a very large table using row creation timestamps and the planner statistics.md: -------------------------------------------------------------------------------- 1 | # How to estimate the YoY growth of a very large table using row creation timestamps and the planner statistics 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 假设我们有一个创建于多年前的 10 TiB 的大表,无论分区与否,结构如下: 6 | 7 | ```sql 8 | create table t ( 9 | id int8 primary key, -- of course, not int4 10 | created_at timestamptz default now(), 11 | ... 12 | ); 13 | ``` 14 | 15 | 我们需要快速了解这个表的年度增长情况,假设没有删除行 (或删除的数量可以忽略不计)。因此,我们只需统计每年的行数。 16 | 17 | 一个简便方法是: 18 | 19 | ```sql 20 | select 21 | date_trunc('year', created_at) as year, 22 | count(*) 23 | from t 24 | group by year 25 | order by year; 26 | ``` 27 | 28 | 然而,对于 10 TiB 的表,这样的分析可能需要等待数小时甚至数天才能完成。 29 | 30 | 以下是一种快速但不精确的方法来获取每年的行数 (假设表具有最新的统计数据;如果不确定,请先在表上运行 `ANALYZE`): 31 | 32 | ~~~sql 33 | do $$ 34 | declare 35 | table_fqn text := 'public.t'; 36 | year_start int := 2000; 37 | year_end int := extract(year from now())::int; 38 | year int; 39 | explain_json json; 40 | begin 41 | for year in year_start..year_end loop 42 | execute format( 43 | $e$ 44 | explain (format json) select * 45 | from %s 46 | where created_at 47 | between '%s-01-01' and '%s-12-31' 48 | $e$, 49 | table_fqn, 50 | year, 51 | year 52 | ) into explain_json; 53 | 54 | raise info 'Year: %, Estimated rows: %', 55 | year, 56 | explain_json->0->'Plan'->>'Plan Rows'; 57 | end loop; 58 | end $$; 59 | ~~~ -------------------------------------------------------------------------------- /How to find int4 PKs with out-of-range risks in a large database.md: -------------------------------------------------------------------------------- 1 | # How to find int4 PKs with out-of-range risks in a large database 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如今,现代 ORM 比如 Rails 或 Django 使用 int8 (bigint) 类型的主键 (PK)。然而,在旧项目中,可能存在使用 int4 (integer, int, serial) 主键的老表,这些表可能已增长并存在 int4 溢出的风险 — int4 的最大值为 [2,147,483,647](https://postgresql.org/docs/current/datatype-numeric.html),且不停机将主键从 int4 转换为 int8 并不是一项简单的任务 (TODO:在另一篇使用指南中介绍转换过程)。 6 | 7 | 以下是快速检查是否存在 int2 或 int4 PK 表的方法,并了解在每种情况下已使用了多少"容量" (来自postgres-checkup 的查询): 8 | 9 | ```sql 10 | do $$ 11 | declare 12 | min_relpages int8 := 0; -- in very large DBs, skip small tables by setting this to 100 13 | rec record; 14 | out text := ''; 15 | out1 json; 16 | i numeric := 0; 17 | val int8; 18 | ratio numeric; 19 | sql text; 20 | begin 21 | for rec in 22 | select 23 | c.oid, 24 | spcname as tblspace, 25 | nspname as schema_name, 26 | relname as table_name, 27 | t.typname, 28 | pg_get_serial_sequence(format('%I.%I', nspname, relname), attname) as seq, 29 | min(attname) as attname 30 | from pg_index i 31 | join pg_class c on c.oid = i.indrelid 32 | left join pg_tablespace tsp on tsp.oid = reltablespace 33 | left join pg_namespace n on n.oid = c.relnamespace 34 | join pg_attribute a on 35 | a.attrelid = i.indrelid 36 | and a.attnum = any(i.indkey) 37 | join pg_type t on t.oid = atttypid 38 | where 39 | i.indisprimary 40 | and ( 41 | c.relpages >= min_relpages 42 | or pg_get_serial_sequence(format('%I.%I', nspname, relname), attname) is not null 43 | ) and t.typname in ('int2', 'int4') 44 | and nspname <> 'pg_toast' 45 | group by 1, 2, 3, 4, 5, 6 46 | having count(*) = 1 -- skip PKs with 2+ columns (TODO: analyze them too) 47 | loop 48 | raise debug 'table: %', rec.table_name; 49 | 50 | if rec.seq is null then 51 | sql := format('select max(%I) from %I.%I;', rec.attname, rec.schema_name, rec.table_name); 52 | else 53 | sql := format('select last_value from %s;', rec.seq); 54 | end if; 55 | 56 | raise debug 'sql: %', sql; 57 | execute sql into val; 58 | 59 | if rec.typname = 'int4' then 60 | ratio := (val::numeric / 2^31)::numeric; 61 | elsif rec.typname = 'int2' then 62 | ratio := (val::numeric / 2^15)::numeric; 63 | else 64 | assert false, 'unreachable point'; 65 | end if; 66 | 67 | if ratio > 0.1 then -- report only if > 10% of capacity is reached 68 | i := i + 1; 69 | 70 | out1 := json_build_object( 71 | 'table', ( 72 | coalesce(nullif(quote_ident(rec.schema_name), 'public') || '.', '') 73 | || quote_ident(rec.table_name) 74 | ), 75 | 'pk', rec.attname, 76 | 'type', rec.typname, 77 | 'current_max_value', val, 78 | 'capacity_used_percent', round(100 * ratio, 2) 79 | ); 80 | 81 | raise debug 'cur: %', out1; 82 | 83 | if out <> '' then 84 | out := out || e',\n'; 85 | end if; 86 | 87 | out := out || format(' "%s": %s', rec.table_name, out1); 88 | end if; 89 | end loop; 90 | 91 | raise info e'{\n%\n}', out; 92 | end; 93 | $$ language plpgsql; 94 | ``` 95 | 96 | 输出示例 97 | 98 | ```sql 99 | INFO: { 100 | "oldbig": {"table" : "oldbig", "pk" : "id", "type" : "int4", "current_max_value" : 2107480000, "capacity_used_percent" : 98.14}, 101 | "oldbig2": {"table" : "oldbig2", "pk" : "id", "type" : "int4", "current_max_value" : 1107480000, "capacity_used_percent" : 51.57} 102 | } 103 | ``` -------------------------------------------------------------------------------- /How to find the best order of columns to save on storage (Column Tetris).md: -------------------------------------------------------------------------------- 1 | # How to find the best order of columns to save on storage ("Column Tetris") 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 从存储角度来看,Postgres 中的列顺序重要吗? 6 | 7 | 答案是肯定的。我们来看一个例子 (注意:不建议使用 int4 作为主键,这里只是出于教学目的)。 8 | 9 | ```sql 10 | create table t( 11 | id int4 primary key, 12 | created_at timestamptz, 13 | is_public boolean, 14 | modified_at timestamptz, 15 | verified boolean, 16 | published_at timestamptz, 17 | score int2 18 | ); 19 | 20 | insert into t 21 | select 22 | i, 23 | clock_timestamp(), 24 | true, 25 | clock_timestamp(), 26 | true, 27 | clock_timestamp(), 28 | 0 29 | from generate_series(1, 1000000) as i; 30 | 31 | vacuum analyze t; 32 | ``` 33 | 34 | 查看表大小: 35 | 36 | ```sql 37 | nik=# \dt+ t 38 | List of relations 39 | Schema | Name | Type | Owner | Size | Description 40 | --------+------+-------+----------+-------+------------- 41 | public | t | table | postgres | 81 MB | 42 | (1 row) 43 | ``` 44 | 45 | 现在,让我们使用来自 [postgres_dba](https://github.com/NikolayS/postgres_dba) 的报告 p1 (假设已安装): 46 | 47 | ```sql 48 | :dba 49 | 50 | Type your choice and press : 51 | p1 52 | Table | Table Size | Comment | Wasted * | Suggested Columns Reorder 53 | -------+------------+---------+-----------------+----------------------------- 54 | t | 81 MB | | ~23 MB (28.40%) | is_public, score, verified + 55 | | | | | id, created_at, modified_at+ 56 | | | | | published_at 57 | (1 row) 58 | ``` 59 | 60 | 报告显示,仅通过更改列顺序,我们便可以节省大约 28% 的磁盘空间。注意这是一个估算值。 61 | 62 | 让我们检查一下优化后的顺序: 63 | 64 | ```sql 65 | drop table t; 66 | 67 | create table t( 68 | is_public boolean, 69 | verified boolean, 70 | score int2, 71 | id int4 primary key, 72 | created_at timestamptz, 73 | modified_at timestamptz, 74 | published_at timestamptz 75 | ); 76 | 77 | insert into t 78 | select 79 | true, 80 | true, 81 | 0::int2, 82 | i::int4, 83 | clock_timestamp(), 84 | clock_timestamp(), 85 | clock_timestamp() 86 | from generate_series(1, 1000000) as i; 87 | 88 | vacuum analyze t; 89 | ``` 90 | 91 | 再次查看表大小: 92 | 93 | ```sql 94 | nik=# \dt+ t 95 | List of relations 96 | Schema | Name | Type | Owner | Size | Description 97 | --------+------+-------+----------+-------+------------- 98 | public | t | table | postgres | 57 MB | 99 | (1 row) 100 | ``` 101 | 102 | 节省了约 `30%` 的磁盘空间,非常接近预期值 (57 / 81 ~= 0.7037)。 103 | 104 | postgres_dba 的报告 p1 现在也不再显示潜在的存储节省: 105 | 106 | ```sql 107 | Type your choice and press : 108 | p1 109 | Table | Table Size | Comment | Wasted * | Suggested Columns Reorder 110 | -------+------------+---------+----------+--------------------------- 111 | t | 57 MB | | | 112 | (1 row) 113 | ``` 114 | 115 | 为什么可以节省空间? 116 | 117 | 在 Postgres 中,存储系统可能会为列值添加对齐填充。若数据类型的自然对齐要求超过值的大小,Postgres 可能会在值后填充零以进行边界对齐。例如,当小于 8 字节的值后跟一个需要 8 字节对齐的值时,Postgres 会在第一个值后填充,以便在 8 字节边界对齐。对齐数据有助于确保内存中的值与特定硬件架构的 CPU 字边界对齐,从而提高性能。 118 | 119 | 例如,(int4, timestamptz) 的一行占用 16 字节: 120 | 121 | - 4 字节用于 `int4` 122 | - 4 字节的零值用于 8 字节对齐填充 123 | - 8 字节用于 `timestamptz` 124 | 125 | 有些人喜欢先将 16 字节和 8 字节的列放在前面,然后依次放置更小的列。此外,建议将具有 `VARLENA` 类型 (`text`、`varchar`、`json`、`jsonb`、array) 的列放在最后。 126 | 127 | 关于此主题的相关文章: 128 | 129 | - [StackOverflow answer by Erwin Brandstetter](https://stackoverflow.com/questions/2966524/calculating-and-saving-space-in-postgresql/7431468#7431468) 130 | - [Ordering Table Columns in PostgreSQL (GitLab)](https://docs.gitlab.com/ee/development/database/ordering_table_columns.html) 131 | - 官方文档:[Table Row Layout](https://postgresql.org/docs/current/storage-page-layout.html#STORAGE-TUPLE-LAYOUT) -------------------------------------------------------------------------------- /How to flush caches (OS page cache and Postgres buffer pool).md: -------------------------------------------------------------------------------- 1 | # How to flush caches (OS page cache and Postgres buffer pool) 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在实验时,考虑当前缓存的状态非常重要 — 包括 Postgres 缓冲池 (其大小由 `shared_buffers` 控制) 和操作系统的页缓存。如果决定每次实验都从冷状态的缓存开始,我们需要刷新它们。 6 | 7 | ## 刷新Postgres缓冲池 8 | 9 | 要刷新 Postgres 缓冲池,请重新启动 Postgres。 10 | 11 | 要分析当前缓冲池的状态,可以使用 [pg_buffercache](https://postgresql.org/docs/current/pgbuffercache.html)。 12 | 13 | ## 刷新操作系统页缓存 14 | 15 | 刷新 Linux 页缓存: 16 | 17 | ```bash 18 | sync 19 | echo 3 > /proc/sys/vm/drop_caches 20 | ``` 21 | 22 | 查看 Linux 中当前 RAM 的使用情况 (MiB): 23 | 24 | ```bash 25 | free -m 26 | ``` 27 | 28 | 在 macOS 上,刷新页缓存: 29 | 30 | ```bash 31 | sync 32 | sudo purge 33 | ``` 34 | 35 | 查看 macOS 上当前 RAM 的状态: 36 | 37 | ```bash 38 | vm_stat 39 | ``` 40 | 41 | ## 我见 42 | 43 | 在 17 版本中,`pg_buffercache` 新增 `pg_buffercache_evict`,顾名思义,排除指定缓冲区,这样进行实验就要方便多了。 44 | 45 | ~~~sql 46 | postgres=# \dx+ pg_buffercache 47 | Objects in extension "pg_buffercache" 48 | Object description 49 | ---------------------------------------- 50 | function pg_buffercache_evict(integer) 51 | function pg_buffercache_pages() 52 | function pg_buffercache_summary() 53 | function pg_buffercache_usage_counts() 54 | type pg_buffercache 55 | type pg_buffercache[] 56 | view pg_buffercache 57 | (7 rows) 58 | ~~~ 59 | 60 | 另外,对于 page cache,我们还可以借助 fincore/pcstat 这类工具进行观察。 -------------------------------------------------------------------------------- /How to format text output in psql scripts.md: -------------------------------------------------------------------------------- 1 | # How to format text output in psql scripts 2 | 3 | 对于 `psql` 的 `\echo` 命令,可以通过使用 ANSI 颜色代码来实现颜色和基本的文本格式化,比如加粗或下划线。这在构建复杂的 psql 脚本时非常有用 (比如 [postgres_dba](https://github.com/NikolayS/postgres_dba/))。 4 | 5 | 例如: 6 | 7 | ```bash 8 | \echo '\033[1;31mThis is red text\033[0m' 9 | \echo '\033[1;32mThis is green text\033[0m' 10 | \echo '\033[1;33mThis is yellow text\033[0m' 11 | \echo '\033[1;34mThis is blue text\033[0m' 12 | \echo '\033[1;35mThis is magenta text\033[0m' 13 | \echo '\033[1;36mThis is cyan text\033[0m' 14 | \echo '\033[1mThis text is bold\033[0m' 15 | \echo '\033[4mThis text is underlined\033[0m' 16 | \echo '\033[38;2;128;0;128mThis text is purple\033[0m' 17 | 18 | -- RGB – arbitrary color 19 | \echo '\033[38;2;255;100;0mThis text is in orange (RGB 255,100,0)\033[0m' 20 | ``` 21 | 22 | 结果: 23 | 24 | ![img](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0091_result.png) 25 | 26 | **重要提示:**`\033[0m` 序列会将文本格式重置为默认格式。 27 | 28 | 在 `psql` 的非交互模式中,格式会被保留,且与 `ts` 命令结合使用时也会保留 (前缀时间戳,`ts` 是 Ubuntu 中 moreutils 包的一部分): 29 | 30 | ```bash 31 | psql -Xc "\echo '\033[1;35m这是品红色文本\033[0m'" | ts 32 | ``` 33 | 34 | 当使用 `less` 命令时,格式不会生效,但可以通过 `-R` 选项来解决: 35 | 36 | ```bash 37 | psql -Xc "\echo '\033[1;35m这是品红色文本\033[0m'" | less -R 38 | ``` -------------------------------------------------------------------------------- /How to get into trouble using some Postgres features.md: -------------------------------------------------------------------------------- 1 | # How to get into trouble using some Postgres features 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 今天我们有一些相当有趣的素材,但了解 (并避免) 这些问题可以节省您的时间和精力。 6 | 7 | ## NULLs 8 | 9 | `NULL` 值虽然很常见,但在 SQL 中是导致麻烦的主要原因,Postgres 也不例外。 10 | 11 | 例如,有人可能会忘记连接操作符 (||)、算术运算符 (*、/、+、-)、传统比较运算符 (=、<、>、<=、>=、<>) 都不是 NULL-safe 的操作,结果可能会丢失。 12 | 13 | 尤其是当你建立一家初创公司并且一些重要的业务逻辑依赖于它时,查询不能正确处理 NULL,导致用户群或金钱或时间 (或所有) 的损失: 14 | 15 | ```sql 16 | nik=# \pset null ∅ 17 | Null display is "∅". 18 | 19 | nik=# select null + 1; 20 | ?column? 21 | ---------- 22 | 23 | ∅ 24 | 25 | (1 row) 26 | ``` 27 | 28 | `NULL` 值真的很危险,即使是经验丰富的工程师在处理它们时也常常会遇到问题。以下是一些可以帮助你的素材: 29 | 30 | - [NULLs: the good, the bad, the ugly, and the unknown](https://postgres.fm/episodes/nulls-the-good-the-bad-the-ugly-and-the-unknown) (podcast) 31 | - [What is the deal with NULLs?](http://thoughts.davisjeff.com/2009/08/02/what-is-the-deal-with-nulls/) 32 | 33 | 一些技巧 — 如何使代码成为 NULL-safe 的: 34 | 35 | - 考虑使用表达式如 `COALESCE(val, 0)` 将 `NULL` 值替换为某个值 (通常是 `0` 或 `''`)。 36 | - 在比较时,使用 `IS [NOT] DISTINCT FROM` 代替 `=` 或 `<>` (不过请查看 `EXPLAIN` 执行计划)。 37 | - 进行连接时使用 `format('%s %s', var1, var2)`。 38 | - 不要使用 `WHERE NOT IN (SELECT ...) `— 使用 `NOT EXISTS `代替 (参照 [JOOQ blog post](https://jooq.org/doc/latest/manual/reference/dont-do-this/dont-do-this-sql-not-in/))。 39 | - 要小心,`NULL` 值是狡猾的。 40 | 41 | ## 在高负载下使用子事务 42 | 43 | 如果你希望达到数十万或数百万 TPS 并遇到各种问题,请使用子事务。你可能会隐式使用到它们 — 例如,如果使用了 Django、Rails 或 PL/pgSQL 中的`BEGIN/EXCEPTION`块。 44 | 45 | 为什么要完全摆脱子事务的原因:[PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful) 46 | 47 | ## int4 主键 48 | 49 | 如果表有 10 亿行,要想将 `int4` 主键 (也称为 int、integer) 零停机时间转换为 `int8` 主键时,所需的工作量极其之大。由于对齐填充,表 (`id int4, created_at timestamptz`) 将占用与 (`id int8, created_at timestamptz`) 相同的磁盘空间。 50 | 51 | ## (特殊的) SELECT INTO并不像你想的那样 52 | 53 | 有一天我在调试 PL/pgSQL 函数时,复制粘贴了一条查询,像这样,运行在 psql 中: 54 | 55 | ```sql 56 | nik=# select * into var from t1 limit 1; 57 | SELECT 1 58 | ``` 59 | 60 | 它工作了!这真是一个巨大的惊喜 — 在 SQL 上下文中,[SELECT INTO](https://postgresql.org/docs/current/sql-selectinto.html) 是一个 DDL 命令,它会创建一个表并将数据插入其中 (难道这不应该被弃用吗?)。 61 | 62 | ## 认为"事务性DDL"很简单 63 | 64 | 是的,Postgres 具有"事务性 DDL",你可以从中获益良多 — 直到您无法这样做。在高负载下,你无法依赖它 — 而是需要开始使用零停机时间的方法,避免错误 (阅读:[常见的数据库模式更改错误](https://postgres.ai/blog/20220525-common-db-schema-change-mistakes),并依赖于"非事务性" DDL,比如 `CREATE INDEX CONCURRENTLY`,某些操作可能会失败,之后需要清理再重试)。 65 | 66 | 在高负载下进行 DDL 部署的大问题是,默认情况下,你可能会在尝试部署一个非常轻的模式更改时遇到停机 — 除非实现了带有低 `lock_timeout` 和重试的逻辑 (参照 [Zero-downtime Postgres schema migrations need this: lock timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries))。 67 | 68 | ## 一次删除大量行 69 | 70 | 这是一个陷入麻烦的好方法:发出 `DELETE` 数百万行的命令并等待。如果检查点没有调优 (`max_wal_size = 1GB`),如果元组通过 IndexScan 被删除 (意味着使页面变脏的过程相当"随机"),并且磁盘 IO 受到限制,这可能会让你的系统崩溃。即使它幸存下来,你也会碰到: 71 | 72 | - 锁定问题的风险 (`DELETE` 阻止其他用户发出的写入) 73 | - 生成了大量死元组,这些死元组稍后会被 `autovacuum` 转换为膨胀。 74 | 75 | 解决方法: 76 | 77 | - 拆分为批次, 78 | - 如果大量写入不可避免,请考虑临时提高 `max_wal_size`,这不需要重启 (不过:如果服务器在此过程中崩溃,可能会增加恢复时间)。 79 | 80 | 阅读 [common db schema change mistakes](https://postgres.ai/blog/20220525-common-db-schema-change-mistakes#case-4-unlimited-massive-change). 81 | 82 | ## 其他"不要做"文章 83 | 84 | - [Depesz: Don’t do these things in PostgreSQL](https://depesz.com/2020/01/28/dont-do-these-things-in-postgresql/) 85 | - [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) 86 | - [JOOQ: Don't do this](https://jooq.org/doc/latest/manual/reference/dont-do-this/) -------------------------------------------------------------------------------- /How to help others.md: -------------------------------------------------------------------------------- 1 | # How to help others 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 帮助他人 (无论是对团队成员的内部帮助还是外部咨询) 有两个关键原则: 6 | 7 | 1. 依赖可靠的资源。 8 | 2. 测试一切。 9 | 10 | ## 原则1:可靠的资源 11 | 12 | 依赖你所信任的高质量资源。例如,我的观点是,根据质量/可信度等级来排名,大致如下: 13 | 14 | - PostgreSQL文档 – 9/10 15 | - PostgreSQL源码 – 10/10 (真理的来源!) 16 | - StackOverflow上的随机答案 – 5/10或更低(但不包括 [Erwin Brandstetter](https://stackoverflow.com/users/939860/erwin-brandstetter), [Laurenz Albe](https://stackoverflow.com/users/6464308/laurenz-albe), [Peter Eisentraut](https://stackoverflow.com/users/98530/peter-eisentraut) 的回答 — 他们回答得很棒,8/10 或更高)。 17 | - 随机博客文章,同样 5/10 或更低。 18 | - [Suzuki](https://www.interdb.jp/pg/) 和 [Rogov](https://postgrespro.com/blog/pgsql/5969637) 的 Internals 两本书 – 8/10 或更高 19 | 20 | 另外 (非常重要!),始终提供你参考资源的链接;这样做有两个好处: 21 | 22 | - 推广好的资源,回馈它们; 23 | - 在某种程度上分担责任 (如果你经验不足,这非常有帮助;每个人都有可能犯错)。 24 | 25 | ## 原则2:验证 — 数据库实验 26 | 27 | 始终对所有事物保持怀疑,不要轻信语言模型,无论它们的性质如何。 28 | 29 | 如果有人 (包括你或我) 在没有通过实验 (测试) 验证的情况下说了某件事,就需要通过实验进行验证。 30 | 31 | 所有的决策都应该基于数据,而可靠的数据是通过测试收集的。 32 | 33 | 大多数与数据库相关的想法都需要通过数据库实验来验证。 34 | 35 | 两种类型的数据库实验: 36 | 37 | 1. 多会话实验,成熟的基准测试,例如使用 `pgbench`、`JMeter`、`sysbench`、`pgreplay-go` 等工具进行的基准测试。 38 | 39 | 它们的目的是研究 Postgres 的整体行为及其所有组件,必须在专用资源上进行 (在这里,其他人不应有任何工作)。环境应与生产环境相匹配 (虚拟机和磁盘类型、PostgreSQL 的版本、相关设置等)。 40 | 41 | 此类实验包括负载测试、压力测试、性能回归测试等。主要工具是用于研究宏观级别查询分析的工具:`pg_stat_statements`、等待事件分析 (即历史活跃会话或性能/查询洞察)、`auto_explain`、`pgBadger` 等等。 42 | 43 | 关于此类实验的更多信息:详见 [Day 13: How to benchmark](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0013_how_to_benchmark.md). 44 | 45 | 2. 单会话实验,使用单个会话 (有时两个) 测试一个或一系列 SQL 查询,以检查查询语法、研究单个查询行为、优化特定查询等。 46 | 47 | 这些实验可以在共享环境中进行,使用较弱的机器也可以。但是,要研究查询性能,你需要使用相同的 PostgreSQL 版本、相同或类似的数据库,以及匹配的规划器设置 (如何做到这一点:参照[Day 56: How to make the non-production Postgres planner behave like in production](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0056_how_to_imitate_production_planner.md))。需要注意的是,计时指标可能与生产环境相比显著偏差,在查询优化过程中主要应关注执行计划和数据量 (`EXPLAIN` 中的实际行数、`BUFFERS` 选项提供的缓冲区操作计数)。 48 | 49 | 此类实验的示例:检查 SQL 查询序列的语法和逻辑行为,查询性能分析 `EXPLAIN (ANALYZE, BUFFERS)`,测试优化想法,架构更改测试等。快速克隆大型数据库非常有用,而且不需要为存储付额外的钱 (例如 Neon、Amazon Aurora)。如果存储和计算都不需要额外费用,那么测试活动,包括 CI/CD 中的自动化测试,就真正得到了释放 (DBLab Engine [@Database_Lab](https://twitter.com/Database_Lab))。 50 | 51 | ## 总结 52 | 53 | 如你所见,原则非常简单: 54 | 55 | 1. 阅读优质的文章; 56 | 2. 不要盲目相信 — 测试一切。 -------------------------------------------------------------------------------- /How to install Postgres 16 with plpython3u Recipes for macOS, Ubuntu, Debian, CentOS, Docker.md: -------------------------------------------------------------------------------- 1 | # How to install Postgres 16 with plpython3u: Recipes for macOS, Ubuntu, Debian, CentOS, Docker 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | PL/Python 是 PostgreSQL 的一种过程语言扩展,允许你使用 Python (一种广泛使用、高级且功能多样的编程语言) 编写存储过程和触发器。 6 | 7 | `plpython3u` 是 PL/Python 的"不受信任"版本。这种变体允许 Python 函数执行诸如文件 I/O、网络通信等操作,可能会影响服务器的行为或安全。 8 | 9 | 我们在 [Day 23: How to use OpenAI APIs right from Postgres to implement semantic search and GPT chat](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0023_how_to_use_openai_apis_in_postgres.md) 使用了 `plpython3u` ,接下来我们将讨论如何安装它。 10 | 11 | 我有预感,未来我们会在各种任务中使用它。 12 | 13 | 本文适用于自行管理的 Postgres 环境。 14 | 15 | ## macOS (Homebrew) 16 | 17 | ```shell 18 | brew tap petere/postgresql 19 | brew install petere/postgresql/postgresql@16 20 | 21 | psql postgres \ 22 | -c 'create extension plpython3u' 23 | ``` 24 | 25 | ## Ubuntu 22.04 LTS or Debian 12 26 | 27 | ~~~bash 28 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ 29 | > /etc/apt/sources.list.d/pgdg.list' 30 | 31 | curl -fsSL https://postgresql.org/media/keys/ACCC4CF8.asc \ 32 | | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg 33 | 34 | sudo apt update 35 | sudo apt install -y \ 36 | postgresql-16 \ 37 | postgresql-contrib-16 \ 38 | postgresql-plpython3-16 39 | 40 | sudo -u postgres psql \ 41 | -c 'create extension plpython3u' 42 | ~~~ 43 | 44 | ## CentOS Stream 9 45 | 46 | ~~~bash 47 | dnf module reset -y postgresql 48 | dnf module enable -y postgresql:16 49 | 50 | dnf install -y \ 51 | postgresql-server \ 52 | postgresql \ 53 | postgresql-contrib \ 54 | postgresql-plpython3 55 | 56 | postgresql-setup --initdb 57 | 58 | systemctl enable --now postgresql 59 | 60 | sudo -u postgres psql \ 61 | -c 'create extension plpython3u' 62 | ~~~ 63 | 64 | ## Docker 65 | 66 | ~~~bash 67 | echo "FROM postgres:16 68 | RUN apt update 69 | RUN apt install -y postgresql-plpython3-16" \ 70 | > postgres_plpython3u.Dockerfile 71 | 72 | sudo docker build \ 73 | -t postgres-plpython3u:16 \ 74 | -f postgres_plpython3u.Dockerfile \ 75 | . 76 | 77 | sudo docker run \ 78 | --detach \ 79 | --name pg16 \ 80 | -e POSTGRES_PASSWORD=secret \ 81 | -v $(echo ~)/pgdata:/var/lib/postgresql/data \ 82 | postgres-plpython3u:16 83 | 84 | sudo docker exec -it pg16 \ 85 | psql -U postgres -c 'create extension plpython3u' 86 | ~~~ 87 | 88 | -------------------------------------------------------------------------------- /How to make e work in psql on a new machine (editornanovi not found).md: -------------------------------------------------------------------------------- 1 | # How to make "\e" work in psql on a new machine ("editor/nano/vi not found") 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 有时在 psql 中使用 `\e` 命令编辑查询时会遇到以下错误: 6 | 7 | ```sql 8 | nik=# \e 9 | /usr/bin/sensible-editor: 20: editor: not found 10 | /usr/bin/sensible-editor: 31: nano: not found 11 | /usr/bin/sensible-editor: 20: nano-tiny: not found 12 | /usr/bin/sensible-editor: 20: vi: not found 13 | Couldn't find an editor! 14 | Set the $EDITOR environment variable to your desired editor. 15 | ``` 16 | 17 | 设置编辑器非常简单 (使用 `nano` 或喜欢的其他编辑器): 18 | 19 | ```sql 20 | \setenv PSQL_EDITOR vim 21 | ``` 22 | 23 | 但是,如果你在容器内或一台新机器上工作,可能还未安装所需的编辑器。假设有权限运行安装命令,可以直接在 `psql` 中安装编辑器。例如,在基于 Debian 的标准 Postgres 环境中 (不需要 `sudo`): 24 | 25 | ```sql 26 | nik=# \! apt update && apt install -y vim 27 | ``` 28 | 29 | 👉 `\e` 命令就能正常工作了! 30 | 31 | 要持久化该设置,可以将其添加到 `~/.bash_profile` (或 `~/.zprofile`) 中: 32 | 33 | ```bash 34 | echo "export PSQL_EDITOR=vim" >> ~/.bash_profile 35 | source ~/.bash_profile 36 | ``` 37 | 38 | 对于 Window,请参见 [@PavloGolub](https://twitter.com/PavloGolub) 的[博客文章](https://cybertec-postgresql.com/en/psql_editor-fighting-with-sublime-text-under-windows/)。 39 | 40 | 文档: https://postgresql.org/docs/current/app-psql.html -------------------------------------------------------------------------------- /How to make the non-production Postgres planner behave like in production.md: -------------------------------------------------------------------------------- 1 | # How to make the non-production Postgres planner behave like in production 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 为了实现查询优化的目标 — 运行 `EXPLAIN (ANALYZE, BUFFERS)` 并验证各种优化想法,比如新索引,确保 Postgres 规划器的行为与生产数据库的规划器完全相同或者相似至关重要。幸运的是,无论非生产机器上可用的资源 (CPU、RAM、磁盘 IO) 如何,这都是可以实现的。 6 | 7 | 此处描述的技术已用于 DBLab Engine [@Database_Lab](https://twitter.com/Database_Lab),支持数据库克隆/分支,以进行查询优化和 CI/CD 中的数据库测试。 8 | 9 | 要实现规划器的生产/非生产行为一致性,需要两个组件: 10 | 11 | 1. 相匹配的数据库设置 12 | 2. 相同或非常相似的统计信息 (`pg_statistic` 中的内容) 13 | 14 | ## 相匹配的数据库设置 15 | 16 | 在生产数据库上使用以下查询获取会影响规划器行为的非默认设置列表: 17 | 18 | ```sql 19 | select 20 | format(e'%s = %s', name, setting) as configs 21 | from 22 | pg_settings 23 | where 24 | source <> 'default' 25 | and ( 26 | name ~ '(work_mem$|^enable_|_cost$|scan_size$|effective_cache_size|^jit)' 27 | or name ~ '(^geqo|default_statistics_target|constraint_exclusion|cursor_tuple_fraction)' 28 | or name ~ '(collapse_limit$|parallel|plan_cache_mode)' 29 | ); 30 | ``` 31 | 32 | 注意: 33 | 34 | - 规划器的行为不依赖于实际可用的资源,如 CPU 或 RAM,也不依赖于操作系统、文件系统或其设置。 35 | - `shared_buffers `的值无关紧要(!) — 它只会影响执行器的行为和缓冲池的命中/读取比率。对规划器重要的是 `effective_cache_size`,你可以将其设置为显著超过实际可用的 RAM,以"愚弄"规划器 (好的出发点),从而实现与生产环境规划器行为一致的目标。例如,在生产环境中有 1 TiB 的 RAM 和 `shared_buffers = '250GB'`,并且 `effective_cache_size = '750GB'`,你可以在 `shared_buffers = '2GB'` 和 `effective_cache_size = '750GB'` 的 8 GiB 机器上有效地分析和优化查询,规划器会假设你有大量的 RAM,从而选择最优的执行计划。 36 | - 同样,你可以在生产环境中使用快速的 SSD,设置`random_page_cost = 1.1`,但在测试环境中使用廉价低速的磁盘。如果在测试环境中同样使用 `random_page_cost = 1.1`,规划器会认为随机访问并不昂贵。尽管执行仍会很慢,但是所选择的计划和数据量 (实际行数、缓冲区数量) 将与生产环境非常接近。 37 | 38 | ## 相匹配的统计信息 39 | 40 | 一个有趣的想法是将生产环境中的 `pg_statistic` 内容转储并恢复到更小的测试环境中,而无需实际数据。尽管这种方法不常见,但可能可以使用 [pg_dbms_stats](https://github.com/ossc-db/pg_dbms_stats/blob/master/doc/pg_dbms_stats-en.md) 来实现。 41 | 42 | > 🎯 TODO:测试 pg_dbms_stats 并查看其效果。 43 | 44 | 一个简单的思路是使用相同大小的数据库。这里有两个选项: 45 | 46 | - **选项1:物理拷贝**。可以使用 `pg_basebackup`,通过 `rsync` 或其他工具复制 PGDATA,或从物理备份 (比如 `WAL-G` 或 `pgBackRest`) 恢复。 47 | - **选项2:逻辑拷贝**。可以通过转储/恢复,或创建逻辑副本来完成。 48 | 49 | 注意: 50 | 51 | - 物理拷贝的方式效果最佳:不仅"实际行数"匹配,运行 `EXPLAIN (ANALYZE, BUFFERS)` 时提供的缓冲区数量也匹配,因为页面数量 (`relpages`) 相同,膨胀保留等。这是进行优化和微调查询的最佳方法。 52 | - 逻辑方式的行数相匹配,但表和索引的大小会不同 — `relpages` 在新节点上更小,膨胀没有保留,元组通常以不同的顺序存储 (这种膨胀可以称为"好的膨胀",我们希望在测试环境中保留它以匹配生产环境的状态)。这种方法仍然可以实现相当高效的查询优化工作流程,此外,生产环境中保持低膨胀率变得更为重要。 53 | - 在执行转储/恢复后,你必须显式运行 `ANALYZE` (或使用 `vacuumdb --analyze -j `),以在 `pg_statistic` 中收集统计信息,因为 `pg_restore` (或 `psql`) 不会自动运行。 54 | - 如果需要修改数据库内容以移除敏感数据,这几乎肯定会影响规划器的行为。对于某些查询,影响可能较小,但对于其他查询,影响可能非常大,导致查询优化几乎变得不可能。这种负面影响通常比转储/恢复丢失膨胀造成的影响更严重,因为: 55 | - 转储/恢复会影响 `relpages` (丢失膨胀) 和元组顺序,但不会影响 `pg_statistic` 中的内容。 56 | - 移除敏感数据不仅会重新排列元组,产生无关的膨胀 ("坏膨胀"),还会丢失生产环境中 `pg_statistic` 的重要部分。 57 | 58 | ## 总结 59 | 60 | 为了让非生产环境中的 Postgres 规划器与生产环境中一致,执行以下两步: 61 | 62 | 1. 调整非生产环境中的某些 Postgres 设置 (与规划器相关,如`work_mem`),使其与生产环境匹配。 63 | 2. 尽可能从生产环境复制数据库,优先选择物理方式的拷贝,并且可能的话,尽量避免数据修改。对于超快速交付克隆,考虑使用 DBLab Engine [@Database_Lab](https://twitter.com/Database_Lab)。 -------------------------------------------------------------------------------- /How to monitor CREATE INDEX : REINDEX progress in Postgres 12+.md: -------------------------------------------------------------------------------- 1 | # How to monitor CREATE INDEX / REINDEX progress in Postgres 12+ 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 要观察长时间运行的索引创建或重建的进度,你可以使用以下查询: 6 | 7 | ~~~sql 8 | select 9 | now(), 10 | query_start as started_at, 11 | now() - query_start as query_duration, 12 | format('[%s] %s', a.pid, a.query) as pid_and_query, 13 | index_relid::regclass as index_name, 14 | relid::regclass as table_name, 15 | (pg_size_pretty(pg_relation_size(relid))) as table_size, 16 | nullif(wait_event_type, '') || ': ' || wait_event as wait_type_and_event, 17 | phase, 18 | format( 19 | '%s (%s of %s)', 20 | coalesce((round(100 * blocks_done::numeric / nullif(blocks_total, 0), 2))::text || '%', 'N/A'), 21 | coalesce(blocks_done::text, '?'), 22 | coalesce(blocks_total::text, '?') 23 | ) as blocks_progress, 24 | format( 25 | '%s (%s of %s)', 26 | coalesce((round(100 * tuples_done::numeric / nullif(tuples_total, 0), 2))::text || '%', 'N/A'), 27 | coalesce(tuples_done::text, '?'), 28 | coalesce(tuples_total::text, '?') 29 | ) as tuples_progress, 30 | current_locker_pid, 31 | (select nullif(left(query, 150), '') || '...' from pg_stat_activity a where a.pid = current_locker_pid) as current_locker_query, 32 | format( 33 | '%s (%s of %s)', 34 | coalesce((round(100 * lockers_done::numeric / nullif(lockers_total, 0), 2))::text || '%', 'N/A'), 35 | coalesce(lockers_done::text, '?'), 36 | coalesce(lockers_total::text, '?') 37 | ) as lockers_progress, 38 | format( 39 | '%s (%s of %s)', 40 | coalesce((round(100 * partitions_done::numeric / nullif(partitions_total, 0), 2))::text || '%', 'N/A'), 41 | coalesce(partitions_done::text, '?'), 42 | coalesce(partitions_total::text, '?') 43 | ) as partitions_progress, 44 | ( 45 | select 46 | format( 47 | '%s (%s of %s)', 48 | coalesce((round(100 * n_dead_tup::numeric / nullif(reltuples::numeric, 0), 2))::text || '%', 'N/A'), 49 | coalesce(n_dead_tup::text, '?'), 50 | coalesce(reltuples::int8::text, '?') 51 | ) 52 | from pg_stat_all_tables t, pg_class tc 53 | where t.relid = p.relid and tc.oid = p.relid 54 | ) as table_dead_tuples 55 | from pg_stat_progress_create_index p 56 | left join pg_stat_activity a on a.pid = p.pid 57 | order by p.index_relid 58 | ; -- in psql, use "\watch 5" instead of semicolon 59 | ~~~ 60 | 61 | 相同的查询,[采用更好的格式](https://gitlab.com/-/snippets/2138417)。 62 | 63 | 查询工作原理如下: 64 | 65 | 1. 此查询的核心依赖于 `pg_stat_progress_create_index`,该视图是在 PostgreSQL 12 中引入的。 66 | 2. 文档中还列出了创建索引所涉及的所有阶段。特别是像 `CREATE INDEX CONCURRENTLY `和 `REINDEX CONCURRENTLY` (即 CIC 和 RC) 这类高级变体,这些方法用于高负载的生产系统,采用非阻塞方式运行,但耗时更长,CIC 和 RC 在执行过程中引入了更多阶段。当前阶段可以在输出的 `phase` 列中查看。 67 | 3. 查询展示了索引名称 (在 CIC/RC 的情况下,是临时的) 和表名称 (使用了一个有用的技巧,通过 OID 转换成实际名称: `index_relid::regclass AS index_name`)。此外,表的大小对于形成总体持续时间的预期至关重要 — 表越大,创建索引的时间越久。 68 | 4. 查询还利用 `pg_stat_activity` (`pgsa`) 提供了大量额外的有用信息: 69 | - Postgres 后端的 PID 70 | - 实际使用的 SQL 查询 71 | - 查询开始的时间 (`query_start`),用于计算已用时长 (`query_duration`) 72 | - `wait_event_type` 和 `wait_event`,帮助我们了解进程当前在等待什么 73 | - 当发生此类事件时,它还使用 (在单独的子查询中) 获取阻塞我们进程的会话的信息 (`current_locker_pid` 和`current_locker_query`) 74 | 75 | 5. `format(...)` 函数非常有用,可以将数据整合成便捷的形式,避免处理 `NULL` 值时出现问题。如果我们使用常规的字符串连接操作而不使用 `coalesce(...)`,就可能会遇到 `NULL` 值的问题。 76 | 6. 在某些情况下,我们使用 `coalesce(...)` 来填充特殊符号以处理缺失值 (即 `IS NULL`) — 例如 "?" 或 "N/A"。 77 | 7. 另一个有趣的技巧是结合使用 `coalesce(...)` 和 `nullif(...)`。后者可以避免除以零的错误 (通过将 `0` 替换为 `NULL`,使除法结果也为 `NULL`),而前者再次用于将 `NULL` 替换为一些非空值 (在此示例中为 'N/A')。 78 | 79 | 在 `psql` 中执行时,使用 `\watch [seconds]` 命令可以方便地在循环中运行此查询,并实时观察进度。 80 | 81 | ![tracking the progress of index building/rebuilding](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0015_reindex.gif) -------------------------------------------------------------------------------- /How to monitor transaction ID wraparound risks.md: -------------------------------------------------------------------------------- 1 | # How to monitor transaction ID wraparound risks 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 事务 ID 和组事务 ID 的回卷问题是 Postgres 数据库中最严重的事件之一。著名的案例包括: 6 | 7 | - [Sentry (2015)](https://blog.sentry.io/transaction-id-wraparound-in-postgres/) 8 | - [Joyent (2015)](https://tritondatacenter.com/blog/manta-postmortem-7-27-2015) 9 | - [Mailchimp (2019)](https://mailchimp.com/what-we-learned-from-the-recent-mandrill-outage/) 10 | 11 | 一些有价值的参考资源: 12 | 13 | - [Postgres docs](https://postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND) 14 | - Hironobu Suzuki 的书《The Internals of PostgreSQL》: [Vacuum Processing](https://interdb.jp/pg/pgsql06.html) 15 | - Egor Rogov 的书《PostgreSQL 14 Internals》 16 | - [PostgreSQL/Wraparound and Freeze](PostgreSQL/Wraparound and Freeze) 17 | 18 | 每个监控设置必须包含两类特定的指标 (以及适当的警报): 19 | 20 | 1. 非冻结元组中使用的最老 XID 和 MultiXID (又称 MultiXact ID) 值 21 | 2. 监控 `xmin` 视界 22 | 23 | 在这里我们讨论第一类。 24 | 25 | ## 32位事务ID 26 | 27 | XID 和 MultiXact ID 都是 32 位的,所以总的空间大小为 2^32 ≈ 42 亿。但由于使用了取模算术的原因,XID 有一个"未来"的概念 — 空间的一半被认为是过去的,另一半是未来的。因此,我们需要监控的容量是 2^31 ≈ 21 亿。 28 | 29 | 来自维基百科的一个例子: [Wikipedia "wraparound and freeze"](https://en.wikibooks.org/wiki/PostgreSQL/Wraparound_and_Freeze): 30 | 31 | ![Wraparound and freeze](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0044_wraparound_and_freeze.jpg) 32 | 33 | 为了防止 XID/MultiXID 回卷,`autovacuum` 进程除了其他任务之外,还会定期执行"冻结"操作,将老的元组标记为已冻结,表示这些元组是属于过去的。 34 | 35 | 因此,监控元组的 XID/MultiXID 年龄对于控制回卷风险至关重要。 36 | 37 | ## XID和MultiXID回卷风险监控 38 | 39 | 最常见的监控方法是: 40 | 41 | - 检查 `pg_database.datfrozenxid`,以了解 `autovacuum` 是否正常执行"冻结"操作,替换元组中老的 XID。 42 | - 要进一步深入,可以检查每个关系的 `pg_class.relfrozenxid`。 43 | - XID 是数字,可以使用 `age(...)` 函数将这些值与当前 XID 进行比较。 44 | 45 | 此外,还应检查 [MultiXact ID 回卷](https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-FOR-MULTIXACT-WRAPAROUND)的风险 (在监控工具中这通常会缺失): 46 | 47 | - `pg_database` 中的 `datminmxid` 48 | - `pg_class` 中的 `relminmxid` 49 | - 需要使用 `mxid_age(...)`,而不是 `age(...)`。 50 | 51 | 集群级别的高层次监控查询 (数据库级别) 示例: 52 | 53 | ```sql 54 | with data as ( 55 | select 56 | oid, 57 | datname, 58 | age(datfrozenxid) as xid_age, 59 | mxid_age(datminmxid) as mxid_age, 60 | pg_database_size(datname) as db_size 61 | from pg_database 62 | ) 63 | select 64 | *, 65 | pg_size_pretty(db_size) as db_size_hr 66 | from data 67 | order by greatest(xid_age, mxid_age) desc; 68 | ``` 69 | 70 | 特定数据库中的表级监控查询,TOP 25: 71 | 72 | ```sql 73 | with data as ( 74 | select 75 | format( 76 | '%I.%I', 77 | nspname, 78 | c.relname 79 | ) as table_name, 80 | greatest(age(c.relfrozenxid), age(t.relfrozenxid)) as xid_age, 81 | greatest(mxid_age(c.relminmxid), mxid_age(t.relminmxid)) as mxid_age, 82 | pg_table_size(c.oid) as table_size, 83 | pg_table_size(t.oid) as toast_size 84 | from pg_class as c 85 | join pg_namespace pn on pn.oid = c.relnamespace 86 | left join pg_class as t on c.reltoastrelid = t.oid 87 | where c.relkind in ('r', 'm') 88 | ) 89 | select * 90 | from data 91 | order by greatest(xid_age, mxid_age) desc 92 | limit 25; 93 | ``` 94 | 95 | ## 告警 96 | 97 | 如果 XID 或 MultiXID 的年龄增长超过一定的阈值 (通常为 2 亿,参考 [autovacuum_freeze_max_age](https://postgresqlco.nf/doc/en/param/autovacuum_freeze_max_age/)),这表明某些东西阻碍了正常的 autovacuum 工作。 98 | 99 | 因此,根据 autovacuum 的设置,监控系统应配置为在 XID 和 MultiXID 年龄超过预定义阈值 (通常在 3 亿到 10 亿范围内) 时发出警报。年龄超过 10 亿应被视为危险信号,要求紧急缓解措施。 -------------------------------------------------------------------------------- /How to perform initial rough Postgres tuning.md: -------------------------------------------------------------------------------- 1 | # How to perform initial / rough Postgres tuning 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 现代 PostgreSQL 提供了超过 300 个设置 (即所谓的 GUC 变量,"grand unified configuration")。为特定环境、数据库和工作负载对 Postgres 进行精细调优是一项非常复杂的任务。 6 | 7 | 但在大多数情况下,帕累托法则 (即二八法则) 非常有效:你只需要投入有限的精力处理基本的调优领域,然后专注于查询性能。这个方法背后的逻辑很简单:你可能会花费大量时间为 `shared_buffers` 找到比传统的 25% 更合适的值 (很多人认为 25% 并不理想,比如 [Andres Freund's Tweet](https://twitter.com/andresfreundtec/status/1178765225895399424)),但你可能会发现某些查询由于性能不佳 (例如缺少适当的索引) 而破坏了所有调优的正面效果。 8 | 9 | 因此,我建议采用以下方法: 10 | 11 | - 基本的"粗略"调优 12 | - 日志相关设置 13 | - 自动清理调优 14 | - 检查点调优 15 | - 然后专注于查询优化,无论是被动的还是主动的,且仅在有充分理由时对特定领域进行精细调优 16 | 17 | ## 基本的粗略调优 18 | 19 | 对于初步的粗略调优,遵循二八法则的经验工具在大多数情况下已经"足够好" (实际上,可能在这个情况下是 95/5 的法则): 20 | 21 | - [PGTune](https://pgtune.leopard.in.ua) ([source code](https://github.com/le0pard/pgtune)) 22 | - [PostgreSQL Configurator](https://pgconfigurator.cybertec.at) 23 | - 对于 TimescaleDB 用户: [timescaledb-tune](https://github.com/timescale/timescaledb-tune) 24 | 25 | 此外,除了官方文档外,[这个资源](https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server)也是一个很好的参考 (它整合了来自多个来源的信息,不仅仅是官方文档)。例如,检查页面 [random_page_cost](https://postgresqlco.nf/doc/en/param/random_page_cost/),这是一个经常被忽略的参数。 26 | 27 | 如果你使用的是托管 Postgres 服务 (如 RDS),通常在你创建服务器时这个层面的调优已经完成了。但仍然值得再次检查,例如,有些服务提供商会使用 SSD 磁盘配置服务器,但仍然将 `random_page_cost` 保持为默认值 4,这个值适用于磁盘驱动器。如果你使用 SSD,将其设置为 1。 28 | 29 | 在进行查询优化之前,进行这个层面的调优非常重要,否则在调整基本配置后,可能需要重新进行查询优化。 30 | 31 | ## 日志相关设置 32 | 33 | 此处的通用规则是:日志记录越多越好。当然,前提是避免两种饱和情况: 34 | 35 | - 磁盘空间 (日志撑爆磁盘) 36 | - 磁盘 IO (日志写入次数过多导致的磁盘负载) 37 | 38 | 简而言之,我的建议如下 (这些内容值得另写一篇详细的文章): 39 | 40 | - 打开检查点日志:`log_checkpoints='on'` (幸运的是,在 PG15+ 中默认已经开启) 41 | - 打开所有自动清理日志:`log_autovacuum_min_duration=0` (或一个非常小的值) 42 | - 记录临时文件,除了非常小的文件 (例如,`log_temp_files = 100`) 43 | - 记录所有 DDL 语句:`log_statement='ddl'` 44 | - 调整 `log_line_prefix` 45 | - 设置一个较低的 `log_min_duration_statement` (例如 500ms),或使用 `auto_explain` 记录慢查询及其执行计划 46 | 47 | ## 自动清理调优 48 | 49 | 这是一个需要单独讨论的大话题。简而言之,默认设置不适用于任何现代 OLTP 场景 (例如 Web/移动应用),因此自动清理功能必须进行调优。如果不调优,自动清理将把大量死元组"转换"为膨胀,最终会对性能产生负面影响。 50 | 51 | 需要解决两个调优方向: 52 | 53 | 1. 提高处理频率:通过降低 `*_scale_factor` 和 `*_threshold` 设置,让自动清理在累积较少死元组时就开始处理表。 54 | 55 | 2. 分配更多资源进行处理:更多的自动清理工作进程 (`autovacuum_workers`)、更多的内存 (`autovacuum_work_mem`) 以及更高的"配额" (通过 `*_cost_limit` 和 `*_cost_delay` 控制)。 56 | 57 | ## 检查点调优 58 | 59 | 同样,这个话题值得单独讨论。但简而言之,你需要考虑提高 `checkpoint_timeout`,最重要的是增加 `max_wal_size` (默认值对于现代机器和数据量来说非常小,仅为 1GB),以减少检查点的频率,尤其是在大量写操作发生时。然而,朝这个方向改变设置意味着在发生崩溃或从备份中恢复时需要更长的恢复时间 — 这是一种需要针对特定情况进行分析的权衡。 60 | 61 | 就这样,PostgreSQL 配置的初步/粗略调优一般不会花费很长时间。对于某一类集群或特定集群来说,工程师大约只需要 1-2 天的工作。你并不需要 AI 来完成这项任务,经验工具工作得很好 — 除非你想要再提高5-10% (例如,如果你有成千上万的服务器)。 -------------------------------------------------------------------------------- /How to plot graphs right in psql on macOS (iTerm2).md: -------------------------------------------------------------------------------- 1 | # How to plot graphs right in psql on macOS (iTerm2) 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如果你像我一样主要在 psql 中使用 Postgres,那么你可能希望能够在不离开 psql 的情况下绘制一个简单的图表。 6 | 7 | 以下方法最初由 [Alexander Korotkov](https://akorotkov.github.io/blog/2016/06/09/psql-graph/) 描述,我对其进行了[略微调整](https://gist.github.com/NikolayS/d5f1af808f7275dc1491c37fb1e2dd11)以适配Python3。此方法得益于 iTerm2的[内嵌图片协议](https://iterm2.com/documentation-images.html)。对于 Linux,有其他方式可以实现类似效果 (例如,[How do I make my terminal display graphical pictures?](https://askubuntu.com/questions/97542/how-do-i-make-my-terminal-display-graphical-pictures))。 8 | 9 | 1. 获取绘图脚本并安装 matplotlib: 10 | 11 | ```bash 12 | wget \ 13 | -O ~/pg_graph.py \ 14 | https://gist.githubusercontent.com/NikolayS/d5f1af808f7275dc1491c37fb1e2dd11/raw/4f19a23222a6f7cf66eead3cae9617dd39bf07a5/pg_graph 15 | 16 | pip install matplotlib 17 | ``` 18 | 19 | 2. 在 ~/.psqlrc 中定义一个宏 (在 bash、zsh 和 csh 中均可工作): 20 | 21 | ```bash 22 | printf "%s %s %s %s %s %s\n" \\set graph \'\\\\g \| 23 | python3 $(pwd)/pg_graph.py\' \ 24 | >> ~/.psqlrc 25 | ``` 26 | 27 | 3. 启动 psql 并尝试一下 28 | 29 | ```sql 30 | nik=# with avg_temp(month, san_diego, vancouver, london) as ( 31 | values 32 | ('Jan', 15, 4, 5), 33 | ('Feb', 16, 5, 6), 34 | ('Mar', 17, 7, 8), 35 | ('Apr', 18, 10, 11), 36 | ('May', 19, 14, 15), 37 | ('Jun', 21, 17, 17), 38 | ('Jul', 24, 20, 19), 39 | ('Aug', 25, 21, 20), 40 | ('Sep', 23, 18, 17), 41 | ('Oct', 21, 12, 13), 42 | ('Nov', 18, 8, 8), 43 | ('Dec', 16, 5, 6) 44 | ) 45 | select * from avg_temp; 46 | 47 | month | san_diego | vancouver | london 48 | -------+-----------+-----------+-------- 49 | Jan | 15 | 4 | 5 50 | Feb | 16 | 5 | 6 51 | Mar | 17 | 7 | 8 52 | Apr | 18 | 10 | 11 53 | May | 19 | 14 | 15 54 | Jun | 21 | 17 | 17 55 | Jul | 24 | 20 | 19 56 | Aug | 25 | 21 | 20 57 | Sep | 23 | 18 | 17 58 | Oct | 21 | 12 | 13 59 | Nov | 18 | 8 | 8 60 | Dec | 16 | 5 | 6 61 | (12 rows) 62 | 63 | nik=# :graph 64 | ``` 65 | 66 | 结果: 67 | 68 | ![Graph result](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0081_01.png) -------------------------------------------------------------------------------- /How to quickly check data type and storage size of a value.md: -------------------------------------------------------------------------------- 1 | # How to quickly check data type and storage size of a value 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 以下是无需查阅文档便可快速检查值的数据类型和大小的方法。 6 | 7 | ## 如何检查值的数据类型 8 | 9 | 使用 `pg_typeof(...)`: 10 | 11 | ```sql 12 | nik=# select pg_typeof(1); 13 | pg_typeof 14 | ----------- 15 | integer 16 | (1 row) 17 | 18 | nik=# select pg_typeof(1::int8); 19 | pg_typeof 20 | ----------- 21 | bigint 22 | (1 row) 23 | ``` 24 | 25 | ## 如何检查值的存储大小 26 | 27 | 使用 `pg_column_size(...)` — 即使没有实际的列: 28 | 29 | ```sql 30 | nik=# select pg_column_size(1); 31 | pg_column_size 32 | ---------------- 33 | 4 34 | (1 row) 35 | 36 | nik=# select pg_column_size(1::int8); 37 | pg_column_size 38 | ---------------- 39 | 8 40 | (1 row) 41 | ``` 42 | 43 | 👉 `int4` (即 `int` 或 `integer`) 占用 4 字节,而 `int8` (`bigint`) 占用 8 字节。 44 | 45 | 对于 `VARLENA` 类型,比如 `varchar`、`text`、`json`、`jsonb`、数组,长度是可变的 (因此得名),并且还有额外的头部,比如: 46 | 47 | ```sql 48 | nik=# select pg_column_size('ok'::text); 49 | pg_column_size 50 | ---------------- 51 | 6 52 | (1 row) 53 | ``` 54 | 55 | 👉 一个 4 字节的 `VARLENA` 头 (用于行内存储,参见 [struct varlena header](https://github.com/postgres/postgres/blob/c161ab74f76af8e0f3c6b349438525ad9575683b/src/include/c.h#L661-L681)) 和 2 字节的数据。 56 | 57 | Boolean 示例: 58 | 59 | ```sql 60 | nik=# select pg_column_size(true), pg_column_size(false); 61 | pg_column_size | pg_column_size 62 | ----------------+---------------- 63 | 1 | 1 64 | (1 row) 65 | ``` 66 | 67 | 回想一下上一篇指南,[列拼图](https://postgres-howto.cn/#/./docs/84),我们可以得出,不仅需要 1 字节来存储一个比特,如果我们创建一个表 (`c1 boolean`,`c2 int8`),由于对齐填充,它会变成 8 个字节,达到了 64 位!因此,如果以 `text` 存储 'true',也不会浪费存储: 68 | 69 | ```sql 70 | nik=# select pg_column_size('true'::text); 71 | pg_column_size 72 | ---------------- 73 | 8 74 | (1 row) 75 | ``` 76 | 77 | 👉 也是 8 字节 (4 字节 `VARLENA` 头,4 字节实际值)。而对于 `false`,由于使用对齐填充 (如果下一列是 8 或 16 字节),情况很快就会变得更糟: 78 | 79 | ```sql 80 | nik=# select pg_column_size('false'::text); 81 | pg_column_size 82 | ---------------- 83 | 9 84 | (1 row) 85 | ``` 86 | 87 | 👉 这些 9 字节可能因对齐填充被补齐为 16 字节,因此不要使用 `text` 来存储 `true/false`。对于多个布尔"标志"值的存储优化,考虑使用一个整数,将布尔值"打包"其中,并使用位操作符 `<<`、`~`、`|`、`&` 来编码和解码这些值 (参考 [Bit String Functions and Operators](https://postgresql.org/docs/current/functions-bitstring.html)) 。这样的话,便可以在一个 `int8` 中"打包" 64 个布尔值。 88 | 89 | 更多示例: 90 | 91 | ```sql 92 | nik=# select pg_column_size(now()); 93 | pg_column_size 94 | --------------- 95 | 8 96 | (1 row) 97 | 98 | nik=# select pg_column_size(interval '1s'); 99 | pg_column_size 100 | ---------------- 101 | 16 102 | (1 row) 103 | ``` 104 | 105 | ## 如何检查行的存储大小 106 | 107 | 还有几个例子 — 如何检查行的大小: 108 | 109 | ```sql 110 | nik=# select pg_column_size(row(true, now())); 111 | pg_column_size 112 | ---------------- 113 | 40 114 | (1 row) 115 | ``` 116 | 117 | 👉 一个 24 字节的元组头 (23 字节加上 1 个零值填充),然后是 1 字节的布尔值 (填充 7 个零),接着是 8 字节的 `timestamptz` 值。总计:24 + 1+7 + 8 = 40。 118 | 119 | ```sql 120 | nik=# select pg_column_size(row(1, 2)); 121 | pg_column_size 122 | ---------------- 123 | 32 124 | (1 row) 125 | ``` 126 | 127 | 👉 一个 24 字节的元组头,然后是两个 4 字节的整数,无需填充,总计 24 + 4 + 4 = 32。 128 | 129 | ## 无需记忆函数名称 130 | 131 | 在 psql 中,无需记忆函数名称 — 可以使用 `\df+` 来搜索函数名称: 132 | 133 | ```sql 134 | nik=# \df *pg_*type* 135 | List of functions 136 | Schema | Name | Result data type | Argument data types | Type 137 | ------------+-------------------------------------------+------------------+---------------------+------ 138 | pg_catalog | binary_upgrade_set_next_array_pg_type_oid | void | oid | func 139 | pg_catalog | binary_upgrade_set_next_pg_type_oid | void | oid | func 140 | pg_catalog | binary_upgrade_set_next_toast_pg_type_oid | void | oid | func 141 | pg_catalog | pg_stat_get_backend_wait_event_type | text | integer | func 142 | pg_catalog | pg_type_is_visible | boolean | oid | func 143 | pg_catalog | pg_typeof | regtype | "any" | func 144 | (6 rows) 145 | ``` 146 | 147 | ## 我见 148 | 149 | 作为最佳实践,不建议使用 `text` 类型存储布尔值。文中提到的将多个布尔值"打包"的操作,在追求极致优化的场景下可以尝试。 -------------------------------------------------------------------------------- /How to quit from psql.md: -------------------------------------------------------------------------------- 1 | # How to quit from psql 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 只需输入 `quit` (或 `exit`)。就是这样。 6 | 7 | 除非你使用的是 Postgres 10 或更早的版本 — 在这种情况下,使用 `\q`。Postgres 11 将在几周后 ([最后一个小版本 11.22 计划于 11 月 9 日发布](https://www.postgresql.org/support/versioning/)) 正式退休,因此我们可以说对于所有受支持的版本,只需输入 `quit` 即可。 8 | 9 | 如果你需要在非交互模式下使用退出命令,那么也可以使用 `\q`: 10 | 11 | ```bash 12 | psql -c '\q' – 这个命令可用 13 | 14 | psql -c 'quit' – 这个命令不可用 15 | ``` 16 | 17 | 另外,`Ctrl-D` 也可以使用。作为退出控制台的标准方式,它在各种 shell 中都适用,例如 bash, zsh, irb, python, node 等。 18 | 19 | --- 20 | 21 | 这个问题仍然是 [StackOverflow](https://stackoverflow.com/questions/9463318/how-to-exit-from-postgresql-command-line-utility-psql) 和其他平台上排名前五的热门问题之一。 -------------------------------------------------------------------------------- /How to rebuild many indexes using many backends avoiding deadlocks.md: -------------------------------------------------------------------------------- 1 | # How to rebuild many indexes using many backends avoiding deadlocks 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 有时我们需要重建多个索引,甚至全部索引,并希望加快速度。 6 | 7 | 例如,在从 Postgres 14 之前的版本升级到 14+ 后,为了从降低膨胀增长率这一优化项中受益,我们可能希望重建所有 B 树索引。 8 | 9 | 我们可以选择使用单一会话并设置较高的 [max_parallel_maintenance_workers](https://postgresqlco.nf/doc/en/param/max_parallel_maintenance_workers/) 值,一次处理一个索引。但如果我们有足够的资源 (大量的 vCPU 和快速的磁盘),那么 `max_parallel_maintenance_workers` 最大值可能也不足以充分利用资源 (更改 `max_parallel_maintenance_workers` 不需要重启,但不能超过 `max_worker_processes` 的数量,且更改该值需要重启)。在这种情况下,使用 `REINDEX INDEX CONCURRENTLY`,并行处理多个索引可能更合适。 10 | 11 | 但在这种情况下,索引需要按正确的顺序处理。问题在于,如果你尝试并行重建属于同一张表的两个索引,那么会检测到死锁,并且其中一个会话将失败: 12 | 13 | ```sql 14 | nik=# reindex index concurrently t1_hash_record_idx3; 15 | ERROR: deadlock detected 16 | DETAIL: Process 40 waits for ShareLock on virtual transaction 4/2506; blocked by process 1313. 17 | Process 1313 waits for ShareUpdateExclusiveLock on relation 16634 of database 16401; blocked by process 40. 18 | HINT: See server log for query details. 19 | ``` 20 | 21 | 为了解决这个问题,我们可以使用以下方式: 22 | 23 | 1. 决定要使用多少个重建会话,考虑 `max_parallel_maintenance_workers` 和预期资源利用率/饱和的风险 (CPU 和磁盘 IO)。 24 | 2. 假设我们想使用 N 个重建会话,构建索引的完整列表及其所属的表名称,并为每个表"分配"一个特定的重建会话,见下方的查询。 25 | 3. 使用此"分配",将整个索引列表划分为 N 个独立的列表,以确保每个表的所有索引仅出现在一个独立列表中 — 然后我们可以使用这 N 个列表运行 N 个会话。 26 | 27 | 以下查询可以帮助完成步骤 2: 28 | 29 | ```sql 30 | \set NUMBER_OF_SESSIONS 10 31 | 32 | SELECT 33 | format('%I.%I', n.nspname, c.relname) AS table_fqn, 34 | format('%I.%I', n.nspname, i.relname) AS index_fqn, 35 | mod( 36 | hashtext(format('%I.%I', n.nspname, c.relname)) & 2147483647, 37 | :NUMBER_OF_SESSIONS 38 | ) AS session_id 39 | FROM 40 | pg_index idx 41 | JOIN pg_class c ON idx.indrelid = c.oid 42 | JOIN pg_class i ON idx.indexrelid = i.oid 43 | JOIN pg_namespace n ON c.relnamespace = n.oid 44 | WHERE 45 | n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') 46 | -- and ... additional filters if needed 47 | ORDER BY 48 | table_fqn, index_fqn; 49 | ``` 50 | 51 | ## 我见 52 | 53 | 这个技巧挺不错: 54 | 55 | - **NUMBER_OF_SESSIONS**:设置需要的重建会话数量。 56 | - **session_id**:通过 `hashtext` 函数和模运算将表分配给不同的会话。 57 | - **WHERE 子句**:排除系统模式 (`pg_catalog`、`pg_toast` 和 `information_schema`) 中的索引,避免不必要的重建。 -------------------------------------------------------------------------------- /How to redefine a PK without downtime.md: -------------------------------------------------------------------------------- 1 | # How to redefine a PK without downtime 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 重新定义主键并不是一件困难的事情,但需要执行几个较为复杂的步骤。当使用"新列"方法 (单独讨论) 时,该过程也是 `int4`->`int8` 主键转换的一部分。 6 | 7 | 当然,我们可以简单地删除主键并定义一个新主键,如下所示: 8 | 9 | ```sql 10 | ALTER TABLE your_table 11 | DROP CONSTRAINT your_table_pkey; 12 | 13 | ALTER TABLE your_table 14 | ADD PRIMARY KEY (new_column); 15 | ``` 16 | 17 | 但这种直截了当的方式通常是个糟糕的主意,因为它会在表上获取 `AccessExclusiveLock`,并持有锁很长时间,因为: 18 | 19 | - 需要建立唯一约束; 20 | - 需要建立非空约束。 21 | 22 | 这是因为创建一个主键需要两个要素:唯一约束,以及参与主键定义的所有列上的非空约束。幸运的是,在现代 PostgreSQL (PG12+) 中,可以避免长时间的排它锁 — 也就是说,可以实现真正的"在线"或"零停机"操作。 23 | 24 | 以下是假设前提: 25 | 26 | - 新的主键列已经存在并且已经预填充; 27 | - 如果有更多的 `INSERT` 和 `UPDATE` 操作,数据会继续被填充 — 因此数据已经存在; 28 | - 对应的列已经实现了唯一性; 29 | - 对应的列没有任何空值。 30 | 31 | 请注意,最后一条条件非常重要 — 与唯一键不同,主键要求定义中的所有列都必须有非空约束。 32 | 33 | ## NOT NULL:好消息和坏消息 (最终,都是好消息) 34 | 35 | 让我们深入探讨细节 — NOT NULL 值得这么做。我们会有很多好消息和坏消息。我们将深入探讨与主键不一定相关但仍相关的细节。最终我们将回到重新定义主键的任务。请耐心听我说。 36 | 37 | 坏消息:不幸的是,向现有列添加 `NOT NULL` 约束意味着 PostgreSQL 需要执行长时间的全表扫描,而在此期间会获取 `ALTER TABLE` 持有的 `AccessExclusiveLock`,这显然不是我们想要的。 38 | 39 | 好消息:自 PostgreSQL 11 起,我们可以通过一些技巧来应对这一问题。如果我们需要添加一个带有 `NOT NULL` 的列,我们可以受益于 PG11 的新功能 — 新列的非阻塞 `DEFAULT`,并将其与 `NOT NULL` 结合起来,例如: 40 | 41 | ```sql 42 | ALTER TABLE t1 43 | ADD COLUMN new_id int8 NOT NULL DEFAULT -1; 44 | ``` 45 | 46 | 这是非常快的,因为 PG11 对新列的默认值进行了优化 (它是"虚拟"的,不会重写整个表)。 47 | 48 | >ability to avoid a table rewrite for `ALTER TABLE ... ADD COLUMN` with a non-null column default 49 | > 50 | >([PG11 release notes](https://postgresql.org/docs/release/11.0/)) 51 | 52 | 而且由于所有行都是预先填充的 ("虚拟地",但这并不重要),我们可以立即获得 `NOT NULL`,避免长时间等待。 53 | 54 | 坏消息:这种方法仅适用于新列。如果我们要对现有列添加 `NOT NULL` 约束,这个方法行不通。 55 | 56 | 好消息:如果我们只是需要一个"not null",而不考虑其具体定义,我们可以使用 `CHECK` 约束。`CHECK` 约束的好处在于,它的定义可以分为两个阶段: 57 | 58 | 1. 首先,我们可以定义 `CHECK (col1 IS NOT NULL)` 并使用 `NOT VALID` ,这样操作很快,不会阻塞其他会话,因为不会立即检查现有的数据行 (不过仍会阻塞 — 它仍然是一个 ALTER TABLE,但只持续很短的时间;当然,仍然需要重试机制和较低的 lock_timeout,参照 [Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries))。 59 | 2. 然后,我们执行 `ALTER TABLE ... VALIDATE CONSTRAINT ...` 来进行校验,这一步虽然慢,但不会阻塞其他操作。 60 | 61 | 坏消息:由于我们的最终目标是重新定义主键,因此 `CHECK` 约束对我们无效,因为主键需要真正的 `NOT NULL` 约束。 62 | 63 | 好消息:在 PG12+ 中,有一项优化允许 `NOT NULL` 约束依赖于现有的 `CHECK (... IS NOT NULL)` 约束: 64 | 65 | >Allow ALTER TABLE ... SET NOT NULL to avoid unnecessary table scans 66 | > 67 | >([PG12 release notes](https://postgresql.org/docs/release/12.0/)) 68 | 69 | 这意味着我们只需要这样做: 70 | 71 | 1. 创建一个 `CHECK` 约束,确保列不为空,并使用 `NOT VALID` (获取具有较低 `lock_timeout` 的简短排他锁,如果需要,多次重试)。 72 | 2. 在单独的事务中进行校验。 73 | 3. 然后,为列添加 `NOT NULL` 约束,这一步会非常快 (同样,较低的 lock_timeout 以及重试机制)。 74 | 4. 最后,删除 `CHECK` 约束 (同样,较低的 lock_timeout 以及重试机制)。 75 | 76 | 有趣的是,如果我们的最终目标是创建主键,那么可以跳过步骤 3 — 在创建主键期间,将隐式创建 `NOT NULL` 约束;而且由于已经存在 `CHECK (...NOT NULL)`,速度会很快。 77 | 78 | ## 唯一约束 79 | 80 | 创建新的主键时,另一个必要条件是唯一约束。幸运的是,它可以分两个阶段创建,从而避免长时间的独占锁: 81 | 82 | 1. 借助 `CONCURRENTLY` 选项,以"零停机"方式创建唯一索引 — 为该索引命名非常重要,因为我们稍后会使用这个名称: 83 | 84 | ```sql 85 | create unique index concurrently new_unique_index 86 | on your_table using btree(your_column); 87 | ``` 88 | 89 | 2. 定义主键时使用此索引 (... `USING INDEX` ...) 90 | 91 | ## 完整的流程 92 | 93 | 现在,让我们完成拼图,拨云见雾。 94 | 95 | 以零停机方式创建主键包括以下五个步骤: 96 | 97 | 1. 使用 `NOT VALID` 选项创建 `CHECK(...IS NOT NULL)`约束✂️: 98 | 99 | ```sql 100 | alter table your_table 101 | add constraint your_table_your_column_check 102 | check (your_column is not null) not valid; 103 | ``` 104 | 105 | 2. 验证约束 (可能需要较长时间): 106 | 107 | ```sql 108 | alter table your_table 109 | validate constraint your_table_your_column_check; 110 | ``` 111 | 112 | 3. 使用 `CONCURRENTLY` 创建唯一索引: 113 | 114 | ```sql 115 | create unique index concurrently u_your_table_your_column 116 | on your_table using btree(your_column); 117 | ``` 118 | 119 | 4. 基于现有的唯一索引和 `CHECK` 约束定义主键 (隐式创建 `NOT NULL` 跳过全表扫描)✂️:: 120 | 121 | ```sql 122 | alter table your_table 123 | add constraint your_table_pkey primary key 124 | using index u_your_table_your_column; 125 | ``` 126 | 127 | 5. 删除 `CHECK` 约束以清理环境✂️:: 128 | 129 | ```sql 130 | alter table your_table 131 | drop constraint your_table_your_column_check; 132 | ``` 133 | 134 | (✂️ - 建议使用较低的 lock_timeout 和重试机制) -------------------------------------------------------------------------------- /How to reduce WAL generation rates.md: -------------------------------------------------------------------------------- 1 | # How to reduce WAL generation rates 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在一个快速增长的项目中,其中一个重要优化方向便是减少生成的 WAL (预写日志) 的数量。 6 | 7 | ## 为什么这很重要 8 | 9 | WAL 是 PostgreSQL 的核心机制,用于在发生故障时进行恢复、备份和复制。 10 | 11 | 每秒生成的 WAL 数据越多,意味着每秒需要复制和备份的数据量也越大,因此各种风险也可能会增加: 12 | 复制延迟、WAL 归档延迟以及发生故障后,恢复时间也会增加。 13 | 14 | ## 如何衡量 WAL 生成速率 15 | 16 | 当新事务创建一条 WAL 记录时,它会分配一个 LSN (日志序列号)。监控当前 LSN 的位置非常简单: 17 | 18 | ```sql 19 | select pg_current_wal_lsn(); 20 | ``` 21 | 22 | 在任何 PostgreSQL 监控中都应该包含这个指标。 23 | 24 | 两个 LSN 之间的差值就是这段时间内生成的字节数,Postgres 可以执行相关的计算 — 使用 `pg_lsn` 数据类型: 25 | 26 | ```sql 27 | nik=# select pg_size_pretty('3/ED5F1E0'::pg_lsn - '0/110A1E0'); 28 | pg_size_pretty 29 | ---------------- 30 | 12 GB 31 | (1 row) 32 | ``` 33 | 34 | 如果监控没有此功能,你可以通过查看以下内容了解每小时或每天生成了多少 WAL 数据: 35 | 36 | - `pg_wal` 目录中的 WAL 文件名 37 | - 检查备份 (例如,检查 WAL-G 创建的两个完整备份的名称:`wal-g backup-list --detail`) 38 | 39 | 这两种方法都应该可以帮助你获取与两个遥远时间点相对应的两个 LSN 值。 40 | 41 | 要了解更多细节,参照 [Day 9: How to understand the LSN values and WAL file name](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0009_lsn_values_and_wal_filenames.md) 42 | 43 | ## 查询分析中的 WAL 指标 44 | 45 | PostgreSQL 13 及以后的版本中,`pg_stat_statements` 和 `EXPLAIN` 都能提供与 WAL 相关的指标: 46 | 47 | 1. `pg_stat_statements`:`wal_records`、`wal_fpi`、`wal_bytes` 等指标([docs](https://postgresql.org/docs/current/pgstatstatements.html))。一个简单的分析例子: 48 | 49 | ```sql 50 | with time_period(delta_sec) as ( 51 | select extract(epoch from now() - stats_reset) 52 | from pg_stat_statements_info 53 | ) 54 | select 55 | now(), 56 | delta_sec, 57 | round(wal_bytes / delta_sec) as wal_bytes_per_sec, 58 | round(wal_bytes / calls) as wal_bytes_per_call, 59 | queryid 60 | from 61 | pg_stat_statements, 62 | time_period 63 | order by wal_bytes desc 64 | limit 25; 65 | ``` 66 | 67 | 2. `EXPLAIN`:使用 `explain (analyze, buffers, wal)` 来查看执行计划中的 WAL 指标: 68 | 69 | ```sql 70 | nik=# explain (analyze, buffers, wal) insert into t select i from generate_series(1, 100000) as i; 71 | QUERY PLAN 72 | ------------------------------------------------------------------------------------------------------------------------------------- 73 | Insert on t (cost=0.00..1000.00 rows=0 width=0) (actual time=159.378..159.378 rows=0 loops=1) 74 | Buffers: shared hit=100895 dirtied=442 written=442 75 | WAL: records=100000 fpi=1 bytes=5900343 76 | -> Function Scan on generate_series i (cost=0.00..1000.00 rows=100000 width=4) (actual time=26.179..30.696 rows=100000 loops=1) 77 | Planning Time: 1.945 ms 78 | Execution Time: 160.483 ms 79 | (6 rows) 80 | ``` 81 | 82 | ## 全页写 83 | 84 | pg_stat_statements 和 EXPLAIN 结果中的 "fpi" 指标表示发生了多少次全页镜像 (全页写)。 85 | 86 | 如果配置参数 `full_page_write` 为 on (默认情况下也是如此;通过 `show full_page_writes;` 检查),则在每个检查点之后,页面中的第一次更改会导致整个页面被写入 WAL 中。默认情况下,页面大小为 8 KiB,大多数 Postgres 安装中都是如此 (通过 `show block_size` 检查)。这意味着,如果只有很小一部分页面发生更改,在检查点之后仍需要写入整个页面。同一页面中的后续写入将正常进行 (只有更改会记录到 WAL 中),但是一旦发生新的检查点,那么需要再次先进行新的全页写。 87 | 88 | 更多信息可以参考以下文档: 89 | 90 | - Hironobu Suzuki 的 "The Internals of PostgreSQL"。第 9 章 "Write Ahead Logging – WAL",[9.1.3. Full-Page Writes](https://interdb.jp/pg/pgsql09.html#_9.1.3) 91 | - Egor Rogov 的 "PostgreSQL 14 internals","10.4 Recovery" 章节 92 | - Postgres wiki:[Full page writes](https://wiki.postgresql.org/wiki/Full_page_writes) 93 | - Tomas Vondra, [On the impact of full-page writes (2016)](https://2ndquadrant.com/en/blog/on-the-impact-of-full-page-writes/) 94 | 95 | ## 优化思想 96 | 97 | 以下是一些减少 WAL 生成量的优化建议: 98 | 99 | 1. **检查点优化:增加检查点间隔** 100 | 101 | 增加检查点之间的间隔有两个好处,特别是当工作负载包含许多随机写入 (不是连续的,比如在使用 `COPY` 进行大量数据加载时)时: 102 | 103 | - 更少的全页写 104 | - 减少对同一缓冲区的重复刷新 (刷新后,可能会由于新的写入而很快再次变脏)。 105 | 106 | 为了增加间隔,我们只需要增加 `max_wal_size` (默认 `1GB`) 和 `checkpoint_timeout` (默认 5 分钟)。但这需要权衡利弊:检查点之间的间隔越大,意味着在各种情况下需要重放更多的 WAL 才能达到一致性点: 107 | 108 | - 崩溃后的恢复时间更长 109 | 110 | - 从备份中配置新节点的时间更长。 111 | 112 | 不过,这种方法对于较大的规格来说是必不可少的,因为它可以带来实质性的改进。 113 | 114 | 调整 `max_wal_size` 和 `checkpoint_timeout` 不需要重新启动。 115 | 116 | 2. **检查点优化:开启 WAL 压缩** 117 | 118 | 考虑 `wal_compression` — 可以压缩全页写,大多数情况下,这样做是值得的 (尽管有些报告称这会导致更高的 CPU 使用率,并决定回退更改)。 119 | 120 | 修改此参数不需要重启。 121 | 122 | 3. **优化查询** 123 | 124 | 使用 `pg_stat_statements` 和 `EXPLAIN` 来定位生成大量 WAL 的查询,并进行优化。 125 | 126 | 优化写入的方法之一是鼓励 Postgres 使用更多的 `HOT UPDATE` (为此,我们需要确保页面有可用空间 — 令人惊讶的是,些许膨胀在此处是有益的 — 并且不会对表进行过度索引,因此我们正在更改的列不参与索引定义)。 127 | 128 | 4. **删除未使用和冗余的索引** 129 | 130 | 在查询优化期间,请记住,对于非 `HOT UPDATE` 和 `INSERT`,生成的 WAL 的量取决于表的索引数量。索引清理是一种非常有用的方法,可以减少此类写入产生的 WAL 量。 131 | 132 | 5. **分区** 133 | 134 | 对大 (100+ GiB) 表进行分区可以提高写入的数据局部性 — 例如,如果表未分区,那么一堆行的更新可能会分散在许多页面中,而使用定义了旧分区 (几乎没有接收写入) 和包含新数据分区的分区模式,大多数写入将集中在新分区中,这有助于降低 WAL 生成率。 -------------------------------------------------------------------------------- /How to remove a foreign key.md: -------------------------------------------------------------------------------- 1 | # How to remove a foreign key 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 删除一个外键 (FK) 非常简单: 6 | 7 | ```sql 8 | alter table messages 9 | drop constraint fk_messages_users; 10 | ``` 11 | 12 | 然而,在高负载下,这并不是一个安全的操作,原因参照 [zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries) 文中所述: 13 | 14 | - 此操作在两个表上均需要获取短暂的 `AccessExclusiveLock`。 15 | - 如果至少有一个锁无法快速获取,此操作便需要等待,可能会阻塞其他会话 (包括 `SELECT`) 对这两个表的访问。 16 | 17 | 为了解决这个问题,我们需要使用较低的 `lock_timeout` 并进行重试: 18 | 19 | ```sql 20 | set lock_timeout to '100ms'; 21 | alter ... -- be ready to fail and try again. 22 | ``` 23 | 24 | 同样的技巧也需要用于创建一个新外键操作的第一步,当我们使用 `NOT VALID` 选项定义一个新外键时,正如我们在第 70 天的 [How to add a foreign key](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0070_how_to_add_a_foreign_key.md) 中讨论的那样。 25 | 26 | 另外请参考:[day 71, How to understand what's blocking DDL](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0071_how_to_understand_what_is_blocking_ddl.md) -------------------------------------------------------------------------------- /How to set application_name without extra queries.md: -------------------------------------------------------------------------------- 1 | # How to set application_name without extra queries 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | `application_name` 非常有用,可以控制你和他人在 `pg_stat_activity` 中看到的内容 (该视图中有一个同名的列),以及使用此系统视图的各种工具。此外,当 `log_line_prefix` 包含 `%a` 时,它还会出现在 Postgres 的日志中。 6 | 7 | 文档:[application_name](https://postgresql.org/docs/current/runtime-config-logging.html#GUC-APPLICATION-NAME) 8 | 9 | 设置 `application_name` 是一个很好的实践 — 例如,在事件发生期间或之后进行根本原因分析时非常有帮助。 10 | 11 | 以下方法也可以用于设置其他设置 (包括常规的 Postgres 参数,如 `statement_timeout` 或 `work_mem`),但此处我们重点关注 `application_name`。 12 | 13 | 通常,`application_name` 是通过 `SET` 设置的 (抱歉使用了同源词): 14 | 15 | ```sql 16 | nik=# show application_name; 17 | application_name 18 | ------------------ 19 | psql 20 | (1 row) 21 | 22 | nik=# set application_name = 'human_here'; 23 | SET 24 | 25 | nik=# select application_name, pid from pg_stat_activity where pid = pg_backend_pid() \gx 26 | -[ RECORD 1 ]----+----------- 27 | application_name | human_here 28 | pid | 93285 29 | ``` 30 | 31 | 然而,即使是一个非常快的查询,也意味着额外的RTT ([round-trip time](https://en.wikipedia.org/wiki/Round-trip_delay)),影响延迟,尤其是在与远程服务器进行通信时。 32 | 33 | 为了避免这种情况,可以使用 `libpq` 的选项。 34 | 35 | ## 方法1:通过环境变量 36 | 37 | ```bash 38 | ❯ PGAPPNAME=myapp1 psql \ 39 | -Xc "show application_name" 40 | application_name 41 | ------------------ 42 | myapp1 43 | (1 row) 44 | ``` 45 | 46 | (`-X` 表示忽略 `.psqlrc`,这是在自动化脚本中使用 `psql` 时的良好实践。) 47 | 48 | ## 方法2:通过连接URI 49 | 50 | ```bash 51 | ❯ psql \ 52 | "postgresql://?application_name=myapp2" \ 53 | -Xc "show application_name" 54 | application_name 55 | ------------------ 56 | myapp2 57 | (1 row) 58 | ``` 59 | 60 | URI 方法优先于 `PGAPPNAME`。 61 | 62 | ## 在应用程序代码中 63 | 64 | 所描述的方法不仅可以与 psql 一起使用。以 Node.js 为例: 65 | 66 | ```bash 67 | ❯ node -e " 68 | const { Client } = require('pg'); 69 | 70 | const client = new Client({ 71 | connectionString: 'postgresql://?application_name=mynodeapp' 72 | }); 73 | 74 | client.connect() 75 | .then(() => client.query('show application_name')) 76 | .then(res => { 77 | console.log(res.rows[0].application_name); 78 | client.end(); 79 | }); 80 | " 81 | mynodeapp 82 | ``` -------------------------------------------------------------------------------- /How to speed up bulk load.md: -------------------------------------------------------------------------------- 1 | # How to speed up bulk load 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 如果你需要加载大量数据,以下技巧可以帮助你提升加载速度: 6 | 7 | ## 1) 使用 COPY 8 | 9 | 使用 `COPY` 加载数据,它针对批量加载进行了优化。 10 | 11 | ## 2) 减少检查点频率 12 | 13 | 考虑暂时增加 `max_wal_size` 和 `checkpoint_timeout` 的值。 14 | 15 | 调整这些参数不需要重启。更大的值意味着发生故障时恢复时间会更长,但好处是检查点发生的频率减少,因此: 16 | 17 | - 对磁盘的压力减小, 18 | 19 | - 由于相同页面的全页写次数减少,写入的 WAL 数据也减少 (特别是在存在索引的情况下加载数据)。 20 | 21 | ## 3) 增加缓冲池 22 | 23 | 如果可能,增加 `shared_buffers` 的大小。 24 | 25 | ## 4) 减少 (或移除) 索引 26 | 27 | 如果数据加载到一个新表中,可以在数据加载完成后再创建索引。如果是加载到一个现有表中,[避免过多的索引。](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0018_over_indexing.md) 28 | 29 | 每个额外的索引都会显著减慢数据加载的速度。 30 | 31 | ## 5) 减少 (或移除) 外键和触发器 32 | 33 | 与索引类似,外键约束和触发器也可能会显著减慢数据加载速度。可以考虑在批量加载后再 (重新) 创建它们。 34 | 35 | 触发器可以通过 `ALTER TABLE ... DISABLE TRIGGERS ALL` 禁用 — 但如果触发器支持某些一致性检查,需要确保这些检查没有被违反 (例如,数据加载后运行额外的检查)。外键通过隐式触发器实现,`ALTER TABLE ... DISABLE TRIGGERS ALL `也会禁用它们 — 在这种情况下加载数据时要特别小心。 36 | 37 | ## 6) 避免WAL写入 38 | 39 | 如果这是一个新表,可以考虑在数据加载期间完全避免 WAL 写入。有两种选择 (都有限制,需要理解如果发生崩溃,数据可能会丢失): 40 | 41 | - 使用无日志表:`CREATE UNLOGGED TABLE ...`。无日志表不会归档,也不会复制,且不是持久化的 (虽然它们在正常重启时仍然存在)。不过,将无日志表转换为普通表可能需要一些时间(通常需要很长时间 — 值得测试),因为数据需要写入WAL。有关未记录表的更多信息,请参阅这篇文章;另请参阅此 [StackOverflow](https://dba.stackexchange.com/questions/195780/set-postgresql-table-to-logged-after-data-loading/195829#195829) 讨论。 42 | - 使用 `wal_level='minimal'` 的 `COPY`。`COPY` 必须在建表的事务内执行。由于`wal_level='minimal'`,`COPY` 写入的数据不会记录在WAL中(对于 PG16,只有非分区表才能使用此选项)。此外,可以考虑使用 `COPY (FREEZE)`,这一方法的好处是数据加载完成后所有元组都会被冻结。设置 `wal_level='minimal'` 需要重启 Postgres,还需要其他更改 (如 `archive_mode = 'off'`,`max_wal_senders = 0`)。这种方法通常不适合生产环境,但对于单服务器配置可以很好地工作。有关 wal_level='minimal' + COPY (FREEZE) 方法的详细信息,请参阅[此帖](https://www.cybertec-postgresql.com/en/loading-data-in-the-most-efficient-way/)。 43 | 44 | ## 7) 并行化 45 | 46 | 考虑并行化。这可能会加速过程,但也取决于单线程进程的瓶颈 (例如,如果单线程负载使磁盘 IO 饱和,则并行化将无济于事)。有两种选择: 47 | 48 | - 分区表和多进程加载多个分区。可以使用多个工作进程并行加载 ([Day 20: pg_restore tips]())。 49 | - 非分区表和大块数据加载。这需要事先准备大块数据 — 可以将 CSV 文件拆分成多个部分,或使用多个同步的 `REPEATABLE READ` 事务导出表数据的范围 (通过 `SET TRANSACTION SNAPSHOT `在同一个快照下工作)。参照:[Day 8: How to speed up pg_dump](). 50 | 51 | 如果你使用 TimescaleDB,考虑使用 [timescaledb-parallel-copy ](https://github.com/timescale/timescaledb-parallel-copy)工具。 52 | 53 | 最后但同样重要的是:在进行大规模数据加载后,不要忘记运行 `ANALYZE`。 54 | 55 | # 我见 56 | 57 | 过多索引,我写了一篇文章进行分析:[慢工出细活,久久方为功](https://mp.weixin.qq.com/s?__biz=MzUyOTAyMzMyNg==&mid=2247492180&idx=1&sn=a9385efacbcf77564fbb59924ba3a1fe&chksm=fa65ca65cd1243733ce53b24026780cca8bca70ba5b1025a046c423df26bfacd978b3baef2e0&token=1396136569&lang=zh_CN#rd),另外关于 minimal + copy 的技巧,也值得写一篇,PostgreSQL 14 internals 一书中也有介绍,敬请期待。 -------------------------------------------------------------------------------- /How to troubleshoot a growing pg_wal directory.md: -------------------------------------------------------------------------------- 1 | # How to troubleshoot a growing pg_wal directory 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | `$PGDATA/pg_wal` 目录包含 WAL 文件。WAL (预写式日志) 是用于备份、恢复以及物理复制和逻辑复制的核心机制。 6 | 7 | 在某些情况下,`pg_wal` 目录大小会持续增长,这可能会引发担忧,因为它会增加磁盘空间耗尽的风险。 8 | 9 | 以下是排查增长中的`pg_wal`目录问题的步骤。 10 | 11 | ## 步骤1:检查复制槽 12 | 13 | 未使用或滞后的复制槽会阻止 WAL 文件被回收,因此 `pg_wal` 目录的大小会增加。在主库上检查: 14 | 15 | ```sql 16 | select 17 | pg_size_pretty(pg_current_wal_lsn() - restart_lsn) as lag, 18 | slot_name, 19 | wal_status, 20 | active 21 | from pg_replication_slots 22 | order by 1 desc; 23 | ``` 24 | 25 | 参考文档: [The view pg_replication_slots](https://postgresql.org/docs/current/view-pg-replication-slots.html) 26 | 27 | - 如果有非活跃状态的复制槽,考虑删除它们以防止磁盘空间达到 100%。一旦删除了有问题的槽,Postgres 将会移除旧的 WAL 文件。 28 | - 另外,考虑使用 `max_slot_wal_keep_size` (PG13+)。 29 | 30 | ## 步骤2:检查`archive_command`是否正常工作 31 | 32 | 如果配置了 `archive_mode` 和 `archive_command` 来归档 WAL 文件 (例如用于备份),但 `archive_command` 失败 (返回了一个非零的退出代码) 或滞后 (WAL 的生成速率超过归档速度),这可能是 `pg_wal` 增长的另一个原因。 33 | 34 | 如何监控和排查: 35 | 36 | - 检查 [pg_stat_archiver](https://postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ARCHIVER-VIEW) 37 | - 检查 Postgres 日志 (例如,查看`archive command failed with exit code 1`) 38 | 39 | 一旦确定了问题,`archive_command` 需要修复或提升速度 (例如,启用 `wal_compression = on`,增加 `max_wal_size` 以减少 WAL 数据的生成;同时,在归档工具中使用较轻的压缩方式 — 具体取决于 `archive_command `中使用的工具。例如,WAL-G 支持多种压缩选项,使用的 CPU 强度不同)。 40 | 41 | 接下来的两个步骤是附加的,因为它们对 `pg_wal` 大小增长的影响有限 — 它们只会导致一定数量的额外 WAL 文件保存在 `pg_wal` 中 (不像我们前面讨论的两种原因,它们可能会导致更大的影响)。 42 | 43 | ## 步骤3:检查`wal_keep_size` 44 | 45 | 在某些情况下,`wal_keep_size` ([PG13+ docs](https://postgresqlco.nf/doc/en/param/wal_keep_size/);在 PG12 及更早版本中,请参照`wal_keep_segments`)被设置为较大的值。当使用复制槽时,通常不需要使用这个机制 — 这是在复制槽机制出现之前的旧方法,用于避免某些 WAL 文件被删除,导致滞后的副本无法跟上。 46 | 47 | ## 步骤4:检查`max_wal_size`和`checkpoint_timeout` 48 | 49 | 当成功的检查点发生时,Postgres 可以删除旧的WAL文件。在某些情况下,如果调整了检查点设置以减少检查点的频率,这可能会导致存储在 `pg_wal` 中的 WAL 文件要多于预期。如果这对磁盘空间造成问题 (特别是在小型服务器上),可以考虑将 `max_wal_size` 和 `checkpoint_timeout` 设置为较低的值。在某些情况下,手动执行 `CHECKPOINT` 命令也是合理的,以便 Postgres 立即清理一些旧文件。 50 | 51 | ## 总结 52 | 53 | 最重要的检查项: 54 | 55 | - 检查未使用或滞后的复制槽 56 | - 检查失败或滞后的 `archive_command` 57 | 58 | 此外: 59 | 60 | - 检查 `wal_keep_size` 61 | - 检查 `max_wal_size` 和 `checkpoint_timeout` -------------------------------------------------------------------------------- /How to troubleshoot and speed up Postgres stop and restart attempts.md: -------------------------------------------------------------------------------- 1 | # How to troubleshoot and speed up Postgres stop and restart attempts 2 | 3 | #Postgres马拉松第2天。让我们来讨论有关 PostgreSQL 关闭和重启的问题。在实际操作中,遇到长时间甚至失败的关闭/重启操作并不罕见。如果这些问题发生在故障排查期间,往往会引发情绪波动和额外的失误。 4 | 5 | 导致关闭操作时间延长的一些常见原因: 6 | 7 | - 存在长时间运行的事务 8 | - 大量缓冲区是脏的 (更改已在内存中应用,但尚未同步到磁盘,正在等待另一个检查点),导致关闭时的检查点时间过长。 9 | - WAL 归档 (`archive_command`) 滞后。 10 | 11 | 下面,我将讨论每个原因以及如何缓解。 12 | 13 | ## 原因 1: 长时间运行的事务 14 | 15 | 如果你怀疑第一个原因,请检查是否有长时间运行的事务: 16 | 17 | ```sql 18 | select 19 | clock_timestamp(), 20 | clock_timestamp() - xact_start, 21 | * 22 | from pg_stat_activity 23 | where clock_timestamp() - xact_start > interval '10 second' 24 | order by clock_timestamp() - xact_start desc 25 | limit 10; 26 | ``` 27 | 28 | 在这种情况下,通常建议使用 `SIGINT` (`pg_ctl stop -m fast`),即所谓的"快速关闭"模式 (参见 [Postgres 文档](https://www.postgresql.org/docs/current/server-shutdown.html))。 29 | 30 | ## 原因 2: 长时间的关闭时检查点 31 | 32 | 第二个原因是缓冲池中有大量脏缓冲区,这个问题不太容易排查,但幸运的是,解决起来很简单。特别是在以下情况下容易出现: 33 | 34 | - 缓冲池大小 (`shared_buffers`) 很大 35 | - 进行了检查点调整,旨在减少大规模随机写入和减少全页写的开销 (通常意味着增大 `max_wal_size` 和 `checkpoint_timeout`) 36 | - 最近的检查点迄今已过去太久,可以在启用 `log_checkpoint = on` 的情况下从 PG 日志中看到,建议在大多数情况下启用此选项)。 37 | 38 | 脏缓冲区的数量很容易观测,可以使用扩展模块 pg_buffercache (标准的 contrib 模块)并运行以下查询 (可能需要较长时间;详见[文档](https://postgresql.org/docs/current/pgbuffercache.html)): 39 | 40 | ```sql 41 | select count(*), pg_size_pretty(count(*) * 8 * 1024) 42 | from pg_buffercache 43 | where isdirty; 44 | ``` 45 | 46 | 如果数值较大 (例如几个 GiB),在尝试关闭时,Postgres 将执行所谓的"关闭时检查点",将脏缓冲区刷新到磁盘 ([源码](https://gitlab.com/postgres/postgres/blob/ebf76f2753a91615d45f113f1535a8443fa8d076/src/backend/access/transam/xlog.c#L6229))。在此期间,它不会处理查询,这会影响停机时间。解决方法很简单——在尝试关闭/重启之前显式执行一个 CHECKPOINT: 47 | 48 | ```sql 49 | checkpoint; 50 | ``` 51 | 52 | 这将有助于我们使得关闭时检查点非常轻量化,减少停机时间。 53 | 54 | 在某些情况下,可能需要在尝试关闭前连续执行两次显式 CHECKPOINT:如果第一个 CHECKPOINT 很重,可能会花费时间,而在此期间由于持续写入,新的脏缓冲区会积累——我们通过第二个 CHECKPOINT 来减轻这一问题,使关闭检查点保持轻量且快速。 55 | 56 | 要模拟此处描述的情况: 57 | 58 | - 确保 `shared_buffers` 很大 (几 GiB;更改此项需要重启) 59 | - 增大 `max_wal_size` 和 `checkpoint_timeout` (更改不需要重启):例如 `'10GB'` 和 `'60min'` (确保 `pg_wal` 子目录中有足够的磁盘空间) 60 | - 在一个大表 t1 上执行:`set statement_timeout = '60s'; begin; delete from t1;` (将被取消,但会产生大量脏缓冲区) 61 | - 安装 pg_buffercache 并检查上述脏缓冲区的数量 62 | - 持续监控 Postgres 日志中的检查点记录 (`log_checkpoint=on`) 63 | 64 | 然后测量 `pg_ctl stop -m fast` 的时间——它将花费较长时间。重复同样的步骤,但在尝试关闭之前显式执行 CHECKPOINT。 65 | 66 | 这个建议 (显式 CHECKPOINT) 在涉及关闭或重启的自动化中非常重要,特别是在需要可预测的时间时。(例如,即使我们不打算在重启 Postgres 时将停机时间最小化,我们也不希望它花费很多分钟,可能会触发我们自动化工具中的各种超时限制。) 67 | 68 | ## 原因 3: archive_command 失败/滞后 69 | 70 | 这是一个不幸的情况:`pg_ctl stop -m fast` 会断开正在进行的会话,但它给 WAL 归档进程一个归档待处理 WAL 的机会,这会影响关闭的时间 (逻辑很复杂,可以在这些文件中找到:postmaster.c,pgarch.c)。 71 | 72 | WAL 归档滞后应该被视为一个严重事件,因为它会影响备份的健康状态以及 DR 的 RPO/RTO。这需要被监控,在出现显著滞后 (例如,几十个未归档的 WAL 文件) 时,应将其视为一次事件。此类监控应包括 `pg_stat_archiver`,最好还有当前 LSN 的信息,以计算滞后量 (以 WAL 文件中的字节为单位)。至少,监控/告警应涵盖 `last_failed_wal` 和 `last_failed_time`。 73 | 74 | 有趣的是,归档命令慢/失败也会导致切换/故障转移时的停机时间变长——例如,在 Patroni 2.1.2 之前的版本中,这是一个问题,后续版本修复了这一行为,旨在加快故障转移速度 (["Release the leader lock when pg_controldata reports 'shut down'"](https://github.com/zalando/patroni/blob/master/docs/releases.rst#version-212)). 75 | 76 | ## 总结 77 | 78 | - 如果不希望现有会话正常结束工作,请使用"快速模式" (`pg_ctl stop -m fast`) 79 | - 在尝试关闭或重启前始终执行显式 CHECKPOINT 80 | - 监控 `pg_stat_archiver` 并确保它无故障和滞后运行 81 | 82 | 就是这样。注意,我们在这里没有讨论各种超时 (例如 pg_ctl 的 `--timeout` 选项以及等待行为 `-w`, `-W`,详见 [Postgres 文档](https://postgresql.org/docs/current/app-pg-ctl.html)),而只是讨论了可能导致关闭/重启尝试延迟的原因。 希望这对你有所帮助——像往常一样,记得订阅、点赞、分享和评论!💙 83 | 84 | # 我见 85 | 86 | 停止 PostgreSQL 包括三种模式 87 | 88 | 1. “Smart” mode disallows new connections, then waits for all existing clients to disconnect. If the server is in hot standby, recovery and streaming replication will be terminated once all clients have disconnected. 89 | 2. “Fast” mode (the default) does not wait for clients to disconnect. All active transactions are rolled back and clients are forcibly disconnected, then the server is shut down. 90 | 3. “Immediate” mode will abort all server processes immediately, without a clean shutdown. This choice will lead to a crash-recovery cycle during the next server start. 91 | 92 | 为什么停库可能变慢各位还可以参照 [PgSQL · 应用案例 · PG有standby的情况下为什么停库可能变慢?](http://mysql.taobao.org/monthly/2019/09/10/) -------------------------------------------------------------------------------- /How to troubleshoot streaming replication lag.md: -------------------------------------------------------------------------------- 1 | # How to troubleshoot streaming replication lag 2 | 3 | PostgreSQL 流复制允许从主服务器到备服务器进行持续的数据复制,以确保高可用并平衡只读负载。然而,复制延迟可能会发生,从而导致数据同步延迟。本文提供了排查和缓解复制延迟的步骤。 4 | 5 | ## 识别延迟位置 6 | 7 | 在分析之前,我们需要了解实际存在延迟的位置,以及位于复制的哪个阶段: 8 | 9 | - 通过 `walsender` 发送 WAL 流到备服务器的网络传输 10 | - 通过 `walreceiver` 从网络接收 WAL 流到备服务器 11 | - 备服务器上通过 `walreceiver` 写入 WAL 到磁盘 12 | - 作为恢复过程的 WAL 应用 (回放) 13 | 14 | 因此,流复制延迟可以分为三类: 15 | 16 | 1. **写延迟**:事务在主服务器提交后,数据写入备服务器的 WAL 之间的延迟。 17 | 2. **刷新延迟**:事务写入备服务器的 WAL 后,刷新到磁盘之间的延迟。 18 | 3. **应用延迟**:事务从刷新到磁盘后,应用到备服务器数据库的延迟。 19 | 20 | ## 分析查询 21 | 22 | 要识别延迟,请使用以下 SQL 查询: 23 | 24 | ```sql 25 | select 26 | pid, 27 | client_addr, 28 | application_name, 29 | state, 30 | coalesce(pg_current_wal_lsn() - sent_lsn, 0) AS sent_lag_bytes, 31 | coalesce(sent_lsn - write_lsn, 0) AS write_lag_bytes, 32 | coalesce(write_lsn - flush_lsn, 0) AS flush_lag_bytes, 33 | coalesce(flush_lsn - replay_lsn, 0) AS replay_lag_bytes, 34 | coalesce(pg_current_wal_lsn() - replay_lsn, 0) AS total_lag_bytes 35 | from pg_stat_replication; 36 | ``` 37 | 38 | ## 示例 39 | 40 | 查询结果可能如下所示: 41 | 42 | ```sql 43 | postgres=# select 44 | pid, 45 | client_addr, 46 | application_name, 47 | state, 48 | coalesce(pg_current_wal_lsn() - sent_lsn, 0) AS sent_lag_bytes, 49 | coalesce(sent_lsn - write_lsn, 0) AS write_lag_bytes, 50 | coalesce(write_lsn - flush_lsn, 0) AS flush_lag_bytes, 51 | coalesce(flush_lsn - replay_lsn, 0) AS replay_lag_bytes, 52 | coalesce(pg_current_wal_lsn() - replay_lsn, 0) AS total_lag_bytes 53 | from pg_stat_replication; 54 | 55 | pid | client_addr | application_name | state | sent_lag_bytes | write_lag_bytes | flush_lag_bytes | replay_lag_bytes | total_lag_bytes 56 | ---------+----------------+------------------+-----------+----------------+-----------------+-----------------+------------------+----------------- 57 | 3602908 | 10.122.224.101 | backupmachine1 | streaming | 0 | 728949184 | 0 | 0 | 0 58 | 2490863 | 10.122.224.102 | backupmachine1 | streaming | 0 | 519580176 | 0 | 0 | 0 59 | 2814582 | 10.122.224.103 | replica1 | streaming | 0 | 743384 | 0 | 1087208 | 1830592 60 | 3596177 | 10.122.224.104 | replica2 | streaming | 0 | 2426856 | 0 | 4271952 | 6698808 61 | 319473 | 10.122.224.105 | replica3 | streaming | 0 | 125080 | 162040 | 4186920 | 4474040 62 | ``` 63 | 64 | ## 如何解读结果 65 | 66 | `_lsn` 的含义: 67 | 68 | - `sent_lsn`:已经通过网络发送的 WAL (LSN 位置)。 69 | - `write_lsn`:已经发送到操作系统的 WAL (LSN 位置),还未刷至磁盘。 70 | - `flush_lsn`:已经刷新到磁盘的 WAL (LSN 位置),已写入磁盘。 71 | - `replay_lsn`:已经应用到数据库的 WAL (LSN 位置),对查询可见。 72 | 73 | 因此,延迟是 `pg_current_wal_lsn` 与 `replay_lsn` 之间的差值 (`total_lag_bytes`,并且将其添加到监控中是一个好主意)。但为了排查问题,我们需要监控所有四种延迟。 74 | 75 | - `sent_lag_bytes` 的延迟意味着我们在发送数据时遇到问题,例如主服务器上的 `WALsender` CPU 饱和或网络负载过重。 76 | - `write_lag_bytes` 的延迟意味着我们在接收数据时遇到问题,例如备服务器上的 `WALreceiver` CPU 饱和或网络负载过重。 77 | - `flush_lag_bytes` 的延迟意味着我们在备服务器上将数据写入磁盘时遇到问题,例如 `WALreceiver` 的CPU 饱和或 I/O 瓶颈。 78 | - `replay_lag_bytes` 的延迟意味着我们在应用 WAL 时遇到问题,通常是 CPU 饱和或 Postgres 进程的 IO 争用 79 | 80 | 一旦定位了问题,我们需要在操作系统级别排查相关进程,以找出瓶颈所在。 81 | 82 | ## 可能的瓶颈 83 | 84 | 待定 85 | 86 | ## 额外资源 87 | 88 | - [Streaming replication](https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION) (官方文档) 89 | - [pg_stat_replication view](https://www.postgresql.org/docs/current/monitoring-stats.html#PG-STAT-REPLICATION-VIEW) (官方文档) 90 | - [Replication configuration parameters](https://www.postgresql.org/docs/current/runtime-config-replication.html) (官方文档) 91 | 92 | ## 作者/维护者 93 | 94 | - Dmitry Fomin 95 | - Sadeq Dousti 96 | - Nikolay Samokhvalov -------------------------------------------------------------------------------- /How to tune Linux parameters for OLTP Postgres.md: -------------------------------------------------------------------------------- 1 | # How to tune Linux parameters for OLTP Postgres 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 以下是一些 Linux 基础调优的通用建议,以便在高并发 OLTP (比如 Web/移动应用) 负载下运行 Postgres。这些参数大部分都是 [postgresql_cluster](https://github.com/vitabaks/postgresql_cluster) 中的默认设置。 6 | 7 | 请将下列参数视为进一步研究的切入点,文中提供的值仅作为粗略的调优参考,需根据具体情况进行审查。 8 | 9 | 大部分参数可在 `sysctl.conf` 中修改,更改后需要调用: 10 | 11 | ```bash 12 | sysctl -p /etc/sysctl.conf 13 | ``` 14 | 15 | 临时更改 (以 `vm.swappiness` 为例): 16 | 17 | ```bash 18 | sudo sysctl -w vm.swappiness=1 19 | ``` 20 | 21 | 或者: 22 | 23 | ```bash 24 | echo 1 | sudo tee /proc/sys/vm/swappiness 25 | ``` 26 | 27 | ## 内存管理 28 | 29 | 1. `vm.overcommit_memory = 2` 30 | 31 | 避免内存过度分配,防止 OOM killer 影响 Postgres。 32 | 33 | 2. `vm.swappiness = 1` 34 | 35 | 最小化 swap 使用,避免完全禁用。 36 | 37 | > 💀这是一个具有争议的话题。我个人在关键任务系统中在高负载下使用 0 来禁用 swap,承担OOM 风险。不过,许多专家建议不要完全关闭,可以使用较低值—1 或 10。 38 | 39 | **相关优质文章:** 40 | 41 | - [Deep PostgreSQL Thoughts: The Linux Assassin](https://crunchydata.com/blog/deep-postgresql-thoughts-the-linux-assassin) (2021,k8s环境),作者 [@josepheconway](https://twitter.com/josepheconway) 42 | - [PostgreSQL load tuning on Red Hat Enterprise Linux](https://redhat.com/en/blog/postgresql-load-tuning-red-hat-enterprise-linux) (2022) 43 | 44 | 3. `vm.min_free_kbytes = 102400` 45 | 46 | 确保在内存分配高峰期间为 Postgres 预留可用内存。 47 | 48 | 4. `transparent_hugepage/enabled=never`,`transparent_hugepage/defrag=never` 49 | 50 | 禁用透明大页 (THP),以防止不适合 Postgres OLTP 工作负载的延迟和碎片。一般建议在 OLTP 系统中禁用 THP (例如 Oracle)。 51 | 52 | - [Ubuntu/Debian](https://stackoverflow.com/questions/44800633/how-to-disable-transparent-huge-pages-thp-in-ubuntu-16-04lts) 53 | - [Red Hat](https://access.redhat.com/solutions/46111) 54 | 55 | ## I/O管理 56 | 57 | 5. `vm.dirty_background_bytes = 67108864` 58 | 59 | 6. `vm.dirty_bytes = 536870912` 60 | 61 | 调整 [pdflush](https://lwn.net/Articles/326552/) 以防止 IO 延迟峰值,详见 [@grayhemp](https://twitter.com/grayhemp) 的 [PgCookbook - a PostgreSQL documentation project](https://github.com/grayhemp/pgcookbook/blob/master/database_server_configuration.md)。 62 | 63 | ## 网络配置 64 | 65 | > 📝 以下是 ipv4 配置;🎯 **TODO:**ipv6 配置待补充 66 | 67 | 7. `net.ipv4.ip_local_port_range = 10000 65535` 68 | 69 | 允许处理更多客户端连接。 70 | 71 | 8. `net.core.netdev_max_backlog = 10000` 72 | 73 | 应对网络流量高峰,避免丢包。 74 | 75 | 9. `net.ipv4.tcp_max_syn_backlog = 8192` 76 | 77 | 适应高并发连接请求。 78 | 79 | 10. `net.core.somaxconn = 65535` 80 | 81 | 增加套接字连接队列的上限。 82 | 83 | 11. `net.ipv4.tcp_tw_reuse = 1` 84 | 85 | 减少高吞吐量 OLTP 应用的连接建立时间。 86 | 87 | ## NUMA配置 88 | 89 | 12. `vm.zone_reclaim_mode = 0` 90 | 91 | 避免跨 NUMA 节点的内存回收对 Postgres 造成性能影响。 92 | 93 | 13. `kernel.numa_balancing = 0` 94 | 95 | 禁用自动 NUMA 平衡,以提高 Postgres 的 CPU 缓存效率。 96 | 97 | 14. `kernel.sched_autogroup_enabled = 0` 98 | 99 | 改善 Postgres 进程调度的延迟。 100 | 101 | ## 文件系统和文件句柄 102 | 103 | 15. `fs.file-max = 262144` 104 | 105 | 设置 Linux 内核可以分配的最大文件句柄数量。在运行像 Postgres 这样的数据库服务器时,足够的文件描述符对于处理大量连接和文件至关重要。 106 | 107 | > 🎯 TODO:根据不同主流操作系统进行进一步审查和调整。 108 | 109 | ## 我见 110 | 111 | 文中提到了几个不错的工具: 112 | 113 | - [PgCookbook - a PostgreSQL documentation project](https://github.com/grayhemp/pgcookbook) 114 | 115 | > The project is a continuously updating set of articles, scripts and configuration files made to help with PostgreSQL maintenance. The articles and files might be modified as new versions of software or new ways of doing things appear. Stay tuned. 116 | 117 | - [PgToolkit - tools for PostgreSQL maintenance](https://github.com/grayhemp/pgtoolkit) 118 | 119 | >Currently the package contains the only tool `pgcompact`, we are planning to add much more in the future. Stay tuned. 120 | > 121 | >The list of changes can be found in [CHANGES.md](https://github.com/grayhemp/pgtoolkit/blob/master/CHANGES.md). The To-Do List is in [TODO.md](https://github.com/grayhemp/pgtoolkit/blob/master/TODO.md). -------------------------------------------------------------------------------- /How to tune work_mem.md: -------------------------------------------------------------------------------- 1 | # How to tune work_mem 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | `work_mem` 用于查询执行期间排序和哈希操作的内存分配。默认情况下,其大小为 4MB。 由于其性质,调优 `work_mem` 相对复杂。 6 | 7 | 这里介绍了一种可能的调优方法。 8 | 9 | ## work_mem 的粗略调优和安全值 10 | 11 | 首先,按照 [Day 89: Rough configuration tuning (80/20 rule; OLTP)](https://postgres-howto.cn/#/./docs/89) 中的描述进行粗略优化。 12 | 13 | 一个查询可能会多次"消耗" `work_mem` (用于多个操作)。但是,并不是为每个操作都完全分配 — 一些操作可能只需要较少的内存。 14 | 15 | 因此,很难可靠地预测处理工作负载时需要使用多少内存,而不经过对工作负载的实际观察。 16 | 17 | 此外,在 PostgreSQL 13 中,新增了一个参数 [hash_mem_multiplier](https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/),调整了相关逻辑。PG 13-16 版本中的默认值为 2,这意味着单个哈希操作所用的最大内存为 `2 * work_mem`。 18 | 19 | 值得一提的是,在 Linux 中了解会话使用了多少内存本身是非常困难的 — 可以参考Andres Freund 的精彩文章:[Analyzing the Limits of Connection Scalability in Postgres](https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/analyzing-the-limits-of-connection-scalability-in-postgres/ba-p/1757266#memory-usage) (2020 年)。 20 | 21 | 一种安全的调优方法是: 22 | 23 | - 估算可用内存 — 从中减去 `shared_buffers`、`maintenance_work_mem` 等 24 | - 然后将估算的可用内存除以 `max_connections` 和一个附加值 (例如 4-5,或者更多,以确保安全) — 假设每个后端进程最多使用 `4 * work_mem` 或 `5 * work_mem`。当然,这个乘数本身只是一个粗略估计 — 实际上,OLTP 工作负载通常对内存的需求远远低于这个值 (例如,进行大量主键查找意味着平均内存消耗非常低)。 25 | 26 | 在实践中,调整 `work_mem` 至一个较高值是有意义的,但前提是理解了 PostgreSQL 在特定工作负载下的行为之后再进行。以下步骤是进一步调优的迭代方法的一部分。 27 | 28 | ## 临时文件监控 29 | 30 | 监控临时文件的创建频率及其大小。相关信息来源: 31 | 32 | - `pg_stat_database`,`temp_files` 和 `temp_bytes` 列。 33 | - PostgreSQL 日志 — 设置 `log_temp_files` 为一个较低的值,甚至为 0 (需要注意观察者效应)。 34 | - `pg_stat_statements` 中的 `temp_blks_read` 和 `temp_blks_written`。 35 | 36 | 进一步调优 `work_mem` 的目标是完全消除临时文件的创建,或尽量减少其创建频率和大小。 37 | 38 | ## 优化查询 39 | 40 | 如果可能,考虑优化那些涉及临时文件创建的查询。为此投入适当的精力 — 如果没有明显的优化空间,继续执行下一步。 41 | 42 | ## 进一步提升work_mem:多少合适? 43 | 44 | 如果已经做了合理的优化工作,现在可以考虑进一步提高 `work_mem`。 45 | 46 | 然而,提升多少合适呢?答案取决于单个临时文件的大小 — 通过上面描述的监控,我们可以找到最大和平均临时文件大小,并从中估算所需的提升量。 47 | 48 | > 🎯 **TODO:** 详细步骤 49 | 50 | ## 针对部分工作负载提升work_mem 51 | 52 | 针对单个查询提高 `work_mem` 是有意义的。考虑两种方式: 53 | 54 | - 在单个会话或事务中设置 `work_mem` (使用 `set local ...`),或者 55 | - 如果为不同的工作负载部分使用不同的数据库用户,考虑针对特定用户调整 `work_mem` (例如,针对那些执行分析类查询的用户):`alter user ... set work_mem ...`。 56 | 57 | ## 全局提升work_mem 58 | 59 | 只有在前述步骤不适用时 (例如,查询优化困难且无法针对部分工作负载调优 `work_mem`) 才考虑全局提升 `work_mem`,同时评估 OOM 的风险。 60 | 61 | ## 迭代优化 62 | 63 | 一段时间后,审查监控数据,确保情况有所改善,或决定进行下一次迭代。 64 | 65 | ## 额外内容:pg_get_backend_memory_contexts 66 | 67 | 在 PostgreSQL 14 中,新增了一个函数 `pg_get_backend_memory_contexts()` 及其对应的视图;请参考[文档](https://postgresql.org/docs/current/view-pg-backend-memory-contexts.html)。这对于详细分析当前会话如何使用内存非常有帮助,但该功能的主要限制是它只能与当前会话一起使用。 68 | 69 | > 🎯 **TODO:** 如何使用它。 70 | 71 | -------------------------------------------------------------------------------- /How to understand LSN values and WAL filenames.md: -------------------------------------------------------------------------------- 1 | # How to understand LSN values and WAL filenames 2 | 3 | ## 如何读取 LSN 值 4 | 5 | LSN — 日志序列号,是指向预写日志 (WAL) 中某个位置的指针。理解并学会如何使用它,对于处理流复制、逻辑复制、备份和恢复非常重要。Postgres 相关文档: 6 | 7 | - [WAL Internals](https://postgresql.org/docs/current/wal-internals.html) 8 | - [pg_lsn Type](https://postgresql.org/docs/current/datatype-pg-lsn.html) 9 | 10 | LSN 是一个 8 字节 (32 位) 的值 ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L17))。它的表现形式为 `A/B` (更具体地说,`A/BBbbbbbb`,如下所述),其中 `A` 和 `B` 都是 4 字节值。例如: 11 | 12 | ```sql 13 | nik=# select pg_current_wal_lsn(); 14 | pg_current_wal_lsn 15 | -------------------- 16 | 5D/257E19B0 17 | (1 row) 18 | ``` 19 | 20 | - `5D` 是 LSN 的高 4 字节 (32 位) 部分 21 | - `257E19B0` 可以进一步分为两部分: 22 | - `25` — LSN 的低 4 字节部分 (更具体地说,仅是该 4 字节部分最高的 1 字节)。 23 | - `7E19B0` — WAL 的偏移量 (默认情况下为 `16 MiB`;在某些情况下会更改 — 例如 RDS 将其更改为 `64 MiB`)。 24 | 25 | 有趣的是,LSN 值可以进行比较,甚至可以进行减法运算 — 前提是我们使用 `pg_lsn` 数据类型。结果将以字节为单位表示: 26 | 27 | ```sql 28 | nik=# select pg_lsn '5D/257D6780' - '5D/251A8118'; 29 | ?column? 30 | ---------- 31 | 6481512 32 | (1 row) 33 | 34 | nik=# select pg_size_pretty(pg_lsn '5D/257D6780' - '5D/251A8118'); 35 | pg_size_pretty 36 | ---------------- 37 | 6330 kB 38 | (1 row) 39 | ``` 40 | 41 | 这也意味着我们可以通过查看从"点 0" (值 `0/0`) 出发的距离来获得 LSN 的整数值: 42 | 43 | ```sql 44 | nik=# select pg_lsn '5D/257D6780' - '0/0'; 45 | ?column? 46 | -------------- 47 | 400060934016 48 | (1 row) 49 | ``` 50 | 51 | ## 如何读取 WAL 文件名 52 | 53 | 现在,让我们看看 LSN 值如何与 WAL 文件名对应 (文件位于 `$PGDATA/pg_wal` 中)。我们可以使用 `pg_walfile_name()` 函数获取任何给定 LSN 相对应的 WAL 文件名: 54 | 55 | ```sql 56 | nik=# select pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn()); 57 | pg_current_wal_lsn | pg_walfile_name 58 | --------------------+-------------------------- 59 | 5D/257E19B0 | 000000010000005D00000025 60 | (1 row) 61 | ``` 62 | 63 | 这里的 `000000010000005D00000025` 是 WAL 文件名,它由三个 4 字节 (32 位) 组成: 64 | 65 | 1. `00000001` — 时间线 ID (TimeLineID),这是一个从 1 开始的连续 "历史号",当 Postgres 集群初始化时就会启动。它用于 "标识不同的数据库历史记录,以防止在恢复数据库的先前状态之后出现混乱" ([source code](https://gitlab.com/postgres/postgres/blob/4f2994647ff1e1209829a0085ca0c8d237dbbbb4/src/include/access/xlogdefs.h#L50))。 66 | 2. `0000005D` — LSN 序列号的高 4 字节部分。 67 | 3. `00000025` — 可以分为两部分: 68 | - `000000` — 6 个前导零 69 | - `25` — 序列号低位部分的最高字节 70 | 71 | 需要记住的重要一点是,WAL 文件名的第三个 4 字节内容里有 6 个前导零 — 通常,这会导致在比较两个 WAL 文件名以理解其中包含的 LSN 时产生混淆和错误。 72 | 73 | 以下示例中的说明很有用: 74 | 75 | ``` 76 | LSN: 5D/ 25 7E19B0 77 | WAL: 00000001 0000005D 00000025 78 | ``` 79 | 80 | 这在需要处理 LSN 值或 WAL 文件名,或者同时处理它们时非常有帮助。快速导航或比较这些值以了解它们之间的"距离"可能很有用。例如,在以下情况下: 81 | 82 | - 了解服务器每天生成多少字节的数据。 83 | - 自复制槽创建以来经过了多长时间。 84 | - 备份之间的"距离"是多少。 85 | - 为了达到一致性位点,还需要重放多少 WAL 数据。 86 | 87 | 此外,除了官方文档外,还有一些优秀的博客文章值得阅读: 88 | 89 | - [Postgres 9.4 feature highlight - LSN datatype](https://paquier.xyz/postgresql-2/postgres-9-4-feature-highlight-lsn-datatype/) 90 | - [Postgres WAL Files and Sequence Numbers](https://crunchydata.com/blog/postgres-wal-files-and-sequuence-numbers) 91 | - [WAL, LSN, and File Names](https://fluca1978.github.io/2020/05/28/PostgreSQLWalNames) 92 | 93 | --- 94 | 95 | 谢谢阅读!和往常一样,请与从事 #PostgreSQL 的同事和朋友分享这篇文章。 96 | 97 | # 我见 98 | 99 | ![Postgres WAL Files and Sequence Numbers | Crunchy Data Blog](https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/c94fcd44-0fb3-4f28-db7a-ff8076c82a00/public) 100 | 101 | 对于给定一个 LSN,我们可以迅速算出对应的 WAL 文件名,假设 pg_current_wal_insert_lsn() 返回 `0/9A80D10` ,如果当前时间线为 1,那么 logid 就是 `0/9A80D10` 中的第一个数字 — 0,logseg 就是 `0/9A80D10` 中的第二个数字除以 16MB 的大小,9A80D10 左移 6 位,也就是 09。那么根据 WAL 文件的格式timelineID + logid + logseg,则相当于:"00000001"+"00000000"+"00000009",即为:"000000010000000000000009" ,而写位置的偏移量则是第二个数字 "9A80D10" 后六位 "A80D10"。 -------------------------------------------------------------------------------- /How to understand what's blocking DDL.md: -------------------------------------------------------------------------------- 1 | # How to understand what's blocking DDL 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在 [day 60](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0060_how_to_add_a_column.md),我们讨论了如何添加列,并提到在高负载下需要设置较低的 `lock_timeout` 以及进行重试 (参照:[Zero-downtime Postgres schema migrations need this: lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries))。 6 | 7 | 当时将 `max_attempts` 设置为 1000,可能有点过高,但有一个有趣的问题:如果在多次重试后 DDL 仍未成功,如何了解究竟是什么阻塞了它? 8 | 9 | 首先要做的是启用 `log_lock_waits`。这样,在 `deadlock_timeout` (默认为 1 秒,此设置用于定义死锁检测发生的时间) 之后,你会看到一些有关被阻塞会话的信息。 10 | 11 | 但是我们不希望将 `lock_timeout` 设置超过 1 秒 — 这侵入性过强。解决方案是在会话中设置较低的 `deadlock_timeout`。示例 (假设有另一个会话读取表 `t` 并保持事务处于开启的状态): 12 | 13 | ```sql 14 | postgres=# set lock_timeout to '100ms'; 15 | SET 16 | 17 | postgres=# set deadlock_timeout to '50ms'; 18 | SET 19 | 20 | postgres=# set log_lock_waits to 'on'; 21 | SET 22 | 23 | postgres=# alter table t1 add column new_c int8; 24 | ERROR: canceling statement due to lock timeout 25 | ``` 26 | 27 | 在日志中,我们有两条记录 — 一条是在约 50 毫秒 (`deadlock_timeout`) 之后,包含一些有关锁等待的详细信息,另一条是我们的语句在 100 毫秒之后失败的错误信息: 28 | 29 | ```bash 30 | 2023-12-06 19:12:21.823 UTC [197] LOG: process 197 still waiting for AccessExclusiveLock on relation 16384 of database 13481 after 54.022 ms 31 | 2023-12-06 19:12:21.823 UTC [197] DETAIL: Process holding the lock: 211. Wait queue: 197. 32 | 2023-12-06 19:12:21.823 UTC [197] STATEMENT: alter table t1 add column new_c int8; 33 | 2023-12-06 19:12:21.874 UTC [197] ERROR: canceling statement due to lock timeout 34 | 2023-12-06 19:12:21.874 UTC [197] STATEMENT: alter table t1 add column new_c int8; 35 | ``` 36 | 37 | 遗憾的是,当达到 `deadlock_timeout` (在此例中为 50 毫秒) 时写入的日志消息不包含阻塞者的详细内容,只显示了 `PID` (`Process holding the lock: 211`)。如果幸运的话,我们可能会在 Postgres 日志中查看该会话在我们关注的时间戳周围的详细信息 — 我们应该查看错误发生时的时间戳附近的内容。可能会发现有在预防事务 ID 回卷的模式下运行着的 `autovacuum`、某些 DDL (如果通过 `log_statement='ddl'` 记录了 DDL)、或某个已达到 `log_min_duration_statement` 的长时间运行的查询 (建议将其设置为 1 秒或更低的值,以控制日志量,避免"观察者效应")。但在许多情况下,日志中没有其他信息,我们需要另一个解决方案。 38 | 39 | 在这种情况下,我们可以这样做: 40 | 41 | 1. 为执行 DDL 的会话设置自定义的 `application_name` 值,以便更容易在 `pg_stat_activity` 中进行区分 — 可以在 `PGAPPNAME `中设置,或直接通过 `SET` 设置: 42 | 43 | ```sql 44 | set application_name = 'ddl_runner'; 45 | ``` 46 | 47 | 2. 在另一个会话中执行观察查询。例如,使用以下包含 PL/pgSQL 代码的匿名 `DO` 块: 48 | 49 | ```sql 50 | do $$ 51 | declare 52 | i int; 53 | rec record; 54 | wait_more boolean := false; 55 | begin 56 | for i in select generate_series(1, 2000) loop 57 | for rec in 58 | select 59 | pid, 60 | left(query, 50) as query_left50, 61 | * 62 | from pg_stat_activity 63 | where 64 | application_name = 'ddl_runner' 65 | and wait_event_type = 'Lock' 66 | loop 67 | raise log 'DDL session blocked. Session info: pid=%, query_left50=%, wait=%:%. Blockers: %', 68 | rec.pid, 69 | rec.query_left50, 70 | rec.wait_event_type, 71 | rec.wait_event, 72 | ( 73 | select 74 | array_agg(json_build_object( 75 | 'pid', pid, 76 | 'state', state, 77 | 'wait', (wait_event_type || ':' || wait_event), 78 | 'query_l50', left(query, 50) 79 | )) 80 | from pg_stat_activity 81 | where array[pid] <@ pg_blocking_pids(rec.pid) 82 | ); 83 | 84 | wait_more := true; 85 | end loop; 86 | 87 | if wait_more then 88 | perform pg_sleep(1); 89 | 90 | wait_more := false; 91 | else 92 | perform pg_sleep(0.05); 93 | end if; 94 | end loop; 95 | end $$; 96 | ``` 97 | 98 | 此观察代码将在 DDL 会话等待超过 50 毫秒时报告类似信息: 99 | 100 | ```bash 101 | 2023-12-06T19:37:35.746363053Z 2023-12-06 19:37:35.746 UTC [237] LOG: DDL session blocked. Session info: pid=197, query_left50=alter table t1 add column new_c int8;, wait=Lock 102 | :relation. Blockers: {"{"pid" : 211, "state" : "idle in transaction", "wait" : "Client:ClientRead", "query_l50" : "select from t1 limit 0;"}"} 103 | ``` 104 | 105 | 这应该足以进行分析为何试图执行 DDL 却失败了。 106 | 107 | 注意事项: 108 | 109 | - 最好将 PL/pgSQL 代码封装至一个函数中,以替代匿名块,这样日志消息的 `CONTEXT` 和 `STATEMENT` 部分不会占用太多空间。 110 | - 此函数可以通过 `pg_cron` 调用,从而提供一个长期的观察工具。但需要注意的是,这需要一个完整的后台进程来运行,因此可能不适合长期运行 — 可以在当我们知道要执行 DDL 时,才不定期地唤醒。 -------------------------------------------------------------------------------- /How to use Docker to run Postgres.md: -------------------------------------------------------------------------------- 1 | # How to use Docker to run Postgres 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 本指南适用于那些使用或需要使用 Postgres,但对 Docker 不熟悉的用户。 6 | 7 | 在容器中运行 Docker 进行开发和测试可以帮助你在多个环境之间对齐库、扩展和软件版本集。 8 | 9 | ## Docker安装 – macOS 10 | 11 | 使用 [Homebrew](https://brew.sh/) 安装: 12 | 13 | ```bash 14 | brew install docker docker-compose 15 | ``` 16 | 17 | ## Docker安装 – Ubuntu 18 | 19 | ```bash 20 | sudo apt-get update 21 | sudo apt-get install -y \ 22 | apt-transport-https \ 23 | ca-certificates \ 24 | curl \ 25 | gnupg-agent \ 26 | software-properties-common 27 | 28 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 29 | 30 | sudo add-apt-repository -y \ 31 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 32 | 33 | sudo apt-get update && sudo apt-get install -y \ 34 | docker-ce \ 35 | docker-ce-cli \ 36 | containerd.io \ 37 | docker-compose-plugin 38 | ``` 39 | 40 | 避免使用 `sudo` 运行 `docker` 命令: 41 | 42 | ```bash 43 | sudo groupadd docker 44 | sudo usermod -aG docker $USER 45 | newgrp docker 46 | ``` 47 | 48 | ## 在带有持久化PGDATA的容器中运行Postgres 49 | 50 | 假设我们希望数据目录 (`PGDATA`) 位于 `~/pgdata`,并将容器命名为 `pg16`: 51 | 52 | ```bash 53 | sudo docker run \ 54 | --detach \ 55 | --name pg16 \ 56 | -e POSTGRES_PASSWORD=secret \ 57 | -v ~/pgdata:/var/lib/postgresql/data \ 58 | --shm-size=128m \ 59 | postgres:16 60 | ``` 61 | 62 | ## 检查日志 63 | 64 | 查看过去 5 分钟的日志 (带有时间戳) 并观察新的日志条目: 65 | 66 | ```bash 67 | docker logs --since 5m -tf pg16 68 | ``` 69 | 70 | ## 使用psql连接 71 | 72 | 创建表: 73 | 74 | ```bash 75 | ❯ docker exec -it pg16 psql -U postgres -c 'create table t()' 76 | CREATE TABLE 77 | 78 | ❯ docker exec -it pg16 psql -U postgres -c '\d t' 79 | Table "public.t" 80 | Column | Type | Collation | Nullable | Default 81 | --------+------+-----------+----------+--------- 82 | ``` 83 | 84 | 交互式 psql 连接: 85 | 86 | ```bash 87 | docker exec -it pg16 psql -U postgres 88 | ``` 89 | 90 | ## 从外部应用程序连接 91 | 92 | 要从主机所在机器连接应用程序,我们需要映射端口。为此,我们将停止并删除现有容器,并创建一个新容器,并创建一个具有适当端口映射的新容器 — 注意 `PGDATA` 仍然存在 (我们创建的表就在那里): 93 | 94 | ```bash 95 | ❯ docker stop pg16 96 | pg16 97 | 98 | ❯ docker rm pg16 99 | pg16 100 | 101 | ❯ docker run \ 102 | --detach \ 103 | --name pg16 \ 104 | -e POSTGRES_PASSWORD=secret \ 105 | -v ~/pgdata:/var/lib/postgresql/data \ 106 | --shm-size=128m \ 107 | -p 127.0.0.1:15432:5432 \ 108 | postgres:16 109 | 8b5370107e1be7d3fd01a3180999a253c53610ca9ab764125b1512f65e83b927 110 | 111 | ❯ PGPASSWORD=secret psql -hlocalhost -p15432 -U postgres -c '\d t' 112 | Timing is on. 113 | Table "public.t" 114 | Column | Type | Collation | Nullable | Default 115 | --------+------+-----------+----------+--------- 116 | ``` 117 | 118 | ## 包含额外扩展的自定义镜像 119 | 120 | 例如,我们可以创建一个基于原始镜像的自定义镜像,包含 `plpython3u` 扩展 (继续使用相同的 `PGDATA`): 121 | 122 | ```bash 123 | docker stop pg16 124 | docker rm pg16 125 | 126 | echo "FROM postgres:16 127 | RUN apt update 128 | RUN apt install -y postgresql-plpython3-16" \ 129 | > postgres_plpython3u.Dockerfile 130 | 131 | sudo docker build \ 132 | -t postgres-plpython3u:16 \ 133 | -f postgres_plpython3u.Dockerfile \ 134 | . 135 | 136 | docker run \ 137 | --detach \ 138 | --name pg16 \ 139 | -e POSTGRES_PASSWORD=secret \ 140 | -v ~/pgdata:/var/lib/postgresql/data \ 141 | --shm-size=128m \ 142 | postgres-plpython3u:16 143 | 144 | docker exec -it pg16 \ 145 | psql -U postgres -c 'create extension plpython3u' 146 | ``` 147 | 148 | ## 共享内存 149 | 150 | 如果你看到如下错误: 151 | 152 | ```bash 153 | FATAL: could not resize shared memory segment "/PostgreSQL.12345" to 1048576 bytes: No space left on device1 154 | ``` 155 | 156 | 在 `docker run` 命令中增加 `--shm-size` 值。 157 | 158 | ## 如何升级Postgres并保留数据 159 | 160 | 1. 原地升级: 161 | - 传统的 Postgres Docker 镜像仅包含一个主版本的二进制文件,因此无法执行 `pg_upgrade`,除非扩展这些镜像。 162 | - 另一种选择是使用包含多个二进制文件的镜像,例如 [Spilo by Zalando](https://github.com/zalando/spilo). 163 | 164 | 2. 简单的转储/恢复 (这里我展示了在没有不兼容的情况下进行降级;升级可以以相同的方式完成): 165 | 166 | ```bash 167 | docker exec -it pg16 pg_dumpall -U postgres | bzip2 > dumpall.bz2 168 | docker rm -f pg16 169 | 170 | rm -rf ~/pgdata 171 | mkdir ~/pgdata 172 | 173 | docker run \ 174 | --detach \ 175 | --name pg15 \ 176 | -e POSTGRES_PASSWORD=secret \ 177 | -v ~/pgdata:/var/lib/postgresql/data \ 178 | --shm-size=128m \ 179 | postgres:15 180 | 181 | bzcat dumpall.bz2 \ 182 | | docker exec -i pg15 psql -U postgres \ 183 | >>dump_load.log \ 184 | 2> >(tee -a dump_load.err >&2) 185 | ``` -------------------------------------------------------------------------------- /How to use lib_pgquery in shell to normalize and match queries from various sources.md: -------------------------------------------------------------------------------- 1 | # How to use lib_pgquery in shell to normalize and match queries from various sources 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## 为什么使用 lib_pgquery 6 | 7 | 在 [Day 12: How to find query examples for problematic pg_stat_statements records](https://postgres-howto.cn/#/./docs/12) 中提到,可以使用 [lib_pgquery](https://github.com/pganalyze/libpg_query) 标准化查询。这个库可以创建查询文本的树形表示,并计算所谓的"指纹" — 查询的标准化哈希 (移除所有参数后的查询文本)。 8 | 9 | 这对于以下几种情况特别有用: 10 | 11 | - 如果你需要将 `pg_stat_statements` 中的标准化查询与来自 `pg_stat_activity` 或 Postgres 日志的单个查询文本相匹配,并且使用的是 Postgres 14 之前的版本,其中 [compute_query_id](https://postgresqlco.nf/doc/en/param/compute_query_id/) 功能尚未实现来解决此问题。 12 | - 如果你使用的是新版本,但 `compute_query_id` 是 `off`。 13 | - 如果你使用来自不同来源的查询文本,并且不确定标准的 `query_id` (也称为 "queryid" — 这个命名在不同的表中并不统一) 是否可以作为可靠的匹配方式。 14 | 15 | `lib_pgquery` 大多数是用 C 编写的,并且用于各种语言的库中: 16 | 17 | - [Ruby](https://github.com/pganalyze/pg_query) 18 | - [Python](https://pypi.org/project/pglast/) 19 | - [Go](https://github.com/pganalyze/pg_query_go) 20 | - [Node](https://github.com/pyramation/pgsql-parser) 21 | 22 | ## 用于 CLI 的 Docker 版本 23 | 24 | 为了方便使用,我的同事们将 Go 版本封装成了 Docker 镜像,允许通过 CLI 风格在 Shell 中使用: 25 | 26 | ```bash 27 | ❯ docker run --rm postgresai/pg-query-normalize \ 28 | 'select c1 from t1 where c2 = 123;' 29 | { 30 | "query": "select c1 from t1 where c2 = 123;", 31 | "normalizedQuery": "select c1 from t1 where c2 = $1;", 32 | "fingerprint": "0212acd45326d8972d886d4b3669a90be9dd4a9853", 33 | "tree": [...] 34 | ] 35 | } 36 | ``` 37 | 38 | 该命令会返回一个 JSON 格式的结果。 39 | 40 | 为了提取标准化查询,可以使用 `jq`工 具: 41 | 42 | ```bash 43 | ❯ docker run --rm postgresai/pg-query-normalize \ 44 | 'select c1 from t1 where c2 = 123;' \ 45 | | jq -r '.normalizedQuery' 46 | select c1 from t1 where c2 = $1; 47 | ``` 48 | 49 | 仅提取文本形式的指纹: 50 | 51 | ```bash 52 | ❯ docker run --rm postgresai/pg-query-normalize \ 53 | 'select c1 from t1 where c2 = 123;' \ 54 | | jq -r '.fingerprint' 55 | 0212acd45326d8972d886d4b3669a90be9dd4a9853 56 | ``` 57 | 58 | 如果我们使用不同的参数,指纹并不会改变 — 例如,将 123 替换为 0: 59 | 60 | ```bash 61 | ❯ docker run --rm postgresai/pg-query-normalize \ 62 | 'select c1 from t1 where c2 = 0;' \ 63 | | jq -r '.fingerprint' 64 | 0212acd45326d8972d886d4b3669a90be9dd4a9853 65 | ``` 66 | 67 | ## 标准化查询作为输入 68 | 69 | 这是 lib_pgquery 一个非常好的特性!如果查询已经标准化,并且我们有占位符 (如 `$1`、`$2` 等) 代替实际参数,那么查询的指纹将保持不变: 70 | 71 | ```bash 72 | ❯ docker run --rm postgresai/pg-query-normalize \ 73 | 'select c1 from t1 where c2 = $1;' \ 74 | | jq -r '.fingerprint' 75 | 0212acd45326d8972d886d4b3669a90be9dd4a9853 76 | ``` 77 | 78 | 但是,如果查询不同,比如 `SELECT` 子句发生了变化,例如使用 `select *` 代替 `select c1`,则会得到新的指纹值: 79 | 80 | ```bash 81 | ❯ docker run --rm postgresai/pg-query-normalize \ 82 | 'select * from t1 where c2 = $1;' \ 83 | | jq -r '.fingerprint' 84 | 0293106c74feb862c398e267f188f071ffe85a30dd 85 | ``` 86 | 87 | ## 处理 IN 列表 88 | 89 | 与内置的 `queryid` 不同,lib_pgquery 会忽略 `IN` 子句中参数的数量 — 这种行为更适合查询的标准化和匹配。比如,比较以下两条查询: 90 | 91 | ```bash 92 | ❯ docker run --rm postgresai/pg-query-normalize \ 93 | 'select * from t1 where c2 in (1, 2, 3);' \ 94 | | jq -r '.fingerprint' 95 | 022fad3ad8fab1b289076f4cfd6ef0a21a15d01386 96 | 97 | ❯ docker run --rm postgresai/pg-query-normalize \ 98 | 'select * from t1 where c2 in (1000);' \ 99 | | jq -r '.fingerprint' 100 | 022fad3ad8fab1b289076f4cfd6ef0a21a15d01386 101 | ``` 102 | 103 | ## 我见 104 | 105 | 在 Oracle 中,有 sql_id,在 14 版本以后,PostgreSQL 也正式支持了 query_id。 106 | 107 | > 1. 查看等待事件,根据 sql_id 反查会话期间执行的 sql_text; 108 | > 2. 或者根据 sql_id 查看某段时间内,对应 SQL 的执行计划是否发生变化、何时发生变化,然后通过对执行计划的好坏分析进行 SQL 执行计划的固化 109 | 110 | 另外有个值得注意的细节:**PostgreSQL does not generate query_id in the pg_stat_activity until the SQL is parsed.** 111 | 112 | ~~~sql 113 | /* sesion 1 :: Take an exclusive lock on the table */ 114 | 115 | postgres=> BEGIN; 116 | BEGIN 117 | postgres=*> alter table test add column n2 int; 118 | ALTER TABLE 119 | 120 | /* sesion 2 :: Query is blocked due to exclusive lock taken by session 1 */ 121 | 122 | postgres=> select * from test; 123 | 124 | <> 125 | 126 | /* session 3 :: query_id column is blank */ 127 | 128 | select query_id,query from pg_stat_activity where query like '%test%'; 129 | query_id | query 130 | ----------------------+----------------------------------------------------- 131 | | select * from test; 132 | ~~~ 133 | 134 | 推荐阅读: 135 | 136 | - [PostgreSQL中的SQL_ID介绍及使用 ](https://mp.weixin.qq.com/s/pu6b4wS7yv6XOnJwzP3ymA) 137 | - [Peek into Query Hash (query_id) in PostgreSQL](https://virender-cse.medium.com/query-hash-in-postgresql-4522e91b5623) 138 | 139 | -------------------------------------------------------------------------------- /How to use pg_restore.md: -------------------------------------------------------------------------------- 1 | # How to use pg_restore 2 | 3 | 今天,我们将分享一些使用 `pg_restore` 从转储文件中恢复数据库 (或仅恢复其中一部分) 的技巧。 4 | 文档地址:https://postgresql.org/docs/current/app-pgrestore.html 5 | 6 | ## 并行化与单表限制 7 | 8 | 当处理以"目录"格式 (`-Fd`) 创建的转储文件时,你可以使用 `-j` (`--jobs`) 选项,通过运行多个 `pg_restore` 工作进程来加速恢复过程。但是,如果有一个或几个大表,这种并行化并不会有帮助 — 单表的转储或恢复不支持并行化。在第 8 天的 [pg_dump](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0008_how_to_speed_up_pg_dump.md) 系列中,我们讨论过这个问题。 9 | 10 | 例如,如果你创建了一个标准的 `pgbench` 数据库 (如 `pgbench -i -s1000`),你会发现并行化对转储和恢复都没有太大帮助,因为大部分数据都存储在单个表 `pgbench_accounts` 中。但是,如果你使用了分区 (PG13+ 支持,`pgbench -i -s1000 --partitions=16`),你会发现并行化可以加速转储和恢复步骤。 11 | 12 | ## 原子恢复 13 | 14 | 默认情况下,`pg_restore` 不会在出现错误时停止。这可能会让人感到意外,因为我们已经习惯了 Postgres 中的更**严格**的行为。这也可能导致数据库只部分恢复,但这一点被忽视了。要切换到**严格**模式,请使用 `-e` (`--exit-on-error`)。此外,将恢复过程包裹在一个事务中可能也很有帮助,使用选项 `-1` (`--single-transaction`)。 15 | 16 | ## 详细信息 17 | 18 | 要查看恢复的进度和详细信息,请使用 `--verbose` 选项。 19 | 20 | ## 模式与数据分离 21 | 22 | 你可以从两个不同的角度来查看你的转储文件,两者都提供了高层次结构化转储的方法。 23 | 24 | 首先,你可以区分模式和数据 — 使用以下选项: 25 | 26 | - `-s` (`--schema-only`) — 仅恢复模式。 27 | - `-a` (`--data-only`) — 仅恢复数据 (当然,模式必须已经存在)。 28 | 29 | 有趣的是,对于转储来说,这种 "模式 + 数据" 的分离在恢复时间和结果质量方面并不是最有效的:索引是模式的一部分,但如果你先创建索引,然后再加载数据,加载过程会变得更慢,并且索引的质量也会变差。如果在加载数据之后构建索引,效果会更好。 30 | 31 | 因此,有第二种查看转储结构的方式,这种方式对应于完整恢复过程的常规顺序: 32 | 33 | - "pre-data" — 模式定义 (不包括索引、约束、触发器和规则)。 34 | - "data" — 表数据。 35 | - "post-data" — 索引、约束、触发器和规则。 36 | 37 | `--section` 选项允许你仅运行恢复过程中的其中一个步骤。这可能会在你想要在各步骤之间执行其他操作时很有帮助,或者你想为不同步骤使用不同的并行化级别 (`-j`)。 38 | 39 | ## 细粒度控制 40 | 41 | 还有一种方法可以实现更细粒度的控制。 42 | 43 | 对于以 "目录" 格式 (`-Fd`) 创建的转储,你可以控制恢复的内容。为此,有两个方便的选项:`-l` 列出内容,`-L` 过滤你需要的内容。例如,考虑以下例子: 44 | 45 | ```bash 46 | pgbench -i -s100 test --partitions 16 47 | pg_dump -f dump1 -j8 -Fd test 48 | ``` 49 | 50 | 现在我们可以使用 `-l` (`--list`) 快速列出转储的内容: 51 | 52 | ```bash 53 | ❯ pg_restore -l dump1 54 | ; 55 | ; Archive created at 2023-10-15 22:00:43 PDT 56 | ; dbname: test 57 | ; TOC Entries: 94 58 | ; Compression: -1 59 | ; Dump Version: 1.14-0 60 | ; Format: DIRECTORY 61 | ; Integer: 4 bytes 62 | ; Offset: 8 bytes 63 | ; Dumped from database version: 15.4 (Homebrew) 64 | ; Dumped by pg_dump version: 15.4 (Homebrew) 65 | ; 66 | ... 67 | ``` 68 | 69 | 如果我们想要恢复除索引之外的所有内容,我们首先需要准备一个包含转储中所有对象的"列表"文件,但不包括约束和索引: 70 | 71 | ```bash 72 | ❯ pg_restore -l dump1 \ 73 | | grep -v INDEX \ 74 | | grep -v CONSTRAINT \ 75 | > no_indexes.list 76 | ``` 77 | 78 | 然后使用 `-L` (`--use-list`) 选项指定这个列表文件: 79 | 80 | ```bash 81 | ❯ psql -c 'create database restored' 82 | CREATE DATABASE 83 | ❯ pg_restore -j8 -L no_indexes.list --dbname=restored dump1 84 | ``` 85 | 86 | 我们可以看到,恢复后的表没有主键或索引: 87 | 88 | ```sql 89 | ❯ psql restored -c '\d pgbench_accounts_1' 90 | Table "public.pgbench_accounts_1" 91 | Column | Type | Collation | Nullable | Default 92 | ----------+---------------+-----------+----------+--------- 93 | aid | integer | | not null | 94 | bid | integer | | | 95 | abalance | integer | | | 96 | filler | character(84) | | | 97 | Partition of: pgbench_accounts FOR VALUES FROM (MINVALUE) TO (625001) 98 | ``` 99 | 100 | ## 权限、属主等 101 | 102 | 在某些情况下,当在不同环境或集群之间复制或移动架构和数据时,以下选项可以非常有帮助 (这些选项的名称即其含义): 103 | 104 | - `--no-owner` 105 | - `--no-privileges` 106 | - `--no-security-labels` 107 | - `--no-publications` 108 | - `--no-subscriptions` 109 | - `--no-tablespaces` 110 | 111 | 然而,如果原始数据库中使用了RLS (行级安全),没有一个选项可以跳过恢复转储中包含的 `CREATE POLICY` 查询。在这种情况下,我们需要使用前述的细粒度控制方法来移除 `CREATE POLICY` 命令。 112 | 113 | 因此,当在不同环境之间移动架构和数据时,从转储中恢复的可能步骤如下: 114 | 115 | ```bash 116 | pg_restore --list /path/to/dump \ 117 | | grep -v POLICY \ 118 | > dump-no-policies.list 119 | 120 | pg_restore \ 121 | --jobs=8 \ 122 | --dbname=${DBNAME} \ 123 | --use-list=./dump-no-policies.list \ 124 | --no-owner \ 125 | --no-privileges \ 126 | --no-security-labels \ 127 | --no-publications \ 128 | --no-subscriptions \ 129 | --no-tablespaces \ 130 | --verbose \ 131 | --exit-on-error \ 132 | /path/to/dump 133 | ``` 134 | 135 | ## 附录 136 | 137 | 很多人 (包括我) 常常忘记这一步 — 实际上,它应该成为 `pg_restore` 的默认行为,但事实并非如此: 138 | 139 | 在运行 `pg_restore` 之后,不要忘记: 140 | 141 | - 收集统计信息 (除非你想等待 `autovacuum` 来做这件事) — `ANALYZE`。 142 | - 构建可见性映射 — 执行 `VACUUM`。 143 | 144 | 因此,记得运行 `VACUUM ANALYZE`。也可以并行化:`vacuumdb --jobs=$N`。 -------------------------------------------------------------------------------- /How to use subtransactions in Postgres.md: -------------------------------------------------------------------------------- 1 | # How to use subtransactions in Postgres 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## TL;DR 6 | 7 | 如无必要,否则不要使用子事务。 8 | 9 | ## 什么是子事务? 10 | 11 | 子事务,也称为"嵌套事务",是指在已启动的事务范围内通过指令启动的事务 (来源:[Wikipedia](https://en.wikipedia.org/wiki/Nested_transaction))。这一特性允许用户部分回滚事务,这在许多情况下很有帮助:如果发生错误,重新执行的步骤会减少。 12 | 13 | SQL 标准定义了描述这种机制的两个基本指令:`SAVEPOINT` 和扩展的 `ROLLBACK` 语句 — `ROLLBACK TO SAVEPOINT`。PostgreSQL 实现了这些指令,但允许在语法上稍有不同,例如可以在 `RELEASE` 和 `ROLLBACK` 语句中省略 `SAVEPOINT` 关键字。 14 | 15 | 你可能已经在使用子事务,例如: 16 | 17 | - 在 Django 中,使用嵌套的 ["atomic()" blocks](https://docs.djangoproject.com/en/5.0/topics/db/transactions/#savepoints) 18 | - 隐式使用:在 PL/pgSQL 函数中使用 `BEGIN / EXCEPTION WHEN ... / END` 块。 19 | 20 | ## 如何使用 (如果你真的想用) 21 | 22 | 语法: 23 | 24 | - `SAVEPOINT savepoint_name` ([SAVEPOINT](https://postgresql.org/docs/current/sql-savepoint.html)) 25 | - `ROLLBACK [ WORK | TRANSACTION ] TO [ SAVEPOINT ] savepoint_name` ([ROLLBACK TO](https://postgresql.org/docs/current/sql-rollback-to.html)) 26 | - `RELEASE [ SAVEPOINT ] savepoint_name` ([RELEASE SAVEPOINT](https://postgresql.org/docs/current/sql-release-savepoint.html)) 27 | 28 | 一个示例: 29 | 30 | ![Rolled-back subtransaction example](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0035_rolled_back_subtransaction_example.jpg) 31 | 32 | ## 推荐 33 | 34 | 对于任何计划增长的 OLTP 类型负载 (如 Web 和移动应用),我唯一的实际建议是: 35 | 36 | > 尽可能避免子事务 37 | 38 | 如果你不想有一天发生以下情况: 39 | 40 | ![Performance drop for more than 64 subtransactions in a transaction](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0035_performance_drop_too_many_subtx.jpg) 41 | 42 | 或者类似情况: 43 | 44 | ![Performance drop of standby server with primary running a long transaction and many subtransactions](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0035_standby_server_killed.jpg) 45 | 46 | 你可以在这篇文章中找到对子事务四大危险的详细分析:[PostgreSQL Subtransactions Considered Harmful](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful) 47 | 48 | 截至 2023 年 (PG16 版本),这些问题尚未解决,尽管一些优化工作正在进行中: 49 | 50 | - [More scalable multixacts buffers and locking](https://commitfest.postgresql.org/45/2627/) 51 | - [suboverflowed subtransactions concurrency performance optimize](https://postgresql.org/message-id/flat/003201d79d7b%24189141f0%2449b3c5d0%24@tju.edu.cn) (不幸的是,补丁已被撤回) 52 | 53 | 底线是 54 | 55 | - 如果可以,不要使用子事务。 56 | - 关注与子事务相关的 pgsql-hackers 邮件讨论,并参与测试和改进。 57 | - 如果绝对需要使用子事务,请参考 [Problem 3: unexpected use of Multixact IDs](https://postgres.ai/blog/20210831-postgresql-subtransactions-considered-harmful#problem-3-unexpected-use-of-multixact-ids) 以及: 58 | - 仅在低 TPS 系统中使用它们 59 | - 避免深度嵌套 60 | - 在包含子事务的事务中谨慎使用 `SELECT ... FOR UPDATE`。 61 | - 监控 `pg_stat_slru` 数值 (PG13+,[Monitoring stats](https://postgresql.org/docs/current/monitoring-stats.html)),以便迅速发现并解决 SLRU 溢出问题。 62 | 63 | 64 | # 我见 65 | 66 | 在 17 版本中,已经针对 SLRU 的问题进行了极大优化,并且可以配置 SLRU 相关参数 — multixact_member_buffers、multixact_offset_buffers 和 subtransaction_buffers。 67 | 68 | SLRU optimization 69 | 70 | - Make the cache size configurable 71 | - Divide SLRU cache into small associative banks 72 | - Implement bankwise lock to remove the centralized control lock contention 73 | - Remove centralize LRU counter 74 | 75 | Performance 76 | 77 | - 2-3 x performance gain when there is huge load on SLRU -------------------------------------------------------------------------------- /How to work with arrays, part 2.md: -------------------------------------------------------------------------------- 1 | ## How to work with arrays, part 2 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 第一部分可以在[此处](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0028_how_to_work_with_arrays_part_1.md)找到。 6 | 7 | ## 如何搜索 8 | 9 | 数组搜索最有趣的操作符之一是 `<@` — 表示"被包含": 10 | 11 | ```sql 12 | nik=# select array[2, 1] <@ array[1, 3, 2]; 13 | ?column? 14 | ---------- 15 | t 16 | (1 row) 17 | ``` 18 | 19 | 当你需要检查某个标量值是否存在于数组中时,只需创建一个单元素数组并将 `<@` 应用于两个数组。例如,检查 "2" 是否包含在要分析的数组中: 20 | 21 | ```sql 22 | nik=# select array[2] <@ array[1, 3, 2]; 23 | ?column? 24 | ---------- 25 | t 26 | (1 row) 27 | ``` 28 | 29 | 你可以通过创建索引来加速使用 `<@` 的搜索操作。让我们创建一个 GIN 索引,并比较一个包含一百万行的表的查询计划: 30 | 31 | ```sql 32 | nik=# create table t1 (val int8[]); 33 | CREATE TABLE 34 | 35 | nik=# insert into t1 36 | select array( 37 | select round(random() * 1000 + i) 38 | from generate_series(1, 10) 39 | limit (1 + random() * 10)::int 40 | ) 41 | from generate_series(1, 1000000) as i; 42 | INSERT 0 1000000 43 | 44 | nik=# select * from t1 limit 3; 45 | val 46 | ----------------------- 47 | {390,13,405,333,358,592,756,677} 48 | {463,677,585,191,425,143} 49 | {825,918,303,602} 50 | (3 rows) 51 | 52 | nik=# vacuum analyze t1; 53 | VACUUM 54 | ``` 55 | 56 | 我们创建了一个有 100 万行的单列表,每行包含具有各种数字的 `int8[]` 数组。 57 | 58 | 现在,搜索数组值中包含 "123" 的所有行: 59 | 60 | ```sql 61 | nik=# explain (analyze, buffers) select * from t1 where array[123]::int8[] <@ val; 62 | QUERY PLAN 63 | --------------------------------------------------------------------------------------------------------------------- 64 | Gather (cost=1000.00..18950.33 rows=5000 width=68) (actual time=0.212..100.572 rows=2 loops=1) 65 | Workers Planned: 2 66 | Workers Launched: 2 67 | Buffers: shared hit=12554 68 | -> Parallel Seq Scan on t1 (cost=0.00..17450.33 rows=2083 width=68) (actual time=61.293..94.212 rows=1 loops=3) 69 | Filter: ('{123}'::bigint[] <@ val) 70 | Rows Removed by Filter: 333333 71 | Buffers: shared hit=12554 72 | Planning: 73 | Buffers: shared hit=6 74 | Planning Time: 0.316 ms 75 | Execution Time: 100.586 ms 76 | (12 rows) 77 | ``` 78 | 79 | 共有 12554 次缓冲区命中,约为 12554 * 8 / 1024 ~= 98 MiB,只需找到包含 "123" 的两行数据 — 注意 "rows=2"。效率不高,因为这里是 Seq Scan。 80 | 81 | 现在,创建一个 GIN 索引: 82 | 83 | ```sql 84 | nik=# create index on t1 using gin(val); 85 | CREATE INDEX 86 | ``` 87 | 88 | 然后再运行相同的查询: 89 | 90 | ```sql 91 | nik=# explain (analyze, buffers) select * from t1 where array[123]::int8[] <@ val; 92 | QUERY PLAN 93 | ----------------------------------------------------------------------------------------------------------------------- 94 | Bitmap Heap Scan on t1 (cost=44.75..4260.25 rows=5000 width=68) (actual time=0.021..0.022 rows=2 loops=1) 95 | Recheck Cond: ('{123}'::bigint[] <@ val) 96 | Heap Blocks: exact=1 97 | Buffers: shared hit=5 98 | -> Bitmap Index Scan on t1_val_idx (cost=0.00..43.50 rows=5000 width=0) (actual time=0.016..0.016 rows=2 loops=1) 99 | Index Cond: (val @> '{123}'::bigint[]) 100 | Buffers: shared hit=4 101 | Planning: 102 | Buffers: shared hit=16 103 | Planning Time: 0.412 ms 104 | Execution Time: 0.068 ms 105 | (11 rows) 106 | ``` 107 | 108 | 没有顺序扫描,并且只有 5 次缓冲区命中,大约 40 KiB 的内存就能找到所需的 2 行数据。这解释了为什么执行时间从 ~100ms 缩短到 ~0.07ms,这快了 ~1400 倍。 109 | 110 | 更多操作符请参见[官方文档](https://www.postgresql.org/docs/current/functions-array.html#FUNCTIONS-ARRAY)。 -------------------------------------------------------------------------------- /How to work with metadata.md: -------------------------------------------------------------------------------- 1 | # How to work with metadata 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 当处理元数据 (关于数据的数据) 时,Postgres 中的这些参考文档非常有用: 6 | 7 | - [System Catalogs](https://postgresql.org/docs/current/catalogs.html) 8 | - [System Views](https://postgresql.org/docs/current/views.html) 9 | - [The Cumulative Statistics System](https://postgresql.org/docs/current/monitoring-stats.html) 10 | - [The Information Schema](https://postgresql.org/docs/current/information-schema.html) 11 | 12 | 没有必要重复文档中的内容。相反,我们专注于一些技巧和原则,使你的工作更高效。我们将讨论以下主题: 13 | 14 | - `::oid` 和 `::regclass` 15 | - `\?` 和 `ECHO_HIDDEN` 16 | - 性能 17 | - `INFORMATION_SCHEMA` 18 | - `pg_stat_activity` 并不是一张表 19 | 20 | ------ 21 | 22 | ## ::oid,::regclass 23 | 24 | 在 Postgres 的术语中,表、索引、视图、物化视图均被称为"关系"。它们的元数据信息可以通过多种方式查看,但"核心"位置是 [pg_class system catalog](https://postgresql.org/docs/current/catalog-pg-class.html)。换句话说,这是一个存储所有表、索引等信息的表。它有两个主键: 25 | 26 | - PK:oid — 一个数字 ([OID, object identifier](https://postgresql.org/docs/current/datatype-oid.html)) 27 | - UK:一对列 (relname, relnamespace),即关系名和模式 OID。 28 | 29 | 一个值得记住的技巧是:OID 可以快速转化为关系名,反之亦然,使用类型转换为 oid 和 regclass 数据类型。 30 | 31 | 以下是名为 `t1` 的表的简单示例: 32 | 33 | ```sql 34 | nik=# select 't1'::regclass; 35 | regclass 36 | ---------- 37 | t1 38 | (1 row) 39 | 40 | nik=# select 't1'::regclass::oid; 41 | oid 42 | ------- 43 | 74298 44 | (1 row) 45 | 46 | nik=# select 74298::regclass; 47 | regclass 48 | ---------- 49 | t1 50 | (1 row) 51 | ``` 52 | 53 | 因此,不需要执行 `select oid from pg_class where relname = ...` — 只需记住 `::regclass` 和 `::oid` 即可。 54 | 55 | ## ? 和 ECHO_HIDDEN 56 | 57 | `psql` 的 `\?` 命令非常重要 — 通过它可以找到所有命令的相关描述。例如: 58 | 59 | ```sql 60 | \d[S+] list tables, views, and sequences 61 | ``` 62 | 63 | 这些"描述"命令会隐式生成一些SQL语句 — "窥探"它们可能很有帮助。为此,我们首先需要开启 `ECHO_HIDDEN`: 64 | 65 | ```sql 66 | nik=# \set ECHO_HIDDEN on 67 | ``` 68 | 69 | 或者在启动 `psql` 时使用 `-E` 选项。然后我们便可以开始窥探: 70 | 71 | ```sql 72 | nik=# \d t1 73 | /********* QUERY **********/ 74 | SELECT c.oid, 75 | n.nspname, 76 | c.relname 77 | FROM pg_catalog.pg_class c 78 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 79 | WHERE c.relname OPERATOR(pg_catalog.~) '^(t1)$' COLLATE pg_catalog.default 80 | AND pg_catalog.pg_table_is_visible(c.oid) 81 | ORDER BY 2, 3; 82 | /**************************/ 83 | 84 | [... + more queries to get info about "t1" ...] 85 | ``` 86 | 87 | 查看这些查询可以帮助你构建各种处理元数据的工具。 88 | 89 | ## 性能 90 | 91 | 在某些情况下,元数据查询可能比较重且慢。如果出现这种情况,请考虑以下方法: 92 | 93 | - 考虑缓存以减少元数据查询的频率和必要性。 94 | - 检查系统目录膨胀情况。例如,由于频繁的 DDL 操作、临时表的使用等原因,`pg_class` 可能会膨胀。在这种情况下,不幸的是,可能需要执行 `VACUUM FULL` (`pg_repack` 不能处理系统目录)。如果需要,不要忘记 Postgres 中零停机 DDL 的黄金法则 — [use low lock_timeout and retries](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries) 95 | 96 | ## INFORMATION_SCHEMA 97 | 98 | 系统目录和视图是查询表和索引元数据的"本地"方式 — 但这并不是标准的方式。标准方法是 `INFORMATION_SCHEMA`,Postgres 根据 SQL 标准支持它:[Docs](https://postgresql.org/docs/current/information-schema.html)。如何使用: 99 | 100 | - 对于简单的、跨数据库兼容的元数据查询,使用 `information_schema`。 101 | - 对于更复杂的、特定于 Postgres 的查询,或者当需要详细的内部信息时,使用本地系统目录。 102 | 103 | ## pg_stat_activity 不是表 104 | 105 | 必须记住的是,在查询元数据时,你可能正在处理一些看起来像但行为不像普通表的东西。 106 | 107 | 例如,当你从 `pg_stat_activity` 中读取记录时,你并不是在处理一致的表数据快照:读取第一行和理论上的最后一行是在不同时刻产生的,你可能会看到并非同时运行的查询。 108 | 109 | 这种现象也解释了为什么 `select now() - query_start from pg_stat_activity;` 可能会给出负值:函数 `now()` 在事务开始时执行一次,并且在事务内部不会改变其值,无论调用它多少次。 110 | 111 | 要获取精确的时间间隔,请使用 `clock_timestamp()` 代替: 112 | 113 | ```sql 114 | select clock_timestamp() - query_start from pg_stat_activity; 115 | ``` -------------------------------------------------------------------------------- /How to work with pg_stat_statements, part 1.md: -------------------------------------------------------------------------------- 1 | # How to work with pg_stat_statements, part 1 2 | 3 | ## 查询优化的两个分支 4 | 5 | 查询优化有两个主要的分支: 6 | 7 | 1. "微观"优化:分析和改进特定的查询。主要工具是 `EXPLAIN`。 8 | 2. "宏观"优化:分析整个或大部分工作负载,进行细分,研究其特性,从上到下识别并改进表现最差的部分。主要工具是 `pg_stat_statements`(以及其扩展或替代方案)、等待事件分析和 PostgreSQL 日志。 9 | 10 | 今天我们将重点讨论如何从基础开始,阅读和使用 `pg_stat_statements`,并逐步利用它的数据进行宏观优化。 11 | 12 | ## pg_stat_statements 基础 13 | 14 | `pg_stat_statements` 扩展 (简称为 "pgss") 已经成为宏观分析的事实标准。 15 | 16 | 它跟踪所有查询,并将它们聚合到查询组中 — 即"标准化查询",其中参数会被去除。 17 | 18 | 有一些值得注意的限制: 19 | 20 | - 它不会显示正在运行的查询信息 (可以通过 `pg_stat_activity` 找到)。 21 | - 一个主要问题是:它不会跟踪失败的查询,这有时可能会导致错误的结论 (例如:CPU 和磁盘 I/O 负载很高,但 99% 的查询因为 `statement_timeout` 失败,导致系统负载增加却没有产生任何有用的结果——在这种情况下,pgss 是"瞎"的)。 22 | - 如果查询中有 SQL 注释,它们不会被移除,每个标准化查询的查询列中只会保留第一个注释的值。 23 | 24 | pg_stat_statements 视图包含三种类型的列: 25 | 26 | 1. `queryid` – 标准化查询的标识符。在最新的 PostgreSQL 版本中,它还可以用于将 pgss 的数据与 `pg_stat_activity` 和 PostgreSQL 日志连接(JOIN)。需要注意的是:`queryid` 的值可能是负数。 27 | 2. 描述性的列:包括数据库 ID (`dbid`)、用户 ID (`userid`) 和查询本身 (`query`)。 28 | 3. 指标:大部分指标都是累积的,例如 `calls`、`total_time`、`rows` 等。非累积指标包括 `stddev_plan_time`、`stddev_exec_time`、`min_exec_time` 等。在这篇文章中,我们主要关注累积指标。 29 | 30 | // 注:在下文中,我有时会将标准化查询称为"查询组"或简称为"组"。 31 | 32 | 我们来提一些在宏观优化中最常用的指标 (完整列表请参阅[文档](https://www.postgresql.org/docs/current/pgstatstatements.html#PGSTATSTATEMENTS-PG-STAT-STATEMENTS)): 33 | 34 | 1. `calls` – 此查询组 (标准化查询) 的调用次数。 35 | 2. `total_plan_time` 和 `total_exec_time` – 该查询组的规划和执行的聚合持续时间 (请再次记住:失败的查询不会被跟踪,包括那些由于 `statement_timeout` 而失败的查询)。 36 | 3. `rows` – 此查询组中的查询返回行数。 37 | 4. `shared_blks_hit` 和 `shared_blks_read` – 来自缓冲池的命中和读取操作次数。这里有两个重要的注意点: 38 | - 此处的 "read" 是指从缓冲池读取——这不一定意味着物理读取磁盘,因为数据可能已缓存于操作系统的页面缓存中。因此,我们不能直接认为这些读取来自磁盘。一些监控系统会犯这个错误,但在某些情况下,这种细微差别对于我们的分析能否得出正确结论至关重要。 39 | - "blocks hit" 和 "blocks read" 的名称可能会产生误导,暗示我们在讨论数据量 — 块 (缓冲区) 的数量。虽然这些值可以聚合,但我们必须记住同一个缓冲区可能会被多次读取或命中。因此,与其说 "blocks have been hit",不如说 "blocks hits"。 40 | 41 | 5. `wal_bytes` – 此查询组中查询写入 WAL 的字节数。 42 | 43 | 还有许多其他有趣的指标,建议阅读文档来了解它们 ([the docs](https://postgresql.org/docs/current/pgstatstatements.html)).。 44 | 45 | ## 处理 pgss 中的累积指标 46 | 47 | 要读取和解释 pgss 中的数据,需要以下三个步骤: 48 | 49 | 1. 获取两个时间点对应的快照。 50 | 2. 计算每个累积指标的差值,以及这两个点之间的时间差。 51 | - 一个特殊情况是,第一个点是统计数据收集的开始时刻。在 PostgreSQL 14 及更新版本中,有一个单独的视图 `pg_stat_statements_info`,其中包含 pgss 统计数据上次重置的时间信息。不幸的是,在 PostgreSQL 13 及更早版本中,此信息未被存储。 52 | 3. **(最有趣的部分!)** 对每个累积指标的差值计算三种衍生指标——假设 M 是我们的指标:a. **dM/dt** – 指标 M 的基于时间的微分。 b. **dM/dc** – 基于调用次数的微分 (将在下一篇文章中详细解释)。 c. **%M** – 该标准化查询在整个工作负载中指标 M 所占的百分比。 53 | 54 | 第 3 步不仅可以应用于单个主机上的特定标准化查询,还可以应用于更大的组,例如: 55 | 56 | - 所有从节点的聚合工作负载。 57 | - 单个节点 (例如,主节点) 的整个工作负载。 58 | - 更大的分组,例如特定用户或特定数据库的所有查询。 59 | - 特定类型的所有查询——例如所有 `UPDATE` 查询。 60 | 61 | 如果您的监控系统支持 pgss,那么您无需手动处理快照。不过,请注意,我个人不知道有任何监控工具可以完美支持 pgss,并保留本文中讨论的所有信息 (而且我研究过不少 PostgreSQL 监控工具)。 62 | 63 | 假设您已经成功获取了 pgss 的两个快照 (记得记录它们的收集时间戳) 或使用了适当的监控工具,现在我们来看看前面讨论的三种衍生指标的实际意义。 64 | 65 | ## 衍生指标 1:基于时间的微分 66 | 67 | - `dM/dt`,其中 `M` 是 `calls` – 这个指标的意义很简单,它表示每秒的查询次数 (QPS)。如果我们讨论的是特定查询组 (标准化查询),那么它表示该组中所有查询的每秒查询数。例如,如果 QPS 为 `10,000`,这表明可能需要改进客户端 (应用程序) 的行为以减少此值;如果 QPS 为 `10`,那就相对较少 (当然,具体还要看情况)。如果考虑整个节点的 QPS,那么它就是我们的"全局 QPS"。 68 | 69 | - `dM/dt`,其中 `M` 是 `total_plan_time + total_exec_time` – 这是查询宏观分析中最有趣和最关键的指标之一,主要针对资源消耗优化 (目标是减少服务器处理查询所花费的时间)。一个有趣的事实是,它以 "seconds per second" 为单位,表示服务器每秒处理这个查询组所花费的时间。一个粗略(但具有说明性)的解释是:如果该值为 `2 sec/sec`,表示服务器每秒花费 2 秒来处理这些查询——因此我们肯定希望有超过 2 个 vCPU 来处理这个工作负载。尽管这是一个粗略的解释,因为 pgss 无法区分查询是在等待锁还是在 CPU 上执行实际工作。因此,可能会出现该值很高但对 CPU 负载影响不大的情况。 70 | 71 | - `dM/dt`,其中 `M` 是 `rows` – 这个指标表示该查询组每秒返回的行流量。例如,如果每秒返回 1000 行,表示从 PostgreSQL 服务器到客户端有明显的数据流。这里的一个有趣点是,有时我们需要考虑 PostgreSQL 生成的结果会给应用程序节点带来多大的负载——返回过多的行可能会在客户端一侧占用大量资源。 72 | 73 | - `dM/dt`,其中 `M` 是 `shared_blks_hit + shared_blks_read` – 这是每秒的缓冲区操作次数 (仅读取数据,不涉及写入)。这是另一个关键的优化指标。值得将缓冲区操作数转换为字节数。在大多数情况下,缓冲区大小为 8 KiB (`show block_size;`进行确认),因此每秒 `500,000` 次缓冲区命中和读取相当于大约每秒 `500000 bytes/sec * 8 / 1024 / 1024 = ~ 3.8 GiB/s` 的内部数据读取流量 (注意:同一个缓冲区可能会多次处理)。这是一个显著的负载——您可能需要检查其他指标以确定这是否合理,或者是否可以作为优化的候选项。 74 | 75 | - `dM/dt`,其中 `M` 是 `wal_bytes` – 这是每秒写入的 WAL 字节流量。这个指标是在 PostgreSQL 13 及更新版本中引入的,主要用于了解哪些查询对 WAL 写入贡献最大。当然,写入的 WAL 越多,对物理和逻辑复制,以及备份系统的压力也就越大。一个高度病态的工作负载示例如下:一系列这样的事务 `begin; delete from ...; rollback;` 删除大量行并撤销这些操作,这会产生大量 WAL,而没有执行任何有用的工作。(注意:尽管这里使用了 `ROLLBACK`,而且 pgss 无法跟踪失败的语句,但这些语句仍然会被跟踪,因为它们在事务中是成功的。) 76 | 77 | 这就是 pgss 相关使用指南的第一部分。在接下来的部分中,我们将讨论 `dM/dc` 和 `%M`,以及基于 pgss 进行宏观优化的其他实用内容。 78 | 79 | 如果你觉得这篇文章有帮助,请分享给你的同事以及任何使用 PostgreSQL 的人。 -------------------------------------------------------------------------------- /How to work with pg_stat_statements, part 2.md: -------------------------------------------------------------------------------- 1 | # How to work with pg_stat_statements, part 2 2 | 3 | 昨天我们讨论了使用 pg_stat_statements (pgss) 的一些基础知识,以及第一组衍生指标 `dM/dt`——基于时间的微分。今天我们将重点关注第二组指标:`dM/dc`,其中 c 表示调用次数 (pgss 中的 `calls` 列)。 4 | 5 | ## 衍生指标2. 基于调用次数的微分 6 | 7 | 这一组指标的重要性不亚于基于时间的微分,因为它可以为你提供工作负载的系统化视图,是查询性能宏观优化的有力工具。 8 | 9 | 这组指标帮助我们理解每个查询组的平均查询特性。 10 | 11 | 不幸的是,许多监控系统忽视了这种衍生指标。一套好的系统应该展示全部或至少大部分此类指标,并显示这些值随时间变化的图表 (`dM/dc` 时间序列)。 12 | 13 | 获取此类衍生指标的结果非常简单: 14 | 15 | - 计算两个 pgss 快照之间的 M 值 (正在研究的指标) 的差值:`M2 - M1` 16 | - 然后,不使用时间戳,而是获取 "calls" 值的差值:`c2 - c1` 17 | - 接着计算 `(M2 - M1) / (c2 - c1)` 18 | 19 | 让我们看看通过这种方式获得的各种衍生指标的意义: 20 | 21 | - `dM/dc`,当 `M` 是 `calls` 时 — 这是一种退化情形,该值始终为1 (调用次数除以相同的调用次数)。 22 | - `dM/dc`,当 `M` 是 `total_plan_time + total_exec_time` 时 — 这是某个特定 pgss 查询组中查询的平均执行时间,这是查询性能观察中的一个极其重要的指标。它也可以称为 "查询延迟"。当应用于 pgss 中所有标准化查询的聚合值时,其意义是 "服务器上平均查询延迟" (有两个重要的注意事项:pgss 并不追踪失败的查询,并且由于 `pg_stat_statements.max` 限制,有时数据可能偏斜)。Postgres 的主要累积统计信息系统并不提供这种信息 — `pg_stat_database` 仅在 `track_io_timing` 启用时才会跟踪一些时间指标,如 `blk_read_time` 和 `blk_write_time`,在 PG14+ 中还有 `active_time`,但它不包含语句数量的信息,只记录事务数量 (`xact_commit` 和 `xact_rollback`);在某些情况下,我们可以从其他来源获得这些数据 — 例如,pgbench 在用于基准测试时会报告这些数据,pgBouncer 也会报告事务和查询的平均延迟,但在常规情况下,在观测工具中,pgss 被认为是获取查询延迟信息的最通用方式。其重要性怎么估计都不为过 — 例如: 23 | - 如果我们知道通常情况下,平均查询持续时间 <1 ms,那么任何升至 10 ms 的情况都应被视为一个严重事件 (如果这是在部署后发生的,应该重新考虑或回滚该部署)。在故障排除时,它还能帮助我们进行分段分析,确定哪些特定查询组导致了延迟的升高 — 是所有查询组,还是只有特定的一些? 24 | - 在许多情况下,这可以被视为大型负载测试和基准测试中最重要的指标 (例如:比较 PG 15 和 PG 16 的平均查询持续时间,以便为升级到 PG 16 做准备)。 25 | - `dM/dc`,当 `M` 是 `rows` 时——这是查询组中查询返回的平均行数。对于 OLTP 场景来说,具有较大值的查询组 (起始值为几百或更多,具体取决于情况) 应进行审查: 26 | - 如果这是有意为之 (例如,数据导出),则无需采取行动; 27 | - 如果这是面向用户的查询,并且与数据导出无关,那么可能存在错误,例如缺少 `LIMIT` 和适当的分页,应该修正这些查询。 28 | - `dM/dc`,当 `M` 是 `shared_blks_hit + shared_blks_read` 时 — 这是查询组中缓冲池的平均 "命中数" + "读取数"。可以将其转换为字节进行分析:例如,`500,000` 次缓存命中和读取转换为 `500000 GiB * 8 / 1024 / 1024 ≈ 3.8 GiB`,这是单个查询中的一个显著数值,尤其是在查询目标仅仅返回一行或几行的情况下。对于较大的值,应考虑对查询进行优化。补充说明: 29 | - 在许多情况下,单独分析命中和读取的情况也是有意义的 — 例如,某些 pgss 查询组中的查询可能不会导致高磁盘 IO 和页面缓存读取,但在缓存池中的命中数非常多,尽管所有数据都缓存于缓存池中,但其性能仍不理想。 30 | - 要获取实际的磁盘 IO 数值,建议使用 [pg_stat_kcache](https://github.com/powa-team/pg_stat_kcache)。 31 | - 如果某个查询组的此指标值突然发生变化,并且这种变化持续了一段时间,可能是执行计划发生了变化,需要进一步研究。 32 | - 高层聚合值也是观察的重点,例如回答类似 "该服务器上所有查询平均读取了多少 MiB?" 的问题。 33 | - `dM/dc`,当 `M` 是 `wal_bytes`(PG13+)时 — 这是查询组生成的平均 WAL 大小,单位是字节。它有助于识别哪些查询组对 WAL 的生成贡献最大。所有 pgss 记录的"全局"聚合值代表了服务器上所有语句的平均 WAL 字节数。对于 `dM/dc`(`M` 是 `wal_fpi`),在检查点调优场景中,跟踪其变化非常有用:当 `full_page_writes = on` 并且增加检查点之间的距离时,我们应该观察到该值的下降,研究不同的 pgss 组也可能很有意义。 34 | 35 | --- 36 | 37 | 明天我们将完成关于 pgss 相关的第三步教程。 -------------------------------------------------------------------------------- /How to work with pg_stat_statements, part 3.md: -------------------------------------------------------------------------------- 1 | # How to work with pg_stat_statements, part 3 2 | 3 | ## 第三类衍生指标:百分比 4 | 5 | 现在,让我们来研究第三类衍生指标:某个查询组 (标准化查询或更大的组,例如"特定用户的所有语句"或"所有 `UPDATE` 语句") 在整个工作负载中所占的百分比,基于指标 M。 6 | 7 | 如何计算:首先,针对所有查询组应用基于时间的微分 (如第一部分中讨论的 `dM/dt`),然后将特定查询组的值除以所有查询组的总和。 8 | 9 | ## %M 的可视化与解读 10 | 11 | 虽然 `dM/dt` 给我们提供了绝对值,如每秒调用次数或 `GiB/sec`,但 `%M` 值是相对指标。这些值帮助我们识别工作负载中各个方面的"主要参与者" — 频率、时间、IO 操作等。 12 | 13 | 通过分析相对值,我们可以理解每个优化方向的潜在收益大小,并优先处理最具潜力的优化。例如: 14 | 15 | - 如果 QPS 的绝对值看起来很高 — 比如 1000 次调用/秒 — 但它仅占整体工作负载的 `3%`,那么减少这一查询不会带来太大的收益。如果我们关心的是 QPS,我们需要优化其他查询组。 16 | - 然而,如果我们有 1000 次调用/秒,并且它占了整体工作负载的 `50%`,那么单次优化步骤 — 比如将其减少到 10 次调用/秒 — 这将帮助我们减少几乎一半的 QPS。 17 | 18 | 在较大的系统中处理比例值的一种方法是对较大比例值做出反应,将相应的查询组作为优化候选项。例如,在具有大量查询组的系统中,可能有必要使用以下方法: 19 | 20 | - 定期针对某些指标 (例如 `calls`、`total_exec_time`、`total_plan_time`、`shared_blks_dirtied`、`wal_bytes`) 构建 Top-10 列表,显示具有最大 `%M` 值的查询组。 21 | - 如果某个查询组在某些指标上是主要贡献者 — 例如,>20% — 则应考虑将此查询作为优化的候选项。例如,在大多数情况下,我们不希望单个查询组占据 `total_exec_time` 总和的 1/2 ( total total_exec_time”,抱歉使用了赘语)。在某些情况下,可以决定某个查询不需要优化 — 在这种情况下,我们将该组标记为排除项,并在下次分析中跳过它。 22 | 23 | 对比例的分析也可以在监控系统中通过可视化方式隐式完成:观察 `dM/dt` 图表 (例如 QPS,每秒块命中次数),我们可以直观地理解哪个查询组在整个工作负载中对特定指标 `M` 的贡献最大。然而,为此,图表需要"[堆叠式](https://en.wikipedia.org/wiki/Bar_chart#Grouped_.28clustered.29_and_stacked)"显示。 24 | 25 | 如果我们处理两个快照,那么明确获取这些值是有意义的。此外,出于可视化目的,为我们分析的每个指标绘制一个饼图也是有意义的。 26 | 27 | ## %M 示例 28 | 29 | - `%M`,`M` 是调用次数 `calls` — 这为我们提供了 QPS 的比例。例如,如果我们通常有约 `10k QPS`,但如果某个查询组负责约 `7k QPS`,这可能被认为是不正常的,需要在客户端 (通常是应用代码) 进行优化。 30 | - `%M`,`M` 是 `total_plan_time + total_exec_time` — 服务器处理特定查询组所花费时间的百分比。例如,如果绝对值是 `20 秒/秒` (负载相当大的系统——Postgres 每秒需要花费 20 秒来处理查询),并且某个查询组占了该指标的 75%,这意味着我们需要重点优化该查询组。优化方法: 31 | - 如果 QPS (`calls/second`) 很高,那么首先我们需要减少它。 32 | - 如果平均延迟 (`total_exec_time`,或较少见的 `total_plan_time` 或两者) 较高,那么我们需要使用 `EXPLAIN` 和 `EXPLAIN (ANALYZE, BUFFERS)` 进行微观优化。 33 | - 在某些情况下,我们需要结合两种优化方向。 34 | - `%M`,`M` 是 `shared_blks_dirtied` — 分析查询组在缓冲池中执行的更改所占百分比。这种分析可以帮助我们识别工作负载中的写密集部分,并找到减少检查点次数和磁盘 IO 量的机会。 35 | - `%M`,`M` 是 `wal_bytes` — 写入 WAL 的字节数百分比。这帮助我们识别优化 WAL 生成量最有影响的查询组。 36 | 37 | ## 总结:三大宏观优化目标及其使用方法 38 | 39 | 现在,通过我们在前两部分中描述的分析方法,让我们仅基于一个指标 `total_exec_time` 来考虑三种常见的宏观优化类型。 40 | 41 | 理解这三种方法 (然后将这种逻辑应用于其他指标) 可以帮助你理解如何设计监控仪表盘。 42 | 43 | - 宏观优化旨在减少资源消耗。在这里,我们首先希望减少 CPU 利用率、内存和磁盘 IO 操作。为此,我们需要使用 `dM/dt` 类型的衍生指标 — Postgres 每秒处理查询所花费的秒数。减少这一指标 (整个服务器的聚合 "total" 值,以及在其中起重要作用的前 N 个组) 是我们的目标。此外,我们可能还想考虑其他指标,例如 `shared_blks_***`,但时间可能是最好的起点。这种优化可以帮助我们进行容量规划、基础设施成本优化,并降低资源耗尽的风险。 44 | - 宏观优化旨在改善用户体验。在这里,我们希望用户有最佳体验,因此在 OLTP 场景中,我们应重点关注平均延迟,并以减少它们为目标。因此,我们将使用 `dM/dc` — 每个查询平均持续的秒数 (或毫秒)。如果在上一种优化类型中,我们希望在监控系统中看到按 `dM/dt` 值 (以 `seconds/second` 为单位) 排序的前 N 个查询组,这里我们希望按平均延迟 (以秒为单位) 排序的前 N 个组。通常,这会给我们一个非常不同的查询集 — 这些查询可能 QPS 较低,但延迟最差,最让用户感到烦恼。在某些情况下,在这种分析中,我们可能想排除 QPS 较低的查询组 (例如 QPS < 1 call/sec 的那些) 或排除一些工作负载部分,如数据导出活动,它们不可避免地有较长的延迟。 45 | - 宏观优化旨在平衡工作量。这是较为罕见的一种优化类型,但这正是 %M 发挥作用的地方。开发我们的应用程序时,我们可能想不时检查 `total_exec_time + total_plan_time` 的前 N 个百分比,并识别占比最大的查询组 — 正如我们之前讨论的那样。 46 | 47 | 附加内容:播客集 48 | 49 | 相关的 Postgres.FM 播客集: 50 | 51 | - [Intro to query optimization](https://postgres.fm/episodes/intro-to-query-optimization) 52 | - [102 query optimization](https://postgres.fm/episodes/102-query-optimization) 53 | - [pg_stat_statements](https://postgres.fm/episodes/pg_stat_statements) 54 | 55 | --- 56 | 57 | 这就是我们目前对 pgss 的讨论。当然,这不是完整的指南,我们可能会在未来再次讨论这个重要的扩展。如有任何问题或其他反馈,请告诉我! -------------------------------------------------------------------------------- /Index maintenance.md: -------------------------------------------------------------------------------- 1 | # Index maintenance 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在大型项目中,索引维护是不可避免的。越早建立维护流程,性能表现会越好。 6 | 7 | ## 分析索引健康状况 8 | 9 | 索引健康分析包括: 10 | 11 | - 识别无效索引 12 | - 膨胀分析 13 | - 查找未使用的索引 14 | - 查找冗余索引 15 | - 检查索引损坏情况 16 | 17 | ## 无效索引 18 | 19 | 查找无效索引非常简单: 20 | 21 | ```sql 22 | nik=# select indexrelid, indexrelid::regclass, indrelid::regclass 23 | from pg_index 24 | where not indisvalid; 25 | indexrelid | indexrelid | indrelid 26 | ------------+------------+---------- 27 | 49193 | t_id_idx | t1 28 | (1 row) 29 | ``` 30 | 31 | 在 [Postgres DBA](https://github.com/NikolayS/postgres_dba/) 中可以找到更全面详细的查询。 32 | 33 | 在分析这个列表时,请记住,如果索引正在通过 `CREATE INDEX CONCURRENTLY` 或 `REINDEX CONCURRENTLY` 创建或重建,无效索引可能是正常情况,因此还值得检查 `pg_stat_activity `以识别此类进程。 34 | 35 | 其他无效索引必须重建 (`REINDEX CONCURRENTLY`) 或删除 (`DROP INDEX CONCURRENTLY`)。 36 | 37 | ## 膨胀索引 38 | 39 | 如何分析索引膨胀:参照 [Day 46: How to deal with bloat](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/blob/main/0046_how_to_deal_with_bloat.md) 40 | 41 | 膨胀程度较高的索引 (真实的或预估的) 超过 50%,必须通过 `REINDEX CONCURRENTLY `进行重建。 42 | 43 | 由于随着时间的推移,索引健康状况会因更新而下降,因此需要重新索引。以下方法可以帮助减缓这种下降: 44 | 45 | - 升级到 Postgres 14 或更新的版本 (以利用 btree 优化)。 46 | - 优化工作负载和架构,以使用更多的 HOT ([Heap Only Tuple](https://postgresql.org/docs/current/storage-hot.html))。 47 | - 调整 autovacuum,使其更积极地执行清理 (但请记住,`VACUUM` 不会重新平衡 btree 索引)。 48 | 49 | 因此,你需要定期对已知膨胀的索引使用 `REINDEX CONCURRENTLY`。理想情况下,这个过程应该自动化。例如:[pg_auto_reindexer](https://github.com/vitabaks/pg_auto_reindexer)。 50 | 51 | ## 未使用的索引 52 | 53 | 可以在 [pg_stat_user_indexes ](https://postgresql.org/docs/current/monitoring-stats.html)中找到使用信息。[Postgres DBA](https://github.com/NikolayS/postgres_dba/) 中提供了一个用于分析的查询示例。 54 | 55 | 在查找未使用的索引时,请保持谨慎,避免错误: 56 | 57 | - 确保统计信息已经足够长时间没有重置。例如,如果统计信息几天前才被重置,你认为某个索引未被使用,这可能是一个错误 — 如果需要这个索引来支持下个月 1 号的一些报告。 58 | - 不要忘记收集承接工作负载的所有节点的统计信息 — 包括主节点和所有副本。在主节点上看似未使用的索引,可能在副本上是必需的。 59 | - 如果有多个系统安装,请确保分析了所有系统或至少一部分具有代表性的系统。 60 | 61 | 一旦可靠地识别出未使用的索引,那么应该使用 `DROP INDEX CONCURRENTLY` 进行删除。 62 | 63 | 我们能否软删除索引 (将其"隐藏"以确保计划器行为不变,如果没有问题,再执行真正的删除,否则快速恢复至原始状态)?不幸的是,这里没有简单的答案: 64 | 65 | - [HypoPG 1.4.0](https://github.com/HypoPG/hypopg/releases/tag/1.4.0) 有一个"隐藏"索引的功能 — 这非常有用,但需要安装它。更重要的是,在整个工作负载中使用它可能具有挑战性,因为你需要调用 `hypopg_hide_index(oid)`。 66 | 67 | - 有些人使用将 `indisvalid` 设置为 false 的技巧来隐藏索引 — 但有可靠的观点认为这不是一个安全的做法;请参见 [Peter Geoghegan's Tweet](https://twitter.com/petervgeoghegan/status/1599191964045672449): 68 | 69 | > It's unsafe, basically. Though hard to say just how likely it is to break. Here is one hazard that I know of: in general such an update might break a concurrent `pg_index.indcheckxmin = true` check. It will effectively "change the xmin" of your affected row, confusing the check. 70 | > 71 | > 基本上,这是不安全的。尽管很难说它破坏的可能性有多大。我知道的一个风险是:通常这种更新可能会破坏并发的 `pg_index.indcheckxmin = true` 检查。它实际上会"改变受影响行的 xmin",使检查混淆。 72 | 73 | ## 冗余索引 74 | 75 | 如果索引 B 可以有效地支持与索引 A 相同 (甚至更多) 的查询集,那么索引 A 相对于索引 B 是冗余的。几个例子: 76 | 77 | - 列 (a) 上的索引相对于列 (a, b) 上的索引是冗余的。 78 | - 列 (a) 上的索引相对于列 (b, a) 上的索引不是冗余的。 79 | 80 | 具有完全相同定义的索引是彼此冗余的 (即重复索引)。 81 | 82 | 可以在 [Postgres DBA](https://github.com/NikolayS/postgres_dba/) 或 [Postgres Checkup](https://gitlab.com/postgres-ai/postgres-checkup) 中找到用于识别冗余索引的查询示例。 83 | 84 | 冗余索引通常可以安全删除 (在手动仔细检查列表后),即使要删除的索引当前正在使用。 85 | 86 | ## 损坏 87 | 88 | 使用 `pg_amcheck` 识别 btree 索引中的损坏情况 (详细内容将在另一篇操作指南中介绍)。 89 | 90 | 截至 2023 年/PG16,`pg_amcheck` 还不支持以下功能,但未来有计划添加: 91 | 92 | - [检查 GIN 和 GIST 索引](https://commitfest.postgresql.org/45/3733/) 93 | - 检查唯一键 (已经推送,可能在 PG17 中发布) -------------------------------------------------------------------------------- /Learn how to work with schema metadata by spying after psql.md: -------------------------------------------------------------------------------- 1 | # Learn how to work with schema metadata by spying after psql 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## 不确定时,从何开始 6 | 7 | [psql 文档](https://postgresql.org/docs/current/app-psql.html) 8 | 9 | `psql` 有内置帮助文档 — 命令`\?` ;在使用 psql 时值得记住并将其作为起点。 10 | 11 | 当你不确定某个函数的确切名称时,可以使用搜索命令 `\df *关键字*`。例如: 12 | 13 | ```sql 14 | nik=# \df *clock* 15 | List of functions 16 | Schema | Name | Result data type | Argument data types | Type 17 | ------------+-----------------+--------------------------+---------------------+------ 18 | pg_catalog | clock_timestamp | timestamp with time zone | | func 19 | (1 row) 20 | ``` 21 | 22 | ## 如何查看 psql 正在做什么 – ECHO_HIDDEN 23 | 24 | 假设我们想查看表 `t1` 的大小,为此,我们可以构建一个返回表大小的查询 (或者只是在某处找到其大小或询问 LLM)。但是当使用 `psql` 时,我们只需使用 `\dt+ t1`: 25 | 26 | ```sql 27 | nik=# \dt+ t1 28 | List of relations 29 | Schema | Name | Type | Owner | Persistence | Access method | Size | Description 30 | --------+------+-------+----------+-------------+---------------+-------+------------- 31 | public | t1 | table | postgres | permanent | heap | 25 MB | 32 | (1 row) 33 | ``` 34 | 35 | 我们可能想要循环执行,以观察表大小是如何增长的。为此,psql 支持 `\watch` — 但是,它不适用于其他反斜杠命令。 36 | 37 | 解决方案 — 打开 `ECHO_HIDDEN` 并查看 `\dt+` 后面的 SQL 查询 (或者,你可以在启动 `psql` 时使用选项 `--echo-hidden`): 38 | 39 | ```sql 40 | nik=# \set ECHO_HIDDEN 1 41 | nik=# 42 | nik=# \dt+ t1 43 | ********* QUERY ********** 44 | SELECT n.nspname as "Schema", 45 | c.relname as "Name", 46 | CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as "Type", 47 | pg_catalog.pg_get_userbyid(c.relowner) as "Owner", 48 | CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as "Persistence", 49 | am.amname as "Access method", 50 | pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as "Size", 51 | pg_catalog.obj_description(c.oid, 'pg_class') as "Description" 52 | FROM pg_catalog.pg_class c 53 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 54 | LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam 55 | WHERE c.relkind IN ('r','p','t','s','') 56 | AND c.relname OPERATOR(pg_catalog.~) '^(t1)$' COLLATE pg_catalog.default 57 | AND pg_catalog.pg_table_is_visible(c.oid) 58 | ORDER BY 1,2; 59 | ************************** 60 | 61 | List of relations 62 | Schema | Name | Type | Owner | Persistence | Access method | Size | Description 63 | --------+------+-------+----------+-------------+---------------+-------+------------- 64 | public | t1 | table | postgres | permanent | heap | 72 MB | 65 | (1 row) 66 | ``` 67 | 68 | 现在我们得到了 SQL 查询,便可以使用 `\watch 5` 每隔 5 秒查看一次表大小 (并且可以省略不需要的字段): 69 | 70 | ```sql 71 | nik=# SELECT n.nspname as "Schema", 72 | c.relname as "Name", 73 | pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as "Size" 74 | FROM pg_catalog.pg_class c 75 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 76 | LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam 77 | WHERE c.relkind IN ('r','p','t','s','') 78 | AND c.relname OPERATOR(pg_catalog.~) '^(t1)$' COLLATE pg_catalog.default 79 | AND pg_catalog.pg_table_is_visible(c.oid) 80 | ORDER BY 1,2 \watch 5 81 | Thu 16 Nov 2023 08:00:10 AM UTC (every 5s) 82 | 83 | Schema | Name | Size 84 | --------+------+------- 85 | public | t1 | 73 MB 86 | (1 row) 87 | 88 | Thu 16 Nov 2023 08:00:15 AM UTC (every 5s) 89 | 90 | Schema | Name | Size 91 | --------+------+------- 92 | public | t1 | 75 MB 93 | (1 row) 94 | 95 | Thu 16 Nov 2023 08:00:20 AM UTC (every 5s) 96 | 97 | Schema | Name | Size 98 | --------+------+------- 99 | public | t1 | 77 MB 100 | (1 row) 101 | ``` -------------------------------------------------------------------------------- /Postgres major upgrade without any downtime for a very large cluster running under heavy load.md: -------------------------------------------------------------------------------- 1 | # Postgres major upgrade without any downtime for a very large cluster running under heavy load 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 目前,在 HN 首页上有一篇由 knock.app 撰写的关于[零停机升级 Postgres](https://news.ycombinator.com/item?id=38616181) 的文章。 6 | 7 | 本周晚些时候,GitLab 的 [Alexander Sosna](https://twitter.com/xxorde) 将会做一个[演讲](https://www.postgresql.eu/events/pgconfeu2023/schedule/session/4791-how-we-execute-postgresql-major-upgrades-at-gitlab-with-zero-downtime/),解释 GitLab 的大型集群是如何在高负载下进行零停机升级的 — 我强烈推荐大家了解这项工作。 8 | 9 | 实现真正的零停机升级背后有许多细节和挑战需要解决,这里我只提供一个高层次的规划。该方法适用于非常大的 (数十 TB) 集群,具有多个副本,并在高 TPS (每秒数万次事务) 环境下运行。这里描述的过程涉及逻辑复制 (使用了"物理到逻辑"的技巧) 以及 PgBouncer 的 PAUSE/RESUME (假设使用了 PgBouncer)。 10 | 11 | 详细的材料需要编写几篇单独的使用指南。 12 | 13 | 该过程包括两个步骤: 14 | 15 | 1. **升级**:创建新的集群,运行在新的 Postgres 主版本上。 16 | 2. **切换**:逐步切换流量。 17 | 18 | ## 第一步:升级 19 | 20 | 一种"标准"方法是从头创建一个新集群 (`initdb`),然后基于它创建一个全新的逻辑副本 (在创建逻辑订阅时使用 `copy_data = false`)。然而,这种逻辑副本配置方式需要大量时间,在高负载下执行可能会出现很大问题。 21 | 22 | 替代方法之一是获取一个物理副本并将其转换为逻辑副本。只要知道逻辑复制槽的 LSN 位置并使用 `recovery_target_lsn`,这便相对容易做到。这个方法被称为"physical2logical"转换,它允许基于物理副本 (例如,基于几分钟内的云快照创建) 快速可靠地创建逻辑副本。 23 | 24 | 然而,将 physical2logical 转换与 `pg_upgrade` 结合使用需要慎之又慎。详细信息可以在[这里](https://postgresql.org/message-id/flat/20230217075433.u5mjly4d5cr4hcfe%40jrouhaud)找到。 25 | 26 | 步骤: 27 | 28 | 1. 创建一个新集群,它将是使用级联物理复制的辅助集群 (Patroni 支持)。 29 | 2. 确保新集群的主节点没有显著滞后于旧集群的主节点。 30 | 3. 以适当的顺序停止新集群的节点 (确保停止的副本与其主节点完全同步)。 31 | 4. 在旧集群的主节点上,创建一个逻辑复制槽并为所有表创建发布 (这不需要表级锁,因此我们不需要设置较低的 `lock_timeout` 并进行重试),并记下复制槽的位置。 32 | 5. 重新配置新集群的主节点,设置 `recovery_target_lsn` 为所记录的复制槽的 LSN,并禁用 `restore_command`。 33 | 6. 启动新集群的主节点和副本,让它们达到所需的 LSN。然后,便可以提升主节点了。 34 | 7. 再次以适当的顺序停止新集群的节点。 35 | 8. 在新集群的主节点上运行 `pg_upgrade --link`。 36 | 9. 对新集群的副本使用 `rsync --hard-links --size-only` — 这是一个有争议的步骤 (详见[此处](https://www.postgresql.org/message-id/flat/CAM527d8heqkjG5VrvjU3Xjsqxg41ufUyabD9QZccdAxnpbRH-Q%40mail.gmail.com)),但这是大多数人在使用 `pg_upgrade --link` 进行就地 (无需逻辑复制) 升级时采用的方法,目前还没有其他快速的替代方案。 37 | 10. 配置新集群的主节点 (现在已经是主节点) 以使用逻辑复制 — 创建订阅,设置 `copy_data = false`,让它追赶上正在运行的旧集群。 38 | 39 | 在所有这些步骤中,旧集群一直在运行,而新集群对用户是不可见的。这使得你能够在生产环境中测试整个过程 (在较低环境中进行适当测试后),这可以带来巨大的好处。 40 | 41 | ## 第二步:切换 42 | 43 | 首先,切换只读 (RO) 流量是有意义的。如果应用程序允许,首先将部分只读流量重定向到新的副本是有意义的。这需要在负载均衡代码中实现高级的复制滞后检测(参照:[Day 17: How to determine the replication lag - Hybrid case: logical & physical](https://postgres-howto.cn/#/./docs/17))。 44 | 45 | 当需要重定向读写 (RW) 流量时,为了实现零停机,可以使用 PgBouncer 的 PAUSE/RESUME 功能。如果有多个 PgBouncer 节点 (运行在不同的主机/端口上,或涉及 `SO_REUSEPORT`),实现一个优雅的 PAUSE 获取方法非常重要。一旦所有的 PAUSE 都被获取,就可以重定向流量,然后发出 RESUME。在此之前,重要的是确保所有写操作都已完全传播到新的主节点。 46 | 47 | 重要的是要制定措施来防止来自应用程序或用户的写操作影响旧集群 — 例如,调整 `pg_hba.conf` 或关闭集群。然而,为了实现高级的回滚功能,有必要以与"正向"相同的方式实现"反向"逻辑复制,并在切换期间设置。在这种情况下,对旧集群的写操作是允许的 — 但只能来自逻辑复制。反向复制允许即使在整个过程完成后的一段时间内进行回滚,这使整个过程在任何时候都可以完全逆转。 48 | 49 | 如前所述,还有许多方面需要解决,这只是一个高级别的计划。如果有机会参加这个演讲,请在[这里](https://www.postgresql.eu/events/pgconfeu2023/schedule/session/4791-how-we-execute-postgresql-major-upgrades-at-gitlab-with-zero-downtime/)参加。 50 | 51 | ## 我见 52 | 53 | 参照之前的文章 [How to convert a physical replica to logical](https://postgres-howto.cn/#/./docs/57?id=how-to-convert-a-physical-replica-to-logical),另外推荐去阅读一下这篇 PDF:https://www.postgresql.eu/events/pgconfeu2023/schedule/session/4791-how-we-execute-postgresql-major-upgrades-at-gitlab-with-zero-downtime/,针对 logical replication + pg_upgrade 的升级方式进行了优化。 54 | 55 | -------------------------------------------------------------------------------- /Pre- and post-steps for benchmark iterations.md: -------------------------------------------------------------------------------- 1 | # Pre- and post-steps for benchmark iterations 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在进行 PostgreSQL 基准测试时 (参照 [Day 13: How to benchmark]()),我们通常需要在相同的设置上运行多次基准测试迭代。为保证每次迭代的统一性,通常会在每次迭代前后执行以下步骤: 6 | 7 | - 前置:刷新缓存 (或者,相反地,预热缓存)。 8 | - 前置:重置累积统计信息。 9 | - 后置:保存统计信息和其他基准测试结果。 10 | 11 | 这里描述的方法允许以统一的方式进行基准测试。 12 | 13 | ## 前置步骤:刷新缓存 14 | 15 | 在准备工作时,通常有两种策略: 16 | 17 | - 预热缓存 (可以使用 `pg_prewarm` 扩展,或者多次执行基准测试多次,只记录最后一次的结果。)此处就不讨论了。 18 | - 刷新缓存 (清空操作系统页面缓存和 PostgreSQL 缓冲池),使每次迭代从"冷启动"开始。 19 | 20 | 如果选择第二种策略,建议: 21 | 22 | - 增加每次迭代的持续时间,以便缓存有足够时间热起来。 23 | - 查看每次迭代内的时间序列指标 (例如,使用 `pgbench` 的 `-P 10` 选项以每隔 10 秒显示延迟/吞吐量)。 24 | 25 | 如何刷新缓存: 26 | 27 | 1. PostgreSQL 缓冲池 – 只需重启 Postgres 即可: 28 | 29 | ```bash 30 | pg_ctl restart -D $PGDATA -m fast 31 | ``` 32 | 33 | 2. 释放 `pagecache`、`dentry` 和 `inode`: 34 | 35 | ```bash 36 | sync # 将所有缓冲操作写入磁盘 37 | echo 3 > /proc/sys/vm/drop_caches 38 | ``` 39 | 40 | ## 前置步骤:重置统计信息 41 | 42 | 在运行基准测试之前,可能需要重置集群中的所有累积统计信息 (取决于当前的 PostgreSQL 版本),包括标准扩展如 `pg_stat_statements` 和其他扩展如 `pg_wait_sampling`、`pg_stat_kcache`、`pg_qualstats`(如果安装了)。 43 | 44 | 以下是重置统计信息的脚本: 45 | 46 | ```sql 47 | do $$ 48 | declare 49 | reset_cmd_main json := $json${ 50 | "pg_stat_reset()": 90000, 51 | "pg_stat_reset_shared('bgwriter')": 90000, 52 | "pg_stat_reset_shared('archiver')": 90000, 53 | "pg_stat_reset_shared('io')": 160000, 54 | "pg_stat_reset_shared('wal')": 140000, 55 | "pg_stat_reset_shared('recovery_prefetch')": 140000, 56 | "pg_stat_reset_slru(null)": 130000 57 | }$json$; 58 | 59 | reset_cmd_et json := $json${ 60 | "pg_stat_statements_reset()": "pg_stat_statements", 61 | "pg_stat_kcache_reset()": "pg_stat_kcache", 62 | "pg_wait_sampling_reset_profile()": "pg_wait_sampling", 63 | "pg_qualstats_reset()": "pg_qualstats" 64 | }$json$; 65 | 66 | cmd record; 67 | cur_ver int; 68 | begin 69 | cur_ver := current_setting('server_version_num')::int; 70 | raise info 'Current PG version (num): %', cur_ver; 71 | 72 | -- Main reset commands 73 | for cmd in select * from json_each_text(reset_cmd_main) loop 74 | if cur_ver >= (cmd.value)::int then 75 | raise info 'Execute SQL: select %', cmd.key; 76 | execute format ('select %s', cmd.key); 77 | end if; 78 | end loop; 79 | 80 | -- Extension reset commands 81 | for cmd in select * from json_each_text(reset_cmd_et) loop 82 | if '' <> ( 83 | select installed_version 84 | from pg_available_extensions 85 | where name = cmd.value 86 | ) then 87 | raise info 'Execute SQL: select %', cmd.key; 88 | execute format ('select %s', cmd.key); 89 | end if; 90 | end loop; 91 | end 92 | $$; 93 | ``` 94 | 95 | 在基准测试之前运行该步骤,确保统计信息是干净的,并且不会受到准备操作的影响。 96 | 97 | ## 后置步骤:收集基准测试结果 98 | 99 | 建议执行以下步骤: 100 | 101 | 1. 导出所有 `pg_stat_*` 视图的内容: 102 | 103 | ```bash 104 | for viewname in $(psql -tAXc " 105 | select relname 106 | from pg_catalog.pg_class 107 | where relkind = 'view' and relname like 'pg_stat%'" \ 108 | ); do 109 | psql -Xc "copy (select * from ${viewname}) 110 | to stdout with csv header delimiter ','" \ 111 | > "${destination}/${viewname}.csv" 112 | done 113 | 114 | psql -Xc "copy (select * from pg_stat_kcache()) 115 | to stdout with csv header delimiter ','" \ 116 | > "${destination}/pg_stat_kcache.csv" 117 | 118 | psql -Xc "copy ( 119 | select 120 | event_type as wait_type, 121 | event as wait_event, 122 | sum(count) as of_events 123 | from pg_wait_sampling_profile 124 | group by event_type, event 125 | order by of_events desc 126 | ) to stdout with csv header delimiter ','" \ 127 | > "${destination}/pg_wait_sampling_profile.csv" 128 | ``` 129 | 130 | 2. 收集日志文件:压缩并复制日志目录中的文件。 131 | 132 | 3. 导出当前配置 (`pg_settings`): 133 | 134 | ```bash 135 | psql -Xc "copy ( 136 | select * from pg_settings order by name 137 | ) to stdout with csv header delimiter ','" \ 138 | > "${destination}/pg_settings_all.csv" 139 | 140 | psql -Xc " 141 | select name, setting as current_setting, unit, boot_val as default, context 142 | from pg_settings 143 | where source <> 'default'" \ 144 | > "${destination}/pg_settings_non_default.txt" 145 | ``` 146 | 147 | 4. 其他有用的基准测试数据:如果需要,收集系统日志、sar 数据等。 148 | 149 | -------------------------------------------------------------------------------- /Rough configuration tuning (8020 rule; OLTP).md: -------------------------------------------------------------------------------- 1 | # Rough configuration tuning (80/20 rule; OLTP) 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 二八法则 (也称为[帕累托法则](https://en.wikipedia.org/wiki/Pareto_principle)) 通常足以为 OLTP 负载提供良好的性能。在大多数情况下,建议采用这种方法并专注于查询调优。尤其是在系统快速变化时,单纯依赖配置调优来争取"最后的 20%",往往不如通过忽视的模式级优化 (例如缺失索引) 来提升性能有意义。然而,如果工作负载和数据库没有太快变化,或者有很多 (比如几千个) Postgres 节点时,争取这"20%”是很有意义的 (例如,预算方面)。 6 | 7 | 这就是为什么像 [PGTune](https://pgtune.leopard.in.ua) 这样的简易经验性调优工具通常已经足够的原因。此处让我们考虑一个例子:一台中等规模的服务器 (64 vCPU,512 GiB RAM),处理中等规模的 OLTP (比如 Web/移动应用)工作负载。 8 | 9 | 以下设置应作为起点,数值仅为粗略的参考 — 请根据具体情况进行审查,通过非生产环境中的实验验证,并密切监控所有变化。 10 | 11 | 推荐资源: 12 | 13 | - [PGTune](https://pgtune.leopard.in.ua) 14 | - [postgresql.conf configurations](https://postgresqlco.nf) 15 | - [postgresql_cluster's defaults](https://github.com/vitabaks/postgresql_cluster/blob/master/vars/main.yml) 16 | 17 | 1. `max_connections = 200` 18 | 19 | 根据预期的并发连接数来设置。此处假设我们使用了连接池,减少维持大量空闲连接的需求。如果使用PG15+,可以设置更高的值 (参照:[Improving Postgres Connection Scalability: Snapshots](https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/improving-postgres-connection-scalability-snapshots/ba-p/1806462#conclusion-one-bottleneck-down-in-pg-14-others-in-sight))。 20 | 21 | 2. `shared_buffers = 128GB` 22 | 23 | 通常情况下,建议将其设置为总内存的 25%。这是 Postgres 缓存表和索引数据的地方。 24 | 25 | 25% 是最常见的设置,尽管有时有人批评它不是最优的,但在大多数情况下是"足够好"且非常安全的。 26 | 27 | 3. `effective_cache_size = 384GB` 28 | 29 | 提供给查询规划器的内存建议,表示可用于缓存数据的内存量,包括操作系统缓存。 30 | 31 | 4. `maintenance_work_mem = 2GB` 32 | 33 | 提高维护性操作 (比如 `VACUUM`、`CREATE INDEX` 等) 的性能。 34 | 35 | 5. `checkpoint_completion_target = 0.9` 36 | 37 | 控制 checkpoint 的完成目标,通过分散写入活动来减少 IO 峰值。 38 | 39 | 6. `random_page_cost = 1.1` 40 | 41 | 微调此项以反映随机 IO 的实际成本。默认值是 4,`seq_page_cost` 是 1 — 对于旋转硬盘来说是可以接受的。对于 SSD 来说,使用相等或接近的值是有意义的 (Crunchy Data 最近的[基准测试](https://docs.crunchybridge.com/changelog#postgres_random_page_cost_1_1)表明,1.1 比 1 稍微好一些)。 42 | 43 | 7. `effective_io_concurrency = 200` 44 | 45 | 对于 SSD 来说,可以设置比 HDD 更高的值,反映其处理更多 IO 操作的能力。 46 | 47 | 8. `work_mem = 100MB` 48 | 49 | 每个查询的排序和连接操作所用的内存。设置时要小心,因为如果同时运行太多查询,过高的值可能会导致 OOM 的问题。 50 | 51 | 9. `huge_pages = try` 52 | 53 | 使用大页内存可以通过减少页管理开销来提高性能。 54 | 55 | 10. `max_wal_size = 10GB` 56 | 57 | 这是 checkpoint 调优的一部分。10GB 是一个相对较大的值,尽管有些人可能更倾向于使用更大的值,但这也带来了一个权衡: 58 | 59 | - 更大的值有助于更好地处理大量写入 (IO 压力更低) 60 | 61 | - 但同时也会导致崩溃后恢复时间更长。 62 | 63 | > 🎯 TODO:关于 checkpoint 调优的独立指南。 64 | 65 | 11. `max_worker_processes = 64` 66 | 67 | 数据库集群可以使用的最大进程数。对应 CPU 核心数。 68 | 69 | 12. `max_parallel_workers_per_gather = 4` 70 | 71 | 每个 Gather 或 Gather Merge 节点最多可以启动的工作进程数。 72 | 73 | 13. `max_parallel_workers = 64` 74 | 75 | 可用于并行操作的工作进程总数。 76 | 77 | 14. `max_parallel_maintenance_workers = 4` 78 | 79 | 控制并行维护任务 (比如创建索引) 的工作进程数。 80 | 81 | 15. `jit = off` 82 | 83 | 对于 OLTP 工作负载,建议关闭 JIT 编译。 84 | 85 | 16. 超时设置 86 | 87 | > 🎯 **TODO:** 一篇独立指南 88 | 89 | ~~~sql 90 | statement_timeout = 30s 91 | idle_in_transaction_session_timeout = 30s 92 | ~~~ 93 | 94 | 17. Autovacuum调优 95 | 96 | > 🎯 **TODO:** 一篇独立指南 97 | 98 | ~~~bash 99 | autovacuum_max_workers = 16 100 | autovacuum_vacuum_scale_factor = 0.01 101 | autovacuum_analyze_scale_factor = 0.01 102 | autovacuum_vacuum_insert_scale_factor = 0.02 103 | autovacuum_naptime = 1s 104 | # autovacuum_vacuum_cost_limit – increase if disks are powerful 105 | autovacuum_vacuum_cost_delay = 2 106 | ~~~ 107 | 108 | 18. 可观测性与日志 109 | 110 | > 🎯 **TODO:** 一篇独立指南 111 | 112 | ~~~bash 113 | logging_collector = on 114 | log_checkpoints = on 115 | log_min_duration_statement = 500ms # review 116 | log_statement = ddl 117 | log_autovacuum_min_duration = 0 # review 118 | log_temp_files = 0 # review 119 | log_lock_waits = on 120 | log_line_prefix = %m [%p, %x]: [%l-1] user=%u,db=%d,app=%a,client=%h 121 | log_recovery_conflict_waits = on 122 | track_io_timing = on # review 123 | track_functions = all 124 | track_activity_query_size = 8192 125 | ~~~ -------------------------------------------------------------------------------- /psql shortcuts.md: -------------------------------------------------------------------------------- 1 | # psql shortcuts 2 | 3 | >我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | 在我的工具 [postgres_dba](https://github.com/NikolayS/postgres_dba/) 中,安装命令包含: 6 | 7 | ```bash 8 | printf "%s %s %s %s\n" \\set dba \'\\\\i $(pwd)/start.psql\' >> ~/.psqlrc 9 | ``` 10 | 11 | 这种方式可以让我们在 `psql` 中只需输入 `:dba` 即可调用 `start.psql`,而且该命令在 `bash` 和 `zsh` 中均有效 (macOS 几年前将默认 shell 更改为 `zsh`,而 `bash` 通常是 Linux 发行版的默认 shell)。 12 | 13 | 我们可以很容易地在 `.psqlrc` 中添加自己的指令,以定义各种便捷的快捷键。比如: 14 | 15 | ```bash 16 | \set pid 'select pg_backend_pid() as pid;' 17 | \set prim 'select not pg_is_in_recovery() as is_primary;' 18 | \set a 'select state, count(*) from pg_stat_activity where pid <> pg_backend_pid() group by 1 order by 1;' 19 | ``` 20 | 21 | 👉 这添加了简便的快捷方式: 22 | 23 | 1. 当前 PID: 24 | 25 | ```sql 26 | nik=# :pid 27 | pid 28 | -------- 29 | 513553 30 | (1 row) 31 | ``` 32 | 33 | 2. 判断是否为主节点? 34 | 35 | ```sql 36 | nik=# :prim 37 | is_primary 38 | ------------ 39 | t 40 | (1 row) 41 | ``` 42 | 43 | 3. 简单活动摘要 44 | 45 | ```sql 46 | nik=# :a 47 | state | count 48 | ---------------------+------- 49 | active | 19 50 | idle | 193 51 | idle in transaction | 2 52 | | 7 53 | (4 rows) 54 | ``` 55 | 56 | 在 SQL 上下文中,可以使用下面这种有趣的语法,将当前设置的 psql 变量值作为字符串进行传递: 57 | 58 | ```sql 59 | nik=# select :'pid'; 60 | ?column? 61 | --------------------------------- 62 | select pg_backend_pid() as pid; 63 | (1 row) 64 | ``` 65 | 66 | 在编写脚本时,不要忘了使用 `-X` (`--no-psqlrc`)选项,以确保 `.psqlrc` 中的内容不会影响脚本的逻辑。 67 | 68 | -------------------------------------------------------------------------------- /psql tuning.md: -------------------------------------------------------------------------------- 1 | # psql tuning 2 | 3 | > 我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享! 4 | 5 | ## .psqlrc 6 | 7 | 文件 `~/.psqlrc` 可以用于设置一些默认配置。例如: 8 | 9 | ```bash 10 | echo '\timing on' >> ~/.psqlrc 11 | ``` 12 | 13 | 现在,如果我们启动 `psql`: 14 | 15 | ```bash 16 | ❯ psql -U postgres 17 | Timing is on. 18 | psql (15.5 (Debian 15.5-1.pgdg120+1)) 19 | Type "help" for help. 20 | 21 | nik=# select pg_sleep(.5); 22 | pg_sleep 23 | ---------- 24 | 25 | (1 row) 26 | 27 | Time: 508.187 ms 28 | ``` 29 | 30 | ❗**重要提示:**如果脚本涉及到 `psql`,建议使用 `-X` 选项来忽略 `~/.psqlrc` 中的设置,这样脚本逻辑 (例如,分析 `psql` 的输出) 不会依赖于 `~/.psqlrc` 中的任何配置。 31 | 32 | ## pspg 33 | 34 | [pspg](https://github.com/okbob/pspg) 非常棒。如果可能的话,建议安装。 35 | 36 | 例如,对于查询 `select * from pg_class limit 3;`,安装前: 37 | 38 | ![psql ugly output](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0059_psql_ugly_output.png) 39 | 40 | 安装后: 41 | 42 | ![pspg improved output](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0059_pspg_improved_output.png) 43 | 44 | ![pspg menus](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0059_pspg_menus.jpg) 45 | 46 | 安装方法: 47 | 48 | - macOS/Homebrew:`brew install pspg` 49 | - Ubuntu/Debian:`sudo apt update && sudo apt install -y pspg` 50 | 51 | 然后: 52 | 53 | ```bash 54 | echo "\setenv PAGER pspg 55 | \pset border 2 56 | \pset linestyle unicode 57 | \set x '\setenv PAGER less' 58 | \set xx '\setenv PAGER \'pspg -bX --no-mouse\'' 59 | " >> ~/.psqlrc 60 | ``` 61 | 62 | ## NULLs 63 | 64 | 默认情况下,`psql` 输出中的 `NULL` 值是不可见的: 65 | 66 | ```sql 67 | nik=# select null; 68 | ?column? 69 | ---------- 70 | 71 | (1 row) 72 | ``` 73 | 74 | 使用如下方式进行修复 (将其放入 `~/.psqrc` 以确保持久性) 75 | 76 | ```bash 77 | \pset null 'Ø' 78 | ``` 79 | 80 | 现在 `NULL` 值将显示为: 81 | 82 | ```sql 83 | nik=# select null; 84 | ?column? 85 | ---------- 86 | Ø 87 | (1 row) 88 | ``` 89 | 90 | ## postgres_dba 91 | 92 | [postgres_dba](https://github.com/NikolayS/postgres_dba) 是我个人收集的一些用于 `psql` 的脚本集合,带有菜单操作。 93 | 94 | ![postgres_dba menu support](https://gitlab.com/postgres-ai/postgresql-consulting/postgres-howtos/-/raw/main/files/0059_postgres_dba.jpg) --------------------------------------------------------------------------------