PostgreSQL

commands

\l\list meta-command 列出所有数据库
sudo -u postgres psql -c "\l"
\c + 数据库名 来进入数据库:
\dt 列出所有数据库表:

# 查看表结构, 索引
\d table0

# 比上面多几个字段 Storage  | Stats target | Description
\d+ table0

# Turn off printing of column names and result row count footers, etc. This is equivalent to \t or \pset tuples_only.
\t tuples only on/off, tuples only on 的时候 select 语句的输出不带 header

\h
\?

\du 列出所有的用户
# 创建用户
CREATE USER user_0 WITH PASSWORD 'password_0';

# create database, 所有者 user_0
create database database_0 OWNER user_0;

psql -h 192.168.1.100 -p 5432 -U user_0 -d database_0

PGPASSWORD=password_0 psql -h 192.168.1.100 -p 5432 -U user_0 -d database_0 --command 'select version();'

#当表没有其他关系时
TRUNCATE TABLE tablename;

#当表中有外键时,要用级联方式删所有关联的数据
TRUNCATE TABLE tablename CASCADE;

# 查看 表大小
select pg_size_pretty(pg_relation_size('table0'));

# 查看配置文件路径, 切换到 postgres 用户执行
psql -c "show config_file"

# 查看版本
select version();

pacman -S postgresql
psql -h 127.0.0.1 -p 5432 -d database0 -U user0

# create table
create table test(id int, c1 int);
create table table0(field0 json);

# delete table
DROP TABLE table0;
# 查看字段类型
select column_name, data_type from information_schema.columns where table_name='table0';


select * length( "abc"::TEXT)
insert into test select generate_series(1,10000), random()*10;

# 复制表结构到另外一个数据库
pg_dump -U postgres --schema-only source_db | psql -U postgres target_db

导入/导出

export, 导出, 备份

# https://www.postgresql.org/download/linux/ubuntu/
# install pg_dump
sudo apt install -y postgresql-common

# This script will enable the PostgreSQL APT repository on apt.postgresql.org
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh

# ubuntu install pg_dump
sudo apt-get install postgresql-client-17

# -h, host 127.0.0.1
# -p, port 5432
# -t, table: table0, 不加 -t 参数时会导出所有表结构
# -s, 不导出数据
# database: database0
# -F : 指定输出文件的格式,它可以是以下格式之一: c: 自定义格式 d: 目录格式存档 t: tar 文件包 p: SQL 脚本文件
# -W 命令执行时提示输入用户密码(不会直接在命令中写密码)。
pg_dump -h 127.0.0.1 -U username -W -F t db_name > foo.tar

# 导入
# -c --clean	创建数据库对象前先清理(删除)它们。
pg_restore -h 127.0.0.1 -U username -W -d db_name -c foo.tar

pg_dump -h 127.0.0.1 -p 5432 -t table_0 -U postgres database0 > foo.sql
pg_dump -h 127.0.0.1 -p 5432 -s -t table_0 -U postgres database0 > foo.sql

# 导出并压缩
pg_dump -d db_name | gzip > db.gz

pg_dump -a -t table_0 "host=127.0.0.1 hostaddr=127.0.0.1 port=5432 user=user_0 password=password_0 dbname=db_0"
# export insert sql
pg_dump -a -t table_0 "host=127.0.0.1 hostaddr=127.0.0.1 port=5432 user=user_0 password=password_0 dbname=db_0" --inserts

导入

# sql
psql -h 127.0.0.1 -p 5432 -t table0 -U postgres -d database0 -f foo.sql

# csv, https://stackoverflow.com/questions/26701735/extra-data-after-last-expected-column-while-trying-to-import-a-csv-file-into-p
\COPY agency (agency_name, agency_url, agency_timezone) FROM 'myFile.txt' CSV HEADER DELIMITER ',';

导出指定的行

https://stackoverflow.com/questions/12815496/export-specific-rows-from-a-postgresql-table-as-insert-sql-script

# 按过滤条件导出成 csv, 注意: copy 里面的 select SQL 结尾不能有分号
COPY (
SELECT * FROM table_0 WHERE id = '1'
) TO '/tmp/foo.csv';

# 导出 CSV 并自定义 NULL 值表示
# 默认情况下 NULL 值会导出为 \N
COPY (
SELECT * FROM table_0 WHERE id = '1'
) TO '/tmp/foo.csv' WITH (FORMAT CSV, HEADER, NULL 'NULL');

# 或者使用空字符串表示 NULL
COPY (
SELECT * FROM table_0 WHERE id = '1'
) TO '/tmp/foo.csv' WITH (FORMAT CSV, HEADER, NULL '');

# 完整的 CSV 导出选项
COPY (
SELECT * FROM table_0 WHERE id = '1'
) TO '/tmp/foo.csv' WITH (
    FORMAT CSV,           -- CSV 格式
    HEADER true,          -- 包含表头
    DELIMITER ',',        -- 分隔符
    NULL 'NULL',          -- NULL 值表示为 'NULL' 而不是 \N
    QUOTE '"',            -- 引号字符
    ESCAPE '"',           -- 转义字符
    ENCODING 'UTF8'       -- 编码
);

# 导入 csv (默认 \N 为 NULL)
COPY table_0 FROM '/tmp/foo.csv';

# 导入 CSV 并指定 NULL 值表示
COPY table_0 FROM '/tmp/foo.csv' WITH (FORMAT CSV, HEADER, NULL 'NULL');

# 按过滤条件导出成 INSERT SQL 语句
# 方法1: 使用 psql 生成 INSERT 语句
psql -h 127.0.0.1 -p 5432 -U postgres -d database0 -t -A -c "
SELECT 'INSERT INTO table_0 VALUES (' || 
       quote_literal(field1) || ',' || 
       quote_literal(field2) || ');' 
FROM table_0 
WHERE city = 'tokio';" > foo.sql

# 方法2: 使用 pg_dump 配合临时表或视图
# 先创建临时视图
psql -h 127.0.0.1 -p 5432 -U postgres -d database0 -c "
CREATE TEMP VIEW temp_view AS 
SELECT * FROM table_0 WHERE city = 'tokio';"

# 然后导出视图数据为 INSERT 语句
pg_dump -h 127.0.0.1 -p 5432 -U postgres -d database0 -t temp_view -a --inserts > foo.sql

create user

CREATE USER user_0 WITH PASSWORD 'password_0';
CREATE DATABASE db_0 OWNER user_0;
GRANT ALL PRIVILEGES ON DATABASE db_0 TO user_0;

postgresql.conf, postgresql log, 日志, sql 历史

# 查看配置文件路径, ubuntu 24.04 apt 安装的 postgresql
sudo -u postgres psql -c "SHOW config_file;"

https://www.cnblogs.com/qianxunman/p/12149586.html

default log path: /var/lib/postgresql/data/pgdata/log/

PG 安装完成后默认不会记录日志,必须修改对应的 ${PGDATA}/postgresql.conf 配置才可以

${PGDATA} 的值取自 docker 或系统的环境变量 PGDATA

# 默认值: off, 改成 on 之后重启 postgresql 就开始写日志了, 日志目录会是 /var/lib/postgresql/data/pgdata/log/
logging_collector = on

