翻译:PostgreSQL 鲜为人知的特性

简介: 本文揭秘PostgreSQL 17个鲜为人知却极其实用的特性:从Upsert行数区分、列级权限控制,到`DISTINCT ON`取分组首行、`OVERLAPS`判断时间重叠,再到美元符号引用、`NOT VALID`约束延迟验证等,助你深度挖掘已拥有的强大功能。

原文链接 https://hakibenita.com/postgresql-unknown-features
这些特性你早已拥有,只是未曾发现!

2006年,微软曾开展一项客户调研,旨在了解用户希望微软Office新版本新增哪些功能。令他们意外的是,用户提出的需求中超过90%其实早已存在,只是用户自己不知道而已。为了解决这种“功能可发现性”问题,微软推出了如今我们在Office产品中看到的“功能区界面(Ribbon UI)”。

不止Office如此,我们大多数人都无法知晓日常使用工具的全部功能——尤其是像PostgreSQL这样庞大且功能丰富的工具。现在带大家了解PostgreSQL中那些早已存在、但你可能从未留意的小众特性。

本文将为你介绍这些PostgreSQL鲜为人知的特性。

1. 获取Upsert操作中更新和插入的行数

INSERT ON CONFLICT(在Oracle中也称为“merge”,或“upsert”——UPDATE和INSERT的组合词)是一个非常实用的命令,尤其在ETL流程中。通过INSERT语句的ON CONFLICT子句,你可以指定当主键/唯一键列出现冲突时,数据库应执行的操作。

例如,以下查询用于同步employees表的数据:

db=# WITH new_employees AS (
    SELECT * FROM (VALUES
        ('George', 'Sales',    'Manager',   1000),
        ('Jane',   'R&D',      'Developer', 1200)
    ) AS t(
         name,      department, role,       salary
    )
)
INSERT INTO employees (name, department, role, salary)
SELECT name, department, role, salary
FROM new_employees
ON CONFLICT (name) DO UPDATE SET
    department = EXCLUDED.department,
    role = EXCLUDED.role,
    salary = EXCLUDED.salary
RETURNING *;

  name  │ department │   role    │ salary
────────┼────────────┼───────────┼────────
 George │ Sales      │ Manager   │   1000
 Jane   │ R&D        │ Developer │   1200
INSERT 0 2

该查询会向表中插入新的员工数据;如果尝试插入的员工姓名已存在,则会更新该行数据。

RETURNING *

你可以学习如何结合WITHRETURNING实现完整的业务流程。

从上述命令的输出INSERT 0 2中,你能看到有2条员工记录被影响,但无法区分“插入”和“更新”的数量——输出结果并未给出任何线索!

我在优化某ETL流程的日志记录时(该流程使用了上述查询),在Stack Overflow上发现了一个巧妙的解决方案,恰好能解决这个问题:

db=# WITH new_employees AS (
    SELECT * FROM (VALUES
        ('George', 'Sales',    'Manager',   1000),
        ('Jane',   'R&D',      'Developer', 1200)
    ) AS t(
         name,      department, role,       salary
    )
)
INSERT INTO employees (name, department, role, salary)
SELECT name, department, role, salary
FROM new_employees
ON CONFLICT (name) DO UPDATE SET
    department = EXCLUDED.department,
    role = EXCLUDED.role,
    salary = EXCLUDED.salary
RETURNING *, (xmax = 0) AS inserted;

  name  │ department │   role    │ salary │ inserted
────────┼────────────┼───────────┼────────┼──────────
 Jane   │ R&D        │ Developer │   1200 │ t
 George │ Sales      │ Manager   │   1000f
INSERT 0 2

注意RETURNING子句的变化:它新增了计算字段inserted,该字段通过特殊列xmax判断行是插入还是更新。从返回结果可以看出,“Jane”是新插入的行(inserted = t),而“George”已存在,因此是更新操作(inserted = f)。

xmax是PostgreSQL的特殊系统列,其含义为:

删除该行版本的事务ID;若该行未被删除,则值为0。

在PostgreSQL中,更新一行时会先删除旧版本,xmax会记录执行删除操作的事务ID;而插入行时没有旧版本需要删除,因此xmax = 0。这个“小技巧”正是利用这一特性区分插入和更新的行。


2. 为特定列授予权限

假设你有一个users表,包含凭证、密码、个人身份信息(PII)等敏感数据:

db=# CREATE TABLE users (
    id INT,
    username VARCHAR(20),
    personal_id VARCHAR(10),
    password_hash VARCHAR(256)
);
CREATE TABLE

db=# INSERT INTO users VALUES (1, 'haki', '12222227', 'super-secret-hash');
INSERT 1 0

公司内不同人员(如分析师)需要访问该表生成临时报表,但他们不应接触敏感信息。为了给分析师授权,你可以在数据库中创建专用用户:

db=# CREATE USER analyst;
CREATE USER

db=# GRANT SELECT ON users TO analyst;
GRANT

此时analyst用户可以访问整个users表:

db=# \connect db analyst
You are now connected to database "db" as user "analyst".

