'Slow MySQL query on 2mill record table despite indexes
Struggling to understand why my query is taking ~8-12s to complete when my table has millions of rows. I can force it to use a specific index, and depending on which one it will range from 7s-20s to execute.
Here is the query:
SELECT
thread, msgProtocol, MAX(newestMsg) AS newestMsg,
SUM(total) AS total, SUM(unread) AS unread
FROM
(SELECT
msgSender AS thread, msgProtocol,
MAX(IF(msgSent = -1, msgSendDate, tblMsgs.dateCreated)) AS newestMsg,
COUNT(msgID) AS total,
SUM(msgRead = 0 AND (msgRecipient = '5555555555' OR msgRecipient = 'chat@eid:19')) AS unread
FROM tblMsgs
WHERE msgRecipient IN('5555555555', 'chat@eid:19') AND msgStatus != 'D'
GROUP BY msgSender
ORDER BY newestMsg DESC
LIMIT 0, 30) a
UNION ALL
(SELECT
msgRecipient AS thread, msgProtocol,
MAX(IF(msgSent = -1, msgSendDate, tblMsgs.dateCreated)) AS newestMsg,
COUNT(msgID) AS total,
SUM(msgRead = 0 AND msgRecipient = 'chat@eid:19') AS unread
FROM tblMsgs
WHERE msgSender IN('5555555555', 'chat@eid:19') AND msgStatus != 'D'
GROUP BY msgRecipient
ORDER BY newestMsg DESC
LIMIT 0, 30)
ORDER BY newestMsg DESC
LIMIT 0, 30;
The basic premise of this is a chat/texting application that stores messages where the sender and recipient are in two separate columns. Whether a message is "incoming" or "outgoing" is determined in the application. This query is trying to pull a list of most recently updated "threads" or conversations. This data updates in real-time as one would expect for a chat app, so it's crucial it's quick. The first half of the UNION grabs what are effectively the most recently updated incoming threads, and the second half outgoing. They are then joined together and grouped again so I only see the latest 30 threads.
Here is the structure of the table:
CREATE TABLE `tblMsgs` (
`dateCreated` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`msgID` int NOT NULL AUTO_INCREMENT,
`msgStatus` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'A',
`msgSender` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`msgRecipient` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`msgProtocol` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'sms',
`msgBody` varchar(3000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
`msgAttachments` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`msgRead` int NOT NULL DEFAULT '0',
`msgSent` int NOT NULL DEFAULT '0',
`msgDelivered` int DEFAULT '0',
`msgSendDate` datetime(3) DEFAULT NULL,
PRIMARY KEY (`msgID`),
KEY `recipient` (`msgRecipient`,`msgStatus`),
KEY `sender` (`msgSender`,`msgStatus`)
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=2370188 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Here is the EXPLAIN:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | derived2 | ALL | 30 | 100.00 | ||||||
2 | DERIVED | tblMsgs | index | recipient,sender | sender | 1029 | 1802478 | 27.01 | Using where; Using temporary; Using filesort | ||
3 | UNION | tblMsgs | index | recipient,sender | recipient | 1029 | 1802478 | 50.21 | Using where; Using temporary; Using filesort | ||
UNION RESULT | union1,3 | ALL | Using temporary; Using filesort |
I've also tried indexing msgSendDate/dateCreated alone and together and that doesn't seem to help. I've tried forcing both halves of the UNION to use the index "recipient" and "sender" (respectively) and it doesn't help. I'm not sure why the second half doesn't prefer the "sender" index and runs slower with it?
Even using a really simple statement like this will take ~7s (this chooses the "recipient" index as well, and is even slower @ ~9s with "sender"):
SELECT * FROM tblMsgs
WHERE msgSender = '5555555555' AND msgStatus <> 'D'
GROUP BY msgRecipient
ORDER BY msgID DESC
LIMIT 0, 30;
EXPLAIN for this:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | tblMsgs | index | recipient,sender | recipient | 1029 | 1802454 | 50.21 | Using where; Using temporary; Using filesort |
Solution 1:[1]
Based on logic you should not need this union -- the following should give you the same result
SELECT
msgSender AS thread,
msgProtocol,
MAX(IF(msgSent = -1, msgSendDate, dateCreated)) AS newestMsg,
COUNT(msgID) AS total,
SUM(CASE msgRead = 0 AND (msgRecipient = '5555555555' OR msgRecipient = 'chat@eid:19') THEN 1 ELSE 0 END) AS unread
FROM tblMsgs
WHERE (msgRecipient = '5555555555' OR msgRecipient = 'chat@eid:19') AND msgStatus != 'D'
GROUP BY thread
ORDER BY newestMsg DESC
LIMIT 0, 30;
By changing to 1 query from 3 the optimizer should pick the best indexes and give you the best performance.
Also, if msgSendDate is null when no messages are sent you can change the 3rd column to
MAX(COALESCE(msgSendDate, dateCreated)) AS newestMsg,
which would be much faster than an IF
Solution 2:[2]
Like @Hogan suggested, you can re-write the query, and do this:
SELECT
IF(msgRecipient IN('5555555555', 'chat@eid:19'),msgSender,msgRecipient) AS thread,
msgProtocol,
MAX(IF(msgSent = -1, msgSendDate, tblMsgs.dateCreated)) AS newestMsg,
COUNT(msgID) AS total,
SUM(msgRead = 0 AND (msgRecipient = '5555555555' OR msgRecipient = 'chat@eid:19')) AS unread
FROM tblMsgs
WHERE (msgRecipient IN('5555555555', 'chat@eid:19') OR msgSender IN('5555555555', 'chat@eid:19'))
AND msgStatus != 'D'
GROUP BY 1, msgProtocol
ORDER BY newestMsg DESC
LIMIT 0, 30;
see: DBFIDDLE
- As far as I can see, this will use index (checked by removing the comment before
EXPLAIN
in the dbfiddle) - I cannot tell if it is performant enough, because a reproduceable situation is missing (see
Some updates to (TRY TO) reproduce actual data
in the dbfiddle)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Hogan |
Solution 2 | Luuk |