PostgreSQL COUNT
内容:用法 -> 行为差异 -> 性能 -> 最佳实践
在 PostgreSQL / MySQL(以及 SQL 标准)中,常见 4 种:
| 写法 | 是否统计 NULL | 是否统计行 | 是否等价 |
|---|---|---|---|
| COUNT(*) | ✅ 包含 | ✅ | ⭐ 推荐 |
| COUNT(1) | ✅ 包含 | ✅ | ≈ |
| COUNT(col) | ❌ 排除 NULL | ⚠️ | ❌ |
| COUNT(DISTINCT col) | ❌ 排除 NULL | ❌ | ❌ |
SELECT COUNT(*) FROM orders;
行为
- 统计 行数
- 包含 NULL
- PostgreSQL 内部优化最充分
PostgreSQL 内部实现
- 不取任何列
- 不关心列值
- 只统计 tuple(行)
📌 这是标准、最快、最安全的写法
SELECT COUNT(1) FROM orders;
行为
- 统计行数
- 1 永远非 NULL
和 COUNT(*) 的区别?
| 项目 | COUNT(*) | COUNT(1) |
|---|---|---|
| SQL 标准 | ✅ | ✅ |
| 语义清晰 | ✅ | ❌ |
| 优化程度 | ✅ | ✅ |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
📌 在 PostgreSQL 中性能几乎相同,但没有任何优势
SELECT COUNT(email) FROM users;
行为
- ❌ 忽略 NULL
- 只统计 email IS NOT NULL
等价写法
SELECT COUNT(*) FROM users WHERE email IS NOT NULL;
使用场景
- 统计“已填写某字段”的数量
- 数据完整性分析
⚠️ 千万不要用它代替 COUNT(*)
SELECT COUNT(DISTINCT user_id) FROM orders;
行为
- 去重
- 忽略 NULL
- 性能最差的一种
常见误区
COUNT(DISTINCT a, b) -- PostgreSQL 不支持
正确写法:
COUNT(DISTINCT (a, b))
PostgreSQL 中的真实情况
| 写法 | 性能 | 说明 |
|---|---|---|
| COUNT(*) | ⭐⭐⭐⭐⭐ | 最优 |
| COUNT(1) | ⭐⭐⭐⭐ | 无优势 |
| COUNT(col) | ⭐⭐⭐ | 需判断 NULL |
| COUNT(DISTINCT col) | ⭐ | 排序 / Hash |
SELECT COUNT(*)
FROM orders
WHERE created_at >= CURRENT_DATE;
📌 索引 + COUNT(*) 非常高效
❌ 错误写法(重复行)
SELECT COUNT(*)
FROM orders o
JOIN order_items i ON o.id = i.order_id;
一条订单多条 item,会被放大
✅ 正确写法(统计主表)
SELECT COUNT(DISTINCT o.id)
FROM orders o
JOIN order_items i ON o.id = i.order_id;
或者:
SELECT COUNT(*)
FROM orders o
WHERE EXISTS (
SELECT 1 FROM order_items i WHERE i.order_id = o.id
);
SELECT COUNT(*) FROM logs; -- 上亿行,慢
替代方案
SELECT reltuples::bigint
FROM pg_class
WHERE relname = 'logs';
📌 估算值,毫秒级
SELECT COUNT(*) FROM logs
WHERE time >= now() - interval '1 day';
👉 只扫描相关分区
❌ 判断是否存在(错误)
SELECT COUNT(*) > 0 FROM users WHERE email = 'a@b.com';
✅ 正确写法
SELECT EXISTS (
SELECT 1 FROM users WHERE email = 'a@b.com'
);
🚀 EXISTS 会提前返回
from sqlalchemy import select, func
stmt = select(func.count()).select_from(User)
count = session.scalar(stmt)
📌 永远用 func.count() 而不是 count(id)
- COUNT(*) 统计行数,是 SQL 标准中最优、最安全的写法
- COUNT(col) 只统计非 NULL
- COUNT(DISTINCT col) 用于去重但性能最差
- 判断是否存在应使用 EXISTS 而不是 COUNT(*)
- ✅ 统计行数 -> COUNT(*)
- ✅ 统计非空字段 -> COUNT(col)
- ✅ 去重统计 -> COUNT(DISTINCT col)
- ❌ 不要用 COUNT 判断存在性
- ❌ 不要滥用 COUNT 在超大表
在 PostgreSQL 中统计表的行数,确实有多种方式,每种方式适用场景不同,尤其是数据量很大时,效率差异非常大。下面我整理成详细说明:
| 方法 | 示例 SQL | 是否精确 | 适用场景 | 优缺点 |
|---|---|---|---|---|
| COUNT(*) | SELECT COUNT(*) FROM table_name; | ✅ 精确 | 小表、偶尔统计 | 简单直观,但全表扫描,数据量大时慢 |
| COUNT(1) / COUNT(pk) | SELECT COUNT(id) FROM table_name; | ✅ 精确 | 小表、偶尔统计 | 效果和 COUNT(*) 类似,性能几乎一样 |
| pg_class.reltuples | SELECT reltuples::BIGINT AS approx_count FROM pg_class WHERE relname=‘table_name’; | ❌ 估算 | 大表、快速 | 很快,不访问表,只读取统计信息;不精确,可能与真实值差距较大 |
| ANALYZE + pg_stat_all_tables | ANALYZE table_name; SELECT n_live_tup FROM pg_stat_all_tables WHERE relname=‘table_name’; | ❌ 估算 | 大表、快速 | 需要先执行 ANALYZE,返回的 n_live_tup 基于统计信息;比 pg_class.reltuples 更接近真实值 |
| 自建物化计数表 | 通过触发器维护:INSERT/DELETE 时更新计数表 | ✅ 实时 | 高频统计 | 数据量极大或高并发场景最稳妥,但需要维护额外表 |
-
大表(千万条以上):
- 不推荐直接 COUNT(*),会全表扫描
- 推荐使用统计信息查询:
-- 方法 1:pg_stat_all_tables ANALYZE table_name; -- 可放在 cron 执行 SELECT n_live_tup FROM pg_stat_all_tables WHERE relname='table_name';-
快速,消耗低
-
误差通常在 1%-5% 范围内
-
如果需要更精确,可定时使用 COUNT(*) 在夜间低峰执行
-
小表(几万条以下):
- 直接 SELECT COUNT(*) FROM table_name; 就够
-
非常高并发系统:
- 建议 物化计数表 + 触发器 或 事件队列 维护计数
- 查询直接读取计数表,毫秒级返回
- 统计精确行数 -> COUNT(*)(慢)
- 快速估算行数 -> pg_stat_all_tables.n_live_tup 或 pg_class.reltuples
- 每天定时统计大表行数 -> 使用 ANALYZE + n_live_tup,存到监控表
- 实时统计 + 高并发 -> 建计数表 + 触发器