【问题标题】:Mysql count performance on very big tablesMysql 在非常大的表上计算性能
【发布时间】:2012-06-14 03:14: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 的相关 cmets]

我试过 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 亿行和 64 万行新行。它仍然可以正常工作。

【问题讨论】:

  • 如果您在COUNT() 中选择一列,计数会更快,例如:SELECT COUNT(fk) FROM table WHERE fk = 1
  • @ClydeFrog:真的吗?根据the manual,如果SELECT 从一个表中检索,没有检索到其他列,并且没有WHERE 子句,则COUNT(*) 被优化为非常快速地返回。实际上,您链接到的博客表明 COUNT(*)COUNT(column) 快。
  • SELECT COUNT(*) FROM table WHERE fk = 1 需要 16 秒?你有fk 的索引吗?
  • 您是否进行过任何 InnoDB 配置优化,或者它是开箱即用的?
  • 请发布SHOW CREATE TABLE table;SHOW VARIABLES LIKE '%innodb%'; 的输出,以便我们直接看到必要的信息。

标签: mysql sql count query-optimization database-performance


【解决方案1】:

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

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

在我的情况下,PK 是 INT。

【讨论】:

    【解决方案2】:

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

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

    SELECT pk FROM table WHERE fk = 1 LIMIT X
    

    我在 0.9 秒内得到了结果。

    感谢大家的想法!

    【讨论】:

    • 我不明白你是如何计算行数的。介意添加该代码吗?
    • 我的应用程序正在批量处理数据。两个批次之间的时间量取决于需要处理的行数
    • 这种方法的潜在问题是它必须将所有X pks _transfer 到客户端。在某些情况下,网络时间很重要。
    • 例如:如果限制是 5000 万。在您的情况下,5000 万个 pk id 将被发送到 Code。我们需要将它存储在变量中,这将消耗 ram 内存。那么如果这个过程同时发生在多个线程上。你可能会内存不足。有更新的新解决方案吗?
    【解决方案3】:

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

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

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

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

    但是...same 查询两次连续运行 经常会出现:

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

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

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

    COUNT(x) 在计数之前检查x 是否为IS NOT NULL。这会增加少量处理,但不会改变磁盘命中数。

    提供的表有一个 PK 和第二列。我想知道那是不是 real 表?它有所作为--

    • 如果优化器决定读取数据——即按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。因此,任何时间都可能首先必须加载很多东西。然后 second 运行将被完全缓存。 (当然,我不知道fk=1有多少行;大概比所有行都少?)

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

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

    MariaDB 的 LIMIT 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 的结构和大小。
    • 除非您需要空检查,否则不要使用COUNT(x)
    • 不要使用PHP的mysql_*接口;切换到mysqli_*PDO

    【讨论】:

      【解决方案4】:

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

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

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

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

      【讨论】:

      • 它比正常计数慢...感谢您的想法!
      • 感谢您发布结果,@si2w!然而,令人失望的是 MySQL 引擎没有实现这个(简单的?)优化。
      【解决方案5】:

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

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

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

      【讨论】:

      • 这很有趣。您是否测试过这样的解决方案 - 它是否表现良好?
      • @ypercube:我检查了 3M 行的虚拟数据,fk 上没有索引,并且始终在
      • 查询的工作原理如下:从包含fk(或数据,如果没有索引)的索引中读取 5001 个“行”。如果这些行恰好在 RAM 中,这是一个相当快的 CPU 任务。如果这些行在磁盘上,则可能需要更长的时间;但是,它们将被带入缓存,为您的下一个查询使用它们做好准备。在后一种情况下,支票的费用是“免费的”。
      【解决方案6】:

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

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

      【讨论】:

      • 我有一个 where 条件 => 显示表状态将无济于事。我每天有 400 000 条新行...我很幸运!
      • @si2w 我并不是要建议您使用SHOW TABLE STATUS。我讲述了计数表和缓存。
      【解决方案7】:

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

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

      【讨论】:

      • C# 带有最新的官方驱动。我认为驱动程序会给数据一个光标。所以我可以有行数,而不必检索整个数据集。
      • @si2w 有两种方法可以从服务器获取数据:mysql_store_result() 将整个结果集发送到客户端,您可以计算它,mysql_use_result() 发送数据需要,但必须在发出其他命令之前获取所有数据。
      • 无法确认此答案。在我的情况下,COUNT() 持续 1.6 秒,而通常的 SELECT mysql_num_rows 不会检索其 1.8 秒的数据。
      • 这是个糟糕的主意,如果你有一个大表,比如说 GBs 的数据,标准的 PHP 设置会立即耗尽内存!
      猜你喜欢
      • 2021-09-12
      • 1970-01-01
      • 1970-01-01
      • 2016-04-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-24
      • 2021-01-12
      相关资源
      最近更新 更多