# none, ddl, mod, all ---- 控制记录哪些 SQL 语句。
# none 不记录,(默认值)
# ddl 记录所有数据定义命令,比如 CREATE, ALTER, 和 DROP 语句。
# mod 记录所有 ddl 语句, 加上数据修改语句 INSERT,UPDATE等,
# all 记录所有执行的语句,将此配置设置为all可跟踪整个数据库执行的SQL语句。
log_statement = 'all'

# 日志目录名, 默认值: log, 使用默认值, 不需要修改
# log_directory = 'log'

# 默认文件名, 使用默认值, 不需要修改
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'

log_rotation_age = 1d ----  单个日志文件的生存期,默认 1 天,在日志文件大小没有达到 log_rotation_size 时,一天只生成一个日志文件
log_rotation_size = 10MB  ---- 单个日志文件的大小,如果时间没有超过 log_rotation_age,一个日志文件最大只能到 10M,否则将新生成一个日志文件。
log_truncate_on_rotation = off ---- 当日志文件已存在时,该配置如果为off,新生成的日志将在文件尾部追加,如果为on,则会覆盖原来的日志。
log_duration = off ---- 记录每条SQL语句执行完成消耗的时间,将此配置设置为 on, 用于统计哪些 SQL 语句耗时较长。

10.log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements and their durations, > 0 logs only statements running at least this number of milliseconds

# -1表示不可用,0将记录所有SQL语句和它们的耗时,>0只记录那些耗时超过(或等于)这个值(ms)的SQL语句。个人更喜欢使用该配置来跟踪那些耗时较长,可能存在性能问题的SQL语句。虽然使用log_statement和log_duration也能够统计SQL语句及耗时,但是SQL语句和耗时统计结果可能相差很多行,或在不同的文件中,但是log_min_duration_statement会将SQL语句和耗时在同一行记录,更方便阅读。

11.log_connections = off ----是否记录连接日志
12.log_disconnections = off ---- 是否记录连接断开日志

13.log_line_prefix = '%m %p %u %d %r ' ---- 日志输出格式(%m,%p实际意义配置文件中有解释),可根据自己需要设置(能够记录时间,用户名称,数据库名称,客户端IP和端口,方便定位问题)
14.log_timezone = 'Asia/Shanghai' ---- 日志时区,最好和服务器设置同一个时区,方便问题定位

timezone

timezone = 'Asia/Shanghai'

重新加载配置文件

pg_ctl reload

https://www.cnblogs.com/alianbog/p/5596921.html

log_statement = ’none’ to log_statement = ‘all’

Optional: SELECT set_config(’log_statement’, ‘all’, true);

sudo /etc/init.d/postgresql restart or sudo service postgresql restart

Fire query in postgresql select 2+2

Find current log in /var/lib/pgsql/9.2/data/pg_log/

The log files tend to grow a lot over a time, and might kill your machine. For your safety, write a bash script that’ll delete logs and restart postgresql server.

version

  • local: 14.5
  • dev: 11.2
  • test: 11.2
  • prod: 11.14

TO_DATE, to_timestamp

YYYY year (4 and more digits) MM month number (01-12) DD day of month (01-31) HH24 hour of day (00-23)

-- TO_DATE(text,format);
SELECT TO_DATE('20170103','YYYYMMDD');

-- to_timestamp(text, text)
to_timestamp('2018-03-12 18:47:35','yyyy-MM-dd hh24:mi:ss')
SELECT to_timestamp('05 Dec 2000', 'DD Mon YYYY');
SELECT to_timestamp('10:49 2023/01/20', 'HH24:MI YYYY/MM/DD');

时间差

SELECT round(cast(date_part('epoch', start_time - end_time)/60/60 as numeric ),1) as time_diff_hours
FROM table0

install

# archlinux, 没有把 服务端和客户端分成不同的包
sudo pacman -S postgresql

ubuntu install postgresql

sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh

# Import the repository signing key:
sudo apt install curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc

# Create the repository configuration file:
. /etc/os-release
sudo sh -c "echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $VERSION_CODENAME-pgdg main' > /etc/apt/sources.list.d/pgdg.list"

# Update the package lists:
sudo apt update

# Install the latest version of PostgreSQL:
# If you want a specific version, use 'postgresql-17' or similar instead of 'postgresql'
sudo apt -y install postgresql
sudo -u postgres psql
CREATE USER user_0 WITH PASSWORD 'password_0';

# 用 systemctl 启动 postgresql 服务
sudo systemctl restart  postgresql

# 默认配置文件位置 
 /etc/postgresql/17/main/postgresql.conf

# 日志 路径 
/var/lib/postgresql/17/main/log

https://hub.docker.com/_/postgres

nerdctl pull postgres:18.0
sudo mkdir -p /var/lib/postgresql/18/data
sudo chown -R 999:999 /var/lib/postgresql/18/data

# 默认用户名 POSTGRES_USER=postgres
sudo nerdctl run -d --name postgres --network host --restart=always -e POSTGRES_PASSWORD=password_0 -e POSTGRES_USER=postgres -v /var/lib/postgresql/18/data:/var/lib/postgresql/18/docker postgres:18.0

###
docker pull postgres:16.4

# 修改配置文件
log_timezone = 'UTC'
timezone = 'Asia/Shanghai'

# docker
# 默认用户名 postgres
docker run --name postgres \
--restart=always \
-p 5432:5432 \
-e POSTGRES_PASSWORD=password0 \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-v postgres-data:/var/lib/postgresql/data \
-d postgres:16.4

# podman
podman run --name postgres \
-p 5432:5432 \
-e POSTGRES_PASSWORD=password0 \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-v postgres-data:/var/lib/postgresql/data \
-d postgres:14.5

psql -h 127.0.0.1 -p 5432 -U postgres
# password: password0

# 重启服务
su -l postgres -c '/opt/pg9.6/bin/pg_ctl -D /mnt/pgdata start'

# 重载配置
su -l postgres -c '/opt/pg9.6/bin/pg_ctl -D /mnt/pgdata reload'

docker run -i --rm postgres:16.4 cat /usr/share/postgresql/postgresql.conf.sample > postgresql.conf

sql

alter table foo
    rename to bar;

alter table foo
    add time0 timestamptz;
-- create table
create table table0(
  field0 json,
  create_time             timestamp with time zone default now(),
  );
 
-- rename column
alter table table_0
    rename column column_foo to column_bar;

psql 命令行直接执行 sql

PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -d database0 -U user0  --command 'select version();'

psql 执行 sql 文件

PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -d database0 -U user0 -f /path/to/foo.sql

查主键

--查询主键名称
SELECT
    pg_constraint.conname AS pk_name
FROM
    pg_constraint
INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
WHERE
    pg_class.relname = 'table_name_0'
AND pg_constraint.contype = 'p';

--查询主键的详细信息
SELECT
    pg_constraint.conname AS pk_name,
    pg_attribute.attname AS colname,
    pg_type.typname AS typename
FROM
    pg_constraint
INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid
AND pg_attribute.attnum = pg_constraint.conkey [ 1 ]
INNER JOIN pg_type ON pg_type.oid = pg_attribute.atttypid
WHERE
    pg_class.relname = 'table_name_0'
AND pg_constraint.contype = 'p';

查看唯一约束

-- 方法1: 查询表上所有唯一约束的名称和字段
SELECT
    pg_constraint.conname AS constraint_name,
    pg_attribute.attname AS column_name
FROM
    pg_constraint
INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
INNER JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid
    AND pg_attribute.attnum = ANY(pg_constraint.conkey)
