View, выбирающий похожие данные из нескольких таблиц, разделенных по типу
У меня есть несколько таблиц, реализующих схему БД некоторой CMS:
cms.CatalogEntryобозначает любую (всех типов) запись в каталоге.cms.CatalogEntryTypeописывает тип сущности (Artist/Genre/Release/Product/другой), связан с первой с помощью внешнего ключаCatalogEntryTypeIdв ней.dbo.Artistдля полей, специфичных для исполнителя (Name,Description).dbo.Releaseдля полей, специфичных для музыкального релиза (Title,Description,CatalogNumber).- Несколько других таблиц, практически повторяющих две последние. Каждая из подобных таблиц имеет свой собственный
Idи связывается черезCatalogEntryIdс упомянутой вышеcms.CatalogEntry.Id.
Теперь мне нужно создать View, который возвращал бы мне пары (EntityId, CatalogEntryId), где EntityId может быть любым из ключей таблиц-расширений dbo.Artist.Id/dbo.Release.Id/другим, а также некоторую другую общую информацию (тип и DisplayName). У меня есть две основные идеи, как это можно реализовать:
Первая идея - собрать через LEFT JOIN все (~15) типизированные по сущностям таблицы расширений и потом использовать COALESCE, чтобы выбрать колонку, значение из которой использовать.
CREATE VIEW [cms].[CatalogEntryToEntityMap]
WITH SCHEMABINDING
AS
SELECT
[catalogEntry].[Id] AS [CatalogEntryId],
[catalogEntry].[CatalogEntryTypeId] AS [CatalogEntryTypeId],
COALESCE(
[genre].[Id],
[artist].[Id],
N'00000000-0000-0000-0000-000000000000') AS [EntityId],
COALESCE(
[genre].[Name],
[artist].[Name],
N'N/A') AS [EntityDisplayName]
FROM [cms].[CatalogEntry] AS [catalogEntry]
LEFT JOIN [dbo].[Genre] AS [genre] ON [genre].[CatalogEntryId] = [catalogEntry].[Id]
LEFT JOIN [dbo].[Artist] AS [artist] ON [artist].[CatalogEntryId] = [catalogEntry].[Id];
Вторая идея - использовать UNION ALL, чтобы собрать конечный результат из типизированных по сущности подмножеств:
CREATE VIEW [cms].[CatalogEntryToEntityMap]
WITH SCHEMABINDING
AS
SELECT
[genre].[Id] AS [EntityId],
[genre].[CatalogEntryId],
[genre].[Name] AS [EntityDisplayName],
[catalogEntry].[CatalogEntryTypeId]
FROM [dbo].[Genre] AS [genre]
INNER JOIN [cms].[CatalogEntry] AS [catalogEntry]
ON [catalogEntry].[Id] = [genre].[CatalogEntryId]
AND [catalogEntry].[CatalogEntryTypeId] = N'DEVELOPMENT-TIME KNOWN GUID'
UNION ALL
SELECT
[artist].[Id] AS [EntityId],
[artist].[CatalogEntryId],
[artist].[Name] AS [EntityDisplayName],
[catalogEntry].[CatalogEntryTypeId]
FROM [dbo].[Artist] AS [artist]
INNER JOIN [cms].[CatalogEntry] AS [catalogEntry]
ON [catalogEntry].[Id] = [artist].[CatalogEntryId]
AND [catalogEntry].[CatalogEntryTypeId] = N'ANOTHER DEVELOPMENT-TIME KNOWN GUID';
Или по-другому:
CREATE VIEW [cms].[CatalogEntryToEntityMap]
WITH SCHEMABINDING
AS
SELECT
[entity].[Id] AS [EntityId],
[entity].[DisplayName] AS [EntityDisplayName],
[catalogEntry].[Id] AS [CatalogEntryId],
[catalogEntry].[CatalogEntryTypeId] AS [CatalogEntryTypeId]
FROM [cms].[CatalogEntry] AS [catalogEntry]
INNER JOIN
(
SELECT [genre].[Id], [genre].[CatalogEntryId], [genre].[Name] AS [DisplayName] FROM [dbo].[Genre] AS [genre]
UNION ALL
SELECT [artist].[Id], [artist].[CatalogEntryId], [artist].[Name] AS [DisplayName] FROM [dbo].[Artist] AS [artist]
)
AS [entity] ON [entity].[CatalogEntryId] = [catalogEntry].[Id];
Я посмотрел на планы исполнения для всех трех, в последних двух вроде бы нет каких-то очевидных подвохов, почти все происходит через индексы на базовых таблицах. В первом плане есть сомнения по поводу повторяющихся джоинов, но тут, вроде бы, ничего не поделать.
Меня немного беспокоит, что я не могу создатть индексы ни на одном из трех (на первом из-за LEFT JOIN и на вторых из-за UNION ALL и вложенных селектов), количество данных может быть довольно большим, и мне придется делать запросы к этому VIEW на регулярной основе практически на каждом GET-запросе сущности из каталога. Я не планирую писать какие-то сложные селекты, основаные на этом представлении, но я должен иметь возможность INNER JOIN-ить его на множества ключей (CatalogEntryId и EntityId), по которым мне нужны пары.
Видит ли кто-то какие-то дополнительные проблемы с представленными решениями? Меня по большей части интересует производительность.
Я также открыт к другим решениям, если кому-то придет в голову способ сделать это эффективнее.
P.S. Перевод моего вопроса c англоязычного SO.
Ответы (1 шт):
Сомневаюсь, что кто-то за меня напишет полноценный ответ, поэтому...
В конечном итоге остановился на варианте 3:
CREATE VIEW [cms].[CatalogEntryToEntityMap]
WITH SCHEMABINDING
AS
SELECT
[entity].[Id] AS [EntityId],
[entity].[DisplayName] AS [EntityDisplayName],
[catalogEntry].[Id] AS [CatalogEntryId],
[catalogEntry].[CatalogEntryTypeId] AS [CatalogEntryTypeId]
FROM [cms].[CatalogEntry] AS [catalogEntry]
INNER JOIN
(
SELECT [genre].[Id], [genre].[CatalogEntryId], [genre].[Name] AS [DisplayName] FROM [dbo].[Genre] AS [genre]
UNION ALL
SELECT [artist].[Id], [artist].[CatalogEntryId], [artist].[Name] AS [DisplayName] FROM [dbo].[Artist] AS [artist]
)
AS [entity] ON [entity].[CatalogEntryId] = [catalogEntry].[Id];
В плане, сгенерированном для него, отсутствуют повторяющиеся LEFT JOIN-ы, как в первом, и при этом он более лаконичный, чем второй.
Для тестирования создал 3 типа сущностей, заполнив таблицы тестовыми данными по 100к записей в каждой.
Фактический план выполнения запроса, выгребающего все представление:
SELECT * FROM [cms].[CatalogEntryToEntityMap];
То же самое, но с дополнительной фильтрацией через INNER JOIN на список CatalogEntryId:
DECLARE @CatalogEntryIds AS [dbo].[GuidArray];
INSERT INTO @CatalogEntryIds ([Value])
SELECT [Id]
FROM [cms].[CatalogEntry]
WHERE [Id] LIKE N'00%'; -- Все GUID-ы, начинающиеся с 00
SELECT *
FROM [cms].[CatalogEntryToEntityMap] AS [map]
INNER JOIN @CatalogEntryIds AS [id] ON [id].[Value] = [map].[CatalogEntryId];
Как можно заметить, предварительная фильтрация для уменьшения объема данных применяется как на cms.CatalogEntry, так и на присоединяемых dbo.Artist/Genre/Release. Также видно, что везде применяются индексы используемых для формирования представления таблиц, полных сканов таблиц не происходит.
Отдельно хочу выразить благодарность @AlexanderPetrov за ссылку на реализацию наследования средствами EF Core. В версии 6.x доступны только маппинги Table Per Hierarchy и Table Per Type, оба из которых мне не очень подходили, поскольку первый превращает таблицу в "мусорку" из всего возможного для иерархии сущностей набора полей, а второй имеет страдающую производительность. В версии 7.x также появилась Table Per Concrete type, которая должна генерировать схему БД, примерно схожую с моей.




