如何为在线约会网站设计用户表?

3
我正在开发一个本地在线约会网站的下一个版本,基于 PHP 和 MySQL,并希望做得更好。用户表格非常庞大,并且随着新版本的推出,预计会有更多的用户加入,因为将投入大量资金用于推广。
目前的版本可能已经有7-8年了,很可能是由一个不太熟悉 PHP 和 MySQL 的人完成的,所以我必须从头开始重新设计。
社区目前拥有200k+用户,并预计在未来一两年内增长到500k-1mil。每个用户的个人资料中有100多个属性,并且我必须至少能够通过其中30-40个属性进行搜索。
正如你所想象的那样,我有点担心创建一个具有200k行和100列的表格。我的前任将用户表格分成两部分...一部分包含最常用和搜索的列,另一部分包含其余(和大块的)列。但这导致了两个表格之间的大型同步问题。
那么,你认为最好的方法是什么?
6个回答

5
这不是一个完整的答案,但是由于这里很少有答案提到属性值模型,我想分享一下我的生活经验。我曾尝试使用这种模型来处理拥有120多个属性(每年增加5-10个)和约100k+行(每6个月)的表格,索引变得如此庞大,以至于添加或更新单个“user_id”需要很长时间。
我认为这种设计存在问题(并不是说它完全不适合任何情况),你需要在第二个表上将“user_id, attrib”设置为主键。由于无法确定attrib的潜在长度,你通常会使用更大的长度值,从而增加索引。在我的情况下,attribs可能具有3到130个字符。同样,“value”也肯定会受到相同假设的影响。
正如OP所说,这会导致同步问题。想象一下如果每个属性(或至少50%)必须存在。
此外,正如OP建议的那样,搜索需要在30-40个属性上进行,我无法想象如何有效地执行30-40个连接,甚至是由于长度限制而使用“group_concat()”。
我的唯一可行解决方案是回到一个列数与属性数量相同的表格中。我的索引现在要小得多,搜索也更容易。
编辑:当然,有人可以说我应该有一个属性可能值的查找表(减少索引大小),但我必须在那个表上进行连接。

@stereofrog:我不会说“缺乏实践经验”,但只是可能不适用于这种情况。 - Danosaure
@stereofrog:政治正确地说……也许人们没有仔细阅读 OP 的要求,否则,我同意“经验不足”。 - Danosaure

4
您可以将用户数据分成两个表。
1)表:user 这将包含有关用户的“核心”固定信息,例如名字,姓氏,电子邮件,用户名,角色ID,注册日期等等。
2)表:user_profile 与个人资料相关的信息可以放在自己的表中。这将是一个具有键=>值性质的无限可扩展表。
字段:user_id,option,value user_id: 1
option: profile_image value: /uploads/12/myimage.png
和 user_id: 1
option: questions_answered
value: 24
希望对您有所帮助, Paul.

这可能是您最好的选择,因为它遵循规范化范例,虽然可能会产生一些开销,但从长远来看,它比单个具有数百列的表格更容易管理且更快。 - Janis Peisenieks
在我看来,这确实是正确的方式,这也是我会做的同样的事情。 - Not Available
这比OP拥有的更好,但是attribute->value相对于标准化的数据库设计来说也存在自己的问题(查询复杂度、域验证、完整性约束的表达能力和性能)。 - Unreason

1

通常情况下,在关注性能之前,您应该始终正确地获取模式!

这样,您就可以做出有根据的决策,调整模式以解决特定的性能问题,而不是猜测。

您绝对应该采用两个表的方法。这将显著减少存储量、代码复杂度和更改系统以添加新属性所需的工作量。

假设每个属性都可以由序数表示,并且您只寻找对称匹配(即,您试图基于相似属性匹配人,而不是意图表达)....

在简单的级别上,查找合适的匹配项的查询可能非常昂贵。实际上,您正在寻找N维空间中相同接近度的节点,不幸的是,大多数关系型数据库并没有为此类操作进行设置(我相信PostgreSQL支持此类操作)。因此,大多数人可能会从以下内容开始:

SELECT candidate.id, 
 COUNT(*)
FROM users candidate,
  attributes candidate_attrs,
  attributes current_user_attrs
WHERE current_user_attrs.user_id=$current_user 
  AND candidate.user_id<>$current_user
  AND candidate.id=candidate_attrs.user_id
  AND candidate_attrs.attr_type=current_user.attr_type
  AND candidate_attrs.attr_value=current_user.attr_value
GROUP BY candidate.id
ORDER BY COUNT(*) DESC;

