Selecione todos os registros, junte-se à tabela A se houver junção, tabela B se não houver

20

Então, aqui está o meu cenário:

Estou trabalhando na localização para um projeto meu e, normalmente, eu faria isso no código C #, no entanto, quero fazer isso no SQL um pouco mais, pois estou tentando aprimorar um pouco o meu SQL.

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

Nota: a linguagem de programação em si deve ser irrelevante, só a incluo por completo.

Então, eu meio que consegui o que queria, mas não na medida em que queria. Já faz um tempo (pelo menos um ano) desde que eu fiz SQL, JOINexceto os básicos, e isso é bastante complexo JOIN.

Aqui está um diagrama das tabelas relevantes do banco de dados. (Há muito mais, mas não é necessário para esta parte.)

Diagrama do banco de dados

Todos os relacionamentos descritos na imagem estão completos no banco de dados - as restrições PKe FKestão todas configuradas e operacionais. Nenhuma das colunas descritas é nullcapaz. Todas as tabelas têm o esquema dbo.

Agora, eu tenho uma consulta que quase faz o que eu quero: ou seja, considerando QUALQUER ID SupportCategoriese QUALQUER ID Languages, ele retornará:

Se houver uma tradução da direita adequada para essa linguagem para essa string (Ie StringKeyId-> StringKeys.Idexiste, e em LanguageStringTranslations StringKeyId, LanguageIde StringTranslationIdexiste combinação, então cargas StringTranslations.Textpara que StringTranslationId.

Se o LanguageStringTranslations StringKeyId, LanguageIde StringTranslationIdcombinação que não existe, então ele carrega o StringKeys.Namevalor. O Languages.Idé um dado integer.

Minha consulta, seja uma bagunça, é a seguinte:

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

O problema é que ele não é capaz de me fornecer TODOS os SupportCategoriese seus respectivos, StringTranslations.Textse existir, OU seus, StringKeys.Namese não existir. É perfeito para fornecer qualquer um deles, mas nem um pouco. Basicamente, é impor que, se um idioma não possui uma tradução para uma chave específica, o padrão é usar o StringKeys.Nameque é de StringKeys.DefaultLanguageIdtradução. (Idealmente, nem faria isso, mas, ao invés disso, carregaria a tradução StringKeys.DefaultLanguageId, o que eu posso fazer se apontado na direção certa para o restante da consulta.)

Eu gastei muito tempo nisso, e sei que se eu escrevesse em C # (como eu costumo fazer), já estaria feito. Quero fazer isso no SQL e estou tendo problemas para obter a saída que eu gosto.

A única ressalva é que eu quero limitar o número de consultas reais aplicadas. Todas as colunas são indexadas e, como eu gosto, por enquanto, e sem testes de estresse reais, não posso indexá-las mais.

Edit: Outra observação, estou tentando manter o banco de dados o mais normalizado possível, para que não queira duplicar as coisas, se puder evitá-lo.

Dados de exemplo

Fonte

dbo.SupportCategories (totalidade):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 registros, mostrando apenas dois por exemplo):

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 (totalidade):

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 (totalidade):

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

Saída atual

Dada a consulta exata abaixo, ela gera:

Result
Billing

Saída desejada

Idealmente, eu gostaria de poder omitir o específico SupportCategories.Ide obter todos eles, como tal (independentemente do idioma 38 ter Englishsido usado ou 48 Frenchou QUALQUER outro idioma no momento):

Id  Result
0   Billing
1   API
2   Sales

Exemplo Adicional

Dado que eu adicionaria uma localização para French(ou seja, adicionar 1 48 8a LanguageStringTranslations), a saída mudaria para (nota: este é apenas um exemplo, obviamente eu adicionaria uma string localizada a StringTranslations) (atualizada com o exemplo em francês):

Result
Les APIs

Saída desejada adicional

Dado o exemplo acima, a seguinte saída seria desejada (atualizada com exemplo em francês):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Sim, eu sei tecnicamente que isso está errado do ponto de vista da consistência, mas é o que seria desejado na situação.)

Editar:

Pequeno atualizado, mudei a estrutura da dbo.Languagestabela, larguei a Id (int)coluna e a substitui por Abbreviation(que agora é renomeada para Id, e todas as chaves estrangeiras e relacionamentos relativos atualizados). Do ponto de vista técnico, essa é uma configuração mais apropriada, na minha opinião, devido ao fato de a tabela estar limitada aos códigos ISO 639-1, que são exclusivos para começar.

Tl; dr

Assim: a questão, como eu poderia modificar esta consulta para retorno tudo a partir SupportCategoriese depois retornar tanto StringTranslations.Textpara isso StringKeys.Id, Languages.Idcombinação, ou o StringKeys.Namese o fizesse não existe?

Meu pensamento inicial é que, de alguma forma, eu poderia converter a consulta atual para outro tipo temporário como outra subconsulta, e agrupar essa consulta em outra SELECTinstrução e selecionar os dois campos que eu quero ( SupportCategories.Ide Result).

Se não encontrar nada, farei o método padrão que normalmente uso para carregar tudo SupportCategoriesno meu projeto C # e, em seguida, execute a consulta que tenho acima manualmente manualmente em cada um SupportCategories.Id.

Obrigado por toda e qualquer sugestão / comentário / crítica.

Além disso, peço desculpas por ser absurdamente longo, só não quero ambiguidade. Frequentemente, estou no StackOverflow e vejo perguntas que não têm substância, que não desejam cometer esse erro aqui.

Der Kommissar
fonte

Respostas:

16

Aqui está a primeira abordagem que criei:

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;

Basicamente, obtenha as sequências em potencial que correspondem ao idioma escolhido e obtenha todas as sequências padrão e, em seguida, agregue para escolher apenas uma por Idprioridade no idioma escolhido e, em seguida, tome o padrão como substituto.

Provavelmente, você pode fazer coisas semelhantes com UNION/ EXCEPTmas suspeito que isso quase sempre levará a várias varreduras nos mesmos objetos.

Aaron Bertrand
fonte
12

Uma solução alternativa que evita INo agrupamento na resposta de 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;

Conforme observado, a FORCESEEKdica é necessária apenas para obter o plano de aparência mais eficiente devido à baixa cardinalidade da LanguageStringTranslationstabela com os dados de amostra fornecidos. Com mais linhas, o otimizador escolherá uma busca de índice naturalmente.

O próprio plano de execução possui um recurso interessante:

Plano de execução

A propriedade Passagem na última associação externa significa que uma pesquisa na StringTranslationstabela é realizada apenas se uma linha foi encontrada anteriormente na LanguageStringTranslationstabela. Caso contrário, o lado interno dessa junção será ignorado completamente na linha atual.

DDL da tabela

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

Dados de amostra

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 diz que a GoFundMonica
fonte