db=> SELECT * FROM users;
 id │ username │ personal_id │   password_hash
────┼──────────┼─────────────┼───────────────────
  1 │ haki     │ 12222227    │ super-secret-hash

如前所述,分析师仅需访问非敏感数据生成报表,因此PostgreSQL允许你只为表的特定列授予权限,实现精细化控制:

db=# \connect db postgres
You are now connected to database "db" as user "postgres".

db=# REVOKE SELECT ON users FROM analyst;
REVOKE

db=# GRANT SELECT (id, username) ON users TO analyst;
GRANT

撤销全表的SELECT权限后,仅授予analystidusername列的查询权限。此时分析师无法访问受限列:

db=# \connect db analyst
You are now connected to database "db" as user "analyst".

db=> SELECT * FROM users;
ERROR:  permission denied for table users

db=> SELECT id, username, personal_id FROM users;
ERROR:  permission denied for table users

db=> SELECT id, username FROM users;
 id │ username
────┼──────────
  1 │ haki

注意:当analyst尝试显式/隐式(通过*)访问受限列时,会触发“权限拒绝”错误。


3. 匹配多个模式

SQL中的模式匹配很常见,例如查找所有使用gmail邮箱的用户:

SELECT *
FROM users
WHERE email LIKE '%@gmail.com';

该查询通过通配符%匹配以@gmail.com结尾的邮箱。如果需要同时匹配gmail.comyahoo.com,常规写法是用OR拼接:

SELECT *
FROM users
WHERE
    email LIKE '%@gmail.com'
    OR email LIKE '%@yahoo.com';

而PostgreSQL提供了更简洁的方式——使用SIMILAR TO匹配多个模式:

SELECT *
FROM users
WHERE email SIMILAR TO '%@gmail.com|%@yahoo.com';

SIMILAR TO支持用|分隔多个匹配模式,让查询更简洁。

另一种方式是使用正则表达式:

SELECT *
FROM users
WHERE email ~ '@gmail\.com$|@yahoo\.com$';

使用正则时需注意:.是通配符,需用\.转义才能匹配邮箱中的点号。

我在推特分享这一技巧时,PostgreSQL的Python驱动psycopg官方账号还提供了另一种写法:

SELECT *
FROM users
WHERE email ~ ANY(ARRAY['@gmail\.com$', '@yahoo\.com$']);

该写法通过ANY运算符匹配数组中的任意模式,更适合在Python等宿主语言中动态传参:

with connection.cursor() as cursor:
    cursor.execute('''
        SELECT *
        FROM users
        WHERE email ~ ANY(ARRAY%(patterns)s)
    ''', {
   
        'patterns': [
            '@gmail\.com$',
            '@yahoo\.com$',
        ],
    })

SIMILAR TO不同,ANY支持将模式列表绑定为变量,灵活性更高。


4. 获取序列当前值而不推进序列

如果你需要获取序列的当前值,第一反应可能是用currval

db=# SELECT currval('sale_id_seq');
ERROR:  currval of sequence "sale_id_seq" is not yet defined in this session

和大多数人一样,你会发现currval仅在当前会话中使用过该序列后才有效。为了查值而无意义地推进序列显然不可取,因此这不是理想方案。

PostgreSQL 10新增了pg_sequences视图,可便捷查询序列信息:

db=# SELECT * FROM pg_sequences WHERE sequencename = 'sale_id_seq';[ RECORD 1 ]─┬────────────
schemaname    │ public
sequencename  │ sale_id_seq
sequenceowner │ db
data_type     │ integer
start_value   │ 1
min_value     │ 1
max_value     │ 2147483647
increment_by  │ 1
cycle         │ f
cache_size    │ 1
last_value    │ 155

这个视图能满足需求,但它只是信息模式中的普通表,算不上“小众特性”。

另一种方式是使用未文档化的函数pg_sequence_last_value

db=# SELECT pg_sequence_last_value('sale_id_seq');
 pg_sequence_last_value
────────────────────────
                   155

目前不清楚该函数为何未被文档化(官方文档中无相关说明),使用时需注意这一点。

我还发现一个有趣的特性:可以像查询表一样直接查询序列:

db=# SELECT * FROM sale_id_seq;

 last_value │ log_cnt │ is_called
────────────┼─────────┼───────────
        15510 │ t

这不禁让人好奇:PostgreSQL中还有哪些对象可以这样查询?

⚠️ 注意:该特性仅适用于临时查看序列状态,切勿基于此输出更新ID——ID管理应使用nextval


5. 结合\copy使用多行SQL

频繁使用psql的开发者常会用\COPY导出数据,但\COPY的一大痛点是不支持多行查询:

db=# \COPY (
\copy: parse error at end of line

换行时会触发解析错误。

最初我用视图解决这个问题:

db=# CREATE VIEW v_department_dbas AS
    SELECT department, count(*) AS employees
    FROM emp
    WHERE role = 'dba'
    GROUP BY department
    ORDER BY employees;
CREATE VIEW

db=# \COPY (SELECT * FROM v_department_dbas) TO department_dbas.csv WITH CSV HEADER;
COPY 5

