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 JOIN, и он должен возвращать результаты довольно быстро (скажем < 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')

ВОПРОСЫ:

  • Какие варианты лучше/быстрее? Может быть, есть лучший способ сделать это?

a) 4 таблицы (permissions, companies_permissions, user_groups_permissions, users_permissions) b) 2 таблицы (permissions, permissables)

  1. Нужно ли объявлять разные индексы, чем btree на permissions.item_type?

  2. Мне нужно запускать несколько раз в день vacuum analyze для таблиц, чтобы заставить индексы работать (обе опции)?


EDIT1:

Примеры SQLFiddle:

{Я также удалил backticks в неправильных местах благодаря @wildplasser}

Ответ 1

Я бы рекомендовал абстрагировать весь доступ к вашей системе разрешений на пару классов моделей. К сожалению, я обнаружил, что такие системы разрешений иногда становятся узкими местами производительности, и я обнаружил, что иногда необходимо значительно реорганизовать ваше представление данных. Итак, моя рекомендация заключается в том, чтобы попытаться сохранить запросы, связанные с разрешением, изолированными в нескольких классах и попытаться сохранить интерфейс для этих классов независимо от остальной системы.

Примеры хороших подходов здесь - это то, что у вас есть выше. Вы фактически не присоединяетесь к таблице тем; у вас уже есть теги тем, которые вам нравятся при создании разрешений.

Примерами плохих интерфейсов будут интерфейсы классов, которые упрощают объединение таблиц разрешений в произвольный другой SQL.

Я понимаю, что вы задали вопрос с точки зрения SQL, а не конкретной структуры поверх SQL, но из имен ограничений рельсов похоже, что вы используете такую ​​структуру, и я думаю, что использование ее будет полезно ваша будущая надежность кода.

В случае с 10 000 строк я думаю, что любой из этих подходов будет работать нормально. Я на самом деле не уверен, что все подходы будут разными. Если вы думаете о сгенерированных планах запросов, предполагая, что вы получаете небольшое количество строк из таблицы, объединение может обрабатываться с помощью цикла против каждой таблицы точно так же, как или запрос может обрабатываться, если предположить, что индекс скорее всего, вернет небольшое количество строк. Я не подал правдоподобный набор данных в Postgres, чтобы выяснить, действительно ли это действительно дает реальный набор данных. У меня достаточно высокая уверенность в том, что Postgres достаточно умен, чтобы сделать это, если имеет смысл это сделать.

Полиморфный подход дает вам немного больше контроля, и если вы столкнулись с проблемами производительности, вы можете проверить, поможет ли его перемещение. Если вы выберете полиморфный подход, я бы рекомендовал написать код для проверки и убедиться, что ваши данные согласованы. То есть убедитесь, что resource_type и resource_id соответствуют фактическим ресурсам, которые существуют в вашей системе. Я бы сделал эту рекомендацию в любом случае, когда приложение касается вас, чтобы вы денормализовали свои данные, так что ограничений базы данных недостаточно для обеспечения согласованности.

Если вы столкнулись с проблемами производительности, вот что вам нужно сделать в будущем:

  • Создайте кэш в своих приложениях, сопоставляя объекты (например, темы) с набором разрешений для этих объектов.

  • Создайте кэш в приложении, кэшируя все разрешения, предоставляемые данным пользователем (включая группы, в которых они являются членами) для объектов вашего приложения.

  • Материализация прав пользователей. Это создает материализованное представление, которое объединяет разрешения user_group с разрешениями пользователя и членством в группах пользователей.

По моему опыту вещь, которая действительно убивает производительность систем разрешений, - это когда вы добавляете что-то вроде того, что одна группа может быть членом другой группы. В этот момент вы очень быстро дойдете до точки, где вам понадобятся кэширование или материализованные представления.

К сожалению, очень сложно дать более конкретные рекомендации, не имея фактически ваших данных и глядя на реальные планы запросов и реальную производительность. Я думаю, что если вы будете готовиться к будущим изменениям, все будет хорошо.

Ответ 2

Может быть, это очевидный ответ, но я думаю, что вариант с 3 таблицами должен быть в порядке. SQL-базы данных хороши при выполнении операций join, и у вас есть 10 000 записей - это не большой объем данных, поэтому я не уверен, что заставляет вас думать, что будет проблема с производительностью.

С соответствующими индексами (btree должно быть в порядке), он должен работать быстро, и на самом деле вы можете пойти немного дальше и сгенерировать образцы данных для ваших таблиц и посмотреть, как ваш запрос действительно работает на реальном количестве данных.

Я также не думаю, что вам нужно будет беспокоиться о чем-то вроде ручного вакуума вручную.

Что касается опции 2, полиморфной таблицы, она может быть не очень хорошей, так как теперь у вас есть одно поле resource_id, которое может указывать на разные таблицы, которые являются источником проблем (например, из-за ошибки, которую вы можете получить запись с resource_type = User и resource_id, указывающая на Company - структура таблицы не мешает ей).

Еще одно замечание: вы ничего не говорите о взаимоотношениях между User, UserGropup и Company - если они все связаны друг с другом, может быть возможно получить разрешения только с использованием идентификаторов пользователей, присоединения также gropus и компаний к пользователям.

И еще одно: вам не нужно id во многих таблицах, ничего плохого не происходит, если у вас есть, но достаточно иметь permission_id и user_id и сделать их составными первичными ключами.

Ответ 3

Вы можете попытаться денормализовать отношения "многие ко многим" в поле разрешений в каждой из трех таблиц (user, user_group, company).

Вы можете использовать это поле для хранения разрешений в формате JSON и использовать его только для чтения (SELECT). Вы все равно можете использовать таблицы "многие-ко-многим" для изменения разрешений конкретных пользователей, групп и компаний, просто напишите триггер на них, который будет обновлять поля разрешенных разрешений всякий раз, когда есть новое изменение для многих-ко-многим Таблица. С помощью этого решения вы все равно получите быстрое время выполнения запросов на SELECT, сохраняя нормализацию отношений и соблюдая стандарты базы данных.

Вот пример script, который я написал для mysql для отношения "один ко многим", но аналогичная вещь может быть применена и для вашего случая:

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

Я использовал этот подход несколько раз, и имеет смысл, когда инструкции SELECT превосходят и более важны, чем инструкции INSERT, UPDATE и DELETE.

Ответ 4

Если вы не часто меняете свои разрешения, материализованные представления могут значительно увеличить ваш поиск. Сегодня я подготовлю пример, основанный на вашей настройке, и опубликую его. Впоследствии мы можем сделать некоторый бенчмарк.

Тем не менее, материализованные представления требуют обновления материализованного представления после изменения данных. Таким образом, решение может быть быстрым, но ускорит ваши запросы только в том случае, если базовые данные не будут меняться так часто.