希望你做得很好。
我需要一些关于这个数据库的帮助:
这是一个存储投票的数据库。用户选择他们喜欢的音轨,然后投票给他们。他们可以“投票”或“投票”投票。非常简单。但是,当谈到计算统计数据时,它变得毛茸茸。
这是一个键值样式表,存储最常用的统计信息(只是排序缓存):
mysql> SELECT * FROM Meta;
+-------------+-------+
| Key | Value |
+-------------+-------+
| TRACK_COUNT | 2620 |
| VOTE_COUNT | 3821 |
| USER_COUNT | 371 |
+-------------+-------+
投票表持有投票本身。这里唯一有趣的字段是Type
,其值意味着:
0
- 应用投票,用户使用用户界面投票选择了1
- 导入的投票(来自外部服务)2
- 合并投票。实际上与导入的投票相同,但它实际上已经注意到,该用户已经使用外部服务投票支持此曲目,现在他正在重复使用该应用程序。该曲目保留了自己的总统计数据。从外部服务(LikesRP
)喜欢,不喜欢,喜欢外部服务(DislikesRP
),喜欢/不喜欢调整。
该应用需要获得投票:
Vote.Type = 1
)要获得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 | |
+-------+------------+----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
答案 0 :(得分: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个索引,一个在TrackHash
上,一个在CreatedAt
上,Vote
和Type
(这可能会更好地作为3个单独的索引,值得测试两种方式)。 200k行并不是那么多数据,所以使用正确的索引,上个月查询数据的时间不会太长。
还有什么我可以改进的吗?
这是一个非常平衡的行为,它实际上取决于您对最佳进行方式的确切要求。有三种主要方法可以解决问题。
<强> 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中列出一个表,只有一个投票表用于存储今天的投票,一个用于历史投票,并且仍然保持汇总表,然后,您可以将今天的数据与汇总表结合起来,以获取最新结果,而无需查询大量数据。同样,这是额外的维护,更容易出错。