5

希望你一切都好。

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

在此处输入图像描述

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

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

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

投票

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

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

追踪

轨道是为自己保存总统计数据。喜欢、不喜欢、来自外部服务的喜欢 ( 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。到年底,这个数字将是大约 20 万张选票,这很可能会扼杀这个数据库。所以我正在想办法解决这个难题。

现在我归结为这些问题:

  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      |         |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
4

1 回答 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

我建议使用 2 个索引,一个 onTrackHash和一个 on CreatedAtVoteAND Type(​​作为 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 中规定的一个表格,一个投票表仅用于存储今天的投票,一个用于存储历史投票,并且仍然维护一个汇总表,您可以然后将今天的数据与汇总表结合起来,在不查询大量数据的情况下获得最新的结果。同样,这是额外的维护,并且更有可能出错。

于 2013-02-18T18:35:13.553 回答