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


20

だからここに私のシナリオがあります:

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

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

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

それで、私は自分が望むものをやや達成しましたが、私が望んでいた程度ではありませんでした。JOIN基本的なもの以外のSQLを実行してからしばらく(少なくとも1年)経ちましたが、これは非常に複雑JOINです。

データベースの関連テーブルの図を次に示します。(他にもたくさんありますが、この部分には必要ありません。)

データベース図

画像に記述されているすべての関係はデータベースで完全です- PKFK制約はすべて設定および操作されています。説明されている列はどれもnullできません。すべてのテーブルにはスキーマがありますdbo

今、私はほとんど私がしたいことをするクエリを持っています:つまり、ANY Id of SupportCategoriesANY Id of が与えられるとLanguages、それは次のいずれかを返します:

その文字列のためにその言語の右適切な翻訳がある場合(IeはStringKeyId- > StringKeys.Id存在し、中LanguageStringTranslations StringKeyIdLanguageIdStringTranslationIdの組み合わせが存在し、それはロードStringTranslations.TextそのためStringTranslationId

場合はLanguageStringTranslations StringKeyIdLanguageIdStringTranslationIdの組み合わせがなかったしない存在し、それはロード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

問題は、それが私を提供することができないということであるALLSupportCategoriesと、それぞれStringTranslations.Textが存在する場合、または自分StringKeys.Nameが存在していなかった場合。それらのいずれかを提供するのに最適ですが、まったくありません。基本的に、特定のキーの翻訳が言語にない場合、デフォルトでは、StringKeys.NameどちらのStringKeys.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

追加の例

以下のためのローカライズを追加したI考えるFrench(Ieが追加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、その後のいずれかを返すStringTranslations.TextそのためStringKeys.IdLanguages.Id組み合わせ、またはStringKeys.Nameそれがなかった場合、NOT存在しますか?

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

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

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

また、私はそれが不条理に長いことをおizeびします。曖昧さを望みません。私は頻繁に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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.