从一组值中选择最非默认的值

Der*_*sar 4 join sql-server sql-server-2014 coalesce

鉴于以下表格:

CREATE TABLE FeeTestClient (Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, Name VARCHAR(16))
INSERT INTO FeeTestClient (Name)
VALUES ('Test'), ('Test 2'), ('Test 3')

CREATE TABLE FeeTest (FeeId INT IDENTITY(1,1) NOT NULL PRIMARY KEY, ClientId INT, Fee INT, Val VARCHAR(16), Val2 VARCHAR(16))
INSERT INTO FeeTest (ClientId, Fee, Val, Val2)
VALUES (1, 15, 'Default', 'Default'),
       (1, 10, 'Default', 'asdf'),
       (2, 15, 'Default', 'Default'),
       (2, 20, 'Default', 'qwer'),
       (2, 10, 'zxcv', 'asdf'),
       (3, 20, 'Default', 'Default')
Run Code Online (Sandbox Code Playgroud)

我的目标是选择所有FeeTestClient元素,并选择最不默认的费用。在默认费的规则很简单:如果Val2'Default',则Val不能被任何东西,除了 'Default'和每个收费,我们希望第一个地方Val是不是'Default',还是第一个地方Val2是没有'Default'的,否则我们将保证一个Val = 'Default' AND Val2 = 'Default'匹配。

客户将曾经有一个项目相匹配'Default'/'Default',一个项目匹配'Default'/____和一个项目匹配____/____。(虽然最后两行可能不存在。)如果他们有一个,____/____那么他们将永远有一个'Default'/____每个客户都会有一个'Default'/'Default'。他们永远不能有一个____/'Default'——这是应用程序上的一个无效状态,他们永远不能有多个相同的x/y,这是由UNIQUE表上的约束强制执行的。

可能的(在数据库中)为客户有'Default'/a'Default'/b,但在申请视为无效状态,并有一个测试这一点。(用户必须删除两者之一。)

这类似于我之前的一个问题(选择所有记录,如果连接存在则与表 A 连接,如果不存在则与表 B 连接),但不那么令人愉快。因为它们INT(实际上FLOAT在数据库中,但同样的问题适用)它们被聚合在一起,就像我不想要的那样。

我想得到以下结果:

Id  Name    (No column name)
1   Test    10
2   Test 2  10
3   Test 3  20
Run Code Online (Sandbox Code Playgroud)

我试过了:

SELECT Id, Name, COALESCE(f1.Fee, f2.Fee, f3.Fee)
FROM FeeTestClient
LEFT OUTER JOIN FeeTest AS f1 ON f1.ClientId = Id AND f1.Val <> 'Default' AND f1.Val2 <> 'Default'
LEFT OUTER JOIN FeeTest AS f2 ON f2.ClientId = Id AND f2.Val = 'Default' AND f2.Val2 <> 'Default'
LEFT OUTER JOIN FeeTest AS f3 ON f3.ClientId = Id AND f3.Val = 'Default' AND f3.Val2 = 'Default'
Run Code Online (Sandbox Code Playgroud)

这在实时数据集上非常慢,但返回正确的结果(大约 15 秒,在数据上运行基本选择,没有这个连接选择,是 7,计划在这里),我也试过(根据 Joe Obbish 的建议):

SELECT Id, Name, MAX(COALESCE(CASE WHEN Val <> 'Default' AND Val2 <> 'Default' THEN Fee END, CASE WHEN Val = 'Default' AND Val2 <> 'Default' THEN Fee END, CASE WHEN Val = 'Default' AND Val2 = 'Default' THEN Fee END))
FROM FeeTestClient
LEFT OUTER JOIN FeeTest ON FeeTest.ClientId = Id
GROUP BY Id, Name
Run Code Online (Sandbox Code Playgroud)

这同样很慢(计划在这里),并产生错误的输出(尽管如果您COALESCE使用MAXs 则它可以正常工作)。(也许更糟。)

我不知所措,编写这些查询非常痛苦,因此对构建所需输出的任何建议表示赞赏。

我已经提供了实际计划,但它们与测试计划有很大不同(这个 MCVE 是为了证明我想要的结果,答案没有义务,甚至不需要做出任何性能声明),看起来。请忽略上面计划中显示的额外连接 - 它们对示例来说并不重要。

至于分布,实时数据集,1.5%只是'Default'/'Default',44.9%的人'Default'/___'Default'/'Default'和53.6%有三个。

Joe*_*ish 5

我不确定您的 MCVE 是否完全代表了您所遇到的问题,但我会回答给出的问题。这个问题是关于性能的,所以表中只有几行不会削减它。我将您的样本数据复制了一百万次,总共有 600 万行FeeTest和 300 万行FeeTestClient. 代码如下:

DROP TABLE IF EXISTS FeeTestClient;

CREATE TABLE FeeTestClient (Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, [Name] VARCHAR(16));

