想象一下,一个电影应用程序基于这个非常简单的算法为用户重新推出下一部电影:
这是SQL Server数据库的简单设计:
Movies:
Id bigint
Name nvarchar(100)
SeenMovies:
Id bigint
UserId bigint
MovieId bigint
NotInterestedFlags:
Id bigint
UserId bigint
MovieId bigint
要获得下一部电影,我们运行此查询:
select top 1 *
from Movies
where Id not in
(
select MovieId
from SeenMovies
where UserId = 89283
)
and Id not in
(
select MovieId
from NotInterestedFlags
where UserId = 89283
)
通过更多地使用应用程序和更多数据,这种设计变得越来越慢。因此,如果想要一个拥有100K电影的数据库和超过1000万的客户,如何更改此设计以使其水平扩展?
以下是我推荐的代码。
我假设SeenMovies和NotInterestedFlags在UserId上是聚类的,或者至少是索引的。并且该电影聚集在MovieId上。如果没有,添加这些索引将是第一个开始。
我当然没有看到任何理由为什么每个查询的性能都会因为你所讨论的那种卷而导致性能不佳,因为一旦我们将查询局限于特定用户,SeenMovies和NotInterestedFlags最多只能拥有一个该用户每行几千行。
SELECT TOP 1
Movies.*
FROM
Users
CROSS JOIN
Movies
WHERE
NOT EXISTS
(
SELECT NULL
FROM SeenMovies
WHERE
SeenMovies.UserId = Users.Id
AND
SeenMovies.MovieId = Movies.Id
)
AND
NOT EXISTS
(
SELECT NULL
FROM NotInterestedFlags
WHERE
NotInterestedFlags.UserId = Users.Id
AND
NotInterestedFlags.MovieId = Movies.Id
)
AND
Users.Id = 89283
如果使用适当的索引仍然表现不佳,我只能想象,首先可能首先使用SeenMovies和NotInterestedFlags中的MovieId条目为该UserId,然后UNION
ing这些针对电影,可能会提供更好的性能。
另一方面,如果问题是系统的整体性能在许多用户的负载下降低,那么您可能不得不考虑为未见和未列入黑名单的电影的每个用户预先准备一份列表,你查询EXCEPT
。
然后,当用户观看电影或将其列入黑名单(或添加新电影)时,此新表将与单独的SeenMovies和NotInterestedFlags表同时修改。
再说一次,如果这对性能没有帮助,那么你必须考虑实现每日批处理作业,也就是说,每个用户预先准备10个看不见和非黑名单的电影列表,然后这个表就是查询并一次一个地向用户提供。
我坦率地说,如果您有1000万用户的前景,您可能可以负担得起专家编写代码或评估现有系统。
使用“入围”电影为每个用户创建缓存。索引视图可能适用于此。关键是每次用户想要查看列表时都要运行完整查询,但偶尔会更新候选名单。这些单独的列表与用户标志表一起可以通过某些用户属性进行水平扩展。用户位置可能是未来云迁移的理想选择。