db=# DROP VIEW v_department_dbas;
DROP VIEW;

这种方法可行,但如果中途出错,视图会残留。为了自动清理,我改用临时视图:

db=# CREATE TEMPORARY VIEW v_department_dbas AS -- 省略查询逻辑
CREATE VIEW

db=# \COPY (SELECT * FROM v_department_dbas) TO department_dbas.csv WITH CSV HEADER;
COPY 5

临时视图会在会话结束后自动删除,无需手动清理。

直到我在psql文档中发现了更优雅的写法:

db=# COPY (
    SELECT department, count(*) AS employees
    FROM emp
    WHERE role = 'dba'
    GROUP BY department
    ORDER BY employees
) TO STDOUT WITH CSV HEADER \g department_dbas.csv
COPY 5

这个写法的核心逻辑拆解:

  1. COPY替代\COPYCOPY是服务器端命令,支持多行查询;而\COPY是psql客户端命令,不支持多行。
  2. 输出到标准输出(STDOUT):COPY可将结果写入服务器目录,或通过TO STDOUT输出到标准输出。
  3. \g将STDOUT写入本地文件:psql的\g命令可将标准输出内容写入指定文件。

三者结合完美解决了多行查询导出的问题。

💡 进阶技巧:如果需要高频迁移数据,可了解通过Python向PostgreSQL批量加载数据的最快方式。


6. 禁止手动设置自动生成的主键值

在PostgreSQL中使用自动生成主键时,你可能仍在使用SERIAL类型:

CREATE TABLE sale (
    id SERIAL PRIMARY KEY,
    sold_at TIMESTAMPTZ,
    amount INT
);

PostgreSQL会在后台创建序列,用于插入行时生成ID:

db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
INSERT 0 1

db=# SELECT * FROM sale;
 id │           sold_at             │ amount
────┼───────────────────────────────┼────────
  12021-09-25 10:06:56.646298+031000

SERIAL是PostgreSQL特有类型,存在已知问题。因此从版本10开始,SERIAL被软弃用,推荐使用标识列(Identity Column)

CREATE TABLE sale (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    sold_at TIMESTAMPTZ,
    amount INT
);

标识列的行为与SERIAL类似:

db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
INSERT 0 1

db=# SELECT * FROM sale;
 id │           sold_at             │ amount
────┼───────────────────────────────┼────────
  12021-09-25 10:11:57.771121+031000

但存在一个问题场景:

db=# INSERT INTO sale (id, sold_at, amount) VALUES (2, now(), 1000);
INSERT 0 1

db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
ERROR:  duplicate key value violates unique constraint "sale_pkey"
DETAIL:  Key (id)=(2) already exists.

报错原因:

  1. 第一条INSERT手动指定了id=2,序列未被使用,值仍为1;
  2. 第二条INSERT未指定id,序列生成下一个值2,触发主键冲突。

自增ID极少需要手动设置,手动操作易导致混乱。如何禁止这种行为?

CREATE TABLE sale (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sold_at TIMESTAMPTZ,
    amount INT
);

GENERATED BY DEFAULT改为GENERATED ALWAYS即可。重复上述场景验证:

db=# INSERT INTO sale (sold_at, amount) VALUES (now(), 1000);
INSERT 0 1

db=# INSERT INTO sale (id, sold_at, amount) VALUES (2, now(), 1000);
ERROR:  cannot insert into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

变化点:

  • 第一条INSERT未指定id,执行成功;
  • 第二条INSERT尝试手动设置id,直接报错!

错误提示中,PostgreSQL还贴心给出了“确实需要手动设置”的解决方案:

db=# INSERT INTO sale (id, sold_at, amount)
OVERRIDING SYSTEM VALUE VALUES (2, now(), 1000);

INSERT 0 1

添加OVERRIDING SYSTEM VALUE子句后,可显式允许手动设置标识列的值(仍需自行处理主键冲突)。


7. 生成透视表的另外两种方法

我曾在之前的文章中演示过用条件聚合生成透视表,后来又发现了PostgreSQL中另外两种生成透视表的方法。

假设你需要统计各部门不同岗位的员工数量:

db=# WITH employees AS (
    SELECT * FROM (VALUES
        ('Haki',    'R&D',      'Manager'),
        ('Dan',     'R&D',      'Developer'),
        ('Jax',     'R&D',      'Developer'),
        ('George',  'Sales',    'Manager'),
        ('Bill',    'Sales',    'Developer'),
        ('David',   'Sales',    'Developer')
    ) AS t(
        name,       department,  role
    )
)
SELECT role, department, count(*)
FROM employees
GROUP BY role, department;

   role    │ department │ count
───────────┼────────────┼───────
 Developer │ Sales      │     2
 Manager   │ Sales      │     1
 Manager   │ R&D        │     1
 Developer │ R&D        │     2

将结果转为透视表会更直观。在psql中,可使用\crosstabview命令将上一次查询结果转为透视表:

db=# \crosstabview

   role    │ Sales │ R&D
───────────┼───────┼─────
 Developer │     22
 Manager   │     11

