15

以下查询旨在接收用户的未读消息列表。它涉及 3 个表:recipients包含用户与消息 ID 的关系,messages包含消息本身,以及message_readers包含哪些用户已阅读哪些消息的列表。

查询可靠地花费了 4.9 秒——这严重损害了我们的性能,尤其令人担忧,因为我们希望数据库最终会大几个数量级。诚然,它本质上是一个繁重的查询,但数据集很小,直觉上它似乎应该快得多。服务器有足够的内存(32gb),整个数据库应该一直加载到 RAM 中,并且盒子上没有其他东西在运行。

桌子都很小:

recipients: 23581
messages: 9679
message_readers: 2685

查询本身:

SELECT 
    m.*
FROM 
    messages m
INNER JOIN recipients r ON r.message_id = m.id
LEFT JOIN message_readers mr ON mr.message_id = m.id
WHERE
    r.id = $user_id
    AND (mr.read_by_id IS NULL OR mr.read_by_id <> $user_id)

解释计划非常简单:

+----+-------------+-------+--------+-----------------------------------+-----------------------------------+---------+--------------------------------+-------+-------------+
| id | select_type | table | type   | possible_keys                     | key                               | key_len | ref                            | rows  | Extra       |
+----+-------------+-------+--------+-----------------------------------+-----------------------------------+---------+--------------------------------+-------+-------------+
|  1 | SIMPLE      | r     | ref    | index_recipients_on_id            | index_recipients_on_id            | 768     | const                          | 11908 | Using where |
|  1 | SIMPLE      | m     | eq_ref | PRIMARY                           | PRIMARY                           | 4       | db.r.message_id                |     1 | Using index |
|  1 | SIMPLE      | mr    | ALL    | NULL                              | NULL                              | NULL    | NULL                           |  2498 | Using where |
+----+-------------+-------+--------+-----------------------------------+-----------------------------------+---------+--------------------------------+-------+-------------+

上有一个索引message_readers.read_by_id,但我想它不能真正使用它,因为 IS NULL 条件。

我正在使用所有默认设置,但以下设置除外:

key_buffer=4G
query_cache_limit = 256M
query_cache_size = 1G
innodb_buffer_pool_size=12G

谢谢!

4

6 回答 6

4

假设这message_readers是 的一个子集recipients,我建议进行以下更改:

  1. 摆脱message_readers桌子并用桌子上的旗帜代替它recipients。这将消除空检查并删除连接。

  2. 它可能已经是,但请确保您的聚集索引为recipientsisid, message_id而不是message_id, id,因为几乎所有对邮件的搜索都将基于收件人。

这是产生的 SELECT:

SELECT
    r.whatever,
    m.whatever,
    -- ...
FROM
    recipients r
    INNER JOIN messages m ON m.id = r.message_id
WHERE
    r.id = $user_id
    AND r.read_flag = 'N'

更新

这是使用现有方案的查询的正确版本:

SELECT
    r.whatever,
    m.whatever,
    -- ...
FROM
    recipients r
    INNER JOIN messages m ON r.message_id = m.id
    LEFT JOIN message_readers mr ON mr.read_by_id = r.id 
                                 AND mr.message_id = m.id
WHERE
    r.id = $user_id
    AND mr.read_by_id IS NULL

这假设您的聚集索引是预期的:

recipients: id, message_id
messages: id
message_readers: read_by_id, message_id
于 2011-06-27T19:45:31.770 回答
1

当您像这样重写查询时,您可以摆脱 IS NULL 条件:

SELECT 
    count(m.id)
FROM 
    messages m
INNER JOIN recipients r ON re.message_id = m.id
WHERE r.id = $user_id
  AND NOT EXISTS
         (SELECT mr.id 
            FROM message_readers mr 
           WHERE mr.message_id = m.id
             AND mr.read_by_id = $user_id)

基本上这读起来像:get all messagesfor recipientwhere not inmessage_readers并描述问题的简单化。

于 2011-06-27T19:08:58.857 回答
1

