【问题标题】:Should I index a Boolean Field with low 'True' cardinality MySQL?我应该索引具有低“真”基数 MySQL 的布尔字段吗?
【发布时间】:2020-10-06 16:48:24
【问题描述】:

我有一个包含 1M 行(并且还在增长)的 MESSAGE 表。每个消息查询都涉及选择行 WHERE isRequest = True 或 WHERE isRequest = False,但绝不会同时选择两者。我的绝大多数查询都在寻找 isRequest = False。该表的写入非常频繁,我需要保持快速写入(因为用户喜欢以低延迟相互发送消息)。另请注意,MESSAGE 表目前除了主键之外没有任何列索引。

95% 的行有 isRequest = False,只有 5% 的行有 isRequest = True。 在这种情况下索引 isRequest 布尔字段是否更高效?

此外,我了解索引列会消耗内存,但是对于所有列数据类型,包括布尔值,这种开销等效吗?

更新:

在与@Rick James 进一步分析后,我们提出了一个新的表格方案(请注意,所有 PK 都是自动递增的,因此时间相对性是可辨别的):

MESSAGE (id=PK) (sender_id, recipient_id, conversation_id = FKs)
---------------------------------------------------------------
id  sender_id   recipient_id  message            conversation_id
1    1          2            "hows it going"   4
2    2          1            "great! hbu"      4
3    1          8            "hey man"         3
4    9          1            "please respond"  2
5    4          6            "goodnight girl"  1


CONVERSATION (id=PK) (userA_id, userB_id = FKs)
-----------------------------------------------
id  userA_id  userB_id
1   4          6            
2   1          9
3   1          8
4   1          2


USERCONVERSATION (id=PK) (userA/B_id, conver_id, lastMsg_id = FKs)
------------------------------------------------------------------
id   userA_id  userB_id   conver_id  lastMsg_id   isRequest
1    4         6          1          5            False
2    6         4          1          5            False
3    1         9          2          4            True
4    9         1          2          4            True
5    1         8          3          3            False
6    8         1          3          3            False
7    1         2          4          2            False
8    2         1          4          2            False

索引:

MESSAGE: index(id),
         index(conversation_id, id)

CONVERSATION: index(id), 

USERCONVERSATION: index(id),
       index(user_id, isRequest),
       index(user_id, lastMessage_id),
       index(conversation_id)

应用中的查询:

由于如上所述的正确索引,以下查询应该是高性能的。如果可以改进,请与我们联系。

获取变量 userID 的最近 20 个对话(包括最后的消息内容和其他用户的信息):

SELECT  T4.userB_id, T4.username, T4.profilePic, T4.conver_id,
        T4.message 
    (
        SELECT  T1.userB_id, T2.username, T2.profilePic, T1.conversation_id,
                T1.lastMessage_id
            FROM  
            (
                SELECT  userB_id, conversation_id, lastMessage_id
                    FROM  rage.userconversation
                    WHERE  userA_id = {userID}
                      AND  isRequest=False
            ) AS T1
            LEFT JOIN  rage.user AS T2  ON T1.userB_id = T2.id AS T3
    )
    LEFT JOIN  rage.message AS T4  ON T1.lastMessage_id = T4.id
    ORDER BY  T4.id DESC
    LIMIT  20

单词解释:获取最近的 USERCONVERSATION 行中的 20 行,因为 lastMessage 存储在那里。为了找到给定用户最近的 20 个,请选择 user_id = userID 的所有行并按 lastMessage_id DESC 排序。这是准确的,因为 message_id 是自动递增的。除了最后一条消息,我们还需要获取对话中其他用户的一些用户数据(个人资料图片、用户名)。我们通过左连接来实现这一点。

结果:

RESULT (for userID = 1)
---------------------------------------------------------------
userB_id  username   profilePic  message            conver_id
8         John       8.jpg       "hey man"          3
2         Daisy      2.jpg       "great! hbu"       4

