Selezionare tutti i record, unire con la tabella A se esiste un join, in caso contrario la tabella B.


20

Quindi, ecco il mio scenario:

Sto lavorando su Localization per un mio progetto e in genere vorrei fare questo nel codice C #, tuttavia voglio farlo un po 'di più in SQL poiché sto cercando di potenziare un po' il mio SQL.

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

Nota: il linguaggio di programmazione stesso dovrebbe essere irrilevante, lo sto solo includendo per completezza.

Quindi ho realizzato quello che volevo, ma non nella misura in cui volevo. È passato un po 'di tempo (almeno un anno) da quando ho fatto qualsiasi SQL JOINtranne quelli di base, e questo è piuttosto complesso JOIN.

Ecco uno schema delle tabelle pertinenti del database. (Ce ne sono molti altri, ma non necessari per questa porzione.)

Diagramma del database

Tutte le relazioni descritte nell'immagine sono complete nel database: i vincoli PKe FKsono tutti impostati e funzionanti. Nessuna delle colonne descritte è in nullgrado. Tutte le tabelle hanno lo schema dbo.

Ora, ho una query che fa quasi quello che voglio: cioè, dato QUALSIASI ID di SupportCategoriese QUALSIASI ID di Languages, restituirà:

Se c'è una traduzione destro adeguata per tale lingua per quella stringa (Ie StringKeyId-> StringKeys.Idesiste, e in LanguageStringTranslations StringKeyId, LanguageIde StringTranslationIdesiste combinazione, allora carichi StringTranslations.Textper quella StringTranslationId.

Se la LanguageStringTranslations StringKeyId, LanguageIde StringTranslationIdcombinazione ha NON esiste, allora carica il StringKeys.Namevalore. Il Languages.Idè un dato integer.

La mia domanda, sia essa un casino, è la seguente:

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

Il problema è che non è in grado di fornire me ALL del SupportCategoriese la loro rispettiva StringTranslations.Textse esiste, o il loro StringKeys.Namese non esistesse. È perfetto per fornire uno di loro, ma per niente. Fondamentalmente, è per imporre che se una lingua non ha una traduzione per una chiave specifica, l'impostazione predefinita è quella StringKeys.Nameche è di StringKeys.DefaultLanguageIdtraduzione. (Idealmente, non lo farebbe nemmeno, ma caricherà invece la traduzione StringKeys.DefaultLanguageId, cosa che posso fare da solo se puntato nella direzione giusta per il resto della query.)

Ho trascorso MOLTO tempo su questo, e so che se dovessi semplicemente scriverlo in C # (come faccio di solito) ormai sarebbe stato fatto. Voglio farlo in SQL e ho problemi a ottenere l'output che mi piace.

L'unica avvertenza è che voglio limitare il numero di query effettive applicate. Tutte le colonne sono indicizzate e come quelle che mi piacciono per ora, e senza prove di stress reali non posso indicizzarle ulteriormente.

Modifica: un'altra nota, sto cercando di mantenere il database il più possibile normalizzato, quindi non voglio duplicare le cose se posso evitarlo.

Dati di esempio

fonte

dbo.SupportCategories (intero):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 record, mostrandone solo due per esempi):

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

dbo.LanguagesStringTranslations (Entirety):

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 (Entirety):

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 (Entirety):

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

Uscita corrente

Data la query esatta di seguito, produce:

Result
Billing

Uscita desiderata

Idealmente, vorrei essere in grado di omettere lo specifico SupportCategories.Ide di ottenerli tutti, così (indipendentemente dal fatto che Englishfosse usata la lingua 38 , o 48 French, o QUALSIASI altra lingua al momento):

Id  Result
0   Billing
1   API
2   Sales

Esempio aggiuntivo

Dato che dovevo aggiungere una localizzazione per French(cioè aggiungere 1 48 8a LanguageStringTranslations), l'output cambierebbe in (nota: questo è solo un esempio, ovviamente aggiungerei una stringa localizzata a StringTranslations) (aggiornato con l'esempio francese):

Result
Les APIs

Uscita desiderata aggiuntiva

Dato l'esempio sopra, sarebbe desiderato il seguente output (aggiornato con l'esempio francese):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Sì, so tecnicamente che è sbagliato dal punto di vista della coerenza, ma è ciò che sarebbe auspicabile nella situazione.)

Modificare:

Piccolo aggiornato, ho modificato la struttura della dbo.Languagestabella e lasciato cadere la Id (int)colonna da essa, sostituendola con Abbreviation(che ora è stata rinominata Ide tutte le relative chiavi esterne e le relazioni aggiornate). Da un punto di vista tecnico, questa è un'impostazione più appropriata a mio avviso a causa del fatto che la tabella è limitata ai codici ISO 639-1, che sono univoci per cominciare.

Tl; dr

Quindi: la questione, come potrei modificare questo query per restituire tutto da SupportCategoriese poi tornare sia StringTranslations.Textper quella StringKeys.Id, Languages.Idcombinazione, o il StringKeys.Namecaso lo ha fatto non esiste?

Il mio pensiero iniziale è che potrei in qualche modo trasmettere la query corrente a un altro tipo temporaneo come un'altra subquery e racchiudere questa query in un'altra SELECTenunciazione e selezionare i due campi che desidero ( SupportCategories.Ide Result).

Se non trovo nulla, farò semplicemente il metodo standard che di solito uso, che è quello di caricare tutto SupportCategoriesnel mio progetto C #, e quindi eseguo la query che ho sopra manualmente su ciascuno SupportCategories.Id.

Grazie per qualsiasi suggerimento / commento / critica.

Inoltre, mi scuso per il fatto che sia assurdamente lungo, ma non voglio alcuna ambiguità. Sono spesso su StackOverflow e vedo domande che mancano di sostanza, non volevo fare questo errore qui.

Risposte:


16

Ecco il primo approccio che mi è venuto in mente:

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;

Fondamentalmente, ottieni le stringhe potenziali che corrispondono alla lingua scelta e ottieni tutte le stringhe predefinite, quindi aggregale in modo da sceglierne solo una per Idpriorità sulla lingua scelta, quindi prendere il valore predefinito come fallback.

Probabilmente puoi fare cose simili con UNION/ EXCEPTma sospetto che questo porterà quasi sempre a più scansioni contro gli stessi oggetti.


12

Una soluzione alternativa che evita il INe il raggruppamento nella risposta di 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;

Come notato, il FORCESEEKsuggerimento è richiesto solo per ottenere il piano dall'aspetto più efficiente a causa della bassa cardinalità della LanguageStringTranslationstabella con i dati di esempio forniti. Con più righe, l'ottimizzatore sceglierà un indice di ricerca naturale.

Il piano di esecuzione stesso ha una caratteristica interessante:

Progetto esecutivo

La proprietà Pass Through sull'ultimo join esterno significa che una ricerca nella StringTranslationstabella viene eseguita solo se in precedenza era stata trovata una riga nella LanguageStringTranslationstabella. Altrimenti, il lato interno di questo join viene completamente ignorato per la riga corrente.

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

Dati di esempio

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.