PostgreSQL 19 新特性:为什么 COUNT(*) 终于比 COUNT(1) 快了?

前言

很多开发者都纠结过 COUNT(*)COUNT(1) 到底哪个更快。PostgreSQL 19 给了一个明确的答案——​数据库会自动帮你选最快的那个​。

COUNT(*) vs COUNT(1):老生常谈的问题

在 SQL 里,COUNT 是最常用的聚合函数之一。关于这两种写法的争论已经持续了很多年:

  • COUNT(*)​:统计所有行数
  • COUNT(1)​:对每一行计算常量 1,然后统计非 NULL 的数量

理论上,如果表达式永不为 NULL(比如常量 1),这两种写法的结果​完全一样​。但在 PostgreSQL 内部,它们的执行路径却大不相同。

为什么以前 COUNT(1) 反而更慢?

在 PG 19 之前,PostgreSQL 对待 COUNT(1) 是”老实巴交”的:

  1. 表达式计算​:对每一行都要计算一次表达式值(虽然结果就是 1
  2. NULL 检查​:检查计算结果是否为 NULL(虽然 1 永远不是 NULL)
  3. Tuple Deform(解构元组)​:如果表达式涉及列值,数据库可能需要从磁盘行格式中提取出完整的列数据。对于列很多的宽表,这个开销会非常明显

COUNT(*) 走的是特化路径——​什么都不算,直接数行​,天然就少了这些开销。

PG 19 的优化:让规划器替你省事儿

PostgreSQL 19 引入了一个重要的性能补丁(Commit 42473b3),标题就是:

“Have the planner replace COUNT(ANY) with COUNT(*), when possible”

简单说:​查询规划器现在会自动把冗余的 COUNT(表达式) 重写为更高效的 COUNT(*)​。

触发条件

要触发这个优化,必须同时满足:

  1. COUNT() 里的表达式​保证不会为 NULL​(比如常量 1、NOT NULL 约束的列)
  2. 不包含 ORDER BYDISTINCT 子句
-- 优化前:COUNT(1) 有额外计算开销
SELECT COUNT(1) FROM orders;

-- 优化后:PG 19 自动重写为 COUNT(*),无需表达式计算
-- 执行计划内部等价于:
SELECT COUNT(*) FROM orders;

技术内幕:SupportRequestSimplifyAggref

这个补丁引入了一个新的支持请求类型 SupportRequestSimplifyAggref,允许聚合函数的支持函数(prosupport)检查自身的调用是否可以被简化。

对于 COUNT(),PG 新增了一个专门的支持函数,负责判断:

  • 输入表达式是否非空?
  • 有没有 ORDER BY / DISTINCT

如果条件满足,就在查询规划阶段直接把 COUNT(ANY) 替换为 COUNT(*),从源头上消除了不必要的表达式评估和 NULL 检查。

附带收益:更干净的执行计划

这个补丁还顺带改进了 expr_is_nonnullable() 函数,让它能正确识别常量表达式的非空性。

比如以前执行计划里可能出现的这种冗余过滤:

One-Time Filter: (100 IS NOT NULL)

现在规划器知道 100 肯定不是 NULL,直接把这个无用过滤器移除了,执行计划更清爽。

总结

写法PG 19 之前PG 19 及以后
COUNT(*)最快,直接数行依然最快
COUNT(1)慢,有表达式计算和 NULL 检查开销自动优化为 COUNT(*)
COUNT(not_null_col)慢,可能触发 tuple deform自动优化为 COUNT(*)

最终结论

  • PG 19 之前​:请直接写 COUNT(*),它是性能最优的写法。
  • PG 19 及以后​:写 COUNT(1) 也不会吃亏了,数据库会自动帮你优化。但出于习惯和对旧版本的兼容性,​COUNT(*) 依然是最佳实践​。

pg_basebackup 报错:no pg_hba.conf entry for replication connection

pg_basebackup 执行报错 “no pg_hba.conf entry for replication connection” 的解决方法——别忘了 replication 虚拟库也需要一条规则


一、问题现象

执行 pg_basebackup 时报错:

[postgres@pg01 tools]$ pg_basebackup -h 10.0.0.101 -U postgres -F p -P -X stream -R -D $PGDATA -l postgresbackup20260616
2026-06-16 10:17:31.017 CST [1577] FATAL:  no pg_hba.conf entry for replication connection from host "10.0.0.101", user "postgres"
pg_basebackup: error: could not connect to server: FATAL:  no pg_hba.conf entry for replication connection from host "10.0.0.101", user "postgres"

明明 pg_hba.conf 里已经配置了 all 规则,为什么还会被拒绝?


二、问题原因

pg_basebackup 走的是 replication 连接,而 replication 并不是一个真实的数据库,它是一个虚拟库。pg_hba.conf 中的 host all all ... 规则只覆盖普通数据库连接,不覆盖 replication 连接。

因此即使配置了:

host    all             all             0.0.0.0/0               trust

pg_basebackup 依然会因为找不到 replication 规则而拒绝连接。


三、解决方案

在 pg_hba.conf 中添加 replication 规则:

host    replication     all             0.0.0.0/0               trust

重启 PostgreSQL:

pg_ctl restart

再次执行 pg_basebackup:

[postgres@pg01 backup]$ pg_basebackup -h 10.0.0.101 -U postgres -F p -P -X stream -R -D $PGDATA/backup -l postgresbackup20260616
65502/65502 kB (100%), 1/1 tablespace

备份成功。


四、总结

pg_hba.conf 中至少需要两条规则才能同时支持普通连接和 pg_basebackup 备份:

host    all             all             0.0.0.0/0               trust
host    replication     all             0.0.0.0/0               trust
  • 第一条:允许 普通数据库连接(所有数据库、所有用户、任意 IP,trust 认证)
  • 第二条:允许 复制连接(特殊的 replication 虚拟库、所有用户、任意 IP,trust 认证)

两条规则缺一不可。

PG高效去重

方案 1:使用 DISTINCT ON(PostgreSQL 特有,推荐)

-- 只保留每个 id 的第一条
SELECT DISTINCT ON (id) * FROM t 
ORDER BY id, ctid;

方案 2:使用 ROW_NUMBER(推荐)

-- 查看重复行
WITH ranked AS (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS rn
    FROM t
)
SELECT * FROM ranked WHERE rn > 1;

-- 删除重复行
WITH ranked AS (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS rn
    FROM t
)
DELETE FROM t 
WHERE ctid IN (SELECT ctid FROM ranked WHERE rn > 1);

优点: SQL 标准,可读性强,更灵活
缺点: 需要配合 ctid 来实现删除

方案 3:使用 CTID

-- 查看重复行
SELECT * FROM t a 
WHERE a.ctid <> (
    SELECT MIN(b.ctid) 
    FROM t b 
    WHERE a.id = b.id
);

-- 删除重复行
DELETE FROM t 
WHERE ctid <> (
    SELECT MIN(b.ctid) 
    FROM t b 
    WHERE t.id = b.id
);

优点: 快速,直接操作物理位置
缺点: PostgreSQL 特有语法,不够优雅

方案 4:使用 NOT IN(简单但低效)

-- 保留 id 最小的一条
DELETE FROM t 
WHERE id NOT IN (
    SELECT MIN(id) 
    FROM t 
    GROUP BY id
);

缺点: 如果 id 相同但其他字段不同,逻辑可能有问题


postgres启动服务状态卡前台窗口

问题描述

执行systemct start postgresql-16.service,前台窗口一直不退出。检查postgres服务状态,运行正常。

问题分析

症状就是 systemctl start 一直卡在前台窗口。
因为 systemd 没收到 PostgreSQL 服务启动完成的状态通知,才会一直卡在前台窗口。这种问题主要出现在自编译安装或第三方安装的postgres数据库中,如编译过程中没有添加参数 --with-systemd

cat /usr/lib/systemd/system/postgresql-16.service
[Unit]
Description=PostgreSQL 16 database server
Documentation=https://www.postgresql.org/docs/16/static/
After=network.target
[Service]
Type=notify #问题点,编译未添加 --with-systemd
User=postgres
Group=postgres
Environment=PGDATA=/xxx/pgdata/
ExecStart=/xxx/bin/postgres -D ${PGDATA}
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=0
OOMScoreAdjust=-1000
Environment=PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj
Environment=PG_OOM_ADJUST_VALUE=0
[Install]
WantedBy=multi-user.target

问题解决

将notify更改为simple, systemd 只要看到主进程postgres启动就当服务已启动,不等待额外通知。
[Service]
Type=simple

企业CxO数据库选型应该回答清楚的 15 个问题

1、这款数据库的发展历史
2、是不是适合我们当前的场景
3、是不是符合长期发展需求(数据量维度, 数据模型维度, 数据类型, 计算, 检索维度)
4、公司内部有没有会用这款产品的, 有多深
5、公司内部有没有有没有熟悉这个产品的数据库架构师
6、有没有会管理、优化的
7、有没有开发依赖的生态产品, 是否符合公司技术栈, 使用这个产品的时间成本。
8、有没有外部商业化售后服务
9、是否符合行业合规要求
10、有哪些用户在用这个产品
11、数据库源代码生命力 (开源社区的运作逻辑, 是否可以长久运作, 核心组组成,为什么贡献,提交代码的流程,代码质量,全球有多少内核开发者, 有多少国家在贡献, 代码掌握在谁手里, 开源许可, 有没有那个国家控制, 有没有那个公司控制)
12、这款数据库处于什么发展周期(上升, 下降?)
13、这款数据库未来的发展方向
14、以上数据如何量化,对比其他数据库, 有没有更合适的其他数据库产品
15、这款数据库的人才库分布如何?
1)应用开发(SQL)人才
2)应用框架开发人才
3)管理人才
4)数据库架构师人才
5)数据库底座内核开发
6)数据库应用内核开发
7)数据库服务提供商