默认情况下,\crosstabview会用前两列生成透视表,也可通过参数指定列:

db=# \crosstabview department role

 department │ Developer │ Manager
────────────┼───────────┼─────────
 Sales      │         21
 R&D        │         21

另一种方法是使用内置的tablefunc扩展:

db=# CREATE EXTENSION tablefunc;
CREATE EXTENSION

db=# SELECT * FROM crosstab('
    SELECT role, department, count(*) AS employees
    FROM employees
    GROUP BY 1, 2
    ORDER BY role
', '
    SELECT DISTINCT department
    FROM employees
    ORDER BY 1
') AS t(role text, sales int, rnd int);

   role    │ sales │ rnd
───────────┼───────┼─────
 Developer │     22
 Manager   │     11

crosstab函数的缺点是需要提前定义输出列,但优点是返回的是表对象,可作为子查询进一步处理。


8. 美元符号引用(Dollar Quoting)

在数据库中存储文本(尤其是大段文本)时,你大概率遇到过转义字符的问题。例如,要在文本字面量中包含单引号',需用另一个单引号''转义:

db=# SELECT 'John''s Pizza';
   ?column?
──────────────
 John's Pizza

当文本包含换行、反斜杠等字符时,转义会变得非常繁琐。PostgreSQL提供了更便捷的字符串常量写法——美元符号引用:

db=# SELECT $$a long
string with new lines
and 'single quotes'
and "double quotes

PostgreSQL doesn't mind ;)$$ AS text;
           text
───────────────────────────
 a long                   
 string with new lines    
 and 'single quotes'      
 and "double quotes       

 PostgreSQL doesn't mind ;)

字符串首尾的$$`是分隔符,中间的内容无需转义。如果文本本身包含`$$,还可以添加自定义标签:

db=# SELECT $JSON${
   
    "name": "John's Pizza",
    "tagline": "Best value for your $$"
}$JSON$ AS json;

                  json
─────────────────────────────────────────
 {                                      
     "name": "John's Pizza",            
     "tagline": "Best value for your $$"
 }

这里用$JSON$作为分隔符,因此文本中的`$$`会被原样保留。 该特性还可用于快速生成包含特殊字符的`jsonb`对象: ```sql db=# SELECT $JSON${ "name": "John's Pizza", "tagline": "Best value for your $$"
}$JSON$::jsonb AS json;
json
─────────────────────────────────────────────────────────────
{"name": "John's Pizza", "tagline": "Best value for your $$"}

转换后的`jsonb`对象可自由操作。

---

### 9. 为数据库对象添加注释
PostgreSQL支持为几乎所有数据库对象添加注释。例如,为表添加注释:
```sql
db=# COMMENT ON TABLE sale IS 'Sales made in the system';
COMMENT

在psql(及多数IDE)中可查看注释:

db=# \dt+ sale
                                  List of relations
 Schema │ Name │ Type  │ Owner │ Persistence │    Size    │       Description
────────┼──────┼───────┼───────┼─────────────┼────────────┼──────────────────────────
 public │ sale │ table │ haki  │ permanent   │ 8192 bytes │ Sales made in the system

也可为列添加注释,并通过扩展描述查看:

db=# COMMENT ON COLUMN sale.sold_at IS 'When was the sale finalized';
COMMENT

db=# \d+ sale
  ColumnType           │         Description
──────────┼──────────────────────────┼─────────────────────────────
 id       │ integer                  │
 sold_at │ timestamp with time zone │ When was the sale finalized
 amount   │ integer

结合美元符号引用,还可为函数添加详细的多行注释:

COMMENT ON FUNCTION generate_random_string IS $docstring$
Generate a random string at a given length from a list of possible characters.

Parameters:

    - length (int): length of the output string
    - characters (text): possible characters to choose from

Example:

    db=# SELECT generate_random_string(10);
     generate_random_string
    ────────────────────────
     o0QsrMYRvp

    db=# SELECT generate_random_string(3, 'AB');
     generate_random_string
    ────────────────────────
     ABB
$docstring$;

此后无需查阅文档,直接通过psql查看函数注释:

db=# \df+ generate_random_string
List of functions
────────────┬────────────────────────────────────────────────────────────────────────────────
Schemapublic
Name        │ generate_random_string
/* 省略无关字段 */
Description │ Generate a random string at a given length from a list of possible characters.
            │                                                                              
            │ Parameters:                                                                  
            │                                                                              
            │     - length (int): length of the output string                              
            │     - characters (text): possible characters to choose from                  
            │                                                                              
            │ Example:                                                                     
            │                                                                              
            │     db=# SELECT generate_random_string(10);                                   
            │      generate_random_string                                                   
            │     ────────────────────────                                                  
            │      o0QsrMYRvp                                                               
            │                                                                              
            │     db=# SELECT generate_random_string(3, 'AB');                              
            │      generate_random_string                                                   
            │     ────────────────────────                                                  
            │      ABB                                                                      
            │

10. 为每个数据库保留独立的历史命令文件