然后当用户点击对话时,由于我们有对话 ID,我们只需:

SELECT * FROM rage.message WHERE conversation_id={conver_id} ORDER BY id DESC LIMIT 20

希望我们索引 (conversation_id, id) 后排序很快。

【问题讨论】:

  • 我也有类似的情况,相信最好在这里索引一下。
  • 你能解释一下吗?
  • 通过教科书和网络和 SO,特别是通过 DBMS 手册,通过查询引擎了解关系和 SQL 优化/实现的基础知识——所有这些都立即导致索引、计划、统计和 SARGability。在您学习并应用这些基础知识后,要求重新优化。期望对问题进行适当的研究。请参阅 How to Ask、其他 help center 链接和投票箭头鼠标悬停文本。
  • 请用文字描述您的示例试图做什么。也许它正在寻找它们之间的任一方向的消息?但是LEFT JOIN 在做什么呢?
  • @Rage - isRequest 如何适合您的示例??

标签: mysql sql django django-models database-design


【解决方案1】:

对于这个查询,也许还有其他查询,最好在表中有另一列。它会是一个唯一的数字,比如“conversation_id”,它是从一对唯一的发件人和收件人中派生出来的。

一种粗略的方式(但不一定是最佳方式)是从这个有序对的不同值中以某种方式推导出它:

(LEAST(sender_id, recipient_id), GREATEST(recipient_id, sender_id))

那么INDEX(conversation_id, id) 可能是正在讨论的查询的关键。到那时,我们可以重新加入对布尔值的讨论。我怀疑这最终会是最佳索引:

INDEX(conversation_id, isRequest, id)

(或者可能交换了前两列)。

【讨论】:

  • 您当然可能正在做某事。正如我所看到的,conversation_id 列非常适合查询特定对话。但是,在我的应用程序上下文中,我没有会话 ID,而是搜索 20 条最新消息(每条消息都来自一个独特的会话)。也许这将包括一个新表 CONVERSATION,它存储每个用户的对话。列:conversation_id、user_A、user_B。那么前面的查询就是先获取用户参与的所有的conversation_id,然后从message表中查询最大的message_id?
  • 也许对这个解决方案的扩展是拥有一个如前所述的 CONVERSATION 表,以及一个 lastMessage_id,它是 MESSAGE 表中 message_id 的外键。每次现有对话中的某人发送消息时,它不仅会将此消息添加到 MESSAGE 表中,而且还会更新对话的 lastMessage_id 外键,以便对话现在指向最新的消息。现在查询将是:获取此 user_id 参与的所有对话。我们还将有一个指向 lastMessage 的指针。
  • 这种策略的最佳索引是什么样的?
  • 我会将conversation_id 添加到当前表中。它会被一些笨拙的查询初始化,就像我们正在处理的那样,然后一个相当简单的查询就可以让它继续运行。拥有另一个表将是conversation_id(auto_inc PK),加上 2 个用户。但是你最终还是会得到 2 个连接和一个联合(或其他东西)来使有问题的查询更快。
  • 是的,last_message_id 的链接可能有效。但回到布尔值——想想它对更新 last_message_id 的影响。
【解决方案2】:

使用复合索引。让我们看看整个WHERE 子句,以便为您提供准确的详细信息。

例子

WHERE IsRequest = True
  AND UserId = 12345

将受益于

INDEX(IsRequest, UserId)

(无论你把列名放在哪个顺序,它是真还是假都没有关系。)

你的例子

  • OR 破坏了索引的使用
  • 两个查询之间的UNION 可能会避免OR
  • 没有索引对您编写的查询有用。
  • 将有两个嵌套表扫描。

也许

(不知道下面是不是也一样)