索引使用的前提

首先,是前面提到的Access Method, 然后是使用的operator class, 以及opc中定义的operator或function;
其次,遵循CBO的选择
#seq_page_cost = 1.0
#random_page_cost = 4.0
#cpu_tuple_cost = 0.01
#cpu_index_tuple_cost = 0.005
#cpu_operator_cost = 0.0025
#effective_cache_size = 128MB
最后,遵循完CBO的选择, 还需要符合当前配置的Planner 配置
#enable_bitmapscan = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on

函数建分区表

按周生成分区表

do                                                                                                               
$$
DECLARE base text; --生命sql类型

pgsqltest text; --字符串为文本类型,执行函数


i int; --i为整数
BEGIN
base = 'create table main_history_p_%s partition of main_history_p for values FROM (''%s'') to (''%s'')';


FOR i IN 0..11 loop --不是左闭右开
pgsqltest = format(base,
to_char('2024-01-01'::timestamp + (i || 'week')::INTERVAL, --第一个%s占位符
'YYYYMMDD'),
'2024-01-01'::timestamp + (i || 'week')::INTERVAL, --第二个%s占位符
'2024-01-01'::timestamp + (i + 1 || 'week')::INTERVAL); --第三个%s占位符
--raise notice '%', sqlstring;
EXECUTE pgsqltest; --执行sqlstring
END loop; --结束loop
END --结束begin
$$language plpgsql; --结束函数

