在不使用 where 子句中的函数的情况下计算年龄

Calculating Age without using a function in the where clause

提问人:Stevie Gray 提问时间:11/16/2023 最后编辑:Stevie Gray 更新时间:11/17/2023 访问量:73

问:

我一直在阅读有关避免在 WHERE 子句中使用函数的文章(因为它们通常必须使用扫描并且通常效率不高)。

我有一个 SP,它根据客户在过去几天是否过生日来计算他们的年龄。

我正在努力将生日的计算从 where 子句移动到 select 语句中。这是当前的 SP。

CREATE PROCEDURE dbo.SP_CalculateAge (
@LastNDays int
) AS
        UPDATE Customers
           SET CustomerAge = DATEDIFF (yy, CustomerBirthDate, GETDATE ())
         WHERE DATEADD (yy, DATEDIFF (yy, CustomerBirthDate, GETDATE ()), CustomerBirthDate) BETWEEN
            DATEADD (dd, ISNULL (@LastNDays, -4), GETDATE ()) AND GETDATE ();

我对它所创造的时代感到满意。查看其他建议,我还可以通过执行以下操作来计算 AGE

SELECT DATEDIFF(year, CustomerBirthDate, getdate()) + CASE WHEN (DATEADD(year,DATEDIFF(year, CustomerBirthDate, getdate()) , CustomerBirthDate) > getdate()) THEN - 1 ELSE 0 END
      from customers' 

我的问题是我现在不确定我可以将搜索以查看生日是否发生在过去 4 天内的部分放在哪里。我只能在WHERE子句中想到它,而不是在SELECT中。

BETWEEN
            DATEADD (dd, ISNULL (@LastNDays, -4), GETDATE ()) AND GETDATE ();

我假设我需要某种 CASE 语句或 IF。

目前,这贯穿于每个客户,每一次......我想专门为过去 N 天内生日的人运行它。

