3

We've been running into a very odd problem on our MySQL RDS instances with SELECT COUNT(*) commands and after three times of it happening and no luck finding a solution, I figure I'd ask here too.

The issue happens when (but not always) we do a SELECT COUNT(*) FROM on relatively large (1M+ rows) tables. The query, for some reason, spikes the server's CPU to 100% usage and takes way longer than it should.

enter image description here

We try to kill it via the KILL {threadId} command and the query will just switch on the process list to status "killed" but nothing will happen.

enter image description here

I understand that running KILL on the thread only sets a flag that the query should be terminated - there's no immediate way to terminate it - but this is literally a SELECT COUNT(*) operation on a table to see how many rows are there. No WHERE clause, no JOINs, no need for the optimizer to create a virtual table and nothing to undo.

This has happened in production but also in our read-only replica mirror RDS, so it's not even a deadlock with other transactions [save for the replica process itself perhaps?] -- we couldn't find any notable deadlocks in the INNODB STATUS log either.

We can't figure out why

  1. such a simple command just up and murders the CPU
  2. why the thread just won't die.

We took the liberty last time to let the command keep running overnight to see if it was "just a matter of time and it will get killed when it's done processing whatever it is doing" but after 9 hours we had to reboot the server.

All for times this happened - thrice in Replica and unfortunately once in production - we had no choice but to reboot.

I can't tell if this is a MySQL bug [we're running 8.0.19] or what could be happening here... Any help on where to look or if this is a known issue would be greatly appreciated.

[[EDIT]] Ran the same command today to test for deadlocks. No deadlocks show up in the SHOW ENGINE INNODB STATUS results, but we noticed that the CPU only spikes once the KILL command is issued. The query itself only showed a moderate climb in Read IOPS but CPU usage was minimal - in fact it was actually falling when the SELECT was run, and only climbed once the KILL was issued.

enter image description here

[UPDATE]

@zedfoxus the table does have a primary key autoincrement column. Here's the EXPLAIN of the original query:

enter image description here

And here's the explain of a select count on the id field:

enter image description here

[UPDATE 2] as recommended in comments by @zedfoxus, I tried running

 select count(*) 
 from push_notification_async_queue 
 use index (primary)

The query ran successfully in 6 minutes. As a test I decided to run the query and kill it preemptively, which caused the bad result to repeat.

Interesting note: the select with use index(primary) ran in 6 minutes but the CPU usage only peaked at 70% and never really settled too high.

The killed query, however, immediately climbed to 100% and stayed there for 12+ minutes, so it's not that it was just ignoring the kill flag and running normally.... enter image description here

[UPDATE 3] As requested by @Wilsonhauck, here's the SHOW CREATE TABLE:

CREATE TABLE `push_notification_async_queue` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `customer_id` varchar(50) DEFAULT NULL,
  `store_id` smallint DEFAULT NULL,
  `token` varchar(256) NOT NULL,
  `title` varchar(47) DEFAULT NULL,
  `body` longtext NOT NULL,
  `image` text,
  `url` text,
  `category` varchar(256) DEFAULT NULL,
  `provider` varchar(50) DEFAULT NULL,
  `sent_on` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `IX_push_notification_async_queue-sent_on` (`sent_on`)
) ENGINE=InnoDB AUTO_INCREMENT=19082441 DEFAULT CHARSET=utf8

And here's the SHOW TABLE STATUS for it [apologies, I don't know how to copy a table properly here]:

Name    Engine  Version Row_format  Rows    Avg_row_length  Data_length Max_data_length Index_length    Data_free   Auto_increment  Create_time Update_time Check_time  Collation   Checksum    Create_options  Comment
push_notification_async_queue   InnoDB  10  Dynamic 14616288    2894    42310041600 0   302432256   8388608 19082441    2021-09-24 23:38:49 2021-12-10 23:44:42     utf8_general_ci     ""  ""
O. Jones
  • 103,626
  • 17
  • 118
  • 172
