Виберіть усі записи, приєднайтеся до таблиці A, якщо приєднання існує, таблиці B якщо ні


20

Тож ось мій сценарій:

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

Навколишнє середовище: Стандарт SQL Server 2014, C # (.NET 4.5.1)

Примітка: сама мова програмування має бути неактуальною, я включаю її лише для повноти.

Тож я начебто здійснив те, що хотів, але не в тій мірі, яку хотів. Минув деякий час (щонайменше рік), оскільки я робив будь-які SQL JOIN, крім базових, і це досить складно JOIN.

Ось схема відповідних таблиць бази даних. (Є ще багато, але не потрібно для цієї порції.)

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

Всі відносини , описані в зображенні є повними в БД - PKі FKобмеження все настройки і експлуатації. Жоден із описаних стовпців не nullвдається. Усі таблиці мають схему dbo.

Тепер у мене є запит, який майже робить те, що я хочу: тобто, з урахуванням БУДЬ-якого ІД SupportCategoriesта будь-якого ІД 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.SupportКатегорії (цілий):

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 (Цілий):

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 (цілий):

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 (Цілий):

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, які є унікальними для початку.

Тл; д-р

Отже: питання про те , як я міг змінити цей запит, який повертає всі від SupportCategoriesі потім повертати або StringTranslations.Textдля того StringKeys.Id, Languages.Idкомбінації, абоStringKeys.Name якщо вона НЕ існує?

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

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

Дякую за будь-які пропозиції та коментарі / критику.

Крім того, прошу вибачення за те, що це абсурдно довго, я просто не хочу ніякої двозначності. Я часто буваю на StackOverflow і бачу питання, яким не вистачає суті, не хотів би тут помилитися.

Відповіді:


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таблиці з наданими зразками даних. З більшою кількістю рядків оптимізатор вибере природно шукати індекс.

Сам план виконання має цікаву особливість:

План виконання

Властивість "Пройти через" на останньому зовнішньому з'єднанні означає, що пошук у 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.