按月生成分区表

do                                                                                                               
$$
DECLARE base text; --生命sql类型

pgsqltest text; --字符串为文本类型,执行函数

i int; --i为整数
BEGIN
base = 'create table main_history_p_%s partition of main_history_p for values FROM (''%s'') to (''%s'')';

FOR i IN 0..11 loop --不是左闭右开
pgsqltest = format(base,
to_char('2024-01-01'::timestamp + (i || 'month')::INTERVAL, --第一个%s占位符
'YYYYMMDD'),
'2024-01-01'::timestamp + (i || 'month')::INTERVAL, --第二个%s占位符
'2024-01-01'::timestamp + (i + 1 || 'month')::INTERVAL); --第三个%s占位符
--raise notice '%', sqlstring;
EXECUTE pgsqltest; --执行sqlstring
END loop; --结束loop
END --结束begin
$$language plpgsql; --结束函数

PL/pgSQL编写造数据脚本

1.编写SQL脚本,插入到main_history表

1.1 创建表

CREATE TABLE main_history (
amount int4 NULL,
"content" varchar NULL,
main_id int4 NULL
);

1.2 批量插入

--_configList 使用 “_” 前缀来标识变量,用于区分sql中的字段 
CREATE OR REPLACE FUNCTION batchInsert(_configList varchar[][], _main_id int) RETURNS void AS $$
DECLARE
_config varchar[];
_content varchar;
_amount int;
BEGIN
--获取二维数组的每个一维数组
FOREACH _config SLICE 1 IN ARRAY (_configList) LOOP
_content := _config[1];
_amount := _config[2];
--把变量输出到控制台
RAISE NOTICE 'config = %, content = %, amount = % main_id = %', _config, _content, _amount, _main_id;
--用变量拼接sql语句并且实际运行在server上
INSERT INTO main_history (amount, content, main_id) VALUES (_amount, _content, _main_id);
END LOOP;
END;
$$ LANGUAGE plpgsql;
--使用函数
select batchInsert(ARRAY[['ccontent1', '1'], ['content2', '2']], 1);
--删除函数
drop function batchInsert;

2. 函数说明

2.1 声明数组

声明的时候可以不额外区分一维、二维数组
_configList varchar[][] 和 _configList varchar[] 是一样的

2.2 数组赋值

声明为varchar后,赋值时也要是varchar类型。
_configList varchar[][] := (ARRAY[[‘ccontent1’, ‘1’], [‘content2’, ‘2’]]);

2.3 数组遍历

二维数组的遍历
–_configList 由外部传入, 而FOREACH SLICE IN ARRAY都是关键字
FOREACH _config SLICE 1 IN ARRAY (_configList) LOOP
_content := _config[1];
_amount := _config[2];
END LOOP;

PG无法释放空间问题分析

近期对PG数据库的两张分区表进行数据删除操作,近40G数据。当PG的两张分区表完成操作后,执行vaccum full table发现磁盘空间无任何的变动。

操作步骤:

1、建表导入无数据。例如:
CREATE TABLE t1 AS SELECT * FROM t2 WITH NO DATA;

2、导入数据。例如:
insert into t1 select * from t2 where create_time > ‘2021-01-01’ and create_time < ‘2021-07-01’;

3、校验导入的数据量。例如:
select count(id) from t1;

4、删除数据并校验。例如:
delete from t1 where create_time > ‘2020-07-01’ and create_time < ‘2021-01-01’;

5、释放磁盘空间。例如:
VACUUM FULL t1

执行了vaccum full t1无任何的变化。经过分析,大表进行分区操作后,需要对每张分区表执行vaccum full t1_01,
vaccum full t1_02 至到
vaccum full t1_0x最后一张表。