问题描述
我们正在和一些朋友一起做一个移动应用程序,但是由于未知,我们在数据库结构方面遇到了问题。我认为这是一个可以帮助很多人的好问题,如果有知识的人可以好好解释一下。该应用程序包括向客户提供各种服务(将来可以添加更多服务)。他们已登录并有权访问我们的服务。起初,我们想到了一个包含所有客户数据和服务的列的表。然后我们看到创建另一个名为“services”的单独表并通过 id 标识用户更有效。现在问题出现在这张桌子上。我们不知道是将所有服务(例如数组)制作成一列还是每个服务制作一列。我拍了一张照片,以便更容易观察我的提议。
问题在于,就性能而言,这些选项中的哪一个(显然可能还有三分之一我们没有考虑过)是最好的。 我认为第二个选项我看到了几个缺陷,但我不确定。在延迟和速度方面,遍历数组(如果添加服务则更多,或者可能因为用户先租用 service2 然后 1 而乱序)比选项 1 中的要高得多。此外,事实是 a用户在服务下,这意味着遍历整个数组,寻找它并消除它。不知道各位高手,你们有什么推荐吗?所有这些都会上传到云端(azure),所以所有的请求都会上传到云端
解决方法
选项 2 比选项 1 好。但是,就尊重而言,它仍然不好。
永远不要在数据列中存储逗号分隔的事物列表。如果你这样做,你会后悔的。 (搜索它们的成本非常高。)
你想要这样的东西。三个表,一个用于用户,另一个用于服务,还有一个所谓的 JOIN 表,用于在两者之间建立多对多关系。
+-----------+ +-------------+ +-----------+
|user | |user_service | |service |
+-----------+ +-------------+ +-----------+
|user_id +--->|user_id |<----+service_id |
|givennamee | |service_id | |name |
|surname | +-------------+ +-----------+
|is_active |
+-----------+
user_service
中的每一行表示用户有权使用该服务。要授权用户,请插入一行。要撤销授权,请删除该行。
要了解用户是否可以使用服务,请使用此查询。
SELECT user.user_id
FROM user
JOIN user_service USING (user_id)
JOIN service USING (service_id)
WHERE user.givenname = 'Bill' AND user.surname='Gates'
AND service.name = 'CharityNavigator'
AND user.is_active > 0;
如果您的查询返回 user_id
,则所选用户可以使用所选服务。
要获取每个用户的服务列表,请使用此查询。
SELECT user.user_id,user.givenname,user.surname,GROUP_CONCAT(service.name) service_names
FROM user
JOIN user_service USING (user_id)
JOIN service USING (service_id)
WHERE user.is_active > 0
GROUP BY user.user_id
一些解释: 几乎总是最好为诸如服务之类的内容构建带有行的表,而不是列或以逗号分隔的列中的列表。为什么?
-
您可以添加新服务 - 随心所欲 - 数年后无需重新编写数据库代码。
-
DBMS,包括 MySQL,可以很好地处理 JOIN 操作。
-
在大多数关系数据库管理系统中,执行
WHERE commalist_column SOMEHOW_CONTAINS (some_id)
的效率低得令人厌恶。执行WHERE column = some_id
的效率要高得多,因为它可以使用索引。 -
列数少的行通常比列数多的行效果更好。
-
在生产环境中向数据库添加行比添加列要便宜得多。添加列意味着更改表定义。该操作可能需要停机。
当您将列用于服务等内容时,您正在创建一个封闭的系统。当您使用行时,您的系统是开放式的。
我可以建议您阅读有关 database normalization 的内容吗?不要被所有的 CS 术语吓倒。看看如何规范化各种数据库的一些例子。
也许读过entity-relationship database modeling?
编辑 根据评论者的建议,我建议您使 user_service
表的主键包含两列 (user_id,service_id)
。我还建议您使用两列 (service_id,user_id)
创建反向索引,以便您的查询可以从服务和用户开始快速查找。您的表定义可能如下所示:
CREATE TABLE user (
user_id INT UNSIGNED NOT NULL AUTO_INCREMENT,givenname VARCHAR(50) NULL DEFAULT NULL,surname VARCHAR(50) NULL DEFAULT NULL,is_active TINYINT NOT NULL DEFAULT '1',PRIMARY KEY (user_id)
)
COLLATE='utf8mb4_general_ci';
CREATE TABLE service (
service_id INT UNSIGNED NOT NULL AUTO_INCREMENT,name VARCHAR(50) NULL DEFAULT NULL,PRIMARY KEY (service_id)
)
COLLATE='utf8mb4_general_ci';
CREATE TABLE user_service (
user_id INT UNSIGNED NOT NULL,service_id INT UNSIGNED NOT NULL,PRIMARY KEY (user_id,service_id),INDEX reverse_index (service_id,user_id),CONSTRAINT FK_service
FOREIGN KEY (service_id)
REFERENCES service (service_id)
ON UPDATE RESTRICT ON DELETE RESTRICT,CONSTRAINT FK_user
FOREIGN KEY (user_id)
REFERENCES user (user_id)
ON UPDATE RESTRICT ON DELETE RESTRICT
);
如果您尝试使用此主键为用户插入重复的服务授权,dbms 会拒绝它。
请务必在这些表中使用相同的“INT UNSIGNED NOT NULLdata type for
user_idand
service_id”。
这是一种非常常见的数据库设计模式:它是在两个不同表的行之间创建多对多关系的规范方式。
,第三种方式(最节省空间)
请参阅 SET
数据类型。它允许说明这 6 个服务的哪个组合适用。
INT UNSIGNED
(大小合适)是另一种拥有“集合”的方式。
SET 或 TINYINT 只需要 1 个字节就可以表示最多 8 个项目。
您的 6 列选择需要 6 个字节。
“{serv1,... }”可能是一个 VARCHAR
,平均为 10-20 个字节。
所以,我的建议显然是为了节省空间。但也许这并不重要?你有数百万或行吗?你有更多的 tnan 64“服务”吗? (SET 和 BIGINT UNSIGNED 的限制为 64。)
但是哪个?
是关于编码的问题吗?好吧,任何方法都需要付出一些努力才能将位/列/字符串分开以在屏幕上构建按钮。可能需要类似的工作量,而且可能比构建屏幕的工作量少。性能同上。
我强烈建议您选择两种解决方案并同时实施。你会发现
- 它们在性能、代码数量等方面的相似程度。
- 这个问题多么微不足道。
- 您对数据库了解了多少额外的知识。
- “尝试”和“放弃”另一种做某事的方式是多么容易。
- 延迟、性能等差异如何微不足道。 (这就是我们真正为您解答的问题。)
大局
您已经指出了此数据结构的一种用途。我担心这种数据结构有或将会有其他用途。其他因素才是决定哪种方法最好的真正决定因素。 (到时候,你就可以愉快地复活被丢弃的版本了!)
第四种方式
JSON。但它会比你的 VARCHAR 方式更冗长(占用更多空间)。它可能更容易使用,也可能不会更容易 - 这取决于其余要求。