在SELECT字段中多次使用相同的公式

时间:2014-04-09 13:55:10

标签: sql-server tsql

我有一个查询,我按如下方式计算WorkingTime的值:

 SELECT
    SUM(CASE WHEN a.Type = 1
                THEN CAST(DATEDIFF(s,
                                ca.StartTime,
                                ca.EndTime) AS FLOAT)
                    / 60.0
                ELSE NULL
        END) AS WorkingTime ,
    AnnotherResult ...

AnotherResult的值也用一些方程计算,在这个等式中我需要使用WorkingTime。

我是否需要重复SUM(CASE ....),它用于在我需要的任何地方计算WorkingTime,或者我可以在以后的字段值计算中直接引用WorkingTime值?

3 个答案:

答案 0 :(得分:0)

您有多种选择,最常见的是

  1. 派生表
  2. CTE

  3. SELECT WorkingTime
        FROM (
               SELECT SUM(CASE WHEN a.Type = 1 THEN CAST(DATEDIFF(s, ca.StartTime, ca.EndTime) AS FLOAT) / 60.0
                               ELSE NULL
                          END) AS WorkingTime .....)
    

    ;WITH WT AS (SELECT SUM(CASE WHEN a.Type = 1 THEN CAST(DATEDIFF(s, ca.StartTime, ca.EndTime) AS FLOAT) / 60.0
                                   ELSE NULL
                              END) AS WorkingTime .....)
    SELECT WorkingTime
    FROM WT
    JOIN .....
    

答案 1 :(得分:0)

您可以使用此语法

Select ... , something/WorkingTime, something*WorkingTime, ...
From
(
Select *, CASE WHEN a.Type = 1
                THEN CAST(DATEDIFF(s,
                                ca.StartTime,
                                ca.EndTime) AS FLOAT)
                    / 60.0
                ELSE NULL
        END) AS WorkingTime 
from myTable
) t

现在您可以在查询的其他部分使用工作时间

答案 2 :(得分:0)

其他答案涵盖了其中一些项目(派生表,公用表表达式),但我想将它们整合到一个带有可执行代码的答案中,并提供一些其他选择(交叉应用,CLR SQL Server Aggregate )。

首先,我将讨论建议用户定义函数的注释。您当然可以将CASE语句及其计算封装到用户定义的函数中。我建议使用内联表值函数,它将输入开始时间,结束时间和类型。这将是一个纯粹的计算,它将是确定性的。它不会对性能产生负面影响,它会封装代码以供重用。这是一个聚合函数;聚合仍会在其结果上发生。请参阅代码示例中的方法#4,了解它将如何使用。

另一个用户定义的函数选项是创建一个包含基本聚合的内联表值用户定义函数。您可以使用该函数开始查询,然后将其结果用于其他计算。请参阅代码示例中的方法#5,以了解它将如何使用。但是,如果您有更复杂的逻辑并且避免代码重复很重要,您仍然会在函数本身中使用其他方法之一(CTE,派生表),或者您必须将多个用户定义的函数分层以执行连续的堆叠计算。

不建议使用用户定义(本机SQL)函数(标量,多语句表值)的其他选项,因为它们通常会有显着的性能损失。

另一个功能选项是创建CLR SQL Server Aggregate。 SQL Server本质上将数据列传递给在服务器上运行的程序集(在SQL Server之外),该程序集执行计算并使SQL Server可以访问结果以用于查询的其余部分。一些DBA不鼓励使用CLR功能,因为它增加了一些次要的服务器管理开销和安全性考虑。

纯SQL(不需要创建函数)方法包括使用派生表或公用表表达式(CTE)。 CTE本质上是派生表,可以在语句中多次重复使用,因此它们通常比派生表更灵活。但是,它们在SQL Server 2005之前不可用。Another answer提供了派生表解决方案,但将其称为“子查询”。从技术上讲,a derived table is not a subquerysubquery通常是在整个SQL语句用于返回在语句中其他位置使用的单个值时。 derived table是一个单独的SELECT语句,它生成一组值,并在语句的其余部分用作“虚拟表”。

值得注意的是,派生表(下面的#2)和CTE方法(下面的#1)比APPLY方法(下面的#3)提供了更好的代码减少,因为APPLY方法只是封装了计算,而不是聚合。聚合排序“接管”查询,因此为了使用这些聚合结果,必须将它们封装到CTE或派生表中。

在我对SQL Server 2005的测试中,前三个“纯SQL”方法都生成了相同的查询计划,因此方法的选择主要是语法偏好而不是性能。

DECLARE @CA TABLE
(
    StartTime datetime,
    EndTime datetime,
    ActivityType int,
    Employee varchar(50)
);
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-01 1:00 PM', '2014-04-01 2:00 PM', 1, 'Jim');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-03 1:00 PM', '2014-04-03 3:00 PM', 1, 'Jim');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-04 9:00 AM', '2014-04-04 11:00 AM', 1, 'Jim');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-05 10:00 AM', '2014-04-04 11:13 AM', 0, 'Jim');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-02 10:00 AM', '2014-04-02 3:00 PM', 1, 'Sally');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-04-04 1:55 PM', '2014-04-04 3:11 PM', 1, 'Sally');
INSERT INTO @CA (StartTime, EndTime, ActivityType, Employee) VALUES ('2014-03-30 1:45 PM', '2014-04-01 1:00 AM', 0, 'Sally');

