如果不满足限制,则将默认行添加到查询结果中

11 sql-server date-math

我有一个 SQL 查询,用于在表中查找某个日期范围内的值。

如果没有找到记录,我想生成具有默认值的行。

表现有记录之一的示例:

设备ID 时间 语境 价值
1 2022-02-10 连接的 错误的

因此,限制时间列在 2022-02-07 和 2022-02-10 之间的查询必须为 2 月 7 日、8 日和 9 日创建假行,但不能为 10 日创建假行,因为该行已经存在。

预期结果:

设备ID 时间 语境 价值
1 2022-02-7 伪造的 错误的
1 2022-02-8 伪造的 错误的
1 2022-02-9 伪造的 错误的
1 2022-02-10 连接的 错误的

我怎样才能做到这一点?使用递归 CTE?

AMt*_*two 10

当我想到你想要完成什么时,我会用“简单的英语”这样描述它:

  • 返回某个查询的结果
  • 但如果不存在结果,则返回一些默认值。

我的思维过程立即跳跃到“如果我总是包含默认值,然后在存在实际结果时以某种方式过滤掉它们会怎么样?

在思考了我早上喝的咖啡后,我意识到使用 CTE 实际上很容易做到这一点。不需要递归,但我将使用两个 CTE。

那个真正的查询

让我们首先将真实的查询放入 CTE 中。这使得多次引用查询结果变得非常容易。在此示例中,我将查询 sys.objects,并将整个该死的东西放入 CTE 中:

DECLARE @ObjectName nvarchar(128) = N'sysschobjs';

RealQuery AS (
    SELECT object_id, name
    FROM sys.objects
    WHERE name = @ObjectName
    )
SELECT *
FROM RealQuery;
Run Code Online (Sandbox Code Playgroud)

现在使用默认值

我这里也做同样的治疗。只需制作一些默认占位符,并将它们抽象到我可以轻松引用的 CTE 中。也许您的默认值存储在某个表中的某个位置,或者您可能更喜欢将它们填充到#temptable 或 table中@variable,在这种情况下,您不需要在此处使用 CTE。

WITH Defaults AS (
    SELECT *
    FROM (VALUES (1,N'One'),(2,N'Two'),(3,N'Three')) AS x(Id,Name)
    )
SELECT *
FROM Defaults;
Run Code Online (Sandbox Code Playgroud)

是时候进行混搭了

现在,通过 CTE 中的真实查询和另一个 CTE 中的“默认”值,我只需UNION ALL“真实”结果和默认占位符。“魔法”是用来WHERE NOT EXISTS (SELECT 1 FROM RealQuery)控制是否包含这些默认值。

这将返回与对象名称匹配的单行sysschobjs,并且不返回默认占位符:

DECLARE @ObjectName nvarchar(128) = N'sysschobjs';

WITH Defaults AS (
    SELECT *
    FROM (VALUES (1,N'One'),(2,N'Two'),(3,N'Three')) AS x(Id,Name)
    ),
RealQuery AS (
    SELECT object_id, name
    FROM sys.objects
    WHERE name = @ObjectName
    )
SELECT *
FROM RealQuery
UNION ALL
SELECT *
FROM Defaults
WHERE NOT EXISTS (SELECT 1 FROM RealQuery);
Run Code Online (Sandbox Code Playgroud)

sysschobjs 的一个值的 SSMS 结果

如果您将第一行更改为不存在的值,那么sys.objects您将获得占位符默认结果:

DECLARE @ObjectName nvarchar(128) = N'AMtwo';
Run Code Online (Sandbox Code Playgroud)

三个占位符结果的图像,数字一、二、三用文字拼写出来

还有其他方法。

我的解决方案有效,但可能并不理想。例如,如果您查看执行计划,您将看到它运行“真实”查询两次。对于这个简单的查询来说,这完全没问题,但对于其他情况可能不太适用。

您最好简单地运行“真实”查询来插入#Results临时表,然后检查临时表中有多少行。

