Wählen Sie alle Datensätze aus, verbinden Sie sie mit Tabelle A, wenn eine Verknüpfung vorhanden ist, und Tabelle B, wenn nicht

20

Also hier ist mein Szenario:

Ich arbeite an der Lokalisierung für ein Projekt von mir, und normalerweise würde ich dies im C # -Code tun, aber ich möchte dies in SQL ein bisschen mehr tun, da ich versuche, mein SQL ein bisschen aufzupolstern.

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

Hinweis: Die Programmiersprache selbst sollte irrelevant sein, ich beziehe sie nur der Vollständigkeit halber ein.

Also habe ich irgendwie erreicht, was ich wollte, aber nicht in dem Maße, wie ich wollte. Es ist eine Weile her (mindestens ein Jahr), dass ich keine SQL- JOINAnweisungen außer den grundlegenden ausgeführt habe, und dies ist ziemlich komplex JOIN.

Hier ist ein Diagramm der relevanten Tabellen der Datenbank. (Es gibt viel mehr, aber nicht notwendig für diese Portion.)

Database Diagramme

Alle im Bild beschriebenen Beziehungen sind in der Datenbank vollständig - die PKund FKEinschränkungen sind alle eingerichtet und in Betrieb. Keine der beschriebenen Spalten ist in der nullLage. Alle Tabellen haben das Schema dbo.

Jetzt habe ich eine Abfrage, die beinahe das macht, was ich will: mit JEDER ID von SupportCategoriesund JEDER ID vonLanguages , wird entweder zurückgegeben:

Wenn es eine rechts richtige Übersetzung für diese Sprache für diese Saite (Ie StringKeyId-> StringKeys.Idexistiert, und in LanguageStringTranslations StringKeyId, LanguageIdund StringTranslationIdKombination vorhanden ist , dann lädt es StringTranslations.Textfür dieStringTranslationId .

Wenn das LanguageStringTranslations StringKeyId, LanguageIdund StringTranslationIdKombination ist nicht vorhanden ist , dann gibt er den StringKeys.NameWert. Das Languages.Idist eine Selbstverständlichkeitinteger .

Meine Frage, sei es ein Chaos, lautet wie folgt:

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

Das Problem ist , dass es nicht in der Lage ist , mich zu schaffen ALL von der SupportCategoriesund deren jeweilige StringTranslations.Textwenn es vorhanden ist , oder ihr , StringKeys.Namewenn sie nicht existiert. Es ist perfekt für die Bereitstellung eines von ihnen, aber überhaupt nicht. Grundsätzlich gilt: Wenn eine Sprache keine Übersetzung für einen bestimmten Schlüssel hat, wird standardmäßig StringKeys.Namedie StringKeys.DefaultLanguageIdÜbersetzung verwendet. (Idealerweise würde es das gar nicht machen, sondern die Übersetzung für ladenStringKeys.DefaultLanguageId , die ich selbst machen kann, wenn ich für den Rest der Abfrage in die richtige Richtung zeige.)

Ich habe viel Zeit damit verbracht, und ich weiß, wenn ich es einfach in C # schreiben würde (wie ich es normalerweise tue), wäre es jetzt erledigt. Ich möchte dies in SQL tun, und ich habe Probleme, die Ausgabe zu bekommen, die ich mag.

Die einzige Einschränkung ist, dass ich die Anzahl der tatsächlich angewendeten Abfragen begrenzen möchte. Alle Spalten sind indiziert und so, wie ich sie jetzt mag, und ohne wirklichen Stresstest kann ich sie nicht weiter indizieren.

Bearbeiten: Ein weiterer Hinweis, ich versuche, die Datenbank so normal wie möglich zu halten, damit ich Dinge nicht duplizieren möchte, wenn ich es vermeiden kann.

Beispieldaten

Quelle

dbo.SupportCategories (Entirety):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 Datensätze, davon nur zwei als Beispiele):

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

dbo.LanguagesStringTranslations (Entirity):

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

Aktueller Output

In Anbetracht der genauen Abfrage unten wird Folgendes ausgegeben:

Result
Billing

Gewünschte Ausgabe

Idealerweise möchte ich in der Lage sein, das Spezifische wegzulassen SupportCategories.Idund alle davon zu erhalten (unabhängig davon, ob die Sprache 38 Englishoder 48 Frenchoder JEDE andere Sprache im Moment verwendet wurde):