WHERE
    pg_class.relname = 'table_name_0'
    AND pg_constraint.contype = 'u'  -- 'u' 表示 unique constraint
ORDER BY pg_constraint.conname, pg_attribute.attnum;

-- 方法2: 使用 information_schema 查询唯一约束
SELECT
    tc.constraint_name,
    kcu.column_name
FROM
    information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
    ON tc.constraint_name = kcu.constraint_name
WHERE
    tc.constraint_type = 'UNIQUE'
    AND tc.table_name = 'table_name_0'
ORDER BY tc.constraint_name, kcu.ordinal_position;

-- 方法3: 查看表结构时一起显示约束信息
\d table_name_0

-- 方法4: 查询唯一约束的详细信息(包括索引)
SELECT
    con.conname AS constraint_name,
    con.contype AS constraint_type,
    array_agg(att.attname ORDER BY array_position(con.conkey, att.attnum)) AS columns,
    pg_get_constraintdef(con.oid) AS constraint_definition
FROM
    pg_constraint con
JOIN pg_class rel ON con.conrelid = rel.oid
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = ANY(con.conkey)
WHERE
    rel.relname = 'table_name_0'
    AND con.contype = 'u'
GROUP BY con.conname, con.contype, con.oid;

bigserial

postgresql 序列号(SERIAL)类型包括 smallserial(smallint,short), serial(int)和 bigserial(bigint,long long int), 不管是 smallserial, serial 还是 bigserial,其范围都是(1,9223372036854775807),但是序列号类型其实不是真正的类型, 当声明一个字段为序列号类型时其实是创建了一个序列,INSERT时如果没有给该字段赋值会默认获取对应序列的下一个值。

日期格式化

SELECT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
-- 时区
to_char(NOW() AT TIME ZONE 'Asia/Dubai','YYYY-MM-DD HH24:MI:SS')

日期时间计算

select now() + interval '1 days'; 
select now() + interval '1 month'; 
select now() + interval '1 years'; 
select NOW(), NOW() - interval '1 hours 4 minutes';

字符串连接

string||string

postgresql single quote 转义

-- 单引号转义 ''
UPDATE user SET username = 'Peter''s Name' WHERE id = 1;

to_number

SELECT to_number('12345', '9999999999999999999')//12345
SELECT to_number('12345', '99999')//12345
SELECT to_number(''||12345, '9999')//1234,由于模式是4位,结果忽略最后一位;
SELECT to_number('    12345', '9999999999999999999')//12345
SELECT to_number('  ab  ,1,2a3,4b5', '9999999999999999999')//12345,会忽略所有字符串中非数字字符

sequence, 序列

CREATE SEQUENCE seq_0 START 1;
CREATE SEQUENCE seq_0 INCREMENT 1 MINVALUE 1 START 1 CACHE 1;  
  • INCREMENT, 步长
  • max/MINVALUE, 最大/小值
  • START, 初始值
  • CACHE, 缓存, 某个客户端调用 nextval() 之后, 服务端为其预分配的 seq 值的缓存, 如果客户端挂掉或重启缓存里的数据都会被丢弃.
  • cycle, 循环产生
-- 查看 seq 定义
\d+ seq_0

-- 查看 seq 的当前值, 查询不会导致 sequence 值变化 , 每次查都 是一样的, 如果 没有其它客户端调用 nextval()
SELECT last_value FROM sequence_name;

-- 查看 seq 的下一个值, 每查一次 seq 值 会加 1
SELECT nextval('seq_0')

-- 更新 seq 的值 
select setval('seq_0 ', 88);

删除外键限制

首先找出数据库表的外键名称:

\d [tablename]
"table_name_id_fkey" FOREIGN KEY (id) REFERENCES other_table(id) ....
-- 然后使用下面的命令删除外键:

ALTER TABLE [tablename] DROP CONSTRAINT table_name_id_fkey;

版权声明:本文为CSDN博主「亮子介」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/henryhu712/article/details/104092141

查看外键

查看表结构的时候能看到外键 \d table0

SELECT
     tc.constraint_name, tc.table_name, kcu.column_name, 
     ccu.table_name AS foreign_table_name,
     ccu.column_name AS foreign_column_name,
     tc.is_deferrable,tc.initially_deferred
FROM
     information_schema.table_constraints AS tc 
     JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
     JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = 'table0';

postgresql 数据类型

  • smallint, 2 字节, 小范围整数, -32768 到 +32767
  • timestamp, without time zone
  • timestamp with time zone
  • timestampz
  • BOOLEAN
  • json
  • jsonb

timestamptz 时区处理详解

timestamptz (timestamp with time zone) 是 PostgreSQL 中推荐使用的时间类型,它能自动处理时区转换。

核心特性

存储机制:

  • ✅ 始终以 UTC+0 格式存储(内部存储为 Unix 时间戳)
  • ✅ 不受 PostgreSQL 服务器操作系统时区影响
  • ✅ 不受数据库 timezone 配置影响
  • ✅ 自动转换带时区信息的输入为 UTC+0

工作原理:

  1. 写入时:PostgreSQL 将任何带时区的时间自动转换为 UTC+0 存储
  2. 读取时:根据客户端/会话时区设置返回对应时区的时间

示例:

-- 插入 UTC+3 时间
INSERT INTO table_name (created_at) 
VALUES ('2025-12-15 15:00:00+03:00');

-- 数据库实际存储为: 2025-12-15 12:00:00+00 (UTC+0)

-- 查询时,如果客户端时区是 UTC+3
SELECT created_at FROM table_name;
-- 返回: 2025-12-15 15:00:00+03

-- 如果客户端时区是 UTC+8
SELECT created_at FROM table_name;
-- 返回: 2025-12-15 20:00:00+08

-- 不管服务器在哪个时区,存储都是 UTC+0
-- 场景1: PostgreSQL 服务器在中国(UTC+8)
INSERT INTO table_name (ts) VALUES ('2025-12-15 15:00:00+03:00');
-- 存储为: 2025-12-15 12:00:00+00 (UTC+0)

-- 场景2: PostgreSQL 服务器在美国(UTC-5)
INSERT INTO table_name (ts) VALUES ('2025-12-15 15:00:00+03:00');
-- 存储为: 2025-12-15 12:00:00+00 (UTC+0)
-- 结果完全相同!

代码层面最佳实践

关键点:

  • ✅ 确保传入的 datetime 对象带时区信息(timezone-aware)
  • ✅ PostgreSQL 会自动转换为 UTC 存储,无需手动转换
  • ❌ 不要在应用层手动转换时区,让数据库处理
  • ⚠️ 避免使用不带时区的时间(naive datetime),否则会按服务器时区解释

Python 示例:

from datetime import datetime, timezone
import pytz

# 创建 UTC+3 时间
tz = pytz.timezone('Europe/Moscow')  # UTC+3
dt = datetime.now(tz)

# 直接插入,PostgreSQL 会自动转换为 UTC 存储
cursor.execute(
    "INSERT INTO table_name (created_at) VALUES (%s)", 
    (dt,)
)

Java 示例:

import java.time.ZonedDateTime;
import java.time.ZoneId;

// 创建带时区的时间对象
ZonedDateTime dt = ZonedDateTime.now(ZoneId.of("Europe/Moscow")); // UTC+3

// 直接插入,JDBC 会正确处理时区
PreparedStatement pstmt = conn.prepareStatement(
    "INSERT INTO table_name (created_at) VALUES (?)"
);
pstmt.setObject(1, dt);
pstmt.executeUpdate();

