Select count(*) 的优化

首先说明:
select count(*) 和 select count(1)的效率相差无几。

这里开始引用自“德哥@Digoal”的博客,原文链接:http://blog.163.com/digoal@126/blog/static/163877040201331252945440/

–引用部分开始–

引用块内容
PostgreSQL 的count确实是一大软肋, 特别是全表的count.
在9.2以前全表的count只能通过扫描全表来得到, 即使有pk也必须扫描全表.
9.2版本增加了index only scan的功能, count(*)可以通过仅仅扫描pk就可以得到.
但是如果是一个比较大的表, pk也是很大的, 扫描pk也是个不小的开销.
那么有没有办法来优化count全表的操作呢, 如果你的场景真的有必要频繁的count全表, 那么可以尝试一下使用以下方法来优化你的场景.
其实非常简单, 就是给表建立几个触发器, 每次插入,删除,truncate表时触发, 将表的记录数更新到一个记录表中.
但是要知道, 这样会带来一个问题, 并发的插入和删除操作, 如果仅仅使用1条记录来存储表的count(*)值的话, 会有严重的锁冲突的问题.
例如两个session ,同时插入1条记录, 在触发触发器时, 由于都要更新count表的同一条记录, 那么会发生行锁等待.
因此, 可以使用多条记录来缓解行锁冲突的问题, 如下 :
创建测试表, a, 假设要经常count(*) from a.

create table a(id serial4 primary key, info text, crt_time timestamp(0) default now());

创建记录a表记录数的表

create table cnt_a(id int primary key, cnt int);

为了缓解行锁冲突, 这里使用了1001条记录来存储count(*) from a的值.

insert into cnt_a select generate_series(0,1000),0;

在计算count(*) a时, 使用sum(cnt) from cnt_a就可以了. 因此只需要扫描1001行.
后面会看到当a表的记录数越多, 性能提升约明显.
创建插入/删除/truncate触发器

CREATE OR REPLACE FUNCTION public.tg_insert_a()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
declare
  m_id int;
  rm numeric;
begin
  select max(id),random() into m_id,rm from cnt_a;
  update cnt_a set cnt=cnt+1 where id=(rm*m_id)::int;
  return null;
end;
$function$;

CREATE OR REPLACE FUNCTION public.tg_delete_a()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
declare
  m_id int;
  rm numeric;
begin
  select max(id),random() into m_id,rm from cnt_a;
  update cnt_a set cnt=cnt-1 where id=(rm*m_id)::int;
  return null;
end;
$function$;

CREATE OR REPLACE FUNCTION public.tg_truncate_a()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
declare
begin
  update cnt_a set cnt=0 where not cnt=0;
  return null;
end;
$function$;

create trigger tg1 after insert on a for each row execute procedure tg_insert_a();
create trigger tg2 after delete on a for each row execute procedure tg_delete_a();
create trigger tg3 after truncate on a for each statement execute procedure tg_truncate_a();

插入以下记录后,测试完后通过count(*) 和sum(cnt)比对数据是否一致

postgres=# select count(*) from a;
  count  
---------
 1755964
(1 row)
Time: 285.491 ms
postgres=# select sum(cnt) from cnt_a ;
   sum   
---------
 1755964
(1 row)
Time: 0.689 ms

当记录数到达千万级别后, 性能以及提升几千倍了.

–引用部分开始–

“德哥@Digoal”又做了一些后续的优化,详情可点击链接自行移步查看:http://blog.163.com/digoal@126/blog/static/163877040201331252945440/

我在实际应用中也发现了其中存在死锁现象,认为是触发器函数中select的“Shared Lock”和update的“Exclusive Lock”竞争产生的。详见我的博客:《Transaction中的SQL死锁http://blog.youkuaiyun.com/fm0517/article/details/52242285

于是我也做了一些改进,即:假设表cnt_a中的记录数是固定的,所以在insert触发器和delete触发器中,不再从cnt_a中去取max(id),从而避免了死锁。
改进的触发器函数:

CREATE OR REPLACE FUNCTION tg_insert_a()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
declare
  rm numeric;
begin
  select random() into rm;
  update trigger_cnt set cnt=cnt+1 where id=(rm*1000)::int;
  return null;
end;
$function$;

CREATE OR REPLACE FUNCTION tg_delete_a()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
declare
  rm numeric;
begin
  select random() into rm;
  update trigger_cnt set cnt=cnt-1 where id=(rm*1000)::int;
  return null;
end;
$function$;

最后值得注意的是:该方法虽然在大数据量的情况下能够大幅提高select count(*)的效率,但是增加了insert和delete时数据库的负担。所以使用时要谨慎综合考虑实际情况再做决定。

<think>好的,我现在需要帮助用户优化一个包含千万级数据的MySQL表中执行SELECT COUNT(*)查询的性能。首先,我得理解用户的问题场景。用户可能有一个非常大的表,执行COUNT(*)的时候速度很慢,想要优化这个查询的性能。 首先,我应该回忆一下MySQL中COUNT(*)的工作原理。COUNT(*)在没有WHERE条件的情况下会统计表中的所有行数。对于不同的存储引擎,比如InnoDB和MyISAM,处理方式可能不同。MyISAM会缓存行数,所以COUNT(*)很快,但InnoDB因为MVCC机制,需要实时计算,导致速度变慢,尤其是大表的时候。用户的表是InnoDB,所以这里可能需要考虑如何绕过InnoDB的实时行数计算。 接下来,考虑用户可能的解决方案。首先,检查是否真的需要精确的行数。如果用户只是需要估计值,可以使用EXPLAIN中的rows字段或者查询information_schema.tables中的TABLE_ROWS。不过这可能不够准确,特别是当表经常更新的时候。 如果用户确实需要精确的计数,那么可能需要考虑其他方法。比如维护一个计数器表,每次插入或删除时更新这个计数器。这种方法在写入频繁的情况下可能会有性能问题,但读取的时候非常快。不过,用户的应用场景是否允许这样的设计呢?需要根据具体情况判断。 另外,可以考虑使用覆盖索引。如果COUNT(*)带有WHERE条件,那么合适的索引可以加快查询速度。例如,如果WHERE条件中的列有索引,MySQL可以只扫描索引而不需要访问数据行,从而提升速度。所以,用户可能需要检查他们的查询是否有条件,并创建合适的索引。 还有一种方法是使用汇总表。定期运行一个任务,将行数统计到另一张表中,这样查询的时候直接从汇总表获取数据。不过这样会有一定的延迟,需要根据业务需求来决定是否可以接受。 用户提到的是SELECT COUNT(*)而没有WHERE条件,所以覆盖索引可能不适用,因为COUNT(*)在没有条件时会扫描主键索引。InnoDB的主键索引是聚集索引,所以扫描起来可能还是比较慢,尤其是当表非常大的时候。 另外,可以考虑使用近似值。例如,使用SHOW TABLE STATUS命令,里面的Rows字段虽然不精确,但对于某些场景可能足够。或者使用像Percona的pt-query-digest工具来估算。 还有一个点,MySQL 8.0引入了并行查询的功能,可能对COUNT(*)有帮助,但需要确认用户的MySQL版本是否支持,并且是否启用了相关配置。 另外,是否可以考虑分库分表?如果表实在太大,单表查询性能无法满足,可能需要水平拆分,但这对应用架构改动较大,需要权衡利弊。 最后,物理上的优化,比如使用更快的存储设备,或者优化服务器配置,比如增加内存,让更多的数据可以被缓存,从而减少磁盘I/O。不过这可能属于硬件层面的优化,用户可能希望从查询或数据库设计层面解决。 总结一下,可能的优化方法包括: 1. 使用近似值代替精确值。 2. 维护计数器表或触发器。 3. 利用汇总表定期统计。 4. 检查是否有合适的索引(如果带WHERE条件)。 5. 升级MySQL版本或调整配置参数。 6. 考虑分库分表或使用其他存储引擎。 需要进一步了解用户的具体情况,比如是否有WHERE条件,表的结构,存储引擎,MySQL版本等,才能给出更具体的建议。但根据现有信息,可以提出这些常见的优化策略。</think>### 优化千万级数据表`SELECT COUNT(*)`性能的方法 #### 1. **避免直接使用`SELECT COUNT(*)`** - **InnoDB存储引擎特性**:InnoDB的`COUNT(*)`需要实时扫描表(或最小的二级索引)来统计行数,因涉及MVCC机制,无法直接缓存结果[^1]。对于千万级数据表,此操作可能耗时数秒甚至更久。 - **替代方案**: - **近似值**:通过`SHOW TABLE STATUS`或查询`information_schema.tables`获取估算值: ```sql SELECT TABLE_ROWS FROM information_schema.tables WHERE table_name = 'your_table' AND table_schema = 'your_database'; ``` - **专用计数器表**:创建一个表(如`row_counter`),通过触发器或业务逻辑在数据增删时更新计数器,直接查询该表获取精确值[^2]。 #### 2. **利用覆盖索引优化带条件的统计** 如果需统计满足特定条件的行数(如`SELECT COUNT(*) FROM table WHERE status = 'active'`): - **创建覆盖索引**:对`status`字段建立索引,使得查询仅扫描索引而非数据行: ```sql ALTER TABLE your_table ADD INDEX (status); ``` - **性能提升**:索引大小通常远小于数据行,可显著减少I/O操作。 #### 3. **使用汇总表定期统计** - **实现方式**:通过定时任务(如每日)将统计结果写入汇总表: ```sql CREATE TABLE stats_table ( id INT PRIMARY KEY, row_count BIGINT, last_updated TIMESTAMP ); -- 定时更新 REPLACE INTO stats_table (id, row_count, last_updated) VALUES (1, (SELECT COUNT(*) FROM your_table), NOW()); ``` - **适用场景**:允许一定延迟的统计需求(如报表系统)。 #### 4. **分库分表与分布式统计** - **水平拆分**:若单表数据量持续增长,可将表按规则(如时间、哈希)拆分为多个子表,分别统计后汇总结果。 - **工具支持**:通过中间件(如Vitess、ShardingSphere)或应用层聚合实现。 #### 5. **调整MySQL配置** - **增大缓冲池**:确保`innodb_buffer_pool_size`足够大(通常设为物理内存的70%-80%),使索引和数据常驻内存。 - **启用并行查询**(MySQL 8.0+):通过`innodb_parallel_read_threads`参数提升扫描速度。 #### 6. **其他注意事项** - **避免频繁统计**:通过缓存(如Redis)存储结果,定期刷新。 - **监控表碎片**:定期执行`OPTIMIZE TABLE`或`ALTER TABLE ... ENGINE=InnoDB`减少碎片对扫描性能的影响。 --- ### 示例代码:计数器表实现 ```sql -- 创建计数器表 CREATE TABLE row_counter ( table_name VARCHAR(64) PRIMARY KEY, count BIGINT NOT NULL DEFAULT 0 ); -- 插入初始化数据 INSERT INTO row_counter (table_name, count) VALUES ('your_table', (SELECT COUNT(*) FROM your_table)); -- 通过触发器更新(插入/删除时) DELIMITER // CREATE TRIGGER after_insert AFTER INSERT ON your_table FOR EACH ROW BEGIN UPDATE row_counter SET count = count + 1 WHERE table_name = 'your_table'; END; // CREATE TRIGGER after_delete AFTER DELETE ON your_table FOR EACH ROW BEGIN UPDATE row_counter SET count = count - 1 WHERE table_name = 'your_table'; END; // DELIMITER ; -- 查询行数 SELECT count FROM row_counter WHERE table_name = 'your_table'; ``` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皓月如我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值