Id  Result
0   Billing
1   API
2   Sales

Zusätzliches Beispiel

Wenn ich eine Lokalisierung für Frenchhinzufügen würde (dh hinzufügen 1 48 8zu LanguageStringTranslations), würde sich die Ausgabe ändern zu (Hinweis: Dies ist nur ein Beispiel, offensichtlich würde ich eine lokalisierte Zeichenfolge hinzufügen zu StringTranslations) (aktualisiert mit französischem Beispiel):

Result
Les APIs

Zusätzlicher gewünschter Output

In Anbetracht des obigen Beispiels wäre die folgende Ausgabe wünschenswert (aktualisiert mit französischem Beispiel):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Ja, ich weiß, dass dies technisch gesehen falsch ist, aber es ist das, was in der Situation erwünscht wäre.)

Bearbeiten:

Klein aktualisiert, habe ich die Struktur der dbo.LanguagesTabelle geändert und die Id (int)Spalte daraus entfernt und durch Abbreviation(die jetzt in umbenannt Idwird und alle relativen Fremdschlüssel und Beziehungen aktualisiert) ersetzt. Aus technischer Sicht ist dies meines Erachtens ein geeigneterer Aufbau, da die Tabelle auf ISO 639-1-Codes beschränkt ist, die zunächst einmalig sind.

Tl; dr

Also: die Frage, wie könnte ich diese Abfrage ändern, um alles von zurückzugeben SupportCategoriesund dann entweder StringTranslations.Textfür das StringKeys.Id, die Languages.IdKombination oder das zurückzugeben, StringKeys.Namewenn es NICHT existiert?

Mein erster Gedanke ist, dass ich die aktuelle Abfrage irgendwie in einen anderen temporären Typ als eine andere Unterabfrage umwandeln und diese Abfrage in eine weitere SELECTAnweisung einschließen und die beiden gewünschten Felder ( SupportCategories.Idund Result) auswählen könnte .

Wenn ich nichts finde, benutze ich einfach die Standardmethode, die ich normalerweise verwende, um alles SupportCategoriesin mein C # -Projekt zu laden , und führe dann die oben angegebene Abfrage manuell für jedes Objekt aus SupportCategories.Id.

Vielen Dank für alle Vorschläge / Kommentare / Kritik.

Außerdem entschuldige ich mich dafür, dass es absurd lang ist, ich will einfach keine Mehrdeutigkeit. Ich bin oft auf StackOverflow und sehe Fragen, denen es an Substanz mangelt. Diesen Fehler wollte ich hier nicht machen.

410_Vergangen
quelle

Antworten:

16

Hier ist der erste Ansatz, den ich mir ausgedacht habe:

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;

Ermitteln Sie im Grunde genommen die potenziellen Zeichenfolgen, die der ausgewählten Sprache entsprechen, und ermitteln Sie alle Standardzeichenfolgen. Führen Sie dann eine Aggregation durch, sodass Sie nur eine pro auswählenId Priorisierung für die ausgewählte Sprache und dann die Standardeinstellung als Fallback verwenden.

Sie können wahrscheinlich ähnliche Dinge mit UNION/ tun, EXCEPTaber ich vermute, dass dies fast immer zu mehreren Scans auf dieselben Objekte führt.

Aaron Bertrand
quelle
12

Eine alternative Lösung, die die INund die Gruppierung in Aarons Antwort vermeidet :

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;

Wie bereits erwähnt, ist der FORCESEEKHinweis aufgrund der geringen Kardinalität der LanguageStringTranslationsTabelle mit den bereitgestellten Beispieldaten nur erforderlich, um einen möglichst effizient aussehenden Plan zu erhalten . Bei mehr Zeilen wählt der Optimierer eine Indexsuche auf natürliche Weise aus.

Der Ausführungsplan selbst hat eine interessante Funktion:

Ausführungsplan

Die Pass Through-Eigenschaft bei der letzten äußeren Verknüpfung bedeutet, dass eine Suche nach der StringTranslations Tabelle nur durchgeführt wird, wenn zuvor eine Zeile in der LanguageStringTranslationsTabelle gefunden wurde. Andernfalls wird die Innenseite dieses Joins für die aktuelle Zeile vollständig übersprungen.

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

Beispieldaten

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
Paul White sagt GoFundMonica
quelle