-- Method #1: CTE

WITH BaseData AS
(
    SELECT
        Employee,
        SUM (
                CASE
                    WHEN ca.ActivityType = 1 THEN
                        1
                    ELSE
                        0
                END
            ) AS NumType1Entries,
        SUM (
                CASE
                    WHEN ca.ActivityType = 1 THEN
                        CAST(DATEDIFF(s, ca.StartTime, ca.EndTime) AS FLOAT) / 60.0
                    ELSE
                        NULL
                END
            ) AS TotalMinutesType1
    FROM        @CA ca
    GROUP BY    Employee
)
SELECT
    BaseData.Employee,
    BaseData.NumType1Entries,
    BaseData.TotalMinutesType1,
    CASE
        WHEN BaseData.NumType1Entries > 0 THEN
            BaseData.TotalMinutesType1 / BaseData.NumType1Entries
        ELSE
            NULL
    END AS MinutesPerType1Entry
FROM        BaseData;

-- Method #2: Derived Table

SELECT
    BaseData.Employee,
    BaseData.NumType1Entries,
    BaseData.TotalMinutesType1,
    CASE
        WHEN BaseData.NumType1Entries > 0 THEN
            BaseData.TotalMinutesType1 / BaseData.NumType1Entries
        ELSE
            NULL
    END AS MinutesPerType1Entry
FROM        (
                SELECT
                    Employee,
                    SUM (
                            CASE
                                WHEN ca.ActivityType = 1 THEN
                                    1
                                ELSE
                                    0
                            END
                        ) AS NumType1Entries,
                    SUM (
                            CASE
                                WHEN ca.ActivityType = 1 THEN
                                    CAST(DATEDIFF(s, ca.StartTime, ca.EndTime) AS FLOAT) / 60.0
                                ELSE
                                    NULL
                            END
                        ) AS TotalMinutesType1
                FROM @CA ca
                GROUP BY Employee
            ) AS BaseData;

-- Method #3 - Cross Apply

SELECT
    Employee,
    SUM (Calcs.Type1EntryCounter) AS NumType1Entries,
    SUM (Calcs.Type1EntryTotalMinutes) AS TotalMinutesType1,
    CASE
        WHEN SUM (Type1EntryCounter) > 0 THEN
            SUM (Type1EntryTotalMinutes) / SUM (Type1EntryCounter)
        ELSE
            NULL
    END AS MinutesPerType1Entry
FROM        @CA ca
CROSS APPLY (
                SELECT
                    CASE
                        WHEN ca.ActivityType = 1 THEN
                            1
                        ELSE
                            0
                    END AS Type1EntryCounter,
                    CASE
                        WHEN ca.ActivityType = 1 THEN
                            CAST(DATEDIFF(s, ca.StartTime, ca.EndTime) AS FLOAT) / 60.0
                        ELSE
                            NULL
                    END AS Type1EntryTotalMinutes
            ) AS Calcs
GROUP BY    Employee;

-- Method #4 - Inline Table-Valued User-Defined Function, Deterministic

-- Commented-out because it can't be run without creating the function first.

/*

SELECT
    Employee,
    SUM (Calcs.Type1EntryCounter) AS NumType1Entries,
    SUM (Calcs.Type1EntryTotalMinutes) AS TotalMinutesType1,
    CASE
        WHEN SUM (Type1EntryCounter) > 0 THEN
            SUM (Type1EntryTotalMinutes) / SUM (Type1EntryCounter)
        ELSE
            NULL
    END AS MinutesPerType1Entry
FROM        @CA ca
CROSS APPLY dbo.CalcType1Statistics
            (
                ca.ActivityType,
                ca.StartTime,
                ca.EndTime
            ) AS Calcs
GROUP BY    Employee;

*/

-- Method #5 - Inline Table-Valued User-Defined Function, Non-Deterministic Aggregation

-- Commented-out because it can't be run without creating the function first.
/*

SELECT
    EmpSummary.Employee,
    EmpSummary.NumType1Entries,
    EmpSummary.TotalMinutesType1,
    CASE
        WHEN EmpSummary.NumType1Entries THEN
            EmpSummary.TotalMinutesType1 / EmpSummary.NumType1Entries
        ELSE
            NULL
    END AS MinutesPerType1Entry
FROM    dbo.GetEmployeeSummary () AS EmpSummary

*/

P.S:

派生表的一个重要限制是您不能在派生表的定义中引用语句中的其他字段。他们必须独自站立;在SSMS中设计它们时,您需要能够单独运行它们才能被视为真正的派生表。您可以在加入时限制其结果并引用其他表。如果需要有一组与主SELECT的每一行相关(即相关)生成的值,则必须使用APPLY运算符。