web-dev-qa-db-fra.com

Sélectionnez tous les enregistrements, joignez-vous à la table A si la jointure existe, sinon la table B

Voici donc mon scénario:

Je travaille sur la localisation pour un de mes projets, et en général j'allais le faire dans le code C #, mais je veux le faire un peu plus en SQL car j'essaye de buff un peu mon SQL.

Environnement: SQL Server 2014 Standard, C # (.NET 4.5.1)

Remarque: le langage de programmation lui-même ne devrait pas être pertinent, je ne l'inclus que pour être complet.

J'ai donc en quelque sorte accompli ce que je voulais, mais pas autant que je le voulais. Cela fait un moment (au moins un an) que je n'ai fait aucun SQL JOINs sauf les basiques, et c'est assez complexe JOIN.

Voici un diagramme des tableaux pertinents de la base de données. (Il y en a beaucoup plus, mais pas nécessaire pour cette portion.)

Database Diagramme

Toutes les relations décrites dans l'image sont complètes dans la base de données - les contraintes PK et FK sont toutes configurées et opérationnelles. Aucune des colonnes décrites n'est nullable. Toutes les tables ont le schéma dbo.

Maintenant, j'ai une requête qui presque fait ce que je veux: c'est-à-dire, étant donné [~ # ~] tout [~ # ~] Id de SupportCategories et [~ # ~] tout [~ # ~] Id de Languages, il renverra soit:

S'il existe une traduction correcte pour cette langue pour cette chaîne (Ie StringKeyId -> StringKeys.Id Existe et dans LanguageStringTranslationsStringKeyId, LanguageId et StringTranslationId existe, puis il charge StringTranslations.Text pour cette StringTranslationId.

Si la combinaison LanguageStringTranslationsStringKeyId, LanguageId et StringTranslationId ne PAS existait , puis il charge la valeur StringKeys.Name. Le Languages.Id Est un integer donné.

Ma requête, que ce soit un gâchis, est la suivante:

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

Le problème est qu'il n'est pas capable de me fournir TOUS des SupportCategories et leurs StringTranslations.Text Respectifs si cela existe, OU leur StringKeys.Name s'il n'existait pas. Il est parfait pour fournir l'un d'eux, mais pas du tout. Fondamentalement, c'est pour imposer que si une langue n'a pas de traduction pour une clé spécifique, la valeur par défaut est d'utiliser StringKeys.Name Qui est de la traduction de StringKeys.DefaultLanguageId. (Idéalement, il ne ferait même pas cela, mais chargerait plutôt la traduction de StringKeys.DefaultLanguageId, Ce que je peux faire moi-même s'il pointe dans la bonne direction pour le reste de la requête.)

J'ai passé BEAUCOUP de temps à ce sujet, et je sais que si je devais simplement l'écrire en C # (comme je le fais habituellement), ce serait fait maintenant. Je veux le faire en SQL et j'ai du mal à obtenir la sortie que j'aime.

La seule mise en garde, c'est que je veux limiter le nombre de requêtes réelles appliquées. Toutes les colonnes sont indexées et telles que je les aime pour l'instant, et sans véritable test de stress, je ne peux pas les indexer davantage.

Edit: Une autre note, j'essaie de garder la base de données aussi normalisée que possible, donc je ne veux pas dupliquer les choses si je peux l'éviter.

données d'exemple

Source

dbo.SupportCategories (Intégralité):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 enregistrements, dont deux par exemple):

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

dbo.LanguagesStringTranslations (Intégralité):

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 (Intégralité):

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 (Intégralité):

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

sortie actuelle

Étant donné la requête exacte ci-dessous, il génère:

Result
Billing

sortie souhaitée

Idéalement, j'aimerais pouvoir omettre le SupportCategories.Id Spécifique, et les obtenir tous comme tels (peu importe si la langue 38 English a été utilisée, ou 48 French, ou [~ # ~] toute [~ # ~] autre langue pour le moment):

Id  Result
0   Billing
1   API
2   Sales

Exemple supplémentaire

Étant donné que je devais ajouter une localisation pour French (c'est-à-dire ajouter 1 48 8 À LanguageStringTranslations), la sortie changerait en (note: ceci n'est qu'un exemple, évidemment j'ajouterais un chaîne localisée en StringTranslations) (mise à jour avec l'exemple français):

Result
Les APIs

Sortie souhaitée supplémentaire

Étant donné l'exemple ci-dessus, la sortie suivante serait souhaitée (mise à jour avec l'exemple français):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Oui, je sais techniquement que c'est faux du point de vue de la cohérence, mais c'est ce qui serait souhaité dans la situation.)

Éditer:

Petite mise à jour, j'ai changé la structure de la table dbo.Languages, J'ai supprimé la colonne Id (int), et je l'ai remplacée par Abbreviation (qui est maintenant renommée Id, et toutes les clés étrangères et relations relatives mises à jour). D'un point de vue technique, c'est une configuration plus appropriée à mon avis, car le tableau est limité aux codes ISO 639-1, qui sont uniques au départ.

Tl; dr

Donc: la question, comment pourrais-je modifier cette requête pour retourner tout de SupportCategories puis retourner soit StringTranslations.Text Pour cette combinaison StringKeys.Id, Languages.Id, ou le StringKeys.Name si c'est le cas N'EXISTE PAS ?

Ma pensée initiale est que je pourrais en quelque sorte convertir la requête actuelle en un autre type temporaire en tant que autre sous-requête, et envelopper cette requête dans une autre instruction SELECT et sélectionner les deux champs que je veux (SupportCategories.Id Et Result).

Si je ne trouve rien, je vais simplement utiliser la méthode standard que j'utilise généralement, qui consiste à charger tous les SupportCategories dans mon projet C #, puis à exécuter la requête que j'ai ci-dessus manuellement contre chaque SupportCategories.Id.

Merci pour toutes suggestions/commentaires/critiques.

De plus, je m'excuse pour sa longueur absurde, je ne veux tout simplement aucune ambiguïté. Je suis souvent sur StackOverflow et je vois des questions qui manquent de substance, je ne voulais pas faire cette erreur ici.

20
Der Kommissar

Voici la première approche que j'ai trouvée:

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;

Fondamentalement, obtenez les chaînes potentielles qui correspondent à la langue choisie et obtenez toutes les chaînes par défaut, puis agrégez de sorte que vous n'en choisissez qu'une par Id - donnez la priorité à la langue choisie, puis prenez la valeur par défaut comme solution de rechange.

Vous pouvez probablement faire des choses similaires avec UNION/EXCEPT mais je soupçonne que cela mènera presque toujours à plusieurs analyses contre les mêmes objets.

16
Aaron Bertrand

Une solution alternative qui évite le IN et le regroupement dans la réponse d'Aaron:

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;

Comme indiqué, l'indicateur FORCESEEK n'est requis que pour obtenir le plan le plus efficace en raison de la faible cardinalité de la table LanguageStringTranslations avec les exemples de données fournis. Avec plus de lignes, l'optimiseur choisira naturellement une recherche d'index.

Le plan d'exécution lui-même présente une caractéristique intéressante:

Execution Plan

La propriété Pass Through sur la dernière jointure externe signifie qu'une recherche dans la table StringTranslations n'est effectuée que si une ligne a déjà été trouvée dans la table LanguageStringTranslations. Sinon, le côté intérieur de cette jointure est complètement ignoré pour la ligne actuelle.

Tableau 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)
);

Exemples de données

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
12
Paul White 9