然而,这会强制系统比较每个可用的候选项以找到最佳匹配。应用一些启发式算法,您可以得到非常有效的查询:

SELECT candidate.id, 
 COUNT(*)
FROM users candidate,
   attributes candidate_attrs,
   attributes current_user_attrs
WHERE current_user_attrs.user_id=$current_user 
  AND candidate.user_id<>$current_user
  AND candidate.id=candidate_attrs.user_id
  AND candidate_attrs.attr_type=current_user.attr_type
  AND candidate_attrs.attr_value 
     BETWEEN current_user.attr_value+$tolerance
     AND current_user.attr_value-$tolerance
GROUP BY candidate.id
ORDER BY COUNT(*) DESC;

($tolerance的值将影响返回的行数和查询性能 - 如果您在attr_type、attr_value上建立了索引)。

这可以进一步细化为一个积分评分系统:

SELECT candidate.id, 
  SUM(1/1+
      ((candidate_attrs.attr_value - current_user.attr_value)
        *(candidate_attrs.attr_value - current_user.attr_value))
  ) as match_score
FROM users candidate,
  attributes candidate_attrs,
  attributes current_user_attrs
WHERE current_user_attrs.user_id=$current_user 
  AND candidate.user_id<>$current_user
  AND candidate.id=candidate_attrs.user_id
  AND candidate_attrs.attr_type=current_user.attr_type
  AND candidate_attrs.attr_value 
   BETWEEN current_user.attr_value+$tolerance
   AND current_user.attr_value-$tolerance
GROUP BY candidate.id
ORDER BY COUNT(*) DESC;

这种方法让你可以做很多不同的事情 - 包括按属性子集搜索,例如:

SELECT candidate.id, 
  SUM(1/1+
      ((candidate_attrs.attr_value - current_user.attr_value)
        *(candidate_attrs.attr_value - current_user.attr_value))
  ) as match_score
FROM users candidate,
  attributes candidate_attrs,
  attributes current_user_attrs,
  attribute_subsets s
WHERE current_user_attrs.user_id=$current_user 
  AND candidate.user_id<>$current_user
  AND candidate.id=candidate_attrs.user_id
  AND candidate_attrs.attr_type=current_user.attr_type
  AND candidate_attrs.attr_value
  AND s.subset_name=$required_subset
  AND s.attr_type=current_user.attr_type 
   BETWEEN current_user.attr_value+$tolerance
   AND current_user.attr_value-$tolerance
GROUP BY candidate.id
ORDER BY COUNT(*) DESC;

显然,这并不适用于非序数数据(例如出生符号、最喜欢的流行乐队)。如果不了解现有数据结构的更多信息,很难准确地说这将有多大效果。

如果您想添加更多属性,则无需对PHP代码或数据库架构进行任何更改-它可以完全由数据驱动。

另一种方法是识别刻板印象-即N维空间中的参考点,然后计算出特定用户最接近哪个点。您将所有属性折叠成单个复合标识符-然后只需要应用相同的方法在已匹配到刻板印象的候选子集中找到最佳匹配。


1

你的回答没有多少意义。在大型生产网站中,这是关系型的,通常最好在RDBMS上实现无模式设计,而不是使用NoSQL软件。只有少数几个大型网站出于很好的原因而使用它们。 - Dan Grossman

1

一般来说,你不应该为了性能而牺牲数据库的完整性。

关于这个问题,我首先会创建一个有100万行虚拟数据的表格,并使用诸如ab的压力测试工具测试一些典型查询。很可能结果会表明它的性能表现良好 - 对于MySQL来说,100万行数据易如反掌。因此,在尝试解决问题之前,请确保您确实遇到了问题。

如果您发现性能较差并且数据库确实成为瓶颈,请考虑一般优化,例如缓存(在所有级别上,从MySQL查询缓存到HTML缓存),获取更好的硬件等。这在大多数情况下都可以解决问题。


这是个好观点。我想我会拿现有的数据并测试所有建议的解决方案。乍一看,EAV 看起来有些问题,因为我无法想象带有40个条件的搜索会如何进行。答案可能在 symcbean 的回答中,但我不是很擅长 SQL,需要进行一些研究才能理解它。感谢大家的贡献。 - pandronic

0

没有看到数据库结构,很难提出建议。通常情况下,MySQL 数据库必须规范化至少到 3NF 或 BCNF。但目前看来,一个表有100列,似乎并没有被规范化。

此外 - 你可以使用事务和 INNODB 引擎轻松地使用外键来强制执行引用完整性。


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