Pilih semua catatan, gabung dengan tabel A jika bergabung ada, tabel B jika tidak


20

Jadi inilah skenario saya:

Saya sedang mengerjakan Pelokalan untuk proyek saya, dan biasanya saya akan melakukan ini dalam kode C #, namun saya ingin melakukan ini dalam SQL sedikit lebih karena saya mencoba sedikit meningkatkan SQL saya.

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

Catatan: bahasa pemrograman itu sendiri seharusnya tidak relevan, saya hanya memasukkannya untuk kelengkapan.

Jadi saya semacam mencapai apa yang saya inginkan, tetapi tidak sejauh yang saya inginkan. Sudah lama (setidaknya satu tahun) sejak saya melakukan SQL JOINkecuali yang dasar, dan ini cukup kompleks JOIN.

Berikut adalah diagram dari tabel-tabel basis data yang relevan. (Ada banyak lagi, tetapi tidak perlu untuk porsi ini.)

Diagram Database

Semua hubungan yang dijelaskan dalam gambar lengkap dalam database - PKdan FKsemua kendala adalah pengaturan dan operasi. Tak satu pun dari kolom yang dijelaskan nullmampu. Semua tabel memiliki skema dbo.

Sekarang, saya punya pertanyaan yang hampir tidak apa yang saya inginkan: yang, diberikan APAPUN Id dari SupportCategoriesdan APAPUN Id dari Languages, itu akan kembali baik:

Jika ada terjemahan yang tepat-tepat untuk bahasa yang untuk string yang (Ie StringKeyId-> StringKeys.Idada, dan dalam LanguageStringTranslations StringKeyId, LanguageIddan StringTranslationIdkombinasi ada, maka beban StringTranslations.Textuntuk itu StringTranslationId.

Jika LanguageStringTranslations StringKeyId, LanguageId, dan StringTranslationIdkombinasi itu tidak ada, maka beban StringKeys.Namenilai. Itu Languages.Iddiberikan integer.

Permintaan saya, baik berantakan, adalah sebagai berikut:

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

Masalahnya adalah bahwa hal itu tidak mampu memberikan saya semua dari SupportCategoriesdan masing-masing StringTranslations.Textjika ada, OR mereka StringKeys.Namejika tidak ada. Ini sempurna dalam menyediakan salah satu dari mereka, tetapi tidak sama sekali. Pada dasarnya, itu untuk menegakkan bahwa jika bahasa tidak memiliki terjemahan untuk kunci tertentu, maka default adalah menggunakan StringKeys.Nameyang dari StringKeys.DefaultLanguageIdterjemahan. (Idealnya, itu bahkan tidak akan melakukan itu, tetapi malah memuat terjemahan untuk StringKeys.DefaultLanguageId, yang bisa saya lakukan sendiri jika menunjuk ke arah yang benar untuk sisa permintaan.)

Saya telah menghabiskan banyak waktu untuk hal ini, dan saya tahu jika saya hanya menulisnya dalam C # (seperti biasanya saya lakukan) itu akan dilakukan sekarang. Saya ingin melakukan ini dalam SQL, dan saya mengalami kesulitan mendapatkan output yang saya suka.

Satu-satunya peringatan, adalah saya ingin membatasi jumlah permintaan aktual yang diterapkan. Semua kolom diindeks dan seperti saya suka untuk saat ini, dan tanpa pengujian stres nyata saya tidak dapat mengindeks lebih lanjut.

Sunting: Catatan lain, saya mencoba untuk menjaga database agar senormalisasi mungkin, jadi saya tidak ingin menduplikasi hal-hal jika saya bisa menghindarinya.

Contoh Data

Sumber

dbo.SupportCategories (Entirety):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 catatan, hanya menampilkan dua untuk contoh):

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

Output Saat Ini

Dengan kueri persis di bawah, ini menghasilkan:

Result
Billing

Output yang Diinginkan

