PostgreSQL - "полиморфная таблица" vs 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 (+indexes declaration)
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
s, и он должен возвращать результаты довольно быстро (скажем,
Я думал об объявлении 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')
Вопросы:
- какие варианты лучше / быстрее? Может быть, есть лучший способ сделать это?
А) 4 таблицы (permissions, companies_permissions, user_groups_permissions, users_permissions
)
Б) 2 таблицы (permissions, permissables
)
-
Нужно ли объявлять другие индексы, чем
btree
наpermissions.item_type
? -
Нужно ли запускать несколько раз в день
vacuum analyze
для таблиц, чтобы индексы работали (оба варианта)?
EDIT1:
Примеры SQLFiddle:
- предложение wildplasser (из комментария), не работает: http://sqlfiddle.com/#!15 / 9723f8 / 1
- исходный запрос (4 таблицы): http://sqlfiddle.com/#!15 / 9723f8 / 2
{ я также убрал backticks в неправильных местах благодаря @wildplasser }
4 ответа:
Я бы рекомендовал абстрагировать весь доступ к вашей системе разрешений на пару классов моделей. К сожалению, я обнаружил, что системы разрешений, подобные этой, иногда оказываются узкими местами производительности, и я обнаружил, что иногда необходимо значительно переработать представление данных. Итак, моя рекомендация состоит в том, чтобы попытаться изолировать запросы, связанные с разрешением, в нескольких классах и попытаться сохранить интерфейс к этим классам независимым от остальной части система.
Примеры хороших подходов вот то, что вы имеете выше. На самом деле вы не объединяетесь с таблицей "темы"; У вас уже есть идентификаторы тем, которые вас интересуют при создании разрешений.
Примерами плохих интерфейсов могут быть интерфейсы классов, которые позволяют легко объединять таблицы разрешений в произвольные другие SQL.
Я понимаю, что вы задали вопрос в терминах SQL, а не конкретной структуры поверх SQL, но из ограничения rails похоже, что вы используете такой фреймворк, и я думаю, что использование его будет полезно для вашей будущей ремонтопригодности кода.
В случае 10 000 строк, я думаю, что любой подход будет работать нормально. На самом деле я не уверен, что подходы будут настолько разными. Если вы думаете о созданных планах запросов, предполагая, что вы получаете небольшое количество строк из таблицы, соединение может быть обработано циклом для каждой таблицы точно так же, как запрос or может обрабатываться при условии, что индекс, скорее всего, вернет небольшое число строк. Я не скармливал правдоподобный набор данных в Postgres, чтобы выяснить, действительно ли это то, что он делает, учитывая реальный набор данных. У меня достаточно высокая уверенность в том, что Postgres достаточно умна, чтобы сделать это, если это имеет смысл.
Полиморфный подход дает вам немного больше контроля, и если вы столкнетесь с проблемами производительности, вы можете проверить, поможет ли вам переход к нему. Если вы выберете полиморфный подход, я бы рекомендовал написать код, чтобы пройти и проверить, чтобы убедиться, что ваши данные непротиворечивы. То есть убедитесь, что resource_type и resource_id соответствуют реальным ресурсам, существующим в вашей системе. Я бы сделал эту рекомендацию в любом случае, когда прикладные проблемы заставляют вас денормализовать ваши данные таким образом, что ограничений базы данных недостаточно для обеспечения согласованности.Если вы начинаете сталкиваться с проблемами производительности, вот какие вещи вы возможно, потребуется сделать в будущем:
По моему опыту, то, что действительно убивает производительность систем разрешений, - это когда вы добавляете что-то вроде разрешения одной группе быть членом другой группы. В этот момент Вы очень быстро доберетесь до точки, где вам нужно кэширование или материализованные представления.
Создайте в приложении кэш, сопоставляющий объекты (например, разделы) с набором разрешений для этих объектов.
Создание кэша в приложении кэширование всех разрешений данного пользователя (включая группы, в которые он входит) для объектов в приложении.
Материализация разрешений группы пользователей. То есть создать материализованное представление, которое сочетает в себе европейский уровень, уровень разрешения с разрешения пользователя и членство в группах пользователей.
К сожалению, очень трудно дать более конкретный совет, не имея на самом деле ваших данных и не глядя на реальные планы запросов и реальную производительность. Я думайте,что если вы будете готовиться к будущим изменениям, то все будет хорошо.
Возможно, это очевидный ответ, но я думаю, что вариант с 3 таблицами должен быть просто прекрасным. Базы данных SQL хороши для выполнения операций
join
, и у вас есть 10 000 записей - это не большой объем данных вообще, поэтому я не уверен, что заставляет вас думать, что будет проблема производительности.С правильными индексами (btree должен быть в порядке), он должен работать быстро, и на самом деле вы можете пойти немного дальше и генерировать образцы данных для ваших таблиц и посмотреть, как ваш запрос на самом деле работает на реальном объем данных.
Я также не думаю, что вам нужно беспокоиться о чем-то вроде запуска вакуума вручную.
Что касается второго варианта, полиморфной таблицы, то он может быть не очень хорош, поскольку теперь у вас есть одно полеresource_id
, которое может указывать на разные таблицы, что является источником проблем (например, из - за ошибки у вас может быть запись с resource_type=User
и resource_id, указывающая наCompany
- структура таблицы не препятствует этому).Еще одно замечание: вы ничего не говорите об отношениях между Пользователем, UserGropup и компанией - если все они тоже связаны, то можно получить разрешения просто с помощью идентификаторов пользователей, присоединив также gropus и компании к пользователям.
И еще одно: вам не нужны
id
s во многих-многих таблицах, ничего плохого не случится, если они у вас есть, но достаточно иметьpermission_id
иuser_id
и сделать их составным первичным ключом.
Можно попытаться денормализовать отношения "многие ко многим" в поле разрешений для каждой из 3 таблиц (user, user_group, company).
Это поле можно использовать для хранения разрешений в формате JSON и использовать его только для чтения (SELECTs). Вы все еще можете использовать таблицы "многие ко многим" для изменения разрешений конкретных пользователей, групп и компаний, просто напишите триггер на них, который обновит денормализованное поле разрешений всякий раз, когда будет новое изменение на поле "многие ко многим". стол. С помощью этого решения вы все равно получите быстрое время выполнения запроса на выбор, сохраняя при этом нормализованные отношения и соответствие стандартам базы данных.
Вот пример скрипта, который я написал для mysql для отношения один ко многим, но аналогичная вещь может быть применена и для вашего случая:
Https://github.com/martintaleski/mysql-denormalization/blob/master/one-to-many.sql
Я использовал этот подход несколько раз, и он делает смысл, когда операторы SELECT превосходят по количеству и более важны, чем операторы INSERT, UPDATE и DELETE.
Если вы не часто меняете свои разрешения, материализованные представления могут значительно ускорить ваш поиск. Я подготовлю пример, основанный на вашей установке, позже сегодня и опубликую его. После этого мы можем сделать некоторые отметки.
Тем не менее, материализованные представления требуют обновления материализованного представления после изменения данных. Таким образом, это решение может быть быстрым, но ускорит ваши запросы только в том случае, если основные данные не изменяются так часто.