Выбрать все записи, объединить с таблицей A, если объединение существует, с таблицей B, если нет

20

Итак, вот мой сценарий:

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

Среда: SQL Server 2014 Standard, C # (.NET 4.5.1)

Примечание: сам язык программирования не имеет значения, я включаю его только для полноты.

Так что я выполнил то, что хотел, но не так, как хотел. Прошло много времени (по крайней мере, год) с тех пор, как я выполнил любые SQL, JOINкроме базовых, и это довольно сложно JOIN.

Вот схема соответствующих таблиц базы данных. (Есть еще много, но не обязательно для этой части.)

Диаграмма базы данных

Все отношения, описанные в образе, завершены в базе данных - PKи FKограничения и все настроены и работают. Ни один из описанных столбцов не в nullсостоянии. Все таблицы имеют схему dbo.

Теперь у меня есть запрос, который почти выполняет то, что я хочу: то есть, учитывая ЛЮБОЙ Id SupportCategoriesи ЛЮБОЙ Id Languages, он вернет либо:

Если есть правый правильный перевод этого языка для этой строки (Ie StringKeyId-> StringKeys.Idсуществует, и LanguageStringTranslations StringKeyId, LanguageIdи StringTranslationIdкомбинация существует, то он загружает StringTranslations.Textдля этого StringTranslationId.

Если LanguageStringTranslations StringKeyId, LanguageIdи StringTranslationIdкомбинация ничего НЕ существует, то он загружает StringKeys.Nameзначение. Это Languages.Idдано integer.

Мой запрос, будь то беспорядок, выглядит следующим образом:

SELECT CASE WHEN T.x IS NOT NULL THEN T.x ELSE (SELECT
    CASE WHEN dbo.StringTranslations.Text IS NULL THEN dbo.StringKeys.Name ELSE dbo.StringTranslations.Text END AS Result
FROM dbo.SupportCategories
    INNER JOIN dbo.StringKeys
        ON dbo.SupportCategories.StringKeyId = dbo.StringKeys.Id
    INNER JOIN dbo.LanguageStringTranslations
        ON dbo.StringKeys.Id = dbo.LanguageStringTranslations.StringKeyId
    INNER JOIN dbo.StringTranslations
        ON dbo.StringTranslations.Id = dbo.LanguageStringTranslations.StringTranslationId
WHERE dbo.LanguageStringTranslations.LanguageId = 38 AND dbo.SupportCategories.Id = 0) END AS Result FROM (SELECT (SELECT
    CASE WHEN dbo.StringTranslations.Text IS NULL THEN dbo.StringKeys.Name ELSE dbo.StringTranslations.Text END AS Result
FROM dbo.SupportCategories
    INNER JOIN dbo.StringKeys
        ON dbo.SupportCategories.StringKeyId = dbo.StringKeys.Id
    INNER JOIN dbo.LanguageStringTranslations
        ON dbo.StringKeys.Id = dbo.LanguageStringTranslations.StringKeyId
    INNER JOIN dbo.StringTranslations
        ON dbo.StringTranslations.Id = dbo.LanguageStringTranslations.StringTranslationId
WHERE dbo.LanguageStringTranslations.LanguageId = 5 AND dbo.SupportCategories.Id = 0) AS x) AS T

Проблема заключается в том, что она не способна обеспечить меня все из SupportCategoriesи их соответствующей , StringTranslations.Textесли она существует, или их , StringKeys.Nameесли бы его не существовало. Это идеально подходит для обеспечения любого из них, но не на всех. В основном, это для обеспечения , что если язык не имеет перевода для определенного ключа, то по умолчанию будет использовать , StringKeys.Nameкоторый имеет StringKeys.DefaultLanguageIdперевод. (В идеале, он даже не сделал бы этого, но вместо этого загрузил бы перевод StringKeys.DefaultLanguageId, который я мог бы сделать сам, если бы указывал в правильном направлении для остальной части запроса.)

Я потратил много времени на это, и я знаю, если бы я просто написал это на C # (как я обычно делаю), это было бы сделано к настоящему времени. Я хочу сделать это в SQL, и у меня возникают проблемы с получением результата, который мне нравится.

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

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

Пример данных

Источник

dbo.SupportCategories (Целостность):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 записей, показаны только два примера):

Id  Abbreviation    Family  Name    Native
38  en  Indo-European   English English
48  fr  Indo-European   French  français, langue française

dbo.LanguagesStringTranslations (Completety):