CREATE TABLE [dbo].[Customers](
    [CustomerID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [CustomerFirstName] [varchar](30) NOT NULL,
    [CustomerBirthDate] [datetime] NULL,
    [CustomerAge] [smallint] NULL
     CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED 
(
    [CustomerID] ASC
)
)

CREATE NONCLUSTERED INDEX [ix_Customers_CustomerBirthDate] ON [dbo].[Customers]
(
    [CustomerBirthDate] ASC
)

INCLUDE([CustomerID],[CustomerFirstName],[CustomerAge])
INSERT INTO [dbo].[Customers] ([CustomerFirstName],[CustomerBirthDate])
VALUES('Stephen', '2000-11-14'),
('Sarah', '1980-11-15'),
('Zoe', '1965-01-01')


基于此,如果我们要运行代码,它会将 CustomerAge 添加到 Stephen 和 Sarah,但保留 Zoe NULL。

任何建议将不胜感激。

亲切问候 斯蒂芬

sql-server 函数 日期 t-sql where-子句

评论

7赞 siggemannen 11/16/2023
不要浪费时间计算年龄,因为它永远不正确。创建一个计算列来执行您的函数操作,您将始终具有正确的年龄
3赞 Brad 11/16/2023
此外,您不想在 where 子句中使用函数(这是正确的,它对性能不利),但是如果您在 where 子句中进行计算/逻辑,它就像使用函数一样。
2赞 T N 11/17/2023
同意。不应存储需要不断更新的列。最好将其替换为计算列,例如。请参阅此 db<>fiddle。如果替换列不是一种选择,我不会太担心表扫描。每天对客户表进行一次表扫描并不是性能的杀手。CustomerAge AS <calculation>
1赞 T N 11/17/2023
...尽管每天一次的表扫描不是问题,但如果有任何可能受到影响的触发器或更改跟踪,您可能希望限制应用的实际更新。这可以通过使用 a 来计算更新的值并添加 a 以将更新限制为仅具有更改的行来完成。像.CROSS APPLYWHEREUPDATE C SET CustomerAge = A.CalculatedAge FROM Customers C CROSS APPLY (SELECT CalculatedAge = ...) A WHERE C.CustomerAge <> A.CalculatedAge OR C.CustomerAge IS NULL

答:

0赞 Kiran Ramaswamy 11/17/2023 #1

我认为您上面给出的答案大多是有效的,但是,就我个人而言,我非常不喜欢计算列,无论它们是直接在表定义中定义的,还是通过工作来维护的。我不喜欢前者,因为它们会造成尴尬的索引 - 为了索引计算的列,您需要使其持久化,这意味着每次更新表中的值时都需要重新计算该列,我不喜欢后者,因为它们依赖于作业。

如果你的目标只是让你的年龄计算与你所询问的子句一起工作,我认为这应该为你完成工作,并且也应该是适当的可索引的:WHERE

DECLARE @LastNDays INT = 4

CREATE TABLE #Customers
(
    [CustomerID] [int] IDENTITY PRIMARY KEY,
    [CustomerFirstName] [varchar] (30) NOT NULL,
    [CustomerBirthDate] [datetime] NULL
)

INSERT INTO #Customers ([CustomerFirstName], [CustomerBirthDate])
VALUES
('Stephen', '2000-11-14'),
('Sarah', '1980-11-17'),
('Zoe', '1965-01-01')

SELECT
    CustomerFirstName,
    CustomerBirthDate,
    DATEDIFF(YY, CustomerBirthDate, GETDATE()) + (CASE WHEN DATEADD(YY, DATEDIFF(YY, CustomerBirthDate, GETDATE()), CustomerBirthDate) > GETDATE() THEN -1 ELSE 0 END) AS CustomerAge,
    DATEDIFF(DD, GETDATE(), DATEADD(YY, DATEDIFF(YY, CustomerBirthDate, GETDATE()), CustomerBirthDate)) AS DaysSinceLastBirthDate
FROM #Customers
WHERE DATEDIFF(DD, GETDATE(), DATEADD(YY, DATEDIFF(YY, CustomerBirthDate, GETDATE()), CustomerBirthDate)) BETWEEN -@LastNDays AND 0

DROP TABLE #Customers
``

评论

0赞 Charlieface 11/17/2023
您仍然需要每天重新计算数据,因为数据会不断变化......GETDATE()
0赞 Kiran Ramaswamy 11/17/2023
在运行查询时,你将获得当前年龄。您没有存储任何内容,因此没有任何内容需要更新。
0赞 siggemannen 11/17/2023
不需要保留一个计算列来编制索引,除非我认为它是浮点数。
0赞 Kiran Ramaswamy 11/18/2023
@siggermannen,如果计算列是确定性的,则只能在不保留的情况下编制索引。GETDATE() 不是一个确定性函数。有关列表,请参见 learn.microsoft.com/en-us/sql/relational-databases/...
0赞 Zorkolot 12/5/2023 #2

这是一个递归示例,使用 CTE 获取请求的前几年数的前 3 天。您可以创建一个存储过程,该过程对参数执行相同的操作。CTE 输出的有效日期列表可以追溯到很多年前,因此如果您的生日列被索引,那么它应该是可优化的。

我将使用 2 个 CTE,第一个得到前 3 天。第二个 CTE 递归回溯回 150 年(假设没有人活那么久)来选择这 150 年的前 3 天。

--Create DDL
CREATE TABLE dbo.Customers (
 customer_id int IDENTITY(1,1)
,birthday datetime
)
CREATE NONCLUSTERED INDEX ncx_Customers_birthday ON dbo.Customers(birthday)

INSERT INTO dbo.Customers (birthday) VALUES
 ('1975-12-15'), ('1980-12-17'), ('1992-04-25'), ('2022-02-05'), ('2013-07-06')
,('1982-12-03'), ('2000-12-04'), ('2002-12-02')

------------------------------------------------
DECLARE @checkdate datetime = '2023-12-05';
DECLARE @yearsAgo tinyint = 150
DECLARE @stopHistory datetime = DATEADD(YEAR, -@yearsAgo, @checkdate);

WITH CTE3priordays AS (
 SELECT @checkdate AS [checkdate]
 UNION ALL
 SELECT DATEADD(DAY, -1, @checkdate) 
 UNION ALL
 SELECT DATEADD(DAY, -2, @checkdate)
 UNION ALL
 SELECT DATEADD(DAY, -3, @checkdate)
)

,CTE_150prioryears AS (
 SELECT DATEADD(YEAR, -1, checkdate) AS [byYears]
   FROM CTE3priordays
 UNION ALL
 SELECT DATEADD(YEAR, -1, byYears) AS [byYears]
   FROM CTE_150prioryears
  WHERE byYears >= @stopHistory
)

SELECT *
  FROM dbo.Customers
 WHERE birthday IN (
--The recursive list of dates
SELECT byYears 
  FROM CTE_150prioryears 
)
OPTION(maxrecursion 700) 
--covering 3 days prior for 150 years produces 601 rows, hence we need maxrecursion

将生成示例输出:

customer_id birthday
----------- ------------------------
6           1982-12-03 00:00:00.000
7           2000-12-04 00:00:00.000
8           2002-12-02 00:00:00.000

这似乎是你所追求的。