数据库时区配置

是否需要配置?

  • 对于 timestamptz,数据库时区不影响存储(始终 UTC+0)
  • 但影响不带时区的输入如何解释
  • 建议明确配置为 UTC,避免歧义

配置方式:

-- 查看当前时区
SHOW timezone;

-- 设置数据库级别时区(推荐)
ALTER DATABASE your_db SET timezone = 'UTC';

-- 或设置会话级别
SET timezone = 'UTC';

-- 或设置为具体时区
SET timezone = 'Europe/Moscow';  -- UTC+3

最佳实践:

-- ✅ 推荐:数据库设为 UTC
ALTER DATABASE your_db SET timezone = 'UTC';

-- 应用层使用带时区的时间
-- timestamptz 会自动转换为 UTC 存储
INSERT INTO table_name (created_at) 
VALUES ('2025-12-15 15:00:00+03:00');

timestamp vs timestamptz

-- timestamp without time zone
-- 不存储时区信息,不做任何转换
CREATE TABLE test1 (ts timestamp);
INSERT INTO test1 VALUES ('2025-12-15 15:00:00');
-- 存储什么就是什么,不关心时区

-- timestamp with time zone (timestamptz)
-- 存储 UTC+0,读取时转换
CREATE TABLE test2 (ts timestamptz);
INSERT INTO test2 VALUES ('2025-12-15 15:00:00+03:00');
-- 存储为 UTC+0,显示时根据客户端时区转换

推荐使用 timestamptz,原因:

  1. 自动处理时区转换,避免手动计算
  2. 跨时区应用更可靠
  3. 存储统一(UTC+0),避免混乱
  4. 符合国际化应用最佳实践

常见陷阱

陷阱1:使用 naive datetime

# ❌ 错误:不带时区信息
from datetime import datetime
dt = datetime.now()  # naive datetime
cursor.execute("INSERT INTO table_name (ts) VALUES (%s)", (dt,))
# 会按数据库/服务器时区解释,可能不是你想要的

# ✅ 正确:带时区信息
from datetime import datetime, timezone
dt = datetime.now(timezone.utc)  # 或指定具体时区
cursor.execute("INSERT INTO table_name (ts) VALUES (%s)", (dt,))

陷阱2:手动转换时区

# ❌ 不推荐:手动转换
import pytz
from datetime import datetime

tz_moscow = pytz.timezone('Europe/Moscow')
dt_moscow = datetime.now(tz_moscow)
# 手动转换为 UTC
dt_utc = dt_moscow.astimezone(pytz.UTC)
cursor.execute("INSERT INTO table_name (ts) VALUES (%s)", (dt_utc,))
# 多余的操作,PostgreSQL 会自动转换

# ✅ 推荐:直接传入
dt_moscow = datetime.now(tz_moscow)
cursor.execute("INSERT INTO table_name (ts) VALUES (%s)", (dt_moscow,))
# PostgreSQL 自动转换为 UTC 存储

陷阱3:混用 timestamp 和 timestamptz

-- ❌ 避免混用
CREATE TABLE bad_table (
    ts1 timestamp,      -- 不带时区
    ts2 timestamptz     -- 带时区
);
-- 容易造成混乱和错误

-- ✅ 统一使用 timestamptz
CREATE TABLE good_table (
    created_at timestamptz,
    updated_at timestamptz
);

总结

  • timestamptz 始终以 UTC+0 格式存储
  • 无需在应用代码中手动转换时区
  • 确保传入带时区信息的时间对象
  • 建议数据库时区设为 UTC
  • 推荐在所有需要时间存储的场景使用 timestamptz
  • 服务器时区不影响 timestamptz 的存储(始终 UTC+0)
名字                        别名             描述
character varying [(n)]  varchar [ (n) ]  可变长字符串
character [(n)]          char [ (n) ]     定长字符串
timestamp                                   SQL标准要求仅仅将timestamp类型等于timestamp without time zone 类型
timestamp with time zone   TIMESTAMPTZ       PostgreSQL遵守这个行为timestamptz 作为 timestamp with time zone 的缩写被接受;这是PostgreSQL 的一个扩展。

时区

show timezone;
select * from pg_timezone_names where abbrev='+04';

# 临时设置时区, 退出 psql 之后失效
set time zone "Asia/Shanghai";

select now();
# 查看数据库可供选择的时区:
select * from pg_timezone_names;

substring

SUBSTRING(string, start_position, length)
-- str_pos 

table owner

select * from pg_tables where tablename = 'my_tbl';

lock

-- 查看锁
SELECT locker.pid,
       pc.relname,
       locker.mode,
       locker_act.application_name,
       least(query_start, xact_start)                        start_time,
       locker_act.state,
       CASE
           WHEN granted = 'f' THEN
               'wait_lock'
           WHEN granted = 't' THEN
               'get_lock'
           END                                               lock_satus,
       current_timestamp - least(query_start, xact_start) AS runtime,
       locker_act.query
FROM pg_locks locker,
     pg_stat_activity locker_act,
     pg_class pc
WHERE locker.pid = locker_act.pid
  AND NOT locker.pid = pg_backend_pid()
  AND application_name <> 'pg_statsinfod'
  AND locker.relation = pc.oid
  AND pc.reltype <> 0 --and pc.relname='t'
ORDER BY runtime desc;
--查看 PostgreSQL 正在执行的SQL  
SELECT procpid,
       start,
       now() - start AS lap,
       current_query
FROM (SELECT backendid,
             pg_stat_get_backend_pid(S.backendid)            AS procpid,
             pg_stat_get_backend_activity_start(S.backendid) AS start,
             pg_stat_get_backend_activity(S.backendid)       AS current_query
      FROM (SELECT pg_stat_get_backend_idset() AS backendid) AS S) AS S,
     pg_stat_activity pa
WHERE current_query <> '<IDLE>'
  and procpid <> pg_backend_pid()
  and pa.pid = s.procpid
  and pa.state <> 'idle'
ORDER BY lap DESC;
# 查找是否有 waiting  
ps -ef|grep postgres | grep wait  
--查看当前库表和索引的的大小并排序显示前20条
SELECT nspname,
       relname,
       relkind                                       as "type",
       pg_size_pretty(pg_table_size(C.oid))          AS size,
       pg_size_pretty(pg_indexes_size(C.oid))        AS idxsize,
       pg_size_pretty(pg_total_relation_size(C.oid)) as "total"
FROM pg_class C
         LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  AND nspname !~ '^pg_toast'
  AND relkind IN ('r', 'i')
ORDER BY pg_total_relation_size(C.oid) DESC
LIMIT 20;

空闲连接

-- 最大连接数
show max_connections;
-- 当前连接
select * from pg_stat_activity;
select datname from pg_stat_activity group by datname;
select state from pg_stat_activity group by state;
select * from pg_stat_activity where datname='database0' limit 1;
select pid, query_start,query from pg_stat_activity where datname='database0' and state='idle';
-- 释放空闲连接
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state='idle';

GUI

  • pgAdmin

https://www.postgresql.org/docs/current/app-psql.html

export csv, 导出 csv

sql 后面不要加分号

COPY (
  SELECT foo, bar FROM table0
)
TO '/tmp/foo.csv'
WITH csv header;

