If you have enough experience with MySQL, it is very possible that you stumbled upon an unusually slow SELECT COUNT(*) FROM TABLE; query execution, at least occasionally.

Recently, I had a chance to investigate some of these cases closer, and it stunned me what huge differences there can be depending on the circumstance given the very same table. As the problem turned out to be much more complex than I expected, I decided to write a post to share some facts and observations.

So, why can a simple table row count query be excessively slow while, in other cases pretty fast? Let’s split the possibly related circumstances into two categories.

Well-known/obvious reasons

  • Table size
  • Storage engine (i.e., MyISAM returns the count immediately by design)
  • Concurrent workload
  • Memory cached vs. disk reads

Less obvious reasons

  • MySQL variant and version
  • Transactional context
  • Table fragmentation
  • Parallel reads
  • Table optimization
  • Bugs

Let’s take a closer look at some of the less obvious reasons!

MySQL ecosystem dynamics

Both MySQL and MariaDB are very active projects, and new versions have been released very regularly for many years to this day. This has good and bad sides. For the price of new cool features, we often observe various regression problems related to basic performance. 

I found it pretty interesting to see how different results are observed depending on the database variant for the same transactional contexts with the simple count query. 

MVCC challenge

Let me show two slow log examples of the same count query but executed in two different circumstances:

vs.

Both query instances were executed on the same server and the same, completely idle MariaDB instance. The only difference was that before the second instance, another session started a transaction in which 4k rows were deleted. Here is a quick live example of a similar scenario (faster than above due to a bigger BP):

The select count(*) query, due to being in the REPEATABLE-READ isolation level, did not see the above rows deleted, as the other transaction has not been committed yet. Thanks to the MariaDB verbose slow log, we can nicely determine the situation via the Old_rows_read info, and the cost on the InnoDB engine side via how many pages were read. Now, the cost of having an isolated data view in this case turned out to be surprisingly huge! The query was ~340 times slower! I’d say, much higher than I would have expected!

This problem is completely reproducible, especially when the buffer pool is too small to fit the whole table (BP 128MB vs. table size 2.3GB). I asked myself what was wrong here and how the other MySQL variants and versions behave in that matter. It turns out that there are huge differences! 

Below you will find a quick table from my tests on various versions. I distinguish between three different query contexts:

  1. Executed alone (no other active transactions)
  2. With an open transaction where some rows were deleted, the query is executed in the same session/transaction 
  3. Delete and count transactions are in separate client sessions

When there is a transactional context impact, in InnoDB status, we can see:

The difference in query execution time when it’s run within the same active transaction or as a new connection is visible by comparing the last two columns.

Database variantAloneTrx/Same sessionTrx/Separate session
MySQL 5.1.733.45 sec4.43 sec11 min 8.14 sec
MySQL 5.5.621.34 sec1.67 sec13 min 43.93 sec
MySQL 5.6.341.42 sec1.49 sec2 min 39.68 sec
MySQL 5.6.463.69 sec7.13 sec14 min 12.96 sec
MySQL 5.6.511.52 sec4.06 sec15 min 28.07 sec
MySQL 5.7.172.68 sec2.62 sec2.73 sec
MySQL 5.7.182.00 sec2.35 sec12 min 44.85 sec
MySQL 5.7.442.15 sec2.18 sec13 min 46.51 sec
MySQL 8.0.122.67 sec2.73 sec2.77 sec
MySQL 8.0.131.53 sec2.73 sec14 min 5.74 sec
MySQL 8.0.161.26 sec3.30 sec12 min 38.26 sec
MySQL 8.0.171.93 sec1.92 sec1.74 sec
MySQL 8.0.191.79 sec3.60 sec1.71 sec
MySQL 8.0.208.06 sec9.21 sec15.88 sec
MySQL 8.0.366.86 sec6.88 sec6.88 sec
MySQL 8.2.08.24 sec9.94 sec8.09 sec
MySQL 8.3.08.44 sec9.98 sec8.06 sec
MariaDB 10.0.381.43 sec1.76 sec13 min 31.66 sec
MariaDB 10.1.411.24 sec1.31 sec2 min 36.65 sec
MariaDB 10.2.141.65 sec3.40 sec45.96 sec
MariaDB 10.2.441.83 sec1.93 sec11 min 58.72 sec
MariaDB 10.3.391.947 sec11 min 47.183 sec13 min 30.429 sec
MariaDB 10.4.311.959 sec11 min 41.352 sec13 min 37.713 sec
MariaDB 10.5.231.940 sec13 min 14.588 sec12 min 49.191 sec
MariaDB 10.11.72.591 sec13 min 10.440 sec13 min 0.717 sec
MariaDB 11.3.21.697 sec11 min 48.214 sec11 min 21.800 sec

 