使用CLI工具时,你大概率会频繁搜索历史命令(如bash/psql中按CTRL + R反向搜索)。如果同时操作多个数据库,可为每个数据库配置独立的历史文件:

db=# \set HISTFILE ~/.psql_history- :DBNAME

这样,你在不同数据库中搜索历史命令时,能更快找到相关内容。将该配置写入~/.psqlrc可永久生效。


11. 自动补全大写的保留字

关于SQL关键字应大写还是小写,一直存在大量讨论(甚至玩笑)。如果你和我一样偏好大写关键字,psql提供了自动补全大写关键字的配置:

db=# selec <tab>  -- 未配置前,补全为小写
db=# select

db=# \set COMP_KEYWORD_CASE upper
db=# selec <tab>  -- 配置后,补全为大写
db=# SELECT

设置COMP_KEYWORD_CASE = upper后,按TAB自动补全的关键字会变为大写。


12. 按时间间隔休眠

延迟程序执行在测试、限流等场景中很实用。PostgreSQL中常用pg_sleep实现休眠:

db=# \timing
Timing is on.

db=# SELECT pg_sleep(3);
 pg_sleep
──────────

(1 row)

Time: 3014.913 ms (00:03.015)

pg_sleep接收秒数作为参数,但休眠时间较长时,计算秒数会很麻烦(例如pg_sleep(255)对应4分15秒)。

PostgreSQL提供了更友好的pg_sleep_for函数,支持直接传入时间间隔:

db=# SELECT pg_sleep_for('4 minutes 15 seconds');

相比pg_sleeppg_sleep_for的参数更符合人类阅读习惯。


13. 无需子查询获取分组中的首行/末行

我原本没把这个特性归为“小众”(因为我经常用),但发现很多人仍在用复杂方案解决这个问题,因此值得一提。

假设有学生表:

db=# SELECT * FROM students;

  name  │ class │ height
────────┼───────┼────────
 Haki   │ A     │    186
 Dan    │ A     │    175
 Jax    │ A     │    182
 George │ B     │    178
 Bill   │ B     │    167
 David  │ B     │    178

如何获取每个班级中身高最高的学生完整信息?

初步尝试:

SELECT class, max(height) as tallest
FROM students
GROUP BY class;

 class │ tallest
───────┼─────────
 A     │     186
 B     │     178

该查询仅返回身高,无法获取学生姓名。进阶尝试(子查询):

SELECT *
FROM students
WHERE (class, height) IN (
    SELECT class, max(height) as tallest
    FROM students
    GROUP BY class
);

  name  │ class │ height
────────┼───────┼────────
 Haki   │ A     │    186
 George │ B     │    178
 David  │ B     │    178

💡 补充:(class, height) IN (...)这种多列匹配是PostgreSQL的小众但强大的特性。

该查询返回了所有身高最高的学生,但B班有两名学生身高相同,若只想选其中一位,可使用窗口函数:

SELECT
    students.*,
    ROW_NUMBER() OVER (
        PARTITION BY class
        ORDER BY height DESC, name
    ) AS rn
FROM
    students;

  name  │ class │ height │ rn
────────┼───────┼────────┼────
 Haki   │ A     │    1861
 Jax    │ A     │    1822
 Dan    │ A     │    1753
 David  │ B     │    1781
 George │ B     │    1782
 Bill   │ B     │    1673

ROW_NUMBER()按班级分区(PARTITION BY class),先按身高降序、再按姓名排序,为每行分配行号(姓名保证结果确定性)。

嵌套子查询筛选行号为1的记录:

SELECT
    name, class, height
FROM (
    SELECT
        students.*,
        ROW_NUMBER() OVER (
            PARTITION BY class
            ORDER BY height DESC, name
        ) AS rn
    FROM
        students
) as inner
WHERE
    rn = 1;

 name  │ class │ height
───────┼───────┼────────
 Haki  │ A     │    186
 David │ B     │    178

这已经能满足需求,但PostgreSQL提供了更简洁的方式——DISTINCT ON

SELECT DISTINCT ON (class)
    *
FROM
    students
ORDER BY
    class, height DESC, name;

 name  │ class │ height
───────┼───────┼────────
 Haki  │ A     │    186
 David │ B     │    178

DISTINCT ON是PostgreSQL特有语法(其他数据库无此特性),其执行逻辑可拆解为:

  1. ORDER BY排序数据:
    SELECT * FROM students ORDER BY class, height DESC, name;
    
  2. DISTINCT ON (class)分组;
  3. 保留每个分组的第一行。

DISTINCT ON的唯一要求是:ORDER BY的前导列必须与DISTINCT ON的列一致,后续列用于决定分组内的首行。

例如,获取每个班级最矮的学生:

SELECT DISTINCT ON (class)
    *
FROM
    students
ORDER BY
    class, height, name;

 name │ class │ height
──────┼───────┼────────
 Dan  │ A     │    175
 Bill │ B     │    167

只需修改排序规则,即可获取分组内的末行/特定行。


14. 无需扩展生成UUID

PostgreSQL 13之前,生成UUID需依赖uuid-ossp扩展:

db=# CREATE EXTENSION "uuid-ossp";
CREATE EXTENSION