( SELECT  m1.id, m1.sender_id, m1.recipient_id, m1.message ...
    FROM  myapp_message AS m1
    LEFT JOIN  app_message AS m2
         ON  m1.sender_id = m2.sender_id
        AND  m1.id < m2.id
    WHERE  m2.id IS NULL
      AND  m1.sender_id = {userID}
      AND  m1.isRequest = False
    order by  id desc
    LIMIT  20
) UNION ALL (
SELECT  m1.id, m1.sender_id, m1.recipient_id, m1.message ...
    FROM  myapp_message AS m1
    LEFT JOIN  app_message AS m2
         ON  m1.recipient_id = m2.recipient_id
        AND  m1.id < m2.id
    WHERE  m2.id IS NULL
      AND  m1.recipient_id= {userID}
      AND  m1.isRequest = False
    order by  id desc
    LIMIT  20 
)   ORDER BY id DESC LIMIT 20

如果您要分页,请参阅:http://mysql.rjweb.org/doc.php/pagination#pagination_and_union

更接近

SELECT  m...
    FROM
      ( SELECT xid, MAX(mid) AS mid
        FROM
        (
          ( SELECT  recipient_id AS xid,
                    MAX(mid) AS mid      -- The last message TO each recipient
                FROM  WHERE sender_id = 1234  -- FROM the user in question
                GROUP BY  recipient_id
                ORDER BY 2 DESC   -- ("2nd column")
                LIMIT  20                        
          )
          UNION ALL
          ( SELECT  sender_id AS xid,
                    MAX(mid) AS mid      -- The last message FROM each sender
                FROM  WHERE recipient_id = 1234  -- TO the user
                GROUP BY  sender_id
                ORDER BY 2 DESC
                LIMIT  20
          )
        ) AS y
        GROUP BY xid       -- yes, repeated
        ORDER BY mid DESC  -- yes, repeated
        LIMIT 20           -- yes, repeated
      ) AS x
    JOIN messages AS m  ON m.mid = x.mid

使用这两个索引:

INDEX(sender_id, recipient_id, mid)
INDEX(recipient_id, sender_id, mid)

每个子查询都有一个INDEX。每个都是最优的,加上“覆盖”。

(我没有看到isRequest 的相关性,所以我把它省略了。我怀疑如果需要该列,可以将其添加到索引中而不会降低效率——如果放置在适当的位置。 )

【讨论】:

  • 不幸的是,我的查询比一个 WHERE 子句稍微复杂一些。上面的示例查询用于显示用户最近的对话。它返回从他最近的 20 次对话中发送的最后一条消息。更新了帖子^
  • 两次嵌套表扫描非常“不好”。有什么方法可以提高性能来实现相同的查询?我需要从用户的 20 个最新对话中获取最后一条消息。如果我的上下文不清楚,请告诉我。我希望我有 Instagram 直接消息传递的源代码。
  • @Rage - myapp_messageapp_message 之间的区别是什么?
  • 错字,刚刚修复
  • 我认为您可能正在做一些事情,但查询在 MySQL 中无法编译
【解决方案3】:

您有多种选择。根据您的描述,以下两个之一似乎是合适的:

  • 一个聚集索引,其中第一个键是IsRequest
  • 包含IsRequest 的分区方案。

另一种可能性是两个单独的表。

但是,因为我怀疑您的查询是否返回了 95%(甚至 5%)的行,所以毫无疑问还有其他过滤器。为这些过滤器创建索引可能比为布尔标志创建索引更重要。

【讨论】:

  • 你说得对,我当然还有其他用于查询的过滤器,但它们是已编入索引的外键过滤器,但你是对的,它们没有在此表中一起编入索引。我在上面提供了一个示例查询。过滤器:sender_id(FK)、receiver_id(FK)、isRequest。或许我应该将这三个都编入索引?
猜你喜欢
  • 2020-07-29
  • 2014-08-21
  • 2010-12-23
  • 1970-01-01
  • 2021-09-19
  • 2021-09-07
  • 1970-01-01
  • 1970-01-01
  • 2021-09-15
相关资源
最近更新 更多