———————————————— 版权声明:本文为CSDN博主「df0128」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/df0128/article/details/89673596

导入 csv

psql
\c database0
\copy table0(field0,field1,field2,"field3") from '/home/wiloon/tmp/foo.csv' delimiter ',' csv header;

PostgreSQL 中统计指定字符或者单词或者字符串在一个长字符串中出现总次数,PostgreSQL 统计字符串中某字符出现次数

https://blog.csdn.net/sunny_day_day/article/details/109183521

select array_length(regexp_split_to_array(config,'"id":'),1)-1 from xxx;

postgres 判断主备/主从角色

### 操作系统上查看 WAL 发送进程或 WAL 接收进程
ps -ef | grep "wal" | grep -v "grep"
# 主库会有 postgres: walwriter, postgres: walsender 进程
# 从库只有 postgres: walreceiver 进程

# 通过 pg_controldata 命令查看数据库控制信息,内容包含 WAL 日志信息、checkpoint、数据块等信息,通过 Databasecluster state 信息可判断是主库还是备库
pg_controldata | grep cluster
-- psql 连接到 DB 之后, 可以这样判断
SELECT pg_is_in_recovery();
-- You can use pg_is_in_recovery() which returns True if recovery is still in progress(so the server is running in standby mode). Check the System Administration Functions for further informations.
-- 如果返回 t 说明是备库,返回 f 是主库

检查主从复制状态

第一步:判断当前库角色

在检查主从复制状态之前,需要先确认当前连接的是主库还是从库:

-- 判断当前库是主库还是从库
SELECT pg_is_in_recovery();
-- 返回 f (false) = 主库
-- 返回 t (true) = 从库(处于恢复模式)
# 操作系统层面查看进程
ps -ef | grep "wal" | grep -v "grep"
# 主库会有: postgres: walwriter, postgres: walsender
# 从库会有: postgres: walreceiver

# 通过 pg_controldata 查看
pg_controldata | grep cluster

第二步:根据角色执行相应检查

在主库上执行:

-- 快速查看所有从库 IP
SELECT client_addr FROM pg_stat_replication;
-- 返回 0 条数据 = 没有从库连接,或当前是从库

-- 查看从库 IP 和基本状态
SELECT 
    client_addr AS slave_ip,
    state,
    sync_state
FROM pg_stat_replication;

-- 查看完整的复制连接状态和延迟
SELECT 
    client_addr AS slave_ip,
    state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    sync_state,
    pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replication_lag_bytes,
    write_lag,
    flush_lag,
    replay_lag
FROM pg_stat_replication;

-- 简化版本,查看基本复制状态
SELECT 
    client_addr,
    state,
    sync_state,
    replay_lag
FROM pg_stat_replication;

-- 检查复制槽状态(如果使用了复制槽)
SELECT 
    slot_name,
    slot_type,
    database,
    active,
    restart_lsn,
    confirmed_flush_lsn,
    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots;

在从库上执行:

-- 查看从库接收状态
SELECT 
    status,
    receive_start_lsn,
    receive_start_tli,
    received_lsn,
    received_tli,
    last_msg_send_time,
    last_msg_receipt_time,
    latest_end_lsn,
    latest_end_time,
    slot_name
FROM pg_stat_wal_receiver;

-- 在从库上执行:查看主从延迟
SELECT 
    now() - pg_last_xact_replay_timestamp() AS replication_delay;

-- 查看 WAL 应用状态
SELECT 
    pg_last_wal_receive_lsn() AS receive_lsn,
    pg_last_wal_replay_lsn() AS replay_lsn,
    pg_last_xact_replay_timestamp() AS last_replay_time;

字段说明

  • state: 连接状态(streaming 表示正常流复制)
  • sync_state: 同步状态(async 异步,sync 同步,potential 可能同步)
  • replay_lag: 从库应用 WAL 的延迟时间
  • replication_lag_bytes: 主从之间的字节延迟
  • active: 复制槽是否激活
  • status: WAL 接收器状态

健康检查流程

  1. 判断角色SELECT pg_is_in_recovery();
  2. 主库检查pg_stat_replication 查看是否有从库连接,state 是否为 streaming
    • 如果 pg_stat_replication 返回 0 条:可能是没有配置从库,或从库未连接
  3. 从库检查pg_stat_wal_receiver 查看是否正常接收,计算延迟时间
  4. 延迟监控:关注 replay_lagreplication_lag_bytes,确保在可接受范围内

常见问题排查

Q: pg_stat_replication 返回 0 条数据?

如果 pg_is_in_recovery() 返回 f(主库),但 pg_stat_replication 返回 0 条,说明没有从库连接。可能原因:

  1. 没有配置主从复制(单机主库)
  2. 配置了主从,但从库有问题
    • 从库未启动
    • 从库网络不通
    • 从库配置错误(连接信息、认证失败)
    • 主库 pg_hba.conf 未允许从库连接

如何判断是否配置了主从?

-- 检查是否配置了复制槽(如果使用复制槽)
SELECT * FROM pg_replication_slots;
-- 返回 0 条 = 可能没配置主从

-- 检查主库日志
-- 查看是否有从库连接失败的错误信息
# 检查主库配置文件
grep -E "wal_level|max_wal_senders|wal_keep_size" $PGDATA/postgresql.conf
# wal_level = replica (或 logical) 才能做主从
# max_wal_senders > 0 才能发送 WAL

# 检查主库访问控制
cat $PGDATA/pg_hba.conf | grep replication
# 需要有类似这样的配置才允许从库连接:
# host    replication    all    从库IP/32    md5

如果是主库无从库连接,建议:

  1. 确认是否需要配置从库(业务需求)
  2. 如果需要高可用,应配置从库
  3. 如果只是单机环境,无需担心

https://blog.csdn.net/m15217321304/article/details/88845353

https://www.postgresql.org/docs/15/functions-admin.html

filter

create table test(id int, c1 int);  
insert into test select generate_series(1,10000), random()*10;
select * from test limit 10;
select c1,count(*)  from test group by c1;
select c1,count(*), count(*) filter (where id<1000) from test group by c1;

https://blog.csdn.net/wuyujin1997/article/details/125904177

regclass

regclass 是 oid 的别名,postgresql 自动的为每一个系统表都建立了一个 OId,其中有一个系统表叫做:pg_class, 这个表里记录了数据表、索引 (仍然需要参阅pg_index)、序列、视图、复合类型和一些特殊关系类型的元数据

https://blog.csdn.net/shiyibodec/article/details/52447755

postgresql中如何Kill掉正在执行的SQL语句

–查询任务进展 select query_id,query from pg_stat_activity where state=‘active’;

–kill有两种方式 –第一种是: SELECT pg_cancel_backend(PID); 这种方式只能kill select查询,对update、delete 及DML不生效)

–第二种是: SELECT pg_terminate_backend(PID); 这种可以kill掉各种操作(select、update、delete、drop等)操作 ———————————————— 版权声明:本文为CSDN博主「SunWuKong_Hadoop」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/SunWuKong_Hadoop/article/details/89448075

为什么where子句中无法使用到字段别名作为筛选条件呢

这就涉及到了SQL语句的整个执行顺序,如下表:

(1) FROM <left_table> (3) <join_type> JOIN <right_table> (2) ON <join_condition> (4) WHERE <where_condition> (5) GROUP BY <group_by_list> (6) HAVING <having_condition> (7) SELECT (8) DISTINCT <select_list> (9) ORDER BY <order_by_condition> (10) LIMIT <limit_number>

