MySQL查询优化:如何优化投票计算?

Posted

技术标签:

【中文标题】MySQL查询优化:如何优化投票计算?【英文标题】:MySQL query optimization: how to optimize voting calculations? 【发布时间】:2013-02-18 15:13:37 【问题描述】:

希望你一切都好。

我需要这个数据库的帮助:

这是一个存储投票的数据库。用户选择他们喜欢的音轨,然后投票给他们。他们可以投票“赞成”或“反对”。非常简单。但是,当涉及到计算统计数据时,它就变得毛茸茸了。

这是一个键值样式表,用于存储最常用的统计信息(只是某种缓存):

mysql> SELECT * FROM Meta;
+-------------+-------+
| Key         | Value |
+-------------+-------+
| TRACK_COUNT | 2620  |
| VOTE_COUNT  | 3821  |
| USER_COUNT  | 371   |
+-------------+-------+

投票

投票表本身保存投票。这里唯一有趣的字段是Type,它的值意味着:

    0 - 应用程序投票,用户使用 UI 为曲目投票 1 - 导入投票(来自外部服务) 2 - 合并投票。实际上与 Imported Vote 相同,但它实际上做了一个注释,该用户已经使用外部服务为该曲目投票,现在他正在使用应用程序重复自己。

跟踪

轨道本身包含总统计数据。来自外部服务 (LikesRP) 的喜欢、不喜欢、喜欢的数量、来自外部服务 (DislikesRP) 的不喜欢、喜欢/不喜欢的调整。

应用

该应用需要获得以下投票:

    过去 7 天内投票最多的 5 首曲目 过去 7 天内投票最多的 5 首曲目 过去 7 天内投票最多的 5 首曲目,其中的投票来自外部服务 (Vote.Type = 1) 上个月投票最多的 100 首曲目

为了获得投票最多的 100 首曲目,我使用以下查询:

SELECT
    T.Hash,
    T.Title,
    T.Artist,
    COALESCE(X.VotesTotal, 0) + T.LikesAdjust as VotesAdjusted
FROM (
    SELECT
        V.TrackHash,
        SUM(V.Vote) AS VotesTotal
    FROM
        Vote V
    WHERE
        V.CreatedAt > NOW() - INTERVAL 1 MONTH AND V.Vote = 'up'
    GROUP BY
        V.TrackHash
    ORDER BY
        VotesTotal DESC
) X
RIGHT JOIN Track T
    ON T.Hash = X.TrackHash
ORDER BY
    VotesAdjusted DESC
LIMIT 0, 100;

这个查询工作正常,它支持调整(客户想要调整列表中的轨道位置)。几乎相同的查询用于获得 5 个最上/下投票的曲目。任务 #3 的查询是这样的:

SELECT
    T.Hash,
    T.Title,
    T.Artist,
    COALESCE(X.VotesTotal, 1) as VotesTotal
FROM (
    SELECT
        V.TrackHash,
        SUM(V.Vote) AS VotesTotal
    FROM
        Vote V
    WHERE
        V.Type = '1' AND
        V.CreatedAt > NOW() - INTERVAL 1 WEEK AND
        V.Vote = 'up'
    GROUP BY
        V.TrackHash
    ORDER BY
        VotesTotal DESC
) X
RIGHT JOIN Track T
    ON T.Hash = X.TrackHash
ORDER BY
    VotesTotal DESC
LIMIT 0, 5;

问题是第一个查询大约需要 2 秒才能执行,而我们的投票数少于 4k。到年底,这个数字将是大约 20 万张选票,这很可能会扼杀这个数据库。所以我正在想办法解决这个难题。

现在我想到了这些问题:

    我是否把数据库设计弄错了?我的意思是,它会更好吗? 我的查询有误吗? 还有什么我可以改进的吗?

我做的第一件事是缓存。但是,好的,这彻底解决了问题。但我对 SQL 相关的解决方案很好奇(总是倾向于完美)。

我的第二个想法是将这些计算值放入Meta 表中,并在投票过程中更改它们。但是我的时间很短,只是尝试一下。顺便说一句,这值得吗?或者,企业级应用如何解决这些问题?

谢谢。

编辑

我不敢相信我忘记了包含索引。他们在这里:

mysql> SHOW INDEXES IN Vote;
+-------+------------+-------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table | Non_unique | Key_name                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-------+------------+-------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Vote  |          0 | UNIQUE_UserId_TrackHash |            1 | UserId      | A         |         890 |     NULL | NULL   |      | BTREE      |         |
| Vote  |          0 | UNIQUE_UserId_TrackHash |            2 | TrackHash   | A         |        4450 |     NULL | NULL   |      | BTREE      |         |
| Vote  |          1 | INDEX_TrackHash         |            1 | TrackHash   | A         |        4450 |     NULL | NULL   |      | BTREE      |         |
| Vote  |          1 | INDEX_CreatedAt         |            1 | CreatedAt   | A         |        1483 |     NULL | NULL   |      | BTREE      |         |
| Vote  |          1 | UserId                  |            1 | UserId      | A         |        1483 |     NULL | NULL   |      | BTREE      |         |
+-------+------------+-------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

mysql> SHOW INDEXES IN Track;
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table | Non_unique | Key_name       | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Track |          0 | PRIMARY        |            1 | Hash        | A         |        2678 |     NULL | NULL   |      | BTREE      |         |
| Track |          1 | INDEX_Likes    |            1 | Likes       | A         |          66 |     NULL | NULL   |      | BTREE      |         |
| Track |          1 | INDEX_Dislikes |            1 | Dislikes    | A         |          27 |     NULL | NULL   |      | BTREE      |         |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

【问题讨论】:

是的,我会照你说的做:当有人投票时,修改缓存表,而不是从头开始重新计算所有统计信息。如果您时间紧迫,请按计划离线进行长时间查询,并缓存结果 - 因此如果需要十分钟,那么在短期内您将不得不忍受最多十分钟前的结果。无论如何,这对于这个数据集来说可能不是问题,这取决于数据必须有多活跃。 您的投票表上有哪些索引?如果您的表被正确索引,200k 记录并不是很多。 @GarethD,好吧,从第二个示例执行查询确实需要 2 秒。不确定它是否会扩展,由于缺乏知识,我仍在进行适当的测试。还是谢谢。 【参考方案1】:

这是一个非常主观的问题,因为它在很大程度上取决于您的确切要求,以及这里没有人可以对您的数据进行的性能测试。但我可以回答您的问题并添加一些可能对您有用的通用解决方案:


我是不是搞错了数据库设计?我的意思是,它会更好吗?

没有。这是 OLTP 的理想设计。


我是否做错了查询?

否(尽管子查询中的ORDER BY 是多余的)。查询的性能在很大程度上取决于Vote 表上的索引,因为查询的主要列将在这一部分中:

SELECT  V.TrackHash, SUM(V.Vote) AS VotesTotal
FROM    Vote V
WHERE   V.CreatedAt > NOW() - INTERVAL 1 MONTH AND V.Vote = 'up'
GROUP BY V.TrackHash

我建议使用 2 个索引,一个在 TrackHash 上,一个在 CreatedAtVoteType 上(这可能作为 3 个单独的索引表现更好,值得双向测试)。 200k 行并不是那么多数据,因此使用正确的索引应该不会花费太长时间来查询上个月的数据。


还有什么我可以改进的吗?

这在很大程度上是一种平衡行为,它实际上取决于您对最佳方式的确切要求。您可以通过 3 种主要方法来解决问题。

1.您当前的方法(每次查询投票表)

正如之前提到的,我认为这种方法应该可以为您的应用程序扩展。优点是它不需要任何维护,并且发送到应用程序的所有数据都是最新且准确的。缺点是性能,插入数据(由于更新索引)和选择数据可能需要更长的时间。这将是我的首选方法。

2。 OLAP 方法

这将涉及维护一个汇总表,例如:

CREATE TABLE VoteArchive
(       TrackHash           CHAR(40) NOT NULL,
        CreatedDate         DATE NOT NULL,
        AppMadeUpVotes      INT NOT NULL,
        AppMadeDownVotes    INT NOT NULL,
        ImportedUpVotes     INT NOT NULL,
        ImportedDownVotes   INT NOT NULL,
        MergedUpVotes       INT NOT NULL,
        MergedDownVotes     INT NOT NULL,
    PRIMARY KEY (CreatedDate, TrackHash)
);

这可以通过运行一个简单的查询每晚填充

INSERT VoteArchive
SELECT  TrackHash,
        DATE(CreatedAt),
        COUNT(CASE WHEN Vote = 'Up' AND Type = 0 THEN 1 END),
        COUNT(CASE WHEN Vote = 'Down' AND Type = 0 THEN 1 END),
        COUNT(CASE WHEN Vote = 'Up' AND Type = 1 THEN 1 END),
        COUNT(CASE WHEN Vote = 'Down' AND Type = 1 THEN 1 END),
        COUNT(CASE WHEN Vote = 'Up' AND Type = 2 THEN 1 END),
        COUNT(CASE WHEN Vote = 'Down' AND Type = 2 THEN 1 END)
FROM    Votes
WHERE   CreatedAt > DATE(CURRENT_TIMESTAMP)
GROUP BY TrackHash, DATE(CreatedAt);

然后您可以使用此表来代替您的实时数据。它的优点是日期是聚集索引的一部分,因此任何受日期限制的查询都应该非常快。这样做的缺点是,如果您查询此表,您只能获得准确的统计信息,直到上次填充它,但您将获得更快的查询。维护查询也是一项额外的工作。但是,如果我不能查询实时数据,这将是我的第二选择。

3.投票期间更新统计数据

为了完整起见,我将其包括在内,但恳请您不要使用此方法。您可以在您的应用程序层或通过触发器实现这一点,尽管它确实允许查询最新数据而无需查询“生产”表,但它对错误是开放的,而且我从未遇到过真正提倡的人这种方法。对于每次投票,您都需要执行插入/更新逻辑,这应该将非常快速的插入查询变成更长的过程,这取决于您如何进行维护,有机会(尽管并发问题非常小)。

4.以上的组合

您始终可以拥有 2 个与您的投票表格式相同的表格,以及解决方案 2 中规定的一个表格,一个投票表仅用于存储今天的投票,一个用于存储历史投票,并且仍然保留一个汇总表,然后您可以将今天的数据与汇总表结合起来以获得最新的结果,而无需查询大量数据。同样,这是额外的维护,并且更有可能出错。

【讨论】:

我喜欢想法#4!一个组合,因为您有 createdAt 时间戳。附加提示:使您的 SELECT 成为脏读(无事务性),因为在任何情况下,当它们击中最终用户的眼球时,计数都是脏的。 非常感谢。这正是我一直在寻找的答案。即使我忘记包括索引(我在编辑中添加),它也几乎完全确定了它。再次感谢。

以上是关于MySQL查询优化:如何优化投票计算?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 php 和 mysql 优化这个简单的数据库和查询?

mysql查询所用时间过长 如何优化?

MySQL - IN(...)优化问题中的“选择”查询(=>分层查询)

如何优化这个非常慢的 MySQL 查询?

mysql如何优化以下语句,查询耗时太久了?

如何优化mysql查询