これが私のシナリオです:
私は私のプロジェクトのローカリゼーションに取り組んでおり、通常はC#コードでこれを実行しますが、SQLを少しバフにしようとしているので、SQLでこれをもう少し実行したいと思います。
環境:SQL Server 2014 Standard、C#(.NET 4.5.1)
注:プログラミング言語自体は無関係である必要があります。私は完全を期すためだけに含めています。
だから、私は自分が望んでいたことをある程度達成したが、望んだほどではなかった。基本的なもの以外のSQL JOIN
sを実行してからしばらく(少なくとも1年)、これは非常に複雑なJOIN
です。
これは、データベースの関連テーブルの図です。 (もっとたくさんありますが、この部分には必要ありません。)
画像で説明されているすべての関係はデータベースで完全です。PK
とFK
の制約はすべて設定されて動作しています。説明されているどの列もnull
ableではありません。すべてのテーブルには、スキーマdbo
があります。
今、私はほぼが私が望むことをするクエリを持っています:つまり、[〜#〜] any [〜#〜]SupportCategories
のIDおよび[〜#〜] any [〜#〜]Languages
のID。次のいずれかを返します。
その文字列に対するその言語の適切な翻訳がある場合(つまり、StringKeyId
-> StringKeys.Id
が存在し、LanguageStringTranslations
StringKeyId
、LanguageId
、およびStringTranslationId
の組み合わせが存在する場合、そのStringTranslationId
のStringTranslations.Text
をロードします。
LanguageStringTranslations
StringKeyId
、LanguageId
、および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.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
追加の例
French
のローカリゼーションを追加すると(つまり、1 48 8
をLanguageStringTranslations
に追加)、出力は(注:これは単なる例であり、ローカライズされた文字列をStringTranslations
に追加する)(フランス語の例で更新されます):
Result
Les APIs
追加の望ましい出力
上記の例の場合、次の出力が必要です(フランス語の例で更新)。
Id Result
0 Billing
1 Les APIs
2 Sales
(はい、技術的には一貫性の観点から間違っていると私は知っていますが、それはこの状況で望まれることです。)
少し更新しましたが、dbo.Languages
テーブルの構造を変更してId (int)
列をドロップし、Abbreviation
に置き換えました(Id
に名前が変更され、すべての相対外部キーとリレーションシップが更新されました) )。技術的な観点からは、テーブルが最初から一意であるISO 639-1コードに限定されているため、これは私の意見ではより適切な設定です。
したがって、質問:このクエリを変更して、SupportCategories
からeverythingを返し、そのStringTranslations.Text
のStringKeys.Id
、Languages.Id
の組み合わせ、またはStringKeys.Name
が存在する場合[〜#〜] not [〜#〜]存在しますか?
私の最初の考えは、どういうわけか現在のクエリを別のサブクエリとして別の一時的な型にキャストし、このクエリをさらに別のSELECT
ステートメントでラップし、必要な2つのフィールド(SupportCategories.Id
およびResult
)を選択することです。
何も見つからない場合は、すべてのSupportCategories
をC#プロジェクトにロードし、上記のクエリを各SupportCategories.Id
に対して手動で実行するという、通常使用する標準的な方法を実行します。
あらゆる提案/コメント/批評をありがとう。
また、それが不条理に長いことをお詫びします。あいまいさはほしくありません。私はStackOverflowをよく利用していて、内容が足りない質問があり、ここで間違いを犯したくありません。
ここに私が思いついた最初のアプローチがあります:
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
でも同様のことができますが、ほとんどの場合、同じオブジェクトに対して複数のスキャンが実行されると思います。
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
テーブルのカーディナリティが低いため、最も効率的な計画を取得するためにのみ必要です。行が増えると、オプティマイザはインデックスシークを自然に選択します。
実行計画自体には興味深い機能があります。
最後の外部結合のPass Throughプロパティは、行がStringTranslations
テーブルで以前に見つかった場合にのみ、LanguageStringTranslations
テーブルへのルックアップが実行されることを意味します。それ以外の場合、この結合の内側は現在の行に対して完全にスキップされます。
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