执行顺序依次为:

from :先确定查询范围 ON:确定多表联合查询的条件 JOIN:指定联合哪些数据表 WHERE :全表查询的筛选条件,生成第一个结果集 GROUP BY:分组条件,对第一个结果集进行分组,得到第二个结果集 HAVING :过滤条件,与 group by 连用,对第二个结果集中的每组数据,进行筛选过滤,得到第三个结果集 SELECT:指定获取的列项,得到第四个结果集 DISTINCT :对指定列进行去重操作 ORDER BY:对结果集按照指定字段进行排序整理 LIMIT:对最终结果集进行截取,一般和offset连用,可用于分页 所以,以此可以看出,为什么在where语句中没法使用查询列的别名进行过滤了,因为调用where子句的时候,select子句还没有开始执行,所以不识别,同理,order by子句中是可以使用

在查询mysql的字段别名使用的时候,翻到了mysql的官方文档,里面对别名的使用场景进行了简要的介绍:

意思就是别名可以使在 order by、having、group by 子句中,但是根据上面的SQL执行过程,很明显group by 和 having都在select之前啊,这里值得注意的是,mysql对group by 进行了优化加强,所以在group by子句中可以使用别名进行分类,但是其他数据库还是遵循着SQL的执行顺序 ———————————————— 版权声明:本文为CSDN博主「shenzhou_yh」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/shenzhou_yh/article/details/103185772

ESCAPE

string to int

--把'1234'转成整数

select cast('1234' as integer ) ;

--用substring截取字符串,从第8个字符开始截取2个字符:结果是12

select cast(substring('1234abc12',8,2) as integer)

---使用to_number函数来转换成整数

---to_number(text, text)  返回的类型 numeric     把字串转换成numeric   to_number('12,454.8-', '99G999D9S')

select to_number('12121','999999999')
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/xingxiupaioxue/article/details/78295118

字符串分割函数

  1. SPLIT_PART SPLIT_PART() 函数通过指定分隔符分割字符串,并返回第N个子串。语法:

SPLIT_PART(string, delimiter, position) 1 string : 待分割的字符串 delimiter:指定分割字符串 position:返回第几个字串,从1开始,该参数必须是正数。如果参数值大于分割后字符串的数量,函数返回空串。 示例:

SELECT SPLIT_PART(‘A,B,C’, ‘,’, 2); – 返回B 1 下面我们利用该函数分割日期,获取年月日:

select split_part( current_date::text,’-’,1) as year , split_part( current_date::text,’-’,2) as month, split_part( current_date::text,’-’,3) as day ————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/neweastsun/article/details/120243524

like, ilike

-- 模糊查询,并且不区分大小写
select * from student where name ilike '%tom%'

字符串转bytea:

select cast(’\000’ as bytea) as name from nt_member limit 1;

bytea转字符串:

select encode(’\x00’,’escape’) from nt_member limit 1;

索引

CREATE INDEX index_0 ON table_0 (column_0);

select * from pg_indexes where tablename='network_discovery_routing_data';
select * from pg_statio_all_indexes where relname='network_discovery_routing_data';

select pg_relation_size('network_discovery_routing_data_time_stamp_idx');
select pg_relation_size('constraint_routing_id_time_stamp');
-- 按名字删除索引
DROP INDEX index_0;

--查看索引占用的空间 
SELECT 
    indexrelname AS indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
    pg_relation_size(indexrelid) AS index_size_bytes
FROM pg_stat_user_indexes 
WHERE indexrelname = 'index_0';

临时改变执行计划

set enable_seqscan=off;set enable_bitmapscan = off;set enable_tidscan = off;explain (analyze,buffers,verbose) select count(*) from aken01;

类似 oracle 的 hint 干预执行计划,pg也有 pg_hint 插件,后期再研究。 通常情况下,pg 不会走错执行计划,走错大都是因为统计信息收集不及时导致的,可通过更频繁地运行 analyze 来解决这个问题,更改下列参数只是一个临时方法。(如下参数值均为布尔类型)

set enable_seqscan = off;

enable_seqscan:是否选择全表顺序扫描,这个变量关闭会让优化器存在其他方法时,优先选择其他方法。 enable_indexscan:是否选择索引扫描。 enable_bitmapscan:是否选择位图扫描。 enable_tidscan:是否选择 TID 扫描。 enable_nestloop:多表连接时,是否选择嵌套循环连接。 enable_hashjoin:多表连接时,是否选择 hash 连接。 enable_mergejoin:多表连接时,是否选择 merge 连接。 enable_hashagg:是否使用 hash 聚合。 enable_sort:是否选择排序。

执行计划路径方式 全表扫描(顺序扫描):seq scan,所有数据块,从头扫到尾。 索引扫描:index scan,在索引中找到数据行的位置,然后到表的数据块中把对应的数据读出。 位图索引扫描:bitmap index scan,把满足条件的行或块在内存中建一个位图,扫描完索引后,再根据位图把表的数据文件中相应的数据读取出来。 条件过滤:filter 嵌套循环连接:nestloop join,外表(驱动表)小,内表(被驱动表)大 散列连接:hash join,用较小的表在内存中建立散列表,再去扫描较大的表,连接的表均为小表。 合并连接:merge join,通常散列连接比合并连接性能好,当有索引或结果已经被排序时,合并连接性能好。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/songyundong1993/article/details/122844254

synchronous_commit 同步提交配置

synchronous_commit 控制事务提交时, PostgreSQL 需要等待 WAL 被写到哪个阶段才向客户端返回成功。它是性能与数据安全的核心权衡点。

五个级别详解

synchronous_commit = off | local | remote_write | remote_apply | on

从最弱到最强:

off — 异步提交

客户端发出 COMMIT
PostgreSQL 立即返回成功
    ↓  (后台异步)
WAL 写入操作系统缓冲区
WAL 刷盘
WAL 发送给 Standby
Standby 写入并应用
  • 性能最高, 延迟最低
  • 风险: 服务器崩溃可能丢失最近几个事务 (通常 < 600ms 的数据)
  • 适用: 可容忍少量数据丢失的场景 (日志、统计、临时数据)

local — 本地同步 (默认值的近似)

客户端发出 COMMIT
等待 WAL 刷入本地磁盘 (fsync)
返回成功
    ↓  (后台异步)
WAL 发送给 Standby
  • 保证本地宕机不丢数据
  • 不等待 Standby, 主从复制仍是异步的
  • 这是大多数单机场景的合理默认值

on — 等待 Standby 写入磁盘

客户端发出 COMMIT
等待 WAL 刷入本地磁盘
等待 Standby 确认已将 WAL 写入并刷盘 (flush)
返回成功
  • WAL 已在 Primary 和 Standby 双端落盘
  • Standby 还没有 apply (回放) WAL, 数据在 Standby 上尚不可查询
  • 主节点故障后, Standby 可以恢复所有已提交事务

remote_write — 等待 Standby 写入但不刷盘

等待 Standby 确认已将 WAL 写入操作系统缓冲区 (write, 未 fsync)
  • on 略快 (不等 Standby fsync)
  • 如果 Standby 的操作系统崩溃 (而非 PostgreSQL 崩溃), 可能丢失数据
  • 实践中较少使用

remote_apply — 等待 Standby 应用 WAL ⭐ 最强