INSERT INTO FeeTestClient WITH (TABLOCK)
([Name])
SELECT 'ZZZZZZZ' + CAST(RN AS VARCHAR(7))
FROM
(
    SELECT TOP (3000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS FeeTest_source;

CREATE TABLE FeeTest_source (ClientId INT, Fee INT, Val VARCHAR(16), Val2 VARCHAR(16));
INSERT INTO FeeTest_source (ClientId, Fee, Val, Val2)
VALUES (1, 15, 'Default', 'Default'),
       (1, 10, 'Default', 'asdf'),
       (2, 15, 'Default', 'Default'),
       (2, 20, 'Default', 'qwer'),
       (2, 10, 'zxcv', 'asdf'),
       (3, 20, 'Default', 'Default');


SELECT 3 * client.Id - 2 + client.Id % 3 AS ClientId
, src.Fee
, src.Val
, src.Val2 into #t
FROM FeeTest_source src
INNER JOIN FeeTestClient client ON src.ClientId = 1 + client.Id % 3;    


DROP TABLE IF EXISTS FeeTest;

CREATE TABLE FeeTest (FeeId INT IDENTITY(1,1) NOT NULL PRIMARY KEY, ClientId INT, Fee INT, Val VARCHAR(16), Val2 VARCHAR(16));

INSERT INTO FeeTest WITH (TABLOCK)
(ClientId, Fee, Val, Val2)
SELECT * FROM #t;

DROP TABLE #t;
Run Code Online (Sandbox Code Playgroud)

GROUP BY根据表中数据的性质和针对表定义的索引,使用并仅保留相关聚合可能是一种很好的方法。下面的查询在我的机器上在 2 秒内完成:

SELECT Id, Name, MAX(COALESCE(CASE WHEN Val <> 'Default' AND Val2 <> 'Default' THEN Fee END, CASE WHEN Val = 'Default' AND Val2 <> 'Default' THEN Fee END, CASE WHEN Val = 'Default' AND Val2 = 'Default' THEN Fee END))
FROM FeeTestClient
LEFT OUTER JOIN FeeTest ON FeeTest.ClientId = Id
GROUP BY Id, Name
Run Code Online (Sandbox Code Playgroud)

计划一

计划如我所料。最后有一个散列连接和一个散列聚合。

但是,您还有其他选择,因为您有一张FeeTestClient桌子。另一种策略是使用OUTER APPLY. 一种方法如下:

SELECT Id, [Name], oa.Fee
FROM FeeTestClient ftc
OUTER APPLY (
    SELECT TOP 1 ft.Fee
    FROM FeeTest ft
    WHERE ft.ClientId = ftc.Id
    ORDER BY
        CASE WHEN Val <> 'Default' THEN 2 ELSE 0 END
        + CASE WHEN Val2 <> 'Default' THEN 1 ELSE 0 END
    DESC, ft.ClientId
) oa;
Run Code Online (Sandbox Code Playgroud)

有了APPLYTOP你几乎总是需要对内部表的良好指标。查询优化器通过 spool 为我们建立一个临时索引,查询在我的机器上运行需要 14 秒:

查询 2

我们正在搜索,ClientId所以让我们尝试一个索引:

CREATE INDEX NOT_COVERING ON FeeTest (ClientId);
Run Code Online (Sandbox Code Playgroud)

索引没有覆盖,所以优化器不想使用它。使用它需要大量的键查找来获取不在索引上的列。我可以强制索引使用一个懒惰的、未记录的技巧,你不应该在生产中使用它:

SELECT Id, [Name], oa.Fee
FROM FeeTestClient ftc
OUTER APPLY (
    SELECT TOP 1 ft.Fee
    FROM FeeTest ft
    WHERE ft.ClientId = ftc.Id
    ORDER BY
        CASE WHEN Val <> 'Default' THEN 2 ELSE 0 END
        + CASE WHEN Val2 <> 'Default' THEN 1 ELSE 0 END
    DESC, ft.ClientId
) oa
OPTION (QueryRuleOff BuildSpool);
Run Code Online (Sandbox Code Playgroud)

现在查询在 5 秒内运行。我们可以看到计划中的关键查找:

查询 3

我们最后一次尝试使用覆盖索引:

CREATE INDEX COVERING ON FeeTest (ClientId) INCLUDE (Val, Val2, Fee);
Run Code Online (Sandbox Code Playgroud)

之前的查询现在运行四秒钟:

查询 4

您如何将其应用于庞大的匿名查询?你有很多键查找和一个索引假脱机。尝试定义覆盖索引,以便您可以更有效地访问所需的数据。我不能对整体运行时间做出任何保证,但它应该在某种程度上有所帮助。请注意,我没有时间仔细查看您发布的计划。

万一有人在家里跟进,有一个奇怪的边缘情况可能导致覆盖索引仍然无法使用。这是解决它的一种方法:

ALTER TABLE FeeTest
ADD MAGIC_COLUMN AS CASE WHEN Val <> 'Default' THEN 2 ELSE 0 END + CASE WHEN Val2 <> 'Default' THEN 1 ELSE 0 END;

CREATE INDEX COVERING_2 ON FeeTest (ClientId) INCLUDE (MAGIC_COLUMN, Fee);
Run Code Online (Sandbox Code Playgroud)