41

我在 Innodb 中有一个超过 1 亿行的表。

我必须知道外键 = 1 的行是否超过 5000 行。我不需要确切的数字。

我做了一些测试:

SELECT COUNT(*) FROM table WHERE fk = 1=> 16 秒
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000=> 16 秒
SELECT primary FROM table WHERE fk = 1=> 0.6 秒

我将拥有更大的网络和治疗时间,但可能会超载 15.4 秒!

你有更好的主意吗?

谢谢

编辑:[添加了 OP 的相关评论]

我试过 SELECT SQL_NO_CACHE COUNT(fk) FROM table WHERE fk = 1 但花了 25 秒

Mysql 使用 Mysql Tuner 为 Innodb 进行了调整。

CREATE TABLE table ( pk bigint(20) NOT NULL AUTO_INCREMENT,
fk tinyint(3) unsigned DEFAULT '0', 
PRIMARY KEY (pk), KEY idx_fk (fk) USING BTREE ) 
ENGINE=InnoDB AUTO_INCREMENT=100380914 DEFAULT CHARSET=latin1

数据库资料:

'have_innodb', 'YES' 'ignore_builtin_innodb', 'OFF' 'innodb_adaptive_hash_index', 'ON'    
'innodb_additional_mem_pool_size', '20971520' 'innodb_autoextend_increment', '8' 
'innodb_autoinc_lock_mode', '1' 'innodb_buffer_pool_size', '25769803776' 
'innodb_checksums', 'ON' 'innodb_commit_concurrency', '0',
'innodb_concurrency_tickets', '500' 'innodb_data_file_path',
'ibdata1:10M:autoextend' 'innodb_data_home_dir', '', 'innodb_doublewrite', 'ON'     
'innodb_fast_shutdown', '1' 'innodb_file_io_threads', '4' 
'innodb_file_per_table', 'OFF', 'innodb_flush_log_at_trx_commit', '1' 
'innodb_flush_method', '' 'innodb_force_recovery', '0' 'innodb_lock_wait_timeout', '50' 
'innodb_locks_unsafe_for_binlog', 'OFF' 'innodb_log_buffer_size', '8388608' 
'innodb_log_file_size', '26214400' 'innodb_log_files_in_group', '2' 
'innodb_log_group_home_dir', './' 'innodb_max_dirty_pages_pct', '90'     
'innodb_max_purge_lag', '0' 'innodb_mirrored_log_groups', '1' 'innodb_open_files', 
'300' 'innodb_rollback_on_timeout', 'OFF' 'innodb_stats_on_metadata', 'ON' 
'innodb_support_xa', 'ON' 'innodb_sync_spin_loops', '20' 'innodb_table_locks', 'ON' 
'innodb_thread_concurrency', '8' 'innodb_thread_sleep_delay', '10000'      
'innodb_use_legacy_cardinality_algorithm', 'ON'

15 年更新: 到目前为止,我使用相同的方法,每天有 6 亿行和 640 000 行新行。它仍然工作正常。

4

7 回答 7

27

您似乎对实际计数不感兴趣,所以试试这个:

SELECT 1 FROM table WHERE fk = 1 LIMIT 5000, 1

如果返回一行,则您有 5000 条或更多记录。我认为该fk列已编入索引。

于 2012-06-11T08:44:06.513 回答
22

计数器表或其他缓存机制是解决方案:

InnoDB 不保留表中的内部行数,因为并发事务可能同时“看到”不同数量的行。为了处理 SELECT COUNT(*) FROM t 语句,InnoDB 扫描表的索引,如果索引不完全在缓冲池中,这需要一些时间。如果您的表不经常更改,使用 MySQL 查询缓存是一个很好的解决方案。要获得快速计数,您必须使用您自己创建的计数器表,并让您的应用程序根据它所做的插入和删除来更新它。如果近似行数足够,则可以使用 SHOW TABLE STATUS。请参阅第 14.3.14.1 节,“InnoDB 性能调优技巧”</a>。

于 2012-06-11T08:47:47.580 回答
8

我必须添加另一个答案——到目前为止,我对评论和答案有很多更正/补充。

对于 MyISAM,SELECT COUNT(*)没有WHERE估计是死机——非常快。所有其他情况(包括问题中的 InnoDB)必须通过数据的 BTree 或索引的 BTree 来获得答案。所以我们需要看看要计算多少。

InnoDB 缓存数据和索引块(每个 16KB)。但是当表的数据或索引 BTree 大于innodb_buffer_pool_size时,你肯定会撞到磁盘。访问磁盘几乎总是任何 SQL 中最慢的部分。

当涉及到查询缓存时,通常会产生大约 1 毫秒的查询时间;这似乎不是引用的任何时间的问题。所以我就不多说了。

但是......连续两次运行相同的查询通常会出现:

  • 首次运行:10 秒
  • 第二次运行:1秒

这是第一次运行必须从磁盘中获取大部分块的症状,而第二次在 RAM(buffer_pool)中找到了所有块。我怀疑列出的一些时间是虚假的,因为没有意识到这个缓存问题。(这可以解释16 秒与 0.6 秒。)

我将强调“磁盘命中”或“需要触摸的块”作为 SQL 更快的真正指标。

