判断存在用 LIMIT 1:PostgreSQL 五种 count 计数方式与 NULL 语义

判断存在用 LIMIT 1:PostgreSQL 五种 count 计数方式与 NULL 语义

标签:#PostgreSQL #SQL #性能优化 #计数

一、前言

在函数中、或程序中,不要使用 count(*) 判断是否有数据------很慢。 官方 9.21 原话(性能 Note):"习惯于其他数据库系统的用户可能会对 count 聚合在整表上的性能失望。像 SELECT count(*) FROM sometable; 这样的查询,工作量与表的大小成正比:PostgreSQL 需要扫描整张表、或包含所有行的整个索引。"

判断"有没有数据"的正确姿势是 LIMIT 1(人工检查):

sql 复制代码
SELECT 1 FROM tbl WHERE xxx LIMIT 1;
-- IF FOUND → 存在
-- ELSE     → 不存在

同时要会选取合适的计数方式------五种 count 的 NULL 语义各不相同(官方 9.21 / 4.2.7):

写法 语义 NULL 处理
count(*) 统计输入行数(标准语法) 空值会纳入统计
count(col) 统计 col 列非空记录数 col 为 NULL 不计入
count(distinct col) col 列去重计数 同样忽略 NULL,只统计非空不同值
count((col1, col2)) 多列计数(行构造器) 即使列全为空也会被计数,(null, null) 有效
count(distinct (col1, col2)) 多列去重计数 即使列全为空也会被计数,(null, null) 有效

二、核心开发规范

  1. 存在性判断用 LIMIT 1,不用 count(*)SELECT 1 FROM tbl WHERE xxx LIMIT 1; + IF FOUND------找到第一行立即停,工作量只到第一行命中;count(*) 要数完所有匹配行(官方 9.21:工作量与表大小成正比);
  2. 五种计数方式按需选取 :数全部行用 count(*)(含全 NULL 行);数某列非空用 count(col);数不同非空值用 count(distinct col);数"组合列"用 count((col1, col2))全 NULL 也计入);
  3. COUNT DISTINCT 有去重成本:需要排序 / 哈希聚合,比 count 更贵------能不用就不用,高频路径尤其注意。

三、底层原理通俗讲解

  • 官方 9.21 count 定义count(*) → "计算输入行的数量";count(expression) → "计算输入值不为 NULL 的输入行数量"------这就是 count(*) 纳入空值、count(col) 忽略 NULL 的官方依据;
  • 官方 9.21 性能原话SELECT count(*) FROM sometable; 需要扫描整张表或包含所有行的整个索引------全量计数没有捷径,工作量随表长大;
  • LIMIT 1 为什么快 :Limit 节点找到一行就提前终止执行(第 17 篇讲过)------存在性检查只关心"有没有",第一行命中即停,不用数完全部;
  • 官方 4.2.7 原文 :"count(*) 产生输入行的总数;count(f1) 产生 f1 非空的输入行数,因为 count 忽略 null ;count(distinct f1) 产生 f1 不同的非空值的个数";
  • DISTINCT 的多表达式 :官方 4.2.7------DISTINCT 形式"对表达式的每个不同值(或多个表达式的不同值集合)调用一次聚合"------这就是 count(distinct (col1, col2)) 按组合去重的依据;
  • 为什么 (null, null) 也被计入(col1, col2)行构造器 (官方 4.2.13),生成的是复合值------即使所有字段都是 NULL,复合值本身仍是非 NULL 值(不同于 IS NULL 谓词的"全字段为 NULL"判定)→ 按官方 count 定义"输入值不为 NULL 的行"被计数;验证:SELECT count((NULL, NULL)); -- 返回 1
  • count(distinct col) 的代价:去重需要排序或哈希聚合(官方 4.2.7 的 DISTINCT 语义),比普通 count 多一整层处理,数据量越大越明显。

四、实战错误案例&优化方案

场景1:函数/程序中判断"是否存在"(核心场景)

表设计:orders 表 500 万行,函数要判断某用户是否有订单

