web-dev-qa-db-ja.com

すべてのレコードを選択し、結合が存在する場合はテーブルAと結合し、存在しない場合はテーブルBと結合します

これが私のシナリオです:

私は私のプロジェクトのローカリゼーションに取り組んでおり、通常はC#コードでこれを実行しますが、SQLを少しバフにしようとしているので、SQLでこれをもう少し実行したいと思います。

環境:SQL Server 2014 Standard、C#(.NET 4.5.1)

注:プログラミング言語自体は無関係である必要があります。私は完全を期すためだけに含めています。

だから、私は自分が望んでいたことをある程度達成したが、望んだほどではなかった。基本的なもの以外のSQL JOINsを実行してからしばらく(少なくとも1年)、これは非常に複雑なJOINです。

これは、データベースの関連テーブルの図です。 (もっとたくさんありますが、この部分には必要ありません。)

Database Diagramme

画像で説明されているすべての関係はデータベースで完全です。PKFKの制約はすべて設定されて動作しています。説明されているどの列もnullableではありません。すべてのテーブルには、スキーマdboがあります。

今、私はほぼが私が望むことをするクエリを持っています:つまり、[〜#〜] any [〜#〜]SupportCategoriesのIDおよび[〜#〜] any [〜#〜]LanguagesのID。次のいずれかを返します。

その文字列に対するその言語の適切な翻訳がある場合(つまり、StringKeyId-> StringKeys.Idが存在し、LanguageStringTranslationsStringKeyIdLanguageId、およびStringTranslationIdの組み合わせが存在する場合、そのStringTranslationIdStringTranslations.Textをロードします。

LanguageStringTranslationsStringKeyIdLanguageId、およびStringTranslationIdの組み合わせに[〜#〜] not [〜#〜]が存在する場合、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

問題は、それが私を提供できないことです[〜#〜] all [〜#〜]SupportCategoriesとそれぞれのStringTranslations.Text(存在する場合)[〜#〜]または[〜#〜]存在しない場合、StringKeys.Name。それらのいずれかを提供するのに最適ですが、まったく提供しません。基本的に、言語に特定のキーの翻訳がない場合、デフォルトではStringKeys.NameStringKeys.DefaultLanguageIdを使用することを強制します。 (理想的には、そうすることすらしませんが、代わりにStringKeys.DefaultLanguageIdの翻訳をロードします。これは、残りのクエリで正しい方向を指し示した場合に自分で実行できます。)

私はこれに多くの時間を費やしてきましたが、(通常のように)C#でそれを書くだけでいいかどうかはわかっています。これをSQLで実行したいのですが、好きな出力を取得できません。

唯一の注意点は、適用される実際のクエリの数を制限することです。すべての列にはインデックスが付けられており、今のところそれらが好きです。実際のストレステストなしでは、さらにインデックスを付けることはできません。

編集:別のメモとして、私はデータベースをできる限り正規化するようにしているので、それを避けることができるのであれば、複製したくありません。

サンプルデータ

ソース

dbo.SupportCategories(全体):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages(185レコード、例では2つのみ表示):

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が使用されているか、または[〜#〜] any [〜#〜]現在のところ他の言語):

Id  Result
0   Billing
1   API
2   Sales

追加の例

Frenchのローカリゼーションを追加すると(つまり、1 48 8LanguageStringTranslationsに追加)、出力は(注:これは単なる例であり、ローカライズされた文字列をStringTranslationsに追加する)(フランス語の例で更新されます):

Result
Les APIs

追加の望ましい出力

上記の例の場合、次の出力が必要です(フランス語の例で更新)。

Id  Result
0   Billing
1   Les APIs
2   Sales

(はい、技術的には一貫性の観点から間違っていると私は知っていますが、それはこの状況で望まれることです。)

編集:

少し更新しましたが、dbo.Languagesテーブルの構造を変更してId (int)列をドロップし、Abbreviationに置き換えました(Idに名前が変更され、すべての相対外部キーとリレーションシップが更新されました) )。技術的な観点からは、テーブルが最初から一意であるISO 639-1コードに限定されているため、これは私の意見ではより適切な設定です。

Tl; dr

したがって、質問:このクエリを変更して、SupportCategoriesからeverythingを返し、そのStringTranslations.TextStringKeys.IdLanguages.Idの組み合わせ、またはStringKeys.Nameが存在する場合[〜#〜] not [〜#〜]存在しますか?

私の最初の考えは、どういうわけか現在のクエリを別のサブクエリとして別の一時的な型にキャストし、このクエリをさらに別のSELECTステートメントでラップし、必要な2つのフィールド(SupportCategories.IdおよびResult)を選択することです。

何も見つからない場合は、すべてのSupportCategoriesをC#プロジェクトにロードし、上記のクエリを各SupportCategories.Idに対して手動で実行するという、通常使用する標準的な方法を実行します。

あらゆる提案/コメント/批評をありがとう。

また、それが不条理に長いことをお詫びします。あいまいさはほしくありません。私はStackOverflowをよく利用していて、内容が足りない質問があり、ここで間違いを犯したくありません。

20
Der Kommissar

ここに私が思いついた最初のアプローチがあります:

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;

基本的に、選択した言語に一致する可能性のある文字列を取得しますandすべてのデフォルトの文字列を取得し、集計して、Idごとに1つだけ選択します-選択した言語を優先して、デフォルトを使用しますフォールバックとして。

おそらくUNION/EXCEPTでも同様のことができますが、ほとんどの場合、同じオブジェクトに対して複数のスキャンが実行されると思います。

16
Aaron Bertrand

INと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;

前述のように、FORCESEEKヒントは、提供されるサンプルデータを使用したLanguageStringTranslationsテーブルのカーディナリティが低いため、最も効率的な計画を取得するためにのみ必要です。行が増えると、オプティマイザはインデックスシークを自然に選択します。

実行計画自体には興味深い機能があります。

Execution Plan

最後の外部結合の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
12
Paul White 9