db=# SELECT uuid_generate_v4() AS uuid;
                 uuid
──────────────────────────────────────
 8e55146d-0ce5-40ab-a346-5dbd466ff5f2

PostgreSQL 13新增了内置函数gen_random_uuid(),可直接生成随机(版本4)UUID:

db=# SELECT gen_random_uuid() AS uuid;
                 uuid
──────────────────────────────────────
 ba1ac0f5-5d4d-4d80-974d-521dbdcca2b2

若需生成非版本4的UUID,仍需使用uuid-ossp扩展。


15. 生成可复现的随机数据

生成随机数据在演示、测试中很常用,而“可复现”的随机数据更有价值。

PostgreSQL的random()函数可生成各类随机数据:

db=# SELECT
    random() AS random_float,
    ceil(random() * 10) AS random_int_0_10,
    '2022-01-01'::date + interval '1 days' * ceil(random() * 365) AS random_day_in_2022;[ RECORD 1 ]──────┬────────────────────
random_float       │ 0.6031888056092001
random_int_0_10    │ 3
random_day_in_2022 │ 2022-11-10 00:00:00

重复执行会得到不同结果,但通过setseed可生成可复现的随机数据:

db=# SELECT setseed(0.4050);
 setseed
─────────

(1 row)

db=# SELECT
    random() AS random_float,
    ceil(random() * 10) AS random_int_0_10,
    '2022-01-01'::date + interval '1 days' * ceil(random() * 365) AS random_day_in_2022
FROM
    generate_series(1, 2);

    random_float    │ random_int_0_10 │ random_day_in_2022
────────────────────┼─────────────────┼─────────────────────
 0.192424751679432492022-12-17 00:00:00
 0.972062090823637752022-06-13 00:00:00

即使在新会话/其他数据库中执行相同代码,结果也完全一致:

otherdb=# SELECT setseed(0.4050);
 setseed
─────────

(1 row)

otherdb=# SELECT
    random() AS random_float,
    ceil(random() * 10) AS random_int_0_10,
    '2022-01-01'::date + interval '1 days' * ceil(random() * 365) AS random_day_in_2022
FROM
    generate_series(1, 2);

    random_float    │ random_int_0_10 │ random_day_in_2022
────────────────────┼─────────────────┼─────────────────────
 0.192424751679432492022-12-17 00:00:00
 0.972062090823637752022-06-13 00:00:00

演示或分享脚本时,添加setseed可让结果轻松复现。


16. 添加约束但不立即验证

约束是关系型数据库的核心特性,用于保证数据的清洁性和可靠性。在生产系统中添加新约束时,全表验证可能导致长时间锁表,影响业务。

例如,为大表添加检查约束:

db=# ALTER TABLE orders ADD CONSTRAINT check_price_gt_zero CHECK (price >= 0);
ALTER TABLE
Time: 10745.662 ms (00:10.746)

该操作会扫描全表验证现有数据,耗时约10秒,且期间表被锁定。

PostgreSQL允许将“添加约束”拆分为两步:

  1. 添加约束,但仅验证新数据,不检查历史数据:

    db=# ALTER TABLE orders ADD CONSTRAINT check_price_gt_zero CHECK (price >= 0) NOT VALID;
    ALTER TABLE
    Time: 13.590 ms
    

    NOT VALID告诉PostgreSQL无需验证现有行,因此无需扫描全表,执行几乎瞬时完成。

  2. 用更宽松的锁验证历史数据(验证期间允许表的其他操作):

    db=# ALTER TABLE orders VALIDATE CONSTRAINT check_price_gt_zero;
    ALTER TABLE
    Time: 11231.189 ms (00:11.231)
    

    验证耗时与直接添加约束相当,但拆分后大幅缩短了表的锁定时间。

💡 扩展:NOT VALID还可用于“仅约束未来数据”(即使历史数据有脏值)——只需添加NOT VALID,不执行VALIDATE即可。


17. PostgreSQL中的“同义词”实现

同义词(Synonym)允许用别名引用数据库对象(类似Linux的软链接)。如果你熟悉Oracle,对同义词一定不陌生;PostgreSQL虽无“同义词”特性,但可通过其他方式实现类似效果。

要实现别名,需先了解PostgreSQL的非限定名解析规则:例如,用户haki连接数据库后查询foo表,PostgreSQL会按以下顺序查找:

  1. haki.foo(用户同名模式)
  2. public.foo(默认模式)

该顺序由search_path参数决定:

db=# SHOW search_path;
   search_path
─────────────────
 "$user", public

$user是特殊值,解析为当前连接用户;public是默认模式。

实现方式1:创建用户同名模式

-- 创建public.foo表
db=# CREATE TABLE foo (value TEXT);
CREATE TABLE
db=# INSERT INTO foo VALUES ('A');
INSERT 0 1
db=# SELECT * FROM foo;
 value
───────
 A

-- 创建haki模式及haki.foo表
db=# CREATE SCHEMA haki;
CREATE SCHEMA
db=# CREATE TABLE haki.foo (value text);
CREATE TABLE
db=# INSERT INTO haki.foo VALUES ('B');
INSERT 0 1