查询时间是多少

select distinct message_id
  from message_readers
 where read_by_id <> $user_id

注意:“is null”逻辑应该被 this 捕获,因为 null 不等于任何东西

如果这很快,那么试试这个:

SELECT count(m.id)
FROM messages m
INNER JOIN recipients r ON r.message_id = m.id
where r.id = $user_id
and m.id in (
    select distinct message_id
      from message_readers
     where read_by_id <> $user_id)

原始答案无效: 尝试将 message_id 和 id 包含在收件人的覆盖索引中,看看会发生什么。

于 2011-06-27T19:25:16.023 回答
1

除非我遗漏了什么,否则您似乎根本不需要消息表。您真正想要的是在收件人中为该用户显示的消息 ID 的数量,而不是在 message_readers 中为该用户显示的消息 ID。

如果我在上面,你可以用减号完成你想要的:

SELECT count(message_id)
  FROM (
        SELECT r.message_id  
          FROM recipients r 
         WHERE r.id = $user_id
        MINUS
        SELECT mr.message_id
          FROM message_readers mr
         WHERE mr.read_by_id = $user_id
       )

这完全避免了连接。现在,如果您确实需要消息表中的数据用于生产查询,您可以将消息表连接到此子查询(或将其粘贴在 IN 子句中)。

由于我的经验是在 Oracle 领域,但 MySQL 支持 MINUS,所以这可能值得一试。

于 2011-06-27T20:01:09.077 回答
1

假设您只想要查询中显示的计数),如果您像这样更改连接会发生什么?

我使用 MSSQL,这有可能加快速度。我从未使用过 MySQL,但它应该可以工作,不是吗?

SELECT     count(m.id)
FROM       messages m
INNER JOIN recipients r ON r.message_id = m.id AND r.id = $user_id
LEFT JOIN  message_readers mr ON mr.message_id = m.id AND (mr.read_by_id IS NULL OR mr.read_by_id <> $user_id)

编辑:这个疯狂的想法怎么样?我认为您可以将其拆分OR为两个单独的左连接,然后在其中任何一个返回某些内容时记录。

SELECT     count(m.id)
FROM       messages m
LEFT JOIN  recipients r ON r.message_id = m.id AND r.id = $user_id
LEFT JOIN  message_readers mr ON mr.message_id = m.id AND mr.read_by_id IS NULL
LEFT JOIN  message_readers mr2 ON mr2.message_id = m.id AND mr2.read_by_id <> $user_id
WHERE      COALESCE(mr.message_id, mr2.message_id) IS NOT NULL
于 2011-06-27T19:02:28.353 回答
1

注释计数(m.id)表示计数不为空值,但 m.id 永远不会为空,因此它是额外的。试试看

SELECT count(*)
FROM 
messages m
INNER JOIN recipients r ON r.message_id = m.id  
left join 
(
    select m.id
    messages m
    INNER JOIN message_readers mr 
    ON mr.message_id = m.id     
    and (mr.read_by_id <> $user_id or mr.read_by_id IS NULL)        
)as sub 
on sub.id = m.id        
WHERE r.id = $user_id

一个疑问在您的业务逻辑中可能是正确的,为什么所有用户都可以阅读传入消息(mr.read_by_is null)以及为什么可以为其他人阅读消息或不特定接收者(mr.read_by_id <> $user_id)

它是一个游泳池,我猜

一种更好的方法是通过存在更改子查询中的内部。看到“mr.read_by_id IS NULL”不是必需的,即如果 mr_read_by_id 为空“那么意味着“mr.read_by_id = $user_id”是假的”

SELECT count(*)
FROM 
messages m
INNER JOIN recipients r ON r.message_id = m.id  
left join 
(
    select m.id
    messages m
            where not exists(select * from message_readers mr 
    where mr.message_id = m.id      
    and mr.read_by_id = $user_id)
)as sub 
on sub.id = m.id        
WHERE r.id = $user_id
于 2011-06-27T20:05:31.467 回答