选择所有记录,如果存在联接,则与表A联接,如果不存在,则与表B联接


20

所以这是我的情况:

我正在为我的一个项目进行本地化,通常我会在C#代码中进行此操作,但是我想在SQL中进行更多操作,因为我想稍微弄点点SQL。

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

注意:编程语言本身应该无关紧要,为了完整起见,我仅将其包括在内。

所以我完成了我想要的,但是没有达到我想要的程度。自从我完成了JOIN除基本SQL 之外的任何SQL以来,已经有一段时间了(至少一年),这相当复杂JOIN

这是数据库相关表的示意图。(还有更多,但是这部分不是必需的。)

数据库图

映像中描述的所有关系都已在数据库中完成- PKFK约束都已设置和运行。所描述的列均不null可用。所有表都具有架构dbo

现在,我有一个查询这几乎做什么,我想:即给定任何的标识SupportCategories任何的标识Languages,这将返回:

如果有合适的,正确的翻译是语言该字符串(即StringKeyId- > StringKeys.Id存在,并在LanguageStringTranslations StringKeyIdLanguageId以及StringTranslationId是否同时存在,那么它的负载StringTranslations.TextStringTranslationId

如果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

问题是,它不能够提供我的所有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条记录,仅显示两个示例):

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,或目前使用的任何其他语言):

Id  Result
0   Billing
1   API
2   Sales

附加示例

假设我要为French(即添加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代码,而该代码最初是唯一的。

l

所以说:这个问题,我怎么能修改此查询返回的一切SupportCategories再或者返回StringTranslations.TextStringKeys.IdLanguages.Id组合,StringKeys.Name如果它没有存在吗?

我最初的想法是,我可以以某种方式将当前查询转换为另一个临时类型,作为另一个子查询,然后将此查询包装在另一个SELECT语句中,然后选择我想要的两个字段(SupportCategories.IdResult)。

如果我什么都没找到,我将执行我通常使用的标准方法,将所有方法都加载SupportCategories到我的C#项目中,然后使用它对每个手动运行上面的查询SupportCategories.Id

感谢您提出的所有建议/评论/批评。

另外,我为它太长了而感到抱歉,我只是不想有任何歧义。我经常在StackOverflow上,看到缺乏实质性的问题,不想在这里犯错误。

Answers:


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

避免INAaron答案中的和的替代解决方案:

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.