-- 以haki用户连接,查询foo会优先匹配haki.foo
db=# \conninfo
You are connected to database "db" as user "haki"
db=# SELECT * FROM foo;
value
───────
B

实现方式2:修改search_path

-- 创建synonyms模式及synonyms.foo表
db=# CREATE SCHEMA synonyms;
CREATE SCHEMA
db=# CREATE TABLE synonyms.foo (value text);
CREATE TABLE
db=# INSERT INTO synonyms.foo VALUES ('C');
INSERT 0 1

-- 默认search_path下,foo匹配public.foo
db=# SHOW search_path;
   search_path
─────────────────
 "$user", public
db=# SELECT * FROM foo;
 value
───────
 A

-- 修改search_path,优先匹配synonyms.foo
db=# SET search_path TO synonyms, "$user", public;
SET
db=# SELECT * FROM foo;
 value
───────
 C

同义词的实用场景:零停机迁移

在生产表变更时,常需同时支持新旧版本应用(不同版本期望表结构不同)。例如,需删除users表的active列,但旧版本应用仍依赖该列:

-- 以app用户连接,当前表结构
db=# \conninfo
You are now connected to database "db" as user "app".
db=# SELECT * FROM users;
 username │ active
──────────┼────────
 haki     │ t

-- 以admin用户创建app模式及视图
db=# \conninfo
You are now connected to database "db" as user "admin".
db=# CREATE SCHEMA app;
CREATE SCHEMA
db=# GRANT USAGE ON SCHEMA app TO app;
GRANT
db=# CREATE VIEW app.users AS SELECT username, true AS active FROM public.users;
CREATE VIEW
db=# GRANT SELECT ON app.users TO app;
GRANT

-- 删除public.users的active列
db=# ALTER TABLE users DROP COLUMN active;
ALTER TABLE

-- 以app用户查询users,实际访问的是app.users视图
db=# \connect db app
You are now connected to database "db" as user "app".
db=# SELECT * FROM users;
 username │ active
──────────┼────────
 haki     │ t

通过“同义词”(视图+search_path),旧版本应用无需修改即可正常运行,实现零停机迁移。


18. 查找重叠的时间/数值范围

假设有会议表:

db=# SELECT * FROM meetings;
       starts_at     │        ends_at
─────────────────────┼─────────────────────
 2021-10-01 10:00:002021-10-01 10:30:00
 2021-10-01 11:15:002021-10-01 12:00:00
 2021-10-01 12:30:002021-10-01 12:45:00

安排新会议前,需检查是否与现有会议重叠。重叠场景包括:

  • A:新会议开始于现有会议中,结束于其外;
  • B:新会议结束于现有会议中,开始于其外;
  • C:新会议完全包含于现有会议;
  • D:现有会议完全包含于新会议;
  • E:新会议与现有会议完全重合;
  • F/G:新会议与现有会议首尾相接(非重叠,需排除)。

初始尝试:用BETWEEN判断

WITH new_meetings AS (
    SELECT
        id,
        starts_at::timestamptz as starts_at,
        ends_at::timestamptz as ends_at
    FROM (VALUES
        ('A', '2021-10-01 11:10 UTC', '2021-10-01 11:55 UTC'),
        ('B', '2021-10-01 11:20 UTC', '2021-10-01 12:05 UTC'),
        ('C', '2021-10-01 11:20 UTC', '2021-10-01 11:55 UTC'),
        ('D', '2021-10-01 11:10 UTC', '2021-10-01 12:05 UTC'),
        ('E', '2021-10-01 11:15 UTC', '2021-10-01 12:00 UTC'),
        ('F', '2021-10-01 12:00 UTC', '2021-10-01 12:10 UTC'),
        ('G', '2021-10-01 11:00 UTC', '2021-10-01 11:15 UTC')
    ) as t(id, starts_at, ends_at)
)
SELECT *
FROM meetings, new_meetings
WHERE
    new_meetings.starts_at BETWEEN meetings.starts_at and meetings.ends_at
    OR new_meetings.ends_at BETWEEN meetings.starts_at and meetings.ends_at
    OR meetings.starts_at BETWEEN new_meetings.starts_at and new_meetings.ends_at
    OR meetings.ends_at BETWEEN new_meetings.starts_at and new_meetings.ends_at;

该查询会错误地将F/G(首尾相接)判定为重叠(因BETWEEN是闭区间)。

改进尝试:用开区间判断

WITH new_meetings AS (/* 同上 */)
SELECT *
FROM meetings, new_meetings
WHERE
    (new_meetings.starts_at > meetings.starts_at AND new_meetings.starts_at < meetings.ends_at)
    OR
    (new_meetings.ends_at > meetings.starts_at AND new_meetings.ends_at < meetings.ends_at)
    OR
    (meetings.starts_at > new_meetings.starts_at AND meetings.starts_at < new_meetings.ends_at)
    OR
    (meetings.ends_at > new_meetings.starts_at AND meetings.ends_at < new_meetings.ends_at)
    OR
    (meetings.starts_at = new_meetings.starts_at AND meetings.ends_at = new_meetings.ends_at);