❌ 错误写法(用 count(*) 数完所有匹配行)

sql 复制代码
DO $$
DECLARE cnt bigint;
BEGIN
  SELECT count(*) INTO cnt FROM orders WHERE user_id = 10086;
  IF cnt > 0 THEN
    RAISE NOTICE '存在';
  ELSE
    RAISE NOTICE '不存在';
  END IF;
END $$;
-- count(*) 要扫完 user_id=10086 的所有订单行才能返回
-- 官方 9.21:工作量与表大小(匹配行数)成正比,很慢

✅ 正确写法(LIMIT 1 + FOUND,人工检查)

sql 复制代码
DO $$
DECLARE v_id int;
BEGIN
  SELECT 1 INTO v_id FROM orders WHERE user_id = 10086 LIMIT 1;
  IF FOUND THEN        -- plpgsql FOUND:最近一条 SQL 是否产生了行
    RAISE NOTICE '存在';
  ELSE
    RAISE NOTICE '不存在';
  END IF;
END $$;
-- Limit 节点找到第一行立即终止(第 17 篇)
-- 工作量 = 扫到第一行命中为止,而非数完全部

关键结论判断"有没有"用 LIMIT 1 + IF FOUND,别用 count(*)------一个"找到就停",一个"数完全部",大表上差一个量级(官方 9.21:count 全量计数与表大小成正比)。

场景2:count 星号与 count 列:NULL 语义差异

表设计:orders 表,remark 列 30% 为 NULL,含少量整行全 NULL 的脏数据

✅ 正确认知(官方 4.2.7 / 9.21 语义)

sql 复制代码
SELECT count(*) FROM orders;          -- 所有输入行,全 NULL 行也计入(空值纳入统计)
SELECT count(remark) FROM orders;     -- 只统计 remark 非空的行,NULL 不计入
-- count(f1) = f1 非空的输入行数(count 忽略 null)
SELECT count(*) - count(remark) FROM orders;   -- 差 = remark 为 NULL 的行数

关键结论count(*) 数所有行(含全 NULL 行);count(col) 只数该列非空的行------差一列 NULL 语义就不同,统计口径错位是常见坑。

场景3:去重计数的取舍

表设计:orders 表,user_id 可重复,业务要"有多少个不同用户下过单"

✅ 正确认知(distinct 语义与成本)

sql 复制代码
SELECT count(distinct user_id) FROM orders;   -- 不同的非空 user_id 个数(官方 4.2.7)
SELECT count(user_id) FROM orders;            -- 非空 user_id 的行数(不去重)
-- count(distinct f1) = f1 不同的非空值的个数

-- 高频路径上的去重要谨慎:distinct 需要排序/哈希聚合
-- 可以先 GROUP BY 预聚合到小表,再 count
SELECT count(*) FROM (SELECT user_id FROM orders GROUP BY user_id) t;

关键结论count(distinct col) 忽略 NULL、只统计不同非空值,且带去重成本------数"多少个不同值"才用它;高频查询可先 GROUP BY 预聚合(第 22 篇:聚合一次再复用)。

场景4:多列计数:null 也要被计入(用户核心点)

表设计:orders 表,业务要按 (user_id, status) 组合统计"记录数"与"不同组合数"

✅ 正确写法(行构造器多列计数)

sql 复制代码
-- 多列计数:即使两列全为 NULL 也会被计数,(null, null) 有效
SELECT count((user_id, status)) FROM orders;

-- 多列去重计数:同样 (null, null) 有效
SELECT count(distinct (user_id, status)) FROM orders;

-- 官方依据:count(expression) 统计输入值不为 NULL 的行(9.21)
-- (col1, col2) 是行构造器(4.2.13),生成复合值:字段全 NULL 时复合值本身仍非 NULL → 计入
-- 验证:
SELECT count((NULL, NULL));   -- 返回 1,(null, null) 被计数
SELECT count(distinct (NULL, NULL));  -- 返回 1

关键结论count((col1, col2)) / count(distinct (col1, col2)) 按"行"计数,即使列全为空也会被计数((null, null) 有效) ------与单列 count(col) 忽略 NULL 正好相反,选型时别想当然。

