问题 PostgreSQL - “多态表”与3个表


我正在使用PostgreSQL 9.5(但升级可以说是9.6)。

我有权限表:

CREATE TABLE public.permissions
(
  id integer NOT NULL DEFAULT nextval('permissions_id_seq'::regclass),
  item_id integer NOT NULL,
  item_type character varying NOT NULL,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,
  CONSTRAINT permissions_pkey PRIMARY KEY (id)
)
-- skipping indices declaration, but they would be present
-- on item_id, item_type

3个表用于多对多关联

-companies_permissions(+指数声明)

CREATE TABLE public.companies_permissions
(
  id integer NOT NULL DEFAULT nextval('companies_permissions_id_seq'::regclass),
  company_id integer,
  permission_id integer,
  CONSTRAINT companies_permissions_pkey PRIMARY KEY (id),
  CONSTRAINT fk_rails_462a923fa2 FOREIGN KEY (company_id)
      REFERENCES public.companies (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_rails_9dd0d015b9 FOREIGN KEY (permission_id)
      REFERENCES public.permissions (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

CREATE INDEX index_companies_permissions_on_company_id
  ON public.companies_permissions
  USING btree
  (company_id);

CREATE INDEX index_companies_permissions_on_permission_id
  ON public.companies_permissions
  USING btree
  (permission_id);

CREATE UNIQUE INDEX index_companies_permissions_on_permission_id_and_company_id
  ON public.companies_permissions
  USING btree
  (permission_id, company_id);

-permissions_user_groups(+索引声明)

CREATE TABLE public.permissions_user_groups
(
  id integer NOT NULL DEFAULT nextval('permissions_user_groups_id_seq'::regclass),
  permission_id integer,
  user_group_id integer,
  CONSTRAINT permissions_user_groups_pkey PRIMARY KEY (id),
  CONSTRAINT fk_rails_c1743245ea FOREIGN KEY (permission_id)
      REFERENCES public.permissions (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_rails_e966751863 FOREIGN KEY (user_group_id)
      REFERENCES public.user_groups (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

CREATE UNIQUE INDEX index_permissions_user_groups_on_permission_and_user_group
  ON public.permissions_user_groups
  USING btree
  (permission_id, user_group_id);

CREATE INDEX index_permissions_user_groups_on_permission_id
  ON public.permissions_user_groups
  USING btree
  (permission_id);

CREATE INDEX index_permissions_user_groups_on_user_group_id
  ON public.permissions_user_groups
  USING btree
  (user_group_id);

-permissions_users(+索引声明)

CREATE TABLE public.permissions_users
(
  id integer NOT NULL DEFAULT nextval('permissions_users_id_seq'::regclass),
  permission_id integer,
  user_id integer,
  CONSTRAINT permissions_users_pkey PRIMARY KEY (id),
  CONSTRAINT fk_rails_26289d56f4 FOREIGN KEY (user_id)
      REFERENCES public.users (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_rails_7ac7e9f5ad FOREIGN KEY (permission_id)
      REFERENCES public.permissions (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

CREATE INDEX index_permissions_users_on_permission_id
  ON public.permissions_users
  USING btree
  (permission_id);

CREATE UNIQUE INDEX index_permissions_users_on_permission_id_and_user_id
  ON public.permissions_users
  USING btree
  (permission_id, user_id);

CREATE INDEX index_permissions_users_on_user_id
  ON public.permissions_users
  USING btree
  (user_id);

我将不得不像这样运行SQL查询很多次:

SELECT
"permissions".*,
"permissions_users".*,
"companies_permissions".*,
"permissions_user_groups".* 
FROM "permissions"
LEFT OUTER JOIN
  "permissions_users" ON "permissions_users"."permission_id" = "permissions"."id"
LEFT OUTER JOIN
  "companies_permissions" ON "companies_permissions"."permission_id" = "permissions"."id"
LEFT OUTER JOIN
  "permissions_user_groups" ON "permissions_user_groups"."permission_id" = "permissions"."id"
WHERE
  (companies_permissions.company_id = <company_id> OR
  permissions_users.user_id in (<user_ids> OR NULL) OR
  permissions_user_groups.user_group_id IN (<user_group_ids> OR NULL)) AND
permissions.item_type = 'Topic' 

假设我们在其他表中拥有大约10000多个权限和类似数量的记录。

我需要担心性能吗?

我的意思是......我有4个 LEFT OUTER JOINs它应该很快返回结果(比如说<200ms)。

我正在考虑声明1个“多态”表,例如:

CREATE TABLE public.permissables
(
  id integer NOT NULL DEFAULT nextval('permissables_id_seq'::regclass),
  permission_id integer,
  resource_id integer NOT NULL,
  resource_type character varying NOT NULL,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,
  CONSTRAINT permissables_pkey PRIMARY KEY (id)
)
-- skipping indices declaration, but they would be present

然后我可以像这样运行查询:

SELECT
  permissions.*,
  permissables.*
FROM permissions
LEFT OUTER JOIN
  permissables ON permissables.permission_id = permissions.id
WHERE
  permissions.item_type = 'Topic' AND
  (permissables.owner_id IN (<user_ids>) AND permissables.owner_type = 'User') OR
  (permissables.owner_id = <company_id> AND permissables.owner_type = 'Company') OR
  (permissables.owner_id IN (<user_groups_ids>) AND permissables.owner_type = 'UserGroup')

问题:

  1. 哪个选项更好/更快?也许有更好的方法来做到这一点?

a)4桌(permissions, companies_permissions, user_groups_permissions, users_permissions) b)2张桌子(permissions, permissables

  1. 我需要声明不同的索引吗? btree 上 permissions.item_type ?

  2. 我是否需要每天跑几次 vacuum analyze 表使索引工作(两个选项)?


EDIT1:

SQLFiddle示例:

  1. wildplasser建议(来自评论),不工作: http://sqlfiddle.com/#!15/9723f8/1
  2. 原始查询(4表): http://sqlfiddle.com/#!15/9723f8/2

{我也在错误的地方删除了反引号,感谢@wildplasser}


1656
2018-05-16 21:28


起源

尝试将参考LEFT JOINed表的条件移动到ON ...部分。这将避免所有丑陋 OR NULL在WHERE子句中。并且不要使用反引号作为文字。 - wildplasser
@wildplasser感谢您的回复。是的,我知道backtips(OS X添加了它们而不是单引号)...我尝试了你的解决方案 - 但我得到了更好的结果。 SQLfiddle - > sqlfiddle.com/#!15/9723f8/1 - nothing-special-here
而 old 版本有效 - sqlfiddle.com/#!15/9723f8/2。也 INNER JOIN 不会在这里工作。我需要得到原始版本的东西 (condition OR condition2 OR condition3)。 - nothing-special-here


答案:


我建议将对权限系统的所有访问权限抽象为几个模型类。不幸的是,我发现像这样的权限系统有时最终会成为性能瓶颈,而且我发现有时需要对数据表示进行重要的重构。 因此,我的建议是尝试将与权限相关的查询保留在几个类中,并尝试将接口保持为独立于系统其余部分的那些类。

这里的好方法的例子就是你上面的内容。您实际上并未加入主题表;在构建权限时,您已经拥有了关注的主题ID。

坏接口的示例是类接口,可以很容易地将权限表连接到任意其他SQL。

我理解你用SQL而不是基于SQL的特定框架提出问题,但是从rails约束名称看起来你正在使用这样的框架,我认为利用它对你未来的代码很有用可维护性。

在10,000行的情况下,我认为任何一种方法都可以正常工作。 我真的不确定这些方法会有什么不同。如果您考虑生成的查询计划,假设您从表中获取了少量行,则可以使用针对每个表的循环来处理连接,其方式与处理或查询的方式完全相同,假设索引可能会返回少量行。 我没有给Postgres提供合理的数据集,以确定这是否是它给出真实数据集的实际效果。我有相当高的信心,如果这样做有意义,Postgres足够聪明。

多态方法确实可以让您获得更多控制权,如果您遇到性能问题,您可能需要检查移动它是否会有所帮助。 如果您选择多态方法,我建议您编写代码并检查以确保您的数据一致。也就是说,确保resource_type和resource_id对应于系统中存在的实际资源。 在任何情况下,我都会提出建议,因为应用程序问题会迫使您对数据进行非规范化,这样数据库约束就不足以强制实现一致性。

如果您开始遇到性能问题,以下是您将来可能需要做的事情:

  • 在应用程序中创建缓存,将对象(例如主题)映射到这些对象的权限集。

  • 在应用程序中创建缓存,缓存给定用户拥有的所有权限(包括他们所属的组),以用于应用程序中的对象。

  • 实现用户组权限。这是创建一个物化视图,它将user_group权限与用户权限和用户组成员身份相结合。

根据我的经验,真正杀死权限系统性能的是当你添加类似允许一个组成为另一个组的成员的东西时。此时,您很快就会到达需要缓存或物化视图的位置。

不幸的是,在没有实际拥有数据并查看真实查询计划和真实性能的情况下,提供更具体的建议真的很难。我认为,如果你为未来的改变做准备,你会没事的。


4
2018-05-26 21:30





也许这是一个明显的答案,但我认为3个表的选项应该没问题。 SQL数据库很擅长 join 操作,你有10,000条记录 - 这根本不是大量的数据,所以我不确定是什么让你认为会出现性能问题。

使用适当的索引(btree应该没问题),它应该可以快速工作,实际上您可以更进一步,为您的表生成示例数据,并查看您的查询实际上如何处理实际数据量。

我也不认为你需要担心像手动运行真空一样。

关于选项二,多态表,它可能不是很好,因为你现在有单一 resource_id 可以指出不同表的字段,这是一个问题的来源(例如,由于一个错误,你可以使用resource_type =User 和resource_id指向 Company  - 表结构不会阻止它)。

还有一点需要注意:你没有告诉用户,UserGropup和公司之间的关系 - 如果它们全部相关,也可以仅使用用户ID获取权限,也可以将gropus和公司加入用户。

还有一个:你不需要 id在许多表格中,如果你拥有它们,没有什么不好的事情发生,但它已经足够了 permission_id 和 user_id并使它们成为复合主键。


4
2018-05-25 20:01



我同意。还有一件事:如果你有一个索引 (permission_id, company_id),你不需要一个 (permission_id)。 - Laurenz Albe


您可以尝试在3个表(user,user_group,company)中的每个表的权限字段中对多对多关系进行非规范化。

您可以使用此字段以JSON格式存储权限,并仅将其用于读取(SELECT)。您仍然可以使用多对多表来更改特定用户,组和公司的权限,只需在它们上面写一个触发器,只要多对多数据发生新的更改,就会更新非规范化的权限字段表。使用此解决方案,您仍然可以在SELECT上获得快速查询执行时间,同时保持关系规范化并符合数据库标准。

这是一个示例脚本,我为mysql编写的一对多关系,但类似的东西也适用于你的情况:

https://github.com/martintaleski/mysql-denormalization/blob/master/one-to-many.sql

我已经多次使用过这种方法,当SELECT语句数量超过INSERT,UPDATE和DELETE语句时更有意义。


2
2018-05-26 21:52





如果您不经常更改权限,物化视图可能会极大地加快您的搜索速度。我将在今天晚些时候根据您的设置准备一个示例并将其发布。之后,我们可以做一些基准测试。

然而,物化视图需要在更改数据后更新物化视图。因此,该解决方案可能会很快,但只有在基本数据不经常更改时才会加快查询速度。


2
2018-05-27 07:19