该查询能正确识别重叠/非重叠,但条件过于繁琐。

最优方案:OVERLAPS运算符

PostgreSQL提供的OVERLAPS运算符可简洁解决范围重叠问题:

WITH new_meetings AS (/* 同上 */)
SELECT *
FROM meetings, new_meetings
WHERE
    (new_meetings.starts_at, new_meetings.ends_at)
        OVERLAPS (meetings.starts_at, meetings.ends_at);

OVERLAPS会自动处理所有重叠场景,且排除首尾相接的非重叠场景,代码简洁且易维护。


相关文章
|
1天前
|
人工智能 自然语言处理 Shell
🦞 如何在 Moltbot 配置阿里云百炼 API
本教程指导用户在开源AI助手Clawdbot中集成阿里云百炼API,涵盖安装Clawdbot、获取百炼API Key、配置环境变量与模型参数、验证调用等完整流程,支持Qwen3-max thinking (Qwen3-Max-2026-01-23)/Qwen - Plus等主流模型,助力本地化智能自动化。
🦞 如何在 Moltbot 配置阿里云百炼 API
|
6天前
|
人工智能 API 开发者
Claude Code 国内保姆级使用指南:实测 GLM-4.7 与 Claude Opus 4.5 全方案解
Claude Code是Anthropic推出的编程AI代理工具。2026年国内开发者可通过配置`ANTHROPIC_BASE_URL`实现本地化接入:①极速平替——用Qwen Code v0.5.0或GLM-4.7,毫秒响应,适合日常编码;②满血原版——经灵芽API中转调用Claude Opus 4.5,胜任复杂架构与深度推理。
|
10天前
|
JSON API 数据格式
OpenCode入门使用教程
本教程介绍如何通过安装OpenCode并配置Canopy Wave API来使用开源模型。首先全局安装OpenCode,然后设置API密钥并创建配置文件,最后在控制台中连接模型并开始交互。
4555 8
|
15天前
|
人工智能 JavaScript Linux
【Claude Code 全攻略】终端AI编程助手从入门到进阶(2026最新版)
Claude Code是Anthropic推出的终端原生AI编程助手,支持40+语言、200k超长上下文,无需切换IDE即可实现代码生成、调试、项目导航与自动化任务。本文详解其安装配置、四大核心功能及进阶技巧,助你全面提升开发效率,搭配GitHub Copilot使用更佳。
10350 21
|
2天前
|
人工智能 自然语言处理 Cloud Native
大模型应用落地实战:从Clawdbot到实在Agent,如何构建企业级自动化闭环?
2026年初,开源AI Agent Clawdbot爆火,以“自由意志”打破被动交互,寄生社交软件主动服务。它解决“听与说”,却缺“手与脚”:硅谷Manus走API原生路线,云端自主执行;中国实在Agent则用屏幕语义理解,在封闭系统中精准操作。三者协同,正构建AI真正干活的三位一体生态。
2322 9
|
1天前
|
存储 安全 数据库
使用 Docker 部署 Clawdbot(官方推荐方式)
Clawdbot 是一款开源、本地运行的个人AI助手,支持 WhatsApp、Telegram、Slack 等十余种通信渠道,兼容 macOS/iOS/Android,可渲染实时 Canvas 界面。本文提供基于 Docker Compose 的生产级部署指南,涵盖安全配置、持久化、备份、监控等关键运维实践(官方无预构建镜像,需源码本地构建)。
1182 2
|
23小时前
|
机器人 API 数据安全/隐私保护
只需3步,无影云电脑一键部署Moltbot(Clawdbot)
本指南详解Moltbot(Clawdbot)部署全流程:一、购买无影云电脑Moltbot专属套餐(含2000核时);二、下载客户端并配置百炼API Key、钉钉APP KEY及QQ通道;三、验证钉钉/群聊交互。支持多端,7×24运行可关闭休眠。
|
17天前
|
存储 人工智能 自然语言处理
OpenSpec技术规范+实例应用
OpenSpec 是面向 AI 智能体的轻量级规范驱动开发框架,通过“提案-审查-实施-归档”工作流,解决 AI 编程中的需求偏移与不可预测性问题。它以机器可读的规范为“单一真相源”,将模糊提示转化为可落地的工程实践,助力开发者高效构建稳定、可审计的生产级系统,实现从“凭感觉聊天”到“按规范开发”的跃迁。
2590 18
|
10天前
|
人工智能 前端开发 Docker
Huobao Drama 开源短剧生成平台:从剧本到视频
Huobao Drama 是一个基于 Go + Vue3 的开源 AI 短剧自动化生成平台,支持剧本解析、角色与分镜生成、图生视频及剪辑合成,覆盖短剧生产全链路。内置角色管理、分镜设计、视频合成、任务追踪等功能,支持本地部署与多模型接入(如 OpenAI、Ollama、火山等),搭配 FFmpeg 实现高效视频处理,适用于短剧工作流验证与自建 AI 创作后台。
1381 5