五、绝对禁止的写法汇总

  • 函数 / 程序里用 count(*) 判断是否存在cnt > 0 / cnt = 0)------改用 LIMIT 1 + FOUND(官方 9.21:全量计数与表大小成正比);
  • count(*)count(col) 混用口径(一个含全 NULL 行、一个不含,差值是 NULL 行数);
  • 高频路径直接用 count(distinct col) 不做预聚合(去重有排序/哈希成本);
  • count((col1, col2)) 想当然认为 (null, null) 不计入(行构造器复合值:全 NULL 也计);
  • 报表 / 低频场景外,对大表反复执行全表 count 星号(高频全量计数是大表性能杀手)。

六、最终评审口诀(记住不踩坑)

存在检查LIMIT 1,星号数行慢又全;

单列忽略NULL值,多列null也计全。

七、总结

  • 存在性判断 :函数 / 程序中不要用 count(*) 判断是否有数据,很慢 (官方 9.21:count(*) 全表计数工作量与表大小成正比)------用 SELECT 1 FROM tbl WHERE xxx LIMIT 1; + IF FOUND,找到一行立即停(第 17 篇 Limit 提前终止);
  • 五种计数方式 (官方 9.21 / 4.2.7):count(*) 数所有行(空值纳入统计 );count(col) 数该列非空 行;count(distinct col)不同非空值count((col1, col2)) 多列计数、全 NULL 也计入((null, null) 有效)count(distinct (col1, col2)) 多列去重、同样 (null, null) 有效;
  • 原理 :(col1, col2) 是行构造器(官方 4.2.13)生成复合值,字段全 NULL 时复合值本身非 NULL → 按 count 定义被计入(验证:count((NULL, NULL)) 返回 1);
  • 代价提醒:distinct 带排序/哈希成本,高频路径先预聚合。

标签:PostgreSQL 数据库 性能优化


参考来源

  • PostgreSQL 官方文档 9.21 Aggregate Functions(count() 计算输入行数、count(expression) 统计输入值不为 NULL 的行数;性能 Note:count( ) 全表计数工作量与表大小成正比)------ www.postgresql.org/docs/curren...
  • PostgreSQL 官方文档 4.2.7 Aggregate Expressions(count 忽略 null、count(f1) 非空行数、count(distinct f1) 不同的非空值个数、DISTINCT 多表达式按不同值集合调用)------ www.postgresql.org/docs/curren...
  • PostgreSQL 官方文档 4.2.13 Row Constructors(行构造器生成复合值、IS NULL / IS NOT NULL 判定)------ www.postgresql.org/docs/curren...
相关推荐
旺仔不是程序员1 小时前
IN 操作规范:PostgreSQL 元素数量、EXISTS 替代与 = ANY 写法
数据库·后端·sql
这个DBA有点耶1 小时前
异构数据集成方案:跨平台同步的完整指南与工具选型(含实测)
数据库·sql·程序人生·架构·数据库架构·dba
晚安日记wanna1 小时前
一条 SMEMBERS 干瘫 Redis 节点:单线程的真正边界在哪
redis·后端·面试
码事漫谈1 小时前
32 位事务号的宿命:金仓 V9 如何用 64 位 XID 破解 PG 三十年顽疾
后端
码事漫谈1 小时前
在 Kubernetes 上管好数据库:金仓 KES-Operator 正式落地
前端·后端
蓝速科技1 小时前
会议室门牌显示模板选型与品牌视觉适配落地指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
小蒜学长1 小时前
基于SpringBoot+Vue的游戏论坛系统的设计与实现(代码+数据库+LW)
java·后端·springboot·游戏论坛系统·社区生态
SimonKing1 小时前
文档杂乱怎么查找:用 Papra 搭一个极简文档管理系统
java·后端·程序员
掘金者阿豪1 小时前
金仓、MySQL、PostgreSQL 用什么管理工具?DBeaver、Navicat、KStudio 我都试了一遍
后端