客户端发出 COMMIT
等待 WAL 刷入本地磁盘
等待 Standby 确认已将 WAL replay (应用到数据文件)
返回成功
  • 事务提交后, 数据立即在 Standby 上可查询
  • 这是实现读写分离强一致性的基础
  • 也是用 PostgreSQL 行级锁表实现跨 Failover 强一致分布式锁的必要条件
  • 延迟最高 (需等待网络往返 + Standby apply)

五个级别对比

级别 等待阶段 本地宕机丢数据 Standby 宕机丢数据 Failover 后数据完整 相对延迟
off ⚠️ 可能 ⚠️ 可能 ❌ 可能丢失 最低
local 本地 fsync ✅ 不会 ⚠️ 可能 ❌ 可能丢失
remote_write Standby write ✅ 不会 ⚠️ OS 崩溃时可能 ✅ 基本保证
on Standby fsync ✅ 不会 ✅ 不会 ✅ 保证 中高
remote_apply Standby apply ✅ 不会 ✅ 不会 ✅ 保证且立即可查 最高

配置方式

-- 查看当前值
SHOW synchronous_commit;

-- 数据库级别设置
ALTER DATABASE mydb SET synchronous_commit = 'remote_apply';

-- 会话级别设置 (只影响当前连接)
SET synchronous_commit = 'remote_apply';

-- 事务级别设置 (只影响当前事务)
BEGIN;
SET LOCAL synchronous_commit = 'remote_apply';
-- ... 业务操作
COMMIT;

同时需要配置哪些 Standby 参与同步:

# postgresql.conf on Primary
synchronous_commit = remote_apply
# * 表示任意一个 Standby 确认即可
# 也可以指定名称: synchronous_standby_names = 'standby1'
synchronous_standby_names = '*'

Standby 连接时会携带 application_name, 需与 synchronous_standby_names 匹配:

# recovery.conf 或 postgresql.conf on Standby (PG 12+)
primary_conninfo = 'host=primary_ip port=5432 user=replicator application_name=standby1'

remote_apply 的实际影响

好处:

  1. 读写分离强一致: 向 Primary 写入后立即从 Standby 读取, 数据已存在
  2. Failover 零数据丢失: 所有已提交事务在 Standby 上已完全应用
  3. 分布式锁一致性: 行级锁表的锁记录在 Failover 后完整保留

代价:

  1. 写入延迟增加: 每次提交需要等待网络往返 + Standby apply 时间
    • 典型延迟: 本地 LAN 环境 15ms, 跨机房可能 1050ms+
  2. Standby 不可用时 Primary 阻塞: 如果 Standby 宕机, Primary 的写事务会一直等待直到超时
    • 通过 synchronous_commit_timeout 控制超时行为
  3. 吞吐量下降: 高并发写入场景下影响明显

Standby 不可用时的行为

# 等待超时后降级为异步提交, 避免 Primary 完全阻塞
# 默认值: 1s
wal_sender_timeout = 60s    # Standby 超时踢出同步列表
-- 查看当前同步状态
SELECT application_name, sync_state FROM pg_stat_replication;
-- sync_state = 'sync'      当前同步 Standby
-- sync_state = 'async'     异步 Standby
-- sync_state = 'potential' 候补同步 Standby

如果同步 Standby 全部断线, PostgreSQL 会等待 wal_sender_timeout 后将其移出同步列表, Primary 恢复正常写入 (降级为异步)。

适用场景建议

场景 推荐配置
单机开发/测试 local (默认)
一般生产主从, 允许极少量数据丢失 on
读写分离且要求强一致 remote_apply
用 PG 行级锁表做分布式锁且需跨 Failover 一致 remote_apply
高并发写入, 优先吞吐量 localoff + 业务幂等
金融核心交易, 零数据丢失 remote_apply + 多 Standby

remote_apply 是「强一致但有代价」的选择。在延迟敏感或高并发写入场景, 建议先评估实际延迟影响再决定是否启用。可以考虑只对关键事务通过 SET LOCAL synchronous_commit = 'remote_apply' 单独启用, 而不是全库开启。

Advisory Lock 咨询锁

什么是 Advisory Lock

Advisory Lock (咨询锁) 是 PostgreSQL 提供的一种应用层协调机制。与行锁、表锁保护的是数据库内部的数据对象不同, Advisory Lock 的含义完全由应用自己定义 —— 数据库只负责加锁/解锁的原子性和互斥性, 至于这把锁代表什么、保护什么资源, 完全取决于应用程序的约定。

Advisory Lock 使用一个 64 位整数 (或两个 32 位整数) 作为锁的 key, 存储在共享内存中, 不写入 WAL, 不占用磁盘空间。

核心 API

Session 级别 (会话级)

-- 获取排他锁 (阻塞, 直到获取成功)
SELECT pg_advisory_lock(key bigint);

-- 尝试获取排他锁 (非阻塞, 成功返回 true, 失败返回 false)
SELECT pg_try_advisory_lock(key bigint);

-- 获取共享锁 (多个会话可同时持有共享锁)
SELECT pg_advisory_lock_shared(key bigint);

-- 尝试获取共享锁 (非阻塞)
SELECT pg_try_advisory_lock_shared(key bigint);

-- 释放排他锁
SELECT pg_advisory_unlock(key bigint);

-- 释放共享锁
SELECT pg_advisory_unlock_shared(key bigint);

-- 释放当前会话所有 Advisory Lock
SELECT pg_advisory_unlock_all();

Transaction 级别 (事务级)

-- 获取事务级排他锁 (事务结束时自动释放, 无法手动解锁)
SELECT pg_advisory_xact_lock(key bigint);

-- 尝试获取事务级排他锁
SELECT pg_try_advisory_xact_lock(key bigint);

两个 int4 参数版本

当业务 key 由两个维度组成时 (如 resource_type + resource_id), 可以用两个 int4:

-- 等效于 pg_advisory_lock((int8(class) << 32) | int8(id))
SELECT pg_advisory_lock(class int4, id int4);
SELECT pg_try_advisory_lock(class int4, id int4);

Session 级 vs Transaction 级的区别

维度 Session 级 Transaction 级
释放时机 手动调用 pg_advisory_unlock 或连接断开 事务提交或回滚时自动释放
可重入 ✅ 同一 Session 可多次加锁, 需同等次数解锁 ✅ 事务内多次加锁只需一次 (事务结束统一释放)
可手动解锁 ❌ 只能等事务结束
连接池兼容性 ⚠️ 有风险 (见下文) ✅ 使用 AUTOCOMMIT 时自然归还

Advisory Lock 的重要特性

1. 存储在共享内存, 不写 WAL

Advisory Lock 不记录到 WAL (Write-Ahead Log), 因此:

  • 不会复制到 Standby, 主从切换后锁状态消失
  • 服务器崩溃重启后锁状态消失
  • 性能极高, 无磁盘 I/O

这与行级锁表方案形成对比: 行级锁写入 WAL, 可以通过同步复制保证 Failover 后锁一致性。

2. 可重入性

Session 级 Advisory Lock 是可重入的: 同一会话可以对同一个 key 多次调用 pg_advisory_lock, 内部维护一个计数器, 必须调用相同次数的 pg_advisory_unlock 才能真正释放。

SELECT pg_advisory_lock(100);   -- count = 1
SELECT pg_advisory_lock(100);   -- count = 2
SELECT pg_advisory_unlock(100); -- count = 1, 锁仍持有
SELECT pg_advisory_unlock(100); -- count = 0, 锁释放

