Seleccione todos los registros, únase a la tabla A si la combinación existe, tabla B si no

20

Así que aquí está mi escenario:

Estoy trabajando en la localización para un proyecto mío, y normalmente haría esto en el código C #, sin embargo, quiero hacerlo un poco más en SQL ya que estoy tratando de mejorar mi SQL un poco.

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

Nota: el lenguaje de programación en sí mismo debe ser irrelevante, solo lo incluyo para completarlo.

Así que logré lo que quería, pero no en la medida en que quería. Ha pasado un tiempo (al menos un año) desde que hice cualquier SQL JOIN, excepto los básicos, y esto es bastante complejo JOIN.

Aquí hay un diagrama de las tablas relevantes de la base de datos. (Hay muchos más, pero no son necesarios para esta porción).

Diagrama de base de datos

Todas las relaciones descritas en la imagen están completas en la base de datos; las restricciones PKy FKestán configuradas y operativas. Ninguna de las columnas descritas son nullcapaces. Todas las tablas tienen el esquema dbo.

Ahora, tengo una consulta que casi hace lo que quiero: es decir, dado CUALQUIER ID SupportCategoriesy CUALQUIER ID de Languages, devolverá:

Si hay una traducción correcta, adecuada para ese idioma para esa cadena (Ie StringKeyId-> StringKeys.Idexiste, y en LanguageStringTranslations StringKeyId, LanguageIdy StringTranslationIdexiste combinación, entonces carga StringTranslations.Textpara que StringTranslationId.

Si el LanguageStringTranslations StringKeyId, LanguageIdy StringTranslationIdtenía la combinación no existe, entonces se carga el StringKeys.Namevalor. El Languages.Ides un hecho integer.

Mi consulta, ya sea un desastre, es la siguiente:

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

El problema es que no es capaz de proporcionar mí TODO del SupportCategoriesy sus respectivos StringTranslations.Textsi existe, o su StringKeys.Namesi no existiera. Es perfecto para proporcionar cualquiera de ellos, pero para nada. Básicamente, es para hacer cumplir que si un idioma no tiene una traducción para una clave específica, entonces lo predeterminado es usar StringKeys.Namecuál es la StringKeys.DefaultLanguageIdtraducción. (Idealmente, ni siquiera haría eso, sino que cargaría la traducción para StringKeys.DefaultLanguageId, lo que puedo hacer yo mismo si apunta en la dirección correcta para el resto de la consulta).

He dedicado MUCHO tiempo a esto, y sé que si solo lo escribiera en C # (como lo hago habitualmente) ya estaría hecho. Quiero hacer esto en SQL, y tengo problemas para obtener la salida que me gusta.

La única advertencia es que quiero limitar el número de consultas reales aplicadas. Todas las columnas están indexadas y, por el momento, me gustan, y sin pruebas de tensión reales no puedo indexarlas más.

Editar: Otra nota, estoy tratando de mantener la base de datos lo más normalizada posible, por lo que no quiero duplicar cosas si puedo evitarla.

Datos de ejemplo

Fuente

dbo.SupportCategories (Totalidad):

Id  StringKeyId
0   0
1   1
2   2

dbo.Languages ​​(185 registros, mostrando solo dos como ejemplos):

Id  Abbreviation    Family  Name    Native
38  en  Indo-European   English English
48  fr  Indo-European   French  français, langue française

dbo.LanguagesStringTranslations (totalidad):

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

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

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

Salida de corriente

Dada la consulta exacta a continuación, genera:

Result
Billing

Salida deseada

Idealmente, me gustaría poder omitir el específico SupportCategories.Idy obtener todos ellos, así (independientemente de si Englishse usó el idioma 38 , 48 Frencho CUALQUIER otro idioma en este momento):

Id  Result
0   Billing
1   API
2   Sales

Ejemplo adicional

Dado que debía agregar una localización para French(es decir, agregar 1 48 8a LanguageStringTranslations), la salida cambiaría a (nota: esto es solo un ejemplo, obviamente agregaría una cadena localizada a StringTranslations) (actualizado con un ejemplo francés):

Result
Les APIs

Salida deseada adicional

Dado el ejemplo anterior, se desearía el siguiente resultado (actualizado con el ejemplo francés):

Id  Result
0   Billing
1   Les APIs
2   Sales

(Sí, sé que técnicamente eso está mal desde un punto de vista de coherencia, pero es lo que se desearía en la situación).

Editar:

Pequeño actualizado, cambié la estructura de la dbo.Languagestabla, solté la Id (int)columna y la reemplacé por Abbreviation(que ahora se renombró Idy se actualizaron todas las claves externas y relaciones relativas). Desde un punto de vista técnico, esta es una configuración más apropiada en mi opinión debido al hecho de que la tabla está limitada a los códigos ISO 639-1, que son únicos para empezar.

Tl; dr

Por lo tanto: la pregunta, ¿cómo podría modificar esta consulta para devolver todo a partir SupportCategoriesy luego regresar, ya sea StringTranslations.Textpara que StringKeys.Id, Languages.Idcombinados, o el StringKeys.Namesi lo hizo no existir?

Mi pensamiento inicial es que de alguna manera podría enviar la consulta actual a otro tipo temporal como otra subconsulta, y envolver esta consulta en otra SELECTdeclaración y seleccionar los dos campos que quiero ( SupportCategories.Idy Result).

Si no encuentro nada, simplemente haré el método estándar que normalmente uso, que es cargar todo SupportCategoriesen mi proyecto C #, y luego ejecutar con él la consulta que tengo arriba manualmente en cada uno SupportCategories.Id.

Gracias por todas y cada una de las sugerencias / comentarios / críticas.

Además, me disculpo por ser absurdamente largo, simplemente no quiero ninguna ambigüedad. A menudo estoy en StackOverflow y veo preguntas que carecen de sustancia, no quería cometer ese error aquí.

Der Kommissar
fuente

Respuestas:

16

Aquí está el primer enfoque que se me ocurrió:

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;

Básicamente, obtenga las cadenas potenciales que coincidan con el idioma elegido y obtenga todas las cadenas predeterminadas, luego agregue para que solo elija una por Idprioridad en el idioma elegido, luego tome el predeterminado como alternativa.

Probablemente pueda hacer cosas similares con UNION/ EXCEPTpero sospecho que esto casi siempre conducirá a múltiples escaneos contra los mismos objetos.

Aaron Bertrand
fuente
12

Una solución alternativa que evita INla agrupación en la respuesta 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;

Como se señaló, la FORCESEEKsugerencia solo es necesaria para obtener el plan de aspecto más eficiente debido a la baja cardinalidad de la LanguageStringTranslationstabla con los datos de muestra proporcionados. Con más filas, el optimizador elegirá una búsqueda de índice de forma natural.

El plan de ejecución en sí tiene una característica interesante:

Plan de ejecución

La propiedad Pass Through en la última combinación externa significa que StringTranslationssolo se realiza una búsqueda en la tabla si se encontró previamente una fila en la LanguageStringTranslationstabla. De lo contrario, el lado interno de esta unión se omite por completo para la fila actual.

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

Data de muestra

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 dice GoFundMonica
fuente