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

5

希望你一切都好。

我需要在这个数据库上稍微帮忙一下:

enter image description here

这是一个存储投票的数据库。用户选择他们喜欢的音轨,并为它们投票。他们可以投赞成票或反对票。非常简单。但是,当涉及计算统计数据时,情况就变得复杂了。

元信息

这是一个键-值样式的表格,存储最常用的统计数据(类似于缓存):

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

投票

投票表格保存了投票内容。这里唯一有趣的字段是Type,其值的含义如下:

  1. 0 - 应用程序制作的投票,用户使用UI投票给曲目
  2. 1 - 导入的投票(来自外部服务)
  3. 2 - 合并的投票。实际上与导入的投票相同,但它实际上是一个注释,表示该用户已经使用外部服务为此曲目投票,现在他正在使用应用程序重复投票。

曲目

曲目保存了自身的总体统计信息。点赞数、踩数、来自外部服务的点赞数 (LikesRP)、来自外部服务的踩数 (DislikesRP),以及点赞/踩的调整。

应用程序

应用程序需要获取以下投票信息:

  1. 过去7天内5个最受欢迎的曲目
  2. 过去7天内5个最不受欢迎的曲目
  3. 过去7天内从外部服务导入的5个最受欢迎的曲目的投票 (Vote.Type = 1)
  4. 过去一个月内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的投票。到年底,这个数字将达到大约200k的投票,这很可能会让这个数据库崩溃。所以我正在想办法解决这个难题。
现在我得出以下问题:
1. 我的数据库设计错了吗?我的意思是,它可以更好吗? 2. 我查询的方法有误吗? 3. 我还能做些什么改进吗?
我做的第一件事情是缓存。但是,好吧,这解决了问题。但是我对与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      |         |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

是的,我会按照您说的做:当有人投票时,修改缓存表而不是从头开始重新计算所有统计数据。如果时间紧迫,可以按计划离线执行长查询,并缓存结果 - 因此,如果需要十分钟,那么在短期内,您将不得不接受最多十分钟旧的结果。无论如何,这个数据集可能并不是问题,这取决于数据必须有多实时。 - halfer
你的投票表上有哪些索引?如果你的表建立了正确的索引,20万条记录并不算多。 - GarethD
@GarethD,嗯,从第二个例子执行查询已经需要2秒钟了。不确定它是否可扩展,因为我还在进行适当的测试,由于缺乏知识。无论如何,谢谢。 - Aleksandr Makov
1个回答

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

我的数据库设计有问题吗?我的意思是,它可以更好吗?

不会。这是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

我建议创建两个索引,一个在TrackHash上,另一个在CreatedAt、Vote和Type上(这可能作为三个单独的索引执行效果更好,值得测试两种方式)。20万行数据并不算太多,因此只要有正确的索引,查询最近一个月的数据就不应该花费太长时间。

还有什么可以改进的吗?

这非常取决于您的确切需求,是一个权衡的过程。有三种主要方法可以解决这个问题。

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的一个表格,仅用于存储今天的投票的一个投票表,以及用于历史投票的一个投票表,并仍然维护一个摘要表,然后将今天的数据与摘要表相结合以获得最新结果,而无需查询大量数据。同样,这需要额外的维护,并且存在更多的潜在问题。

我喜欢第四个想法!因为你有createdAt的时间戳。另外一个小建议是,将你的SELECT操作设置为脏读(无事务性),因为总数在最终呈现给最终用户之前已经变得不准确了。 - Julius Musseau
非常感谢。这正是我在寻找的答案。尽管我忘记包含索引(我在编辑中添加了),但这几乎完美地解决了问题。再次感谢。 - Aleksandr Makov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接