3. 连接断开自动释放

持有 Session 级 Advisory Lock 的连接断开后 (正常关闭或异常崩溃), PostgreSQL 会自动释放该会话持有的所有 Advisory Lock。这天然解决了死锁问题 —— 持有锁的进程崩溃后锁不会永久阻塞其他进程。

使用连接池的注意事项

使用 PgBouncer 等连接池时, Session 级 Advisory Lock 存在风险:

应用 A: 从连接池借到连接 conn-1
应用 A: pg_advisory_lock(100) → 加锁成功
应用 A: 完成业务, 将 conn-1 归还连接池  ← 忘记 unlock!

应用 B: 从连接池借到 conn-1
应用 B: 以为是干净连接, 实际上 conn-1 仍持有 key=100 的锁
应用 B: 对其他 key 加锁, 操作共享资源 → 产生意外的锁竞争

解决方案:

  1. 始终在 finally 块中解锁, 确保归还连接前释放锁
  2. 优先使用 Transaction 级 pg_advisory_xact_lock, 事务结束时自动释放, 配合 AUTOCOMMIT 使用时尤其安全
  3. pg_advisory_lock 前先调用 pg_advisory_unlock_all() 作为保险措施 (不推荐, 副作用大)

典型使用场景

场景一: 分布式定时任务防重

多个实例同时运行定时任务时, 用 Advisory Lock 保证只有一个实例执行:

-- 在应用代码中 (伪代码)
connection.execute("SELECT pg_try_advisory_lock(hashtext('daily_report_job'))");
if result == true:
    run_daily_report()
    connection.execute("SELECT pg_advisory_unlock(hashtext('daily_report_job'))")
else:
    log("Another instance is running the job, skip")

场景二: 防止并发处理同一业务对象

-- 对 order_id=12345 加锁, 防止并发处理
SELECT pg_try_advisory_xact_lock(12345);
-- 返回 true: 获取成功, 处理订单
-- 返回 false: 已有其他事务在处理, 跳过或等待

-- 事务提交时自动释放, 无需手动 unlock
COMMIT;

场景三: 与 SELECT FOR UPDATE 配合

先用 Advisory Lock 防止并发, 再用 SELECT FOR UPDATE 锁定行:

BEGIN;
SELECT pg_advisory_xact_lock(order_id);
SELECT * FROM orders WHERE id = order_id FOR UPDATE;
-- 处理业务逻辑
COMMIT;

Advisory Lock vs 其他方案对比

方案 性能 复制到 Standby 崩溃后自动释放 适用场景
Advisory Lock (Session) ⭐⭐⭐⭐⭐ ✅ (连接断开) 任务调度, 轻量协调
Advisory Lock (Xact) ⭐⭐⭐⭐⭐ ✅ (事务结束) 短事务内互斥
行级锁表 ⭐⭐⭐ ✅ (同步复制时) ✅ (TTL + 清理) 强一致性, 跨服务
Redis SET NX ⭐⭐⭐⭐⭐ ❌ (异步复制) ✅ (TTL) 高并发, 允许极低概率失败
ZooKeeper / etcd ⭐⭐⭐ ✅ (Raft) 强一致性要求

主从切换与数据库重启对 Advisory Lock 的影响

结论先行: Advisory Lock 在主从切换和数据库重启后会完全消失。

为什么会消失

Advisory Lock 存储在 PostgreSQL 的共享内存 (Shared Memory) 中, 不写入 WAL (Write-Ahead Log)。这意味着:

  • 进程内存的数据在进程终止后即消失
  • 没有 WAL 就无法复制到 Standby
  • 没有 WAL 就无法在崩溃恢复时重放

场景一: 数据库重启

应用 A 持有 Advisory Lock(100) ← 存在共享内存中

数据库重启 (计划内 or 崩溃)
共享内存清空
Advisory Lock(100) 消失

数据库重启完成后:
- 应用 A 的连接已断开 (连接断开时锁本来就会自动释放)
- 即使应用 A 重新连接, 也需要重新申请锁
- 其他应用可以正常申请 Lock(100)

影响: 数据库重启后锁状态清空, 行为是符合预期的。因为持有锁的连接在重启时已强制断开, 锁随之释放, 不存在「锁记录残留但持有者已消失」的僵尸状态。

场景二: 主从切换 (Failover)

应用 A 连接 Primary, 持有 Advisory Lock(100)
Primary 宕机
Standby 晋升为新 Primary
新 Primary 共享内存中没有 Advisory Lock(100)
应用 B 连接新 Primary, 申请 Advisory Lock(100) → 成功! ⚠️

同时:
应用 A 的连接已断开 (Primary 宕机)
应用 A 重连新 Primary, 发现自己没有锁了

影响: Failover 窗口期内存在互斥性被短暂破坏的风险 —— 原持有锁的客户端还没意识到 Primary 已切换, 而新 Primary 上已经可以申请到同一把锁。

与行级锁表的对比

事件 Advisory Lock 行级锁表 (异步复制) 行级锁表 (同步复制)
数据库重启 锁消失 (正常) 锁记录保留 锁记录保留
主从切换 锁消失 ❌ 可能丢锁 ✅ 锁记录保留
对互斥性的影响 短暂窗口期风险 短暂窗口期风险 安全

实际应对方式

方案一: 业务幂等兜底 (最重要)

无论用何种分布式锁方案, 幂等性都是最后一道防线。Failover 期间即使两个客户端同时获得锁, 幂等检查也能保证业务结果正确:

-- 获得锁后, 先检查业务状态
SELECT pg_try_advisory_xact_lock(order_id);
IF acquired THEN
    -- 检查幂等: 是否已经处理过?
    IF NOT EXISTS (SELECT 1 FROM orders WHERE id = order_id AND status = 'processed') THEN
        -- 处理业务
        UPDATE orders SET status = 'processed' WHERE id = order_id;
    END IF;
END IF;

方案二: 客户端感知重连

应用层在重连后重新申请锁, 并检查业务状态是否需要重新处理。

方案三: 对强一致性要求高时, 改用行级锁表 + 同步复制

如果业务绝对不能容忍 Failover 期间的短暂互斥失效, 应改用行级锁表方案并开启同步复制:

# postgresql.conf
synchronous_commit = remote_apply
synchronous_standby_names = 'standby1'

总结

场景 Advisory Lock 行为 是否需要担心
计划内重启 锁随连接断开消失, 正常 不需要, 持有锁的连接会重新连接并申请
崩溃重启 锁消失, 正常 不需要, 配合业务幂等即可
主从切换 锁消失, 存在短暂窗口期 需要幂等兜底
Standby 上申请锁 Standby 只读, 无法申请 不适用

Advisory Lock 不适合需要跨 Failover 保持严格互斥的场景。 对于这类需求, 应使用行级锁表 + 同步复制, 或 etcd/ZooKeeper。大多数场景下, Advisory Lock + 业务幂等是足够且高效的组合。

查看当前 Advisory Lock 状态

-- 查看当前所有 Advisory Lock
SELECT
    pid,
    locktype,
    classid,
    objid,
    mode,
    granted
FROM pg_locks
WHERE locktype = 'advisory';

-- 结合进程信息
SELECT
    l.pid,
    a.usename,
    a.application_name,
    l.classid,
    l.objid,
    l.mode,
    l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'advisory';