容易踩坑的 Postgres 特性

本页目录

今天我们有一些相当有趣的素材,但了解 (并避免) 这些问题可以节省您的时间和精力。

NULLs

NULL 值虽然很常见,但在 SQL 中是导致麻烦的主要原因,Postgres 也不例外。

例如,有人可能会忘记连接操作符 (||)、算术运算符 (*、/、+、-)、传统比较运算符 (=、<、>、<=、>=、<>) 都不是 NULL-safe 的操作,结果可能会丢失。

尤其是当你建立一家初创公司并且一些重要的业务逻辑依赖于它时,查询不能正确处理 NULL,导致用户群或金钱或时间 (或所有) 的损失:

PSQL
nik=# \pset null ∅
Null display is "∅".

nik=# select null + 1;
    ?column?
 ----------

         ∅

(1 row)

NULL 值真的很危险,即使是经验丰富的工程师在处理它们时也常常会遇到问题。以下是一些可以帮助你的素材:

一些技巧 — 如何使代码成为 NULL-safe 的:

在高负载下使用子事务

如果你希望达到数十万或数百万 TPS 并遇到各种问题,请使用子事务。你可能会隐式使用到它们 — 例如,如果使用了 Django、Rails 或 PL/pgSQL 中的BEGIN/EXCEPTION块。

为什么要完全摆脱子事务的原因:PostgreSQL Subtransactions Considered Harmful

int4 主键

如果表有 10 亿行,要想将 int4 主键 (也称为 int、integer) 零停机时间转换为 int8 主键时,所需的工作量极其之大。由于对齐填充,表 (id int4, created_at timestamptz) 将占用与 (id int8, created_at timestamptz) 相同的磁盘空间。

(特殊的) SELECT INTO并不像你想的那样

有一天我在调试 PL/pgSQL 函数时,复制粘贴了一条查询,像这样,运行在 psql 中:

PSQL
nik=# select * into var from t1 limit 1;
SELECT 1

它工作了!这真是一个巨大的惊喜 — 在 SQL 上下文中,SELECT INTO 是一个 DDL 命令,它会创建一个表并将数据插入其中 (难道这不应该被弃用吗?)。

认为"事务性DDL"很简单

是的,Postgres 具有"事务性 DDL",你可以从中获益良多 — 直到您无法这样做。在高负载下,你无法依赖它 — 而是需要开始使用零停机时间的方法,避免错误 (阅读:common db schema change mistakes),并依赖于"非事务性" DDL,比如 CREATE INDEX CONCURRENTLY,某些操作可能会失败,之后需要清理再重试)。

在高负载下进行 DDL 部署的大问题是,默认情况下,你可能会在尝试部署一个非常轻的模式更改时遇到停机 — 除非实现了带有低 lock_timeout 和重试的逻辑 (参照 Zero-downtime Postgres schema migrations need this: lock timeout and retries)。

一次删除大量行

这是一个陷入麻烦的好方法:发出 DELETE 数百万行的命令并等待。如果检查点没有调优 (max_wal_size = 1GB),如果元组通过 IndexScan 被删除 (意味着使页面变脏的过程相当"随机"),并且磁盘 IO 受到限制,这可能会让你的系统崩溃。即使它幸存下来,你也会碰到:

解决方法:

阅读 common db schema change mistakes.

其他"不要做"文章

讨论与补充

在 GitHub 查看讨论
其他历史讨论(2)

支持作者

感谢支持 PostgreSQL Howto 的长期维护。

微信赞赏码

读者交流群

二维码若已过期,可通过公众号 PostgreSQL 学徒或邮件联系。

PostgreSQL 学徒读者交流群二维码