ConnorU
  • 1,379
  • 2
  • 15
  • 27
  • Does the table have a primary key? If so, can you do `explain select count(id) from bigtable` and put the result into your question. Also put the result of `explain count(*) from bigtable` in your question. – zedfoxus Dec 10 '21 at 19:49
  • @zedfoxus updated the question as requested! – ConnorU Dec 10 '21 at 19:56
  • Can you try this when you get a chance `select count(*) from push_notification_async_queue use index (primary)` or `select count(*) from push_notification_async_queue use index (name-of-the-primary-key-index)` ? Explain seems to be using ix_...sent-on index for your count and that may be a date field. – zedfoxus Dec 10 '21 at 20:07
  • @zedfoxus The query ran and finished in 6 minutes. Beats me why a count without where would be faster by one index over another but interesting to know. However, as a test, I ran the query and killed it preemptively and the same behaviour happened. At present it has been on "killed" state for 11 minutes, so it's not even "just running normally without looking at the kill flag". Also of note, while the CPU did rise with the select, it only peaked at 70%. The killed query went straight to 100% and stayed there. – ConnorU Dec 10 '21 at 20:33
  • I can add that as an answer with some additional commentary so that it may help someone else with the same issue. Which query did you run and kill preemptively? Was that `select count(*) ... use index(primary)`? – zedfoxus Dec 10 '21 at 21:20
  • 1
    Yup that's the one. It ran well enough in 6 minutes - but if I killed it preemptively, the 100% CPU issue appears again. Had to reboot the server after it stayed at 100% CPU for 25 minutes. I think that's a good idea - I'll upvote it as useful since it might help someone but I'm gonna wait and see if someone can add more info on the CPU issue itself! – ConnorU Dec 10 '21 at 21:23
  • 1
    For the CPU issue, you might have to open a support ticket with the RDS provider. They will have better insights or will point you in the right direction. My wild guess is that the cleanup process during the killing of the query execution might be taking significant IO and CPU goes up. Restore it to your local system and see if you see the same behavior with CPU. – zedfoxus Dec 11 '21 at 00:03
  • What other things are going on? (All I can figure is that something else is interfering in some obscure way.) – Rick James Dec 11 '21 at 06:50
  • 1
    @ConnorU Please post text results of A) SHOW CREATE TABLE push_notification_async_queue; and B) SHOW TABLE STATUS WHERE name LIKE 'push_notification_async_queue;'; for important facts about your table to be revealed. Analysis can be completed with more certainty than the assumptions that have been made to this point in time by many 'people'. – Wilson Hauck Dec 11 '21 at 14:19
  • How many CPU cores is your Virtual server allotted? – Rick James Dec 11 '21 at 17:34
  • @RickJames It's an AWS RDS r6g.xlarge instance; 4 vCPU's, 32GB of RAM. – ConnorU Dec 11 '21 at 20:45
  • I assume that 100% CPU means one CPU was saturated? (That would imply, perhaps, that one MySQL connection was CPU-bound.) – Rick James Dec 12 '21 at 01:40
  • 1
    Sorry for the delay, I wanted to confirm this with the Infrastructure team. The metric is an average, so if it shows 100% it means all (in this case 4) vCPUs were at 100%. There was only one connection active during this period - the one running the killed query. – ConnorU Dec 15 '21 at 18:32
  • @ConnorU I am curious. Would you be able to create an index on a field on which an index has not been created yet? Then, do `select count(*) ... use index (new-index-name)` and see if that gets you count any faster. – zedfoxus Dec 15 '21 at 21:16

4 Answers4

1

(This is not a full answer, but is some further details of what is going on.)

In InnoDB, the entire table needs to be scanned for a simple SELECT COUNT(*) without a WHERE clause. This is so that it can correctly count the rows that are 'visible'. If other connections are busy inserting/deleting rows, such rows need to be checked to see if they should be counted.