Idealnya, saya ingin menghilangkan yang spesifik SupportCategories.Id, dan mendapatkan semuanya, seperti itu (terlepas dari apakah bahasa 38 Englishdigunakan, atau 48 French, atau APA SAJA bahasa lain saat ini):

Id  Result
0   Billing
1   API
2   Sales

Contoh tambahan

Mengingat saya menambahkan lokalisasi untuk French(Yaitu menambahkan 1 48 8ke LanguageStringTranslations), output akan berubah menjadi (catatan: ini adalah contoh saja, jelas saya akan menambahkan string terlokalisasi ke StringTranslations) (diperbarui dengan contoh Prancis):

Result
Les APIs

Output yang Diinginkan Tambahan

Diberikan contoh di atas, output berikut akan diinginkan (diperbarui dengan contoh Prancis):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Ya, saya tahu secara teknis itu salah dari sudut pandang konsistensi, tetapi itulah yang diinginkan dalam situasi tersebut.)

Edit:

Kecil diperbarui, saya memang mengubah struktur dbo.Languagestabel, dan menjatuhkan Id (int)kolom dari itu, dan menggantinya dengan Abbreviation(yang sekarang diganti namanya menjadi Id, dan semua kunci asing relatif dan dan hubungan diperbarui). Dari sudut pandang teknis, ini adalah pengaturan yang lebih tepat menurut saya karena tabel tersebut terbatas pada kode ISO 639-1, yang unik untuk memulai.

Tl; dr

Jadi: pertanyaannya, bagaimana bisa saya memodifikasi query ini untuk kembali segala sesuatu dari SupportCategoriesdan kemudian kembali baik StringTranslations.Textuntuk itu StringKeys.Id, Languages.Idkombinasi, atau yang StringKeys.Namejika hal itu tidak ada?

Pikiran awal saya, adalah bahwa saya entah bagaimana bisa mengirimkan kueri saat ini ke jenis sementara lain sebagai subquery lain, dan membungkus kueri ini dalam SELECTpernyataan lain dan memilih dua bidang yang saya inginkan ( SupportCategories.Iddan Result).

Jika saya tidak menemukan apa-apa, saya hanya akan melakukan metode standar yang biasanya saya gunakan yaitu untuk memuat semua SupportCategorieske dalam proyek C # saya, dan kemudian menjalankan query yang saya miliki di atas secara manual terhadap masing-masing SupportCategories.Id.

Terima kasih atas semua dan semua saran / komentar / kritik.

Juga, saya minta maaf karena terlalu panjang, saya hanya tidak ingin ambiguitas. Saya sering berada di StackOverflow dan melihat pertanyaan yang tidak memiliki substansi, tidak ingin membuat kesalahan itu di sini.

Jawaban:


16

Inilah pendekatan pertama yang saya buat:

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;

Pada dasarnya, dapatkan string potensial yang cocok dengan bahasa yang dipilih dan dapatkan semua string default, kemudian agregat sehingga Anda hanya memilih satu per Id- memprioritaskan pada bahasa yang dipilih, kemudian mengambil default sebagai fallback.

Anda mungkin dapat melakukan hal serupa dengan UNION/ EXCEPTtetapi saya menduga ini hampir selalu mengarah ke beberapa pemindaian terhadap objek yang sama.


12

Solusi alternatif yang menghindari INdan pengelompokan dalam jawaban Harun:

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;

Seperti dicatat, FORCESEEKpetunjuk hanya diperlukan untuk mendapatkan rencana yang tampak paling efisien karena kardinalitas LanguageStringTranslationstabel yang rendah dengan data sampel yang disediakan. Dengan lebih banyak baris, pengoptimal akan memilih pencarian indeks secara alami.

Paket eksekusi itu sendiri memiliki fitur yang menarik:

Rencana eksekusi

Properti Pass Through pada gabungan luar terakhir berarti bahwa pencarian ke StringTranslationstabel hanya dilakukan jika baris sebelumnya ditemukan dalam LanguageStringTranslationstabel. Jika tidak, sisi bagian dalam gabungan ini dilewati sepenuhnya untuk baris saat ini.

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

Contoh data

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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.