StringKeyId LanguageId  StringTranslationId
0   38  0
1   38  1
2   38  2
3   38  3
4   38  4
5   38  5
6   38  6
7   38  7
1   48  8 -- added as example

dbo.StringKeys (Completety):

Id  Name    DefaultLanguageId
0   Billing 38
1   API 38
2   Sales   38
3   Open    38
4   Waiting for Customer    38
5   Waiting for Support 38
6   Work in Progress    38
7   Completed   38

dbo.StringTranslations (Completety):

Id  Text
0   Billing
1   API
2   Sales
3   Open
4   Waiting for Customer
5   Waiting for Support
6   Work in Progress
7   Completed
8   Les APIs -- added as example

Текущий выход

Учитывая точный запрос ниже, он выводит:

Result
Billing

Желаемый вывод

В идеале я хотел бы иметь возможность пропустить конкретные SupportCategories.Idи получить все из них, как это так (независимо от того, был ли использован язык 38 English, или 48 French, или ЛЮБОЙ другой язык в данный момент):

Id  Result
0   Billing
1   API
2   Sales

Дополнительный пример

Учитывая, что я должен был добавить локализацию для French(т.е. добавить 1 48 8к LanguageStringTranslations), вывод изменится на (примечание: это только пример, очевидно, я бы добавил локализованную строку в StringTranslations) (обновлено на французском примере):

Result
Les APIs

Дополнительный желаемый выход

Учитывая приведенный выше пример, был бы желателен следующий вывод (обновленный на французском примере):

Id  Result
0   Billing
1   Les APIs
2   Sales

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

Редактировать:

Небольшое обновление. Я изменил структуру dbo.Languagesтаблицы, удалил Id (int)из нее столбец и заменил его Abbreviation(который теперь переименован Id, а все относительные внешние ключи и и отношения обновлены). С технической точки зрения, на мой взгляд, это более подходящая установка, поскольку таблица ограничена кодами ISO 639-1, которые с самого начала являются уникальными.

Tl; др

Итак: вопрос о том , как я мог изменить этот запрос, возвращающий все от SupportCategoriesи затем возвращать либо StringTranslations.Textдля того StringKeys.Id, Languages.Idкомбинации, илиStringKeys.Name если она НЕ существует?

Моя первоначальная мысль, что я мог бы как-то привести текущий запрос к другому временному типу в качестве другого подзапроса, обернуть этот запрос еще одним SELECTоператором и выбрать два поля, которые я хочу ( SupportCategories.Idи Result).

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

Спасибо за любые предложения / комментарии / критику.

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

Der Kommissar
источник

Ответы:

16

Вот первый подход, который я предложил:

DECLARE @ChosenLanguage INT = 48;

SELECT sc.Id, Result = MAX(COALESCE(
   CASE WHEN lst.LanguageId = @ChosenLanguage      THEN st.Text END,
   CASE WHEN lst.LanguageId = sk.DefaultLanguageId THEN st.Text END)
)
FROM dbo.SupportCategories AS sc
INNER JOIN dbo.StringKeys AS sk
  ON sc.StringKeyId = sk.Id
LEFT OUTER JOIN dbo.LanguageStringTranslations AS lst
  ON sk.Id = lst.StringKeyId
  AND lst.LanguageId IN (sk.DefaultLanguageId, @ChosenLanguage)
LEFT OUTER JOIN dbo.StringTranslations AS st
  ON st.Id = lst.StringTranslationId
  --WHERE sc.Id = 1
  GROUP BY sc.Id
  ORDER BY sc.Id;

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

Вы, вероятно, можете делать подобные вещи с UNION/, EXCEPTно я подозреваю, что это почти всегда приведет к многократному сканированию одних и тех же объектов.

Аарон Бертран
источник
12

Альтернативное решение, которое позволяет избежать INгруппирования в ответе Аарона:

DECLARE 
    @SelectedLanguageId integer = 48;

SELECT 
    SC.Id,
    SC.StringKeyId,
    Result =
        CASE
            -- No localization available
            WHEN LST.StringTranslationId IS NULL
            THEN SK.Name
            ELSE
            (
                -- Localized string
                SELECT ST.[Text]
                FROM dbo.StringTranslations AS ST
                WHERE ST.Id = LST.StringTranslationId
            )
        END
FROM dbo.SupportCategories AS SC
JOIN dbo.StringKeys AS SK
    ON SK.Id = SC.StringKeyId
LEFT JOIN dbo.LanguageStringTranslations AS LST
    WITH (FORCESEEK) -- Only for low row count in sample data
    ON LST.StringKeyId = SK.Id
    AND LST.LanguageId = @SelectedLanguageId;

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