Assuming that it is a huge table, it would be best to go through all the rows via the smallest BTree. Note that the Data is stored in a BTree sorted by the PRIMARY KEY and each secondary index (if any) is in a separate BTree indexed by a combination of the index's column(s) together with the PK's column(s). Apparently, such an index is based on a DATETIME .. NULL, which shows up in Explain as key_len=6 (5 for DATETIME plus 1 for NULL).

So, why might reading that entire secondary BTree be slower?

  • The Data's BTree may be fully cached in the buffer pool and the datetime index is not cached and the disk is a slow HDD (not a fast SSD). (Side questions: How big (GB) is the table; what is the setting of innodb_buffer_pool_size; how much RAM is on the server?) Did you run the COUNT(*) twice? That usually compensates for cached vs non-cached.

  • Other connections may be locking rows of the table. This will slow down the COUNT. I do not have a good feel for how this would impact the timings, especially by the extent you are experiencing.

Neither of those points justifies (in my experience) the timing differences you are seeing.

Sizes:

  • The Data BTree is 42GB, but, noting an average row size of 2894 bytes, the 42GB may include "off-record" bulky TEXT columns. The datetime index is no bigger than 0.3GB since that is the combined size of all secondary indexes.
  • Depending on RAM size, the table scan may require lots of I/O, while the secondary index scan should be easily cached -- that is, the second timing of COUNT(*) should be very fast.
  • The number of rows is estimated at 15M by Explain, and cannot be more than 19M due to auto_increment. I estimate at least 200 bytes of on-record data per row (including overhead). Meanwhile, the datetime index seems to be 20 bytes per row in the Index BTree. Hence, barring I/O, I would expect the use of the index to be at least 10 times as fast as using the PK.
  • With 42GB of data and less than that of RAM, it is definitely not repeatedly scanning the data. (Alas, I don't have a good suggestion of what it is doing.)
Rick James
  • 135,179
  • 13
  • 127
  • 222
0

Why don't you just count a column instead.... Like select count(id).... Might work

Amospikins
  • 126
  • 5
  • 1
    It is my understanding that the only difference between count(*) and count(column) is that the latter skips null values on the specific column. I don't think this will make much of a difference - and it will certainly not answer the question of why the CPU is getting massacred by a killed query. I appreciate the thought but I alreayd know I can run a SELECT MAX(id) - MIN(id) to get the row count -- even the explain select gives me the row count. My problem is that if I can't find out why this is happening here, I might run into it again with another query that I can't so easily fix. – ConnorU Dec 10 '21 at 20:01
  • 1
    This is of no relevance, count only counts rows that qualify, the column or expression is not even evaluated. you can prove this by counting 1/0 which [does not result in a divide by zero error](https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=f3bbb8372d46d967b4afcf66ae367ed2) – Stu Dec 10 '21 at 20:29
  • @ConnorU - Max-Min assumes no gaps in an autoincrement id -- There are many causes for gaps. Explain only gives an estimate of the number of rows; sometimes it is quite far off. – Rick James Dec 11 '21 at 06:46
  • @RickJames you are correct - however if you know or can trust that there will be no gaps - such as where row deletion happens only on old records or not at all - it is viable. I did not know about the estimate - it happened to be an exact match in this case but I'll keep that in mind, thank you! – ConnorU Dec 11 '21 at 14:12
  • 1
    @ConnorU - There are cases where the estimate is exact, but it is hard to itemize them. It is best to assume that it is not exact. As for gaps -- `INSERT IGNORE` (and several other cases) can lead to "burned" ids (gaps); so I prefer not to trust counts for that reason, too. – Rick James Dec 11 '21 at 16:45
  • Good point - and I didn't know about the INSERT IGNORE part, I'm gonna have to look into that! Thanks for the heads up! – ConnorU Dec 11 '21 at 17:03
  • 1
    @ConnorU - Also, `REPLACE`, `ROLLBACK`, `IODKU`, Multi-master replication / clustering, etc. – Rick James Dec 11 '21 at 17:49
0

I think these solution can solve your problem. maybe all 3 solution or any one or two. 1 Select Data in the form Chunks. 2 Do Indexing. 3 if your are showing data on view then use pagination 4 if your are using laravel you should use db query because this this more fast then sql query.

0

Quick answer

Doing

select count(*)
from push_notification_async_queue use index (primary)

or

select count(*)
from push_notification_async_queue use index (name-of-the-primary-key-index)

can yield faster results.

But why?

Primary key is auto incrementing. So, if you had IDs 1, 25 and 400 in the database, it is easier to count that in the index on IDs. Giving MySQL the hint to use the primary index (or explicitly giving the name of the primary index you have on IDs) will make MySQL read that index to answer the query.

How can we tell what MySQL was using without the hint?

EXPLAIN select count(*) from push_notification_async_queue

will tell us what index MySQL was choosing to answer the query.

Based on the results of explain, I could tell that MySQL thought it would get count(*) fastest from IX_push_notification_async_queue-sent_on field

id = 1
select_type = SIMPLE
table = push_notification_async_queue
key = IX_push_notification_async_queue-sent_on
key_len = 6
rows = 14870073
filtered = 100
extras = using index 

However, IX_push_notification_async_queue-sent_on seems like a date. MySQL has to work lot harder to find count of rows based on an index that contained date/time. That made me think that using index hint will help.

Why would MySQL do silly things like this?

That's hard to tell. You can optimize or analyze table and see if that helps. It can take a long time, so I recommend you take a backup from your production system, restore on your local computer or a test system and try it there. https://dev.mysql.com/doc/refman/8.0/en/table-maintenance-statements.html gives you details about those commands.

On a table that is written to, deleted from, or read from heavily, statistics could take time to build and that can cause MySQL to choose an index that it feels satisfies the query best. There's some good discussion in this thread: MySQL uses incorrect index. Why?

zedfoxus
  • 35,121
  • 5
  • 64
  • 63
  • _Assuming ENGINE=InnoDB..._ Since the `PRIMARY KEY` is "clustered" with the data, COUNTing via the PK must read all the data. Secondary keys have the column(s) being index, plus the PK. That index on a date is probably `INDEX(dt)` and it implicitly has `id` added on. This BTree is much smaller than the data's BTree, hence it _should_ be much faster to scan. – Rick James Dec 11 '21 at 06:36
  • `OPTIMIZE` copies the table over -- slow. `ANALYZE` probes the indexes in a few places -- fast. – Rick James Dec 11 '21 at 06:37
  • The Primary key is only called "primary". The PK does not have to be an auto_increment. Gaps in an auto_increment PK are irrelevant to this Question. – Rick James Dec 11 '21 at 06:39
  • There is nothing special about indexing a date. That is, it should be no slower than any other datatype. – Rick James Dec 11 '21 at 06:41
  • I am downvoting because you have presented many not-quite-correct statements with an air of authority. – Rick James Dec 11 '21 at 06:44
  • That’s fair. You are welcome to write your own answer suggesting what you understand and help OP understand how count can be sped up by the date index. The OP noticed a speed gain using my recommendation. – zedfoxus Dec 11 '21 at 13:12
  • 1
    The query ran faster utilizing the primary key as per @zedfoxus' suggestion. I believe this may be because the datetime field in question - the one whose index the optimizer chooses by default - has null values in it but I don't know why that would make a difference exactly. – ConnorU Dec 11 '21 at 14:16
  • I think of it this way. You have a book. Behind the book is an index of all dates and pages the date appears. Some pages don’t have any date. A date may appear on multiple pages. You have to count the number of pages in the book. My belief is that MySQL found it easier to flip through each page and count than to go to the first date in the index, count the pages it appears in, go to the next date and continue that operation. If it has to additionally count pages that has no dates, that’d take even longer to count pages. – zedfoxus Dec 11 '21 at 15:21