IF EXISTS (SELECT 1 FROM #Results)
BEGIN
    INSERT INTO #Results(Id,Name)
    SELECT *
    FROM (VALUES (1,N'One'),(2,N'Two'),(3,N'Three')) AS x(Id,Name);
END;

SELECT *
FROM #Results;
Run Code Online (Sandbox Code Playgroud)


Pau*_*ite 8

可以使用pivot和unpivot来有效地实现这一点。

使用稍微扩展的测试数据:

设备ID 时间 语境 价值
1 2022-02-08 00:00:00 连接的 错误的
1 2022-02-10 00:00:00 连接的 错误的
2 2022-02-09 00:00:00 连接的 错误的

要生成从第 7到第 11 行(包括第 7 行和第 11):

WITH TheQuery AS
(
    SELECT V.*
    FROM 
    (
        VALUES
            (1, CONVERT(datetime2(0), '20220208', 112), 'Connected', 'False'),
            (1, CONVERT(datetime2(0), '20220210', 112), 'Connected', 'False'),
            (2, CONVERT(datetime2(0), '20220209', 112), 'Connected', 'False')
    ) AS V (DeviceId, [Time], Context, [Value])
)
SELECT
    U.DeviceId, 
    U.TheDate, 
    Context = IIF(U.Present = 1, U.Context, 'Fake'), 
    [Value] = IIF(U.Present = 1, U.[Value], 'False')
FROM TheQuery AS Q
PIVOT (COUNT([Time]) FOR [Time] IN 
    ([20220207],[20220208],[20220209],[20220210],[20220211])) AS P
UNPIVOT (Present FOR TheDate IN 
    ([20220207],[20220208],[20220209],[20220210],[20220211])) AS U;
Run Code Online (Sandbox Code Playgroud)

枢轴部分将数据转换为:

设备ID 语境 价值 20220207 20220208 20220209 20220210 20220211
1 连接的 错误的 0 1 0 1 0
2 连接的 错误的 0 0 1 0 0

如果该值不存在,则聚合COUNT结果为零,否则为 1。unpivot 将集合旋转回行,其中计数产生零的额外行:

设备ID 语境 价值 展示 日期
1 连接的 错误的 0 20220207
1 连接的 错误的 1 20220208
1 连接的 错误的 0 20220209
1 连接的 错误的 1 20220210
1 连接的 错误的 0 20220211
2 连接的 错误的 0 20220207
2 连接的 错误的 0 20220208
2 连接的 错误的 1 20220209
2 连接的 错误的 0 20220210
2 连接的 错误的 0 20220211

计数中的“当前”列提供了一种简单的方法来决定是否应使用默认值。

最终结果是:

设备ID 日期 语境 价值
1 20220207 伪造的 错误的
1 20220208 连接的 错误的
1 20220209 伪造的 错误的
1 20220210 连接的 错误的
1 20220211 伪造的 错误的
2 20220207 伪造的 错误的
2 20220208 伪造的 错误的
2 20220209 连接的 错误的
2 20220210 伪造的 错误的
2 20220211 伪造的 错误的

执行计划:

计划

该方法适合动态 SQL,因为数据透视和反透视都需要一串用引号引起来的逗号分隔日期。

该字符串可以通过多种方式创建,例如:

DECLARE 
    @Start datetime2(0) = '20220207',
    @End datetime2(0) = '20220211';

WITH R AS 
(
    SELECT TheDate = @Start
    UNION ALL
    SELECT DATEADD(DAY, 1, R.TheDate)
    FROM R
    WHERE R.TheDate < @End
)
SELECT 
    STRING_AGG(QUOTENAME(R.TheDate), ',')
FROM R;
Run Code Online (Sandbox Code Playgroud)


J.D*_*.D. 7

关于这个问题的答案很酷,但是为了填写缺失的日期范围,我更喜欢使用表格来经典地解决这个问题DateDimensions。下面是Aaron Bertrand 编写的如何生成一个示例。

当然,如果您不需要所有额外的维度,您可以只为日期生成它。DateDimensions然后,您所需要的只是通过其他表中的日期字段对表进行简单的外部联接,以填补您缺少的日期的空白,如下所示:

SELECT 
    ISNULL(YT.DeviceId, 1) AS DeviceId, 
    DD.[Date] AS [Time], 
    ISNULL(YT.Context, 'Fake') AS Context,
    ISNULL(YT.[Value], 'False') AS [Value]
FROM DateDimensions AS DD
LEFT JOIN YourTable AS YT
    ON DD.[Date] = YT.[Time]
WHERE DD.[Date] >= '2022-02-07'
    AND DD.[Date] <= '2022-02-10'
Run Code Online (Sandbox Code Playgroud)

对于非常大的日期范围和数据集,这可能不是提高性能的第一方法,但我发现它是最简单的,通常足以让您跨越终点线,特别是在正确索引的情况下。