Сам план выполнения имеет интересную особенность:

План выполнения

Свойство Pass Through в последнем внешнем соединении означает, что поиск в StringTranslationsтаблице выполняется только в том случае, если в LanguageStringTranslationsтаблице ранее была найдена строка . В противном случае внутренняя сторона этого соединения полностью пропускается для текущей строки.

Стол DDL

CREATE TABLE dbo.Languages
(
    Id integer NOT NULL,
    Abbreviation char(2) NOT NULL,
    Family nvarchar(96) NOT NULL,
    Name nvarchar(96) NOT NULL,
    [Native] nvarchar(96) NOT NULL,

    CONSTRAINT PK_dbo_Languages
        PRIMARY KEY CLUSTERED (Id)
);

CREATE TABLE dbo.StringTranslations
(
    Id bigint NOT NULL,
    [Text] nvarchar(128) NOT NULL,

    CONSTRAINT PK_dbo_StringTranslations
    PRIMARY KEY CLUSTERED (Id)
);

CREATE TABLE dbo.StringKeys
(
    Id bigint NOT NULL,
    Name varchar(64) NOT NULL,
    DefaultLanguageId integer NOT NULL,

    CONSTRAINT PK_dbo_StringKeys
    PRIMARY KEY CLUSTERED (Id),

    CONSTRAINT FK_dbo_StringKeys_DefaultLanguageId
    FOREIGN KEY (DefaultLanguageId)
    REFERENCES dbo.Languages (Id)
);

CREATE TABLE dbo.SupportCategories
(
    Id integer NOT NULL,
    StringKeyId bigint NOT NULL,

    CONSTRAINT PK_dbo_SupportCategories
        PRIMARY KEY CLUSTERED (Id),

    CONSTRAINT FK_dbo_SupportCategories
    FOREIGN KEY (StringKeyId)
    REFERENCES dbo.StringKeys (Id)
);

CREATE TABLE dbo.LanguageStringTranslations
(
    StringKeyId bigint NOT NULL,
    LanguageId integer NOT NULL,
    StringTranslationId bigint NOT NULL,

    CONSTRAINT PK_dbo_LanguageStringTranslations
    PRIMARY KEY CLUSTERED 
        (StringKeyId, LanguageId, StringTranslationId),

    CONSTRAINT FK_dbo_LanguageStringTranslations_StringKeyId
    FOREIGN KEY (StringKeyId)
    REFERENCES dbo.StringKeys (Id),

    CONSTRAINT FK_dbo_LanguageStringTranslations_LanguageId
    FOREIGN KEY (LanguageId)
    REFERENCES dbo.Languages (Id),

    CONSTRAINT FK_dbo_LanguageStringTranslations_StringTranslationId
    FOREIGN KEY (StringTranslationId)
    REFERENCES dbo.StringTranslations (Id)
);

Образец данных

INSERT dbo.Languages
    (Id, Abbreviation, Family, Name, [Native])
VALUES
    (38, 'en', N'Indo-European', N'English', N'English'),
    (48, 'fr', N'Indo-European', N'French', N'français, langue française');

INSERT dbo.StringTranslations
    (Id, [Text])
VALUES
    (0, N'Billing'),
    (1, N'API'),
    (2, N'Sales'),
    (3, N'Open'),
    (4, N'Waiting for Customer'),
    (5, N'Waiting for Support'),
    (6, N'Work in Progress'),
    (7, N'Completed'),
    (8, N'Les APIs'); -- added as example

INSERT dbo.StringKeys
    (Id, Name, DefaultLanguageId)
VALUES
    (0, 'Billing', 38),
    (1, 'API', 38),
    (2, 'Sales', 38),
    (3, 'Open', 38),
    (4, 'Waiting for Customer', 38),
    (5, 'Waiting for Support', 38),
    (6, 'Work in Progress', 38),
    (7, 'Completed', 38);

INSERT dbo.SupportCategories
    (Id, StringKeyId)
VALUES
    (0, 0),
    (1, 1),
    (2, 2);

INSERT dbo.LanguageStringTranslations
    (StringKeyId, LanguageId, StringTranslationId)
VALUES
    (0, 38, 0),
    (1, 38, 1),
    (2, 38, 2),
    (3, 38, 3),
    (4, 38, 4),
    (5, 38, 5),
    (6, 38, 6),
    (7, 38, 7),
    (1, 48, 8); -- added as example
Пол Уайт говорит, что GoFundMonica
источник