I am not listing Percona Server for MySQL variants here, as the results were very close to MySQL Community, at least in a couple of versions I checked.

Based on the above results, we can see roughly three types of behavior:

  • Table count is fast regardless of transactional context (undo log entries presence).
  • The query is fast when executed in the same transaction which has deleted rows, but when executed in another transaction, is very slow.
  • Query is very slow with the presence of deleted but uncommitted rows, regardless if we run in the same or other transaction.

Of course, the transactional context may be way more complicated, but testing all possible cases would take ages, so I did only the most basic ones. For example, the more and the bigger separate transactions with undo entries, the slower the select query is expected to be.

But my point is – the same test can yield completely different results depending only on the MySQL or MariaDB version!

It is very interesting to see that MySQL 5.7 was all fast until version 5.7.17, as something got broken since 5.7.18 and applied up to the last available minor version. Ironically, the release notes mention an improvement in that area:

  • InnoDB: SELECT COUNT(*) performance regressed in some cases due to a modification introduced in MySQL 5.7.2 that caused InnoDB to count rows by traversing the clustered index instead of a smaller secondary index. The modification was reverted. (Bug #23046302, Bug #80580)

In the 8.0 series, it was initially fast, then from 8.0.13 till 8.0.16 was slow in the third context, and then since version 8.0.17, it was all fast. Funny enough, regression started after this change was introduced in 8.0.13:

  • Performance of SELECT COUNT(*) FROM tbl_name queries for InnoDB tables was improved for single-threaded workloads and when no extra clauses such as WHERE or GROUP BY are used. (WL #10398)

Versions 8.0.20 and later were a bit slower in all contexts, but I’ll get to this later.

The worst results were with MariaDB; the very old versions were at least fast in the first two contexts, but all the recent ones are very slow in the presence of undo log entries. I reported this issue accordingly:

https://jira.mariadb.org/browse/MDEV-33099

Parallel InnoDB read threads in MySQL 8.0

In MySQL 8.0.14, we can find an important new functionality: for the first time, InnoDB is able to utilize multiple parallel threads for certain queries. In the initial implementation, just the table count:

https://dev.mysql.com/worklog/task/?id=11720

This feature is enabled via the default value of innodb_parallel_read_threads set to 4. 

And this default behavior seemed to work well… until version 8.0.19. There was again quite a surprising regression introduced when this “improvement” was made in version 8.0.20:

  • InnoDB: Changes to parallel read threads functionality introduced in MySQL 8.0.17 caused a degradation in SELECT COUNT(*) performance. Pages were read from disk unnecessarily. (Bug #30766089)

The problem is that in a situation when the table pages are not in memory, or simply the table is bigger than the buffer pool, using parallel threads results in way more disk reads versus when one thread is used. This means the same operation is now much more expensive! Unfortunately, this problem applies to all the later releases, including the latest innovation 8.3.0.

I reported this problem here: https://bugs.mysql.com/bug.php?id=113482

Why is there such a big negative impact in multi-threaded mode? Unfortunately, the verbose slow log adds even more confusion to the problem, as faster execution shows way more IOPS than the slow one.

With innodb_parallel_read_threads = 4:

vs. with innodb_parallel_read_threads = 1:

Let’s test various parallel threads settings:

innodb_parallel_read_threadsexecution time BP 128MBexecution time BP 4GB
13.32 sec1.74 sec
215.62 sec0.42 sec
310.59 sec0.28 sec
48.17 sec0.21 sec
56.51 sec0.17 sec
65.61 sec0.17 sec
75.19 sec0.13 sec
84.72 sec0.12 sec

 

These tests were made on MySQL 8.3.0, table size 2.3 GB, on a system with 16 CPU cores and an NVMe disk.

For clarity – in all tests, the parallel threads were fully utilizing the CPU cores, which means that with the setting of innodb_parallel_read_threads=8, the server was using ~800% of CPUs as compared to ~100% with one thread, but still needed more time to run the query (with small BP)!

New table populated with INSERTs vs. Optimized one

This is not all of our surprises! I found out that also the same table will get quite a different result depending on how the data file was created. Here is a quick test to illustrate:

-rw-r—–. 1 mysql mysql 2.3G Mar 24 06:38 sbtest1.ibd

vs.

-rw-r—–. 1 mysql mysql 2.6G Mar 24 07:09 sbtest1.ibd

Moreover, not only does the select count behave differently in both cases, but also other reads. You will find more details in my report: https://bugs.mysql.com/bug.php?id=114468

Other factors/bugs

In this blog post, I am showing only three example factors that I found particularly interesting. However, the number of variations and different factors impacting the basic select cont(*) from table query may be way bigger. Let me name a few other bug report examples:

https://bugs.mysql.com/bug.php?id=114154 – “Parallel read is slow after partition table is read in parallel”
https://bugs.mysql.com/bug.php?id=99717 – “Performance regression of parallel count”
https://bugs.mysql.com/bug.php?id=97709 – “MySQL 8 Select Count(*) is very slow”
https://bugs.mysql.com/bug.php?id=100597 – “INDEX hint does not affect count(*) execution”
https://bugs.mysql.com/bug.php?id=100966 – “select count(*) works very slow for uncommitted data”
https://bugs.mysql.com/bug.php?id=112767 – “SELECT COUNT(*) degraded performance on 8.0 compared to 5.7”

Summary

From these tests, I think we can come to the following conclusions:

  • None of the MySQL Community versions demonstrated a problem when the count query was executed within the same transaction as the delete.
  • MySQL 5.1, 5.5, and 5.6 are slow with the presence of delete, when executed in a separate transaction (however, due to their long EOL status, I did not test all the oldest minor releases).
  • MySQL 5.7 was fast in all contexts up to version 5.7.17 but is slow in the separate transaction context from 5.7.18 till the last one, 5.7.44.
  • MySQL 8.0, 8.2, and 8.3 are fast in all contexts except minor versions 8.0.13 – 8.0.16.
  • All the recent minor MariaDB versions are slow in the separate transaction context, while the impact is lower in some very old minor versions, like 10.2.14.
  • Unfortunately, since major version 10.3, all MariaDB versions are also slow when the count query is executed within the same transaction, making the MVCC impact very high; for example, MariaDB 10.3+ is ~37000% slower than 10.2 and earlier in that context!
  • MySQL 8.0.20+ introduced another problem related to parallel reads—when the data does not fit in memory, the count query is more expensive in terms of disk reads; using a single thread allows one to work around that issue.

The overall impact of transactional context depends on the number of concurrent write transactions, rows changed, and other factors, but these very simple tests I presented hopefully illustrate how potentially important it can be in investigating slow queries.

Percona Distribution for MySQL is a complete, stable, scalable, and secure, open source MySQL solution, delivering enterprise-grade database environments for your most critical business applications. Deploy anywhere and implement easily with one-to-one compatibility with MySQL Community Edition.

 

Try Percona Distribution for MySQL today!

Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jean-François Gagné

Nice finding Przemysław. About MVCC, I think the variations are caused by a combination of the plan chosen by the optimizer (you imply this by mentioning Bug #80580 and WL #10398)) and the way InnoDB implements MVCC on secondary indexes (you do not mention this at all). I am giving more info about InnoDB MVCC and secondary indexes below.

My recollection is vague on the subject, not much time to find references, but from what I understood (probably from reading content fromn Mark Callaghan or Marko Mäkelä), secondary indexes do not contain “row level MVCC information”, only “page level MVCC information”. So when this page-level information tells InnoDB that something is “outdated” in the secondary index, a clustered-index lookup is needed to get the “good” version of the row. It looks like the slow-log entry showing a slower query matches this as the number of “Pages_accessed” matches the number of Rows_examined (11.510.893 and 10.000.000). So with DELETE, if the optimizer uses the clustered-index, things will be fast (might scan more data), but if using a secondary index, the best case is better (scanning less data) and the worse case is much worse (needing a PK lookup for each row).

I actually found a reference to this in Marko’s talk at the MariaDB 2023 (Un)Conference: How InnoDB Undo Logs and the Purge of History Work (goto 5:38 in YouTube video).