COUNT(x)统计前检查xIS NOT NULL这增加了少量的处理,但不会改变磁盘命中的数量。

提供的表有一个 PK 和第二列。我想知道那是不是真正的桌子?它有所作为——

  • 如果优化器决定读取数据——也就是说,按PRIMARY KEY顺序扫描——它将读取数据 BTree,它通常(但在这个蹩脚的例子中)比二级索引 BTree 宽得多。
  • 如果优化器决定读取二级索引(但不需要进行排序),则要访问的块将更少。因此,速度更快。

对原始查询的评论:

SELECT COUNT(*) FROM table WHERE fk = 1 => 16 seconds
    -- INDEX(fk) is optimal, but see below
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000 => 16 seconds
    -- the LIMIT does nothing, since there is only one row in the result
SELECT primary FROM table WHERE fk = 1 => 0.6 seconds
    -- Again INDEX(fk), but see below

WHERE fk = 1恳求INDEX(fk, ...),最好只是INDEX(fk)。请注意,在 InnoDB 中,每个二级索引都包含 pk 的副本。也就是说,INDEX(fk)是有效的INDEX(fk, primary)。因此,第三个查询可以将其用作“覆盖”,而不需要接触数据。

如果表真的只有两列,那么二级索引 BTree可能会比数据 BTree 胖。但在现实表中,二级索引会更小。因此,索引扫描将比表扫描更快(触摸的块更少)。

第三个查询也提供了一个大的结果集;这可能会导致查询花费很长时间——它不会包含在引用的“时间”中;是网络时间,不是查询时间。

innodb_buffer_pool_size = 25,769,803,776 我猜该表及其二级索引(来自 FK)每个大约 3-4GB。所以,任何时间都可能首先要加载很多东西。然后第二次运行将被完全缓存。(当然,我不知道有多少行fk=1;大概少于所有行?)

但是......在 600M 行时,表及其索引都接近25GB 的 buffer_pool。因此,它成为 I/O 绑定的那一天可能很快就会到来——这会让您希望回到 16(或 25)秒;但你不能。然后我们可以讨论做COUNT.

SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000,1——我们来分析一下。它会扫描索引,但会在 5000 行后停止。您需要的只是“超过 5K”,这是获得它的最佳方式。无论表中的总行数如何,它将始终保持快速(仅触及十几个块)。(它仍然受制于系统的 buffer_pool_size 和缓存特性。但是,即使使用冷缓存,十几个块也需要不到一秒的时间。)

MariaDBLIMIT ROWS_EXAMINED可能值得研究。没有那个,你可以做

SELECT COUNT(*) AS count_if_less_than_5K
    FROM ( SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000 );

可能比将行传递给客户端更快;它必须在 tmp 表内部收集行,但只提供COUNT.

附注:每天插入 640K 行——这接近了 MySQL 中单行的限制,INSERTs而您当前在 HDD(不是 SDD)上的设置。如果您需要讨论潜在的灾难,请打开另一个问题。

底线:

  • 一定要避免查询缓存。(通过使用SQL_NO_CACHE或关闭 QC)
  • 运行任何计时查询两次;使用第二次。
  • 了解所涉及的 BTree(s) 的结构和大小。
  • COUNT(x)除非您需要空值检查,否则不要使用。
  • 不要使用 PHP 的mysql_*接口;切换到mysqli_*PDO
于 2017-05-30T22:36:43.950 回答
1

如果您使用的是 PHP,您可以mysql_num_rows对从中获得的结果进行操作SELECT primary FROM table WHERE fk = 1 => 0.6 seconds,我认为这将是有效的。

但取决于您使用的服务器端语言

于 2012-06-11T08:08:03.127 回答
0

如果您不想知道行数,而只想针对某个值测试 COUNT,则可以使用下面的标准脚本:

SELECT 'X'
FROM mytable
WHERE myfield='A'
HAVING COUNT(*) >5

这将返回一行或根本不返回一行,具体取决于是否满足条件。

此脚本符合 ANSI,无需评估 COUNT(*) 的完整值即可完全运行。如果 MySQL 实现了优化以在满足某些条件后停止评估行(我真的希望这样做),那么您将获得性能改进。不幸的是,我自己无法测试这种行为,因为我没有可用的大型 MySQL 数据库。如果你做这个测试,请在这里分享结果:)

于 2012-06-11T09:30:36.377 回答
0

最后最快的是使用 C# 查询前 X 行并计算行数。

我的应用程序正在批量处理数据。两个批次之间的时间量取决于需要处理的行数

SELECT pk FROM table WHERE fk = 1 LIMIT X

我在 0.9 秒内得到了结果。

谢谢大家的想法!

于 2012-06-11T22:00:51.693 回答
0

这是一个老问题,但我遇到了同样的问题,也许这会对某人有所帮助:有 400 万条记录,COUNT 查询需要超过 20 秒。因此,就我而言,在我添加了一个简单的主键过滤后,它变得更快并且只需要 4 秒。所以最后的查询是:

SELECT COUNT(*) FROM Table
WHERE PK > 0;

就我而言,PK是INT。

于 2021-03-24T11:30:29.023 回答