sql - meses - Cuente los días de trabajo entre dos fechas
dias del mes sql (20)
Aquí hay una versión que funciona bien (creo). La tabla de días festivos contiene las columnas de fechas de vacaciones que contienen las vacaciones que su empresa observa.
DECLARE @RAWDAYS INT
SELECT @RAWDAYS = DATEDIFF(day, @StartDate, @EndDate )--+1
-( 2 * DATEDIFF( week, @StartDate, @EndDate ) )
+ CASE WHEN DATENAME(dw, @StartDate) = ''Saturday'' THEN 1 ELSE 0 END
- CASE WHEN DATENAME(dw, @EndDate) = ''Saturday'' THEN 1 ELSE 0 END
SELECT @RAWDAYS - COUNT(*)
FROM HOLIDAY NumberOfBusinessDays
WHERE [Holiday_Date] BETWEEN @StartDate+1 AND @EndDate
¿Cómo puedo calcular el número de días de trabajo entre dos fechas en SQL Server?
De lunes a viernes y debe ser T-SQL.
Crear una función como:
CREATE FUNCTION dbo.fn_WorkDays(@StartDate DATETIME, @EndDate DATETIME= NULL )
RETURNS INT
AS
BEGIN
DECLARE @Days int
SET @Days = 0
IF @EndDate = NULL
SET @EndDate = EOMONTH(@StartDate) --last date of the month
WHILE DATEDIFF(dd,@StartDate,@EndDate) >= 0
BEGIN
IF DATENAME(dw, @StartDate) <> ''Saturday''
and DATENAME(dw, @StartDate) <> ''Sunday''
and Not ((Day(@StartDate) = 1 And Month(@StartDate) = 1)) --New Year''s Day.
and Not ((Day(@StartDate) = 4 And Month(@StartDate) = 7)) --Independence Day.
BEGIN
SET @Days = @Days + 1
END
SET @StartDate = DATEADD(dd,1,@StartDate)
END
RETURN @Days
END
Puede llamar a la función como:
select dbo.fn_WorkDays(''1/1/2016'', ''9/25/2016'')
O como:
select dbo.fn_WorkDays(StartDate, EndDate)
from table1
En Cálculo de días de trabajo puede encontrar un buen artículo sobre este tema, pero como puede ver, no es tan avanzado.
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N''[dbo].[fn_WorkDays]'')
AND XType IN (N''FN'', N''IF'', N''TF'')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
@StartDate DATETIME,
@EndDate DATETIME = NULL --@EndDate replaced by @StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds @EndDate during date reversal.
DECLARE @Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF @StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF @EndDate IS NULL
SELECT @EndDate = @StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT @StartDate = DATEADD(dd,DATEDIFF(dd,0,@StartDate), 0),
@EndDate = DATEADD(dd,DATEDIFF(dd,0,@EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF @StartDate > @EndDate
SELECT @Swap = @EndDate,
@EndDate = @StartDate,
@StartDate = @Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,@StartDate, @EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,@StartDate, @EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, @StartDate) = ''Sunday''
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, @EndDate) = ''Saturday''
THEN 1
ELSE 0
END)
)
END
GO
Si necesita utilizar un calendario personalizado, es posible que deba agregar algunas verificaciones y algunos parámetros. Con suerte, proporcionará un buen punto de partida.
Encontré el siguiente TSQL una solución bastante elegante (no tengo permisos para ejecutar funciones). Encontré que el DATEDIFF
ignora DATEFIRST
y quería que mi primer día de la semana fuera un lunes. También quería que el primer día de trabajo se estableciera en cero y si se trata de un fin de semana el lunes será cero. Esto puede ayudar a alguien que tiene un requisito ligeramente diferente :)
No maneja feriados bancarios
SET DATEFIRST 1
SELECT
,(DATEDIFF(DD, [StartDate], [EndDate]))
-(DATEDIFF(wk, [StartDate], [EndDate]))
-(DATEDIFF(wk, DATEADD(dd,-@@DATEFIRST,[StartDate]), DATEADD(dd,-@@DATEFIRST,[EndDate]))) AS [WorkingDays]
FROM /*Your Table*/
Eso está funcionando para mí, en mi país los sábados y domingos son días no laborables.
Para mí es importante el momento de @StartDate y @EndDate.
CREATE FUNCTION [dbo].[fnGetCountWorkingBusinessDays]
(
@StartDate as DATETIME,
@EndDate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE @res int
SET @StartDate = CASE
WHEN DATENAME(dw, @StartDate) = ''Saturday'' THEN DATEADD(dd, 2, DATEDIFF(dd, 0, @StartDate))
WHEN DATENAME(dw, @StartDate) = ''Sunday'' THEN DATEADD(dd, 1, DATEDIFF(dd, 0, @StartDate))
ELSE @StartDate END
SET @EndDate = CASE
WHEN DATENAME(dw, @EndDate) = ''Saturday'' THEN DATEADD(dd, 0, DATEDIFF(dd, 0, @EndDate))
WHEN DATENAME(dw, @EndDate) = ''Sunday'' THEN DATEADD(dd, -1, DATEDIFF(dd, 0, @EndDate))
ELSE @EndDate END
SET @res =
(DATEDIFF(hour, @StartDate, @EndDate) / 24)
- (DATEDIFF(wk, @StartDate, @EndDate) * 2)
SET @res = CASE WHEN @res < 0 THEN 0 ELSE @res END
RETURN @res
END
GO
Esta es básicamente la respuesta de CMS sin la dependencia de una configuración de idioma particular. Y dado que estamos buscando genéricos, eso significa que también debería funcionar para todas las configuraciones de @@datefirst
.
datediff(day, <start>, <end>) - datediff(week, <start>, <end>) * 2
/* if start is a Sunday, adjust by -1 */
+ case when datepart(weekday, <start>) = 8 - @@datefirst then -1 else 0 end
/* if end is a Saturday, adjust by -1 */
+ case when datepart(weekday, <end>) = (13 - @@datefirst) % 7 + 1 then -1 else 0 end
datediff(week, ...)
siempre usa un límite de sábado a domingo durante semanas, por lo que la expresión es determinista y no necesita modificación (siempre que nuestra definición de días laborables sea consistentemente de lunes a viernes). Numeración de días varía de acuerdo con la configuración @@datefirst
y los cálculos modificados manejan esta corrección con la pequeña complicación de una aritmética modular.
Una forma más limpia de lidiar con el asunto del sábado / domingo es traducir las fechas antes de extraer el valor del día de la semana. Después del cambio, los valores volverán a estar en línea con una numeración fija (y probablemente más familiar) que comienza con 1 el domingo y termina con 7 el sábado.
datediff(day, <start>, <end>) - datediff(week, <start>, <end>) * 2
+ case when datepart(weekday, dateadd(day, @@datefirst, <start>)) = 1 then -1 else 0 end,
+ case when datepart(weekday, dateadd(day, @@datefirst, <end>)) = 7 then -1 else 0 end
He rastreado esta forma de la solución al menos hasta 2002 y un artículo de Itzik Ben-Gan. ( https://technet.microsoft.com/en-us/library/aa175781(v=sql.80).aspx ) Aunque necesitaba una pequeña modificación, ya que los tipos de date
más nuevos no permiten la aritmética de fecha, de lo contrario es idéntica.
Mi versión de la respuesta aceptada como una función usando DATEPART
, así que no tengo que hacer una comparación de cadenas en la línea con
DATENAME(dw, @StartDate) = ''Sunday''
De todos modos, aquí está mi función de fecha de negocios
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION BDATEDIFF
(
@startdate as DATETIME,
@enddate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE @res int
SET @res = (DATEDIFF(dd, @startdate, @enddate) + 1)
-(DATEDIFF(wk, @startdate, @enddate) * 2)
-(CASE WHEN DATEPART(dw, @startdate) = 1 THEN 1 ELSE 0 END)
-(CASE WHEN DATEPART(dw, @enddate) = 7 THEN 1 ELSE 0 END)
RETURN @res
END
GO
Otro método para calcular los días hábiles es usar un ciclo WHILE que básicamente itera a través de un rango de fechas e incrementarlo en 1 cada vez que los días se encuentren dentro de lunes a viernes. La secuencia de comandos completa para calcular los días de trabajo utilizando el ciclo WHILE se muestra a continuación:
CREATE FUNCTION [dbo].[fn_GetTotalWorkingDaysUsingLoop]
(@DateFrom DATE,
@DateTo DATE
)
RETURNS INT
AS
BEGIN
DECLARE @TotWorkingDays INT= 0;
WHILE @DateFrom <= @DateTo
BEGIN
IF DATENAME(WEEKDAY, @DateFrom) IN(''Monday'', ''Tuesday'', ''Wednesday'', ''Thursday'', ''Friday'')
BEGIN
SET @TotWorkingDays = @TotWorkingDays + 1;
END;
SET @DateFrom = DATEADD(DAY, 1, @DateFrom);
END;
RETURN @TotWorkingDays;
END;
GO
Aunque la opción de ciclo WHILE es más limpia y usa menos líneas de código, tiene el potencial de ser un cuello de botella de rendimiento en su entorno, particularmente cuando su rango de fechas abarca varios años.
Puede ver más métodos sobre cómo calcular días y horas de trabajo en este artículo: https://www.sqlshack.com/how-to-calculate-work-days-and-hours-in-sql-server/
Para días laborables, de lunes a viernes, puede hacerlo con un SELECT único, como este:
DECLARE @StartDate DATETIME
DECLARE @EndDate DATETIME
SET @StartDate = ''2008/10/01''
SET @EndDate = ''2008/10/31''
SELECT
(DATEDIFF(dd, @StartDate, @EndDate) + 1)
-(DATEDIFF(wk, @StartDate, @EndDate) * 2)
-(CASE WHEN DATENAME(dw, @StartDate) = ''Sunday'' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, @EndDate) = ''Saturday'' THEN 1 ELSE 0 END)
Si desea incluir vacaciones, debe resolverlo un poco ...
Para la diferencia entre las fechas, incluidas las vacaciones, fui de esta manera:
1) Tabla con vacaciones:
CREATE TABLE [dbo].[Holiday](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NULL,
[Date] [datetime] NOT NULL)
2) Tenía mi plan de planificación de esta manera y quería completar la columna Días de trabajo que estaba vacía:
CREATE TABLE [dbo].[Plan_Phase](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id_Plan] [int] NOT NULL,
[Id_Phase] [int] NOT NULL,
[Start_Date] [datetime] NULL,
[End_Date] [datetime] NULL,
[Work_Days] [int] NULL)
3) Entonces, para obtener "Días de trabajo" para luego completar mi columna solo tenía que:
SELECT Start_Date, End_Date,
(DATEDIFF(dd, Start_Date, End_Date) + 1)
-(DATEDIFF(wk, Start_Date, End_Date) * 2)
-(SELECT COUNT(*) From Holiday Where Date >= Start_Date AND Date <= End_Date)
-(CASE WHEN DATENAME(dw, Start_Date) = ''Sunday'' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, End_Date) = ''Saturday'' THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where Start_Date = Date) > 0 THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where End_Date = Date) > 0 THEN 1 ELSE 0 END) AS Work_Days
from Plan_Phase
Espero poder ayudar.
Aclamaciones
Si necesita agregar días de trabajo a una fecha determinada, puede crear una función que depende de una tabla de calendario, que se describe a continuación:
CREATE TABLE Calendar
(
dt SMALLDATETIME PRIMARY KEY,
IsWorkDay BIT
);
--fill the rows with normal days, weekends and holidays.
create function AddWorkingDays (@initialDate smalldatetime, @numberOfDays int)
returns smalldatetime as
begin
declare @result smalldatetime
set @result =
(
select t.dt from
(
select dt, ROW_NUMBER() over (order by dt) as daysAhead from calendar
where dt > @initialDate
and IsWorkDay = 1
) t
where t.daysAhead = @numberOfDays
)
return @result
end
Todo el crédito a Bogdan Maxim y Peter Mortensen. Esta es su publicación, acabo de agregar vacaciones a la función (Esto supone que tiene una tabla "tblHolidays" con un campo de fecha y hora "HolDate".
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N''[dbo].[fn_WorkDays]'')
AND XType IN (N''FN'', N''IF'', N''TF'')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
@StartDate DATETIME,
@EndDate DATETIME = NULL --@EndDate replaced by @StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds @EndDate during date reversal.
DECLARE @Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF @StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF @EndDate IS NULL
SELECT @EndDate = @StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT @StartDate = DATEADD(dd,DATEDIFF(dd,0,@StartDate), 0),
@EndDate = DATEADD(dd,DATEDIFF(dd,0,@EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF @StartDate > @EndDate
SELECT @Swap = @EndDate,
@EndDate = @StartDate,
@StartDate = @Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,@StartDate, @EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,@StartDate, @EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, @StartDate) = ''Sunday''
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, @EndDate) = ''Saturday''
THEN 1
ELSE 0
END)
--Subtract all holidays
-(Select Count(*) from [DB04/DB04].[Gateway].[dbo].[tblHolidays]
where [HolDate] between @StartDate and @EndDate )
)
END
GO
-- Test Script
/*
declare @EndDate datetime= dateadd(m,2,getdate())
print @EndDate
select [Master].[dbo].[fn_WorkDays] (getdate(), @EndDate)
*/
Tomé varios ejemplos aquí, pero en mi situación particular tenemos un @PromisedDate para la entrega y un @ReceivedDate para la recepción real del artículo. Cuando se recibía un artículo antes del "PromisedDate", los cálculos no totalizaban correctamente a menos que ordenara que las fechas pasaran a la función por orden de calendario. No queriendo verificar las fechas cada vez, cambié la función para manejar esto por mí.
Create FUNCTION [dbo].[fnGetBusinessDays]
(
@PromiseDate date,
@ReceivedDate date
)
RETURNS integer
AS
BEGIN
DECLARE @days integer
SELECT @days =
Case when @PromiseDate > @ReceivedDate Then
DATEDIFF(d,@PromiseDate,@ReceivedDate) +
ABS(DATEDIFF(wk,@PromiseDate,@ReceivedDate)) * 2 +
CASE
WHEN DATENAME(dw, @PromiseDate) <> ''Saturday'' AND DATENAME(dw, @ReceivedDate) = ''Saturday'' THEN 1
WHEN DATENAME(dw, @PromiseDate) = ''Saturday'' AND DATENAME(dw, @ReceivedDate) <> ''Saturday'' THEN -1
ELSE 0
END +
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN @ReceivedDate AND @PromiseDate
AND DATENAME(dw, HolidayDate) <> ''Saturday'' AND DATENAME(dw, HolidayDate) <> ''Sunday'')
Else
DATEDIFF(d,@PromiseDate,@ReceivedDate) -
ABS(DATEDIFF(wk,@PromiseDate,@ReceivedDate)) * 2 -
CASE
WHEN DATENAME(dw, @PromiseDate) <> ''Saturday'' AND DATENAME(dw, @ReceivedDate) = ''Saturday'' THEN 1
WHEN DATENAME(dw, @PromiseDate) = ''Saturday'' AND DATENAME(dw, @ReceivedDate) <> ''Saturday'' THEN -1
ELSE 0
END -
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN @PromiseDate and @ReceivedDate
AND DATENAME(dw, HolidayDate) <> ''Saturday'' AND DATENAME(dw, HolidayDate) <> ''Sunday'')
End
RETURN (@days)
END
Un enfoque es "recorrer las fechas" de principio a fin junto con una expresión de caso que verifica si el día no es un sábado o un domingo y lo marca (1 para el día de la semana, 0 para el fin de semana). Y al final solo suma banderas (sería igual al conteo de 1-banderas como la otra bandera es 0) para darte el número de días de la semana.
Puede utilizar un tipo de función de utilidad GetNums (startNumber, endNumber) que genera una serie de números para ''bucle'' desde la fecha de inicio hasta la fecha de finalización. Consulte http://tsql.solidq.com/SourceCodes/GetNums.txt para una implementación. La lógica también se puede extender para atender las vacaciones (por ejemplo, si tiene una mesa de vacaciones)
declare @date1 as datetime = ''19900101''
declare @date2 as datetime = ''19900120''
select sum(case when DATENAME(DW,currentDate) not in (''Saturday'', ''Sunday'') then 1 else 0 end) as noOfWorkDays
from dbo.GetNums(0,DATEDIFF(day,@date1, @date2)-1) as Num
cross apply (select DATEADD(day,n,@date1)) as Dates(currentDate)
Usando una tabla de fechas:
DECLARE
@StartDate date = ''2014-01-01'',
@EndDate date = ''2014-01-31'';
SELECT
COUNT(*) As NumberOfWeekDays
FROM dbo.Calendar
WHERE CalendarDate BETWEEN @StartDate AND @EndDate
AND IsWorkDay = 1;
Si no tiene eso, puede usar una tabla de números:
DECLARE
@StartDate datetime = ''2014-01-01'',
@EndDate datetime = ''2014-01-31'';
SELECT
SUM(CASE WHEN DATEPART(dw, DATEADD(dd, Number-1, @StartDate)) BETWEEN 2 AND 6 THEN 1 ELSE 0 END) As NumberOfWeekDays
FROM dbo.Numbers
WHERE Number <= DATEDIFF(dd, @StartDate, @EndDate) + 1 -- Number table starts at 1, we want a 0 base
Deben ser rápidos y eliminar la ambigüedad / complejidad. La primera opción es la mejor, pero si no tienes una tabla de calendario, siempre puedes crear una tabla de números con un CTE.
(Estoy a unos pocos puntos de comentar los privilegios)
Si decide renunciar al día +1 en la elegante solución de CMS , tenga en cuenta que si su fecha de inicio y fecha de finalización son el mismo fin de semana, obtendrá una respuesta negativa. Es decir, 2008/10/26 a 2008/10/26 devuelve -1.
mi solución bastante simplista:
select @Result = (..CMS''s answer..)
if (@Result < 0)
select @Result = 0
RETURN @Result
.. que también establece todas las publicaciones erróneas con la fecha de inicio después de la fecha de finalización en cero. Algo que puede o no estar buscando.
DECLARE @TotalDays INT,@WorkDays INT
DECLARE @ReducedDayswithEndDate INT
DECLARE @WeekPart INT
DECLARE @DatePart INT
SET @TotalDays= DATEDIFF(day, @StartDate, @EndDate) +1
SELECT @ReducedDayswithEndDate = CASE DATENAME(weekday, @EndDate)
WHEN ''Saturday'' THEN 1
WHEN ''Sunday'' THEN 2
ELSE 0 END
SET @TotalDays=@TotalDays-@ReducedDayswithEndDate
SET @WeekPart=@TotalDays/7;
SET @DatePart=@TotalDays%7;
SET @WorkDays=(@WeekPart*5)+@DatePart
RETURN @WorkDays
CREATE FUNCTION x
(
@StartDate DATETIME,
@EndDate DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE @Teller INT
SET @StartDate = DATEADD(dd,1,@StartDate)
SET @Teller = 0
IF DATEDIFF(dd,@StartDate,@EndDate) <= 0
BEGIN
SET @Teller = 0
END
ELSE
BEGIN
WHILE
DATEDIFF(dd,@StartDate,@EndDate) >= 0
BEGIN
IF DATEPART(dw,@StartDate) < 6
BEGIN
SET @Teller = @Teller + 1
END
SET @StartDate = DATEADD(dd,1,@StartDate)
END
END
RETURN @Teller
END
Create Function dbo.DateDiff_WeekDays
(
@StartDate DateTime,
@EndDate DateTime
)
Returns Int
As
Begin
Declare @Result Int = 0
While @StartDate <= @EndDate
Begin
If DateName(DW, @StartDate) not in (''Saturday'',''Sunday'')
Begin
Set @Result = @Result +1
End
Set @StartDate = DateAdd(Day, +1, @StartDate)
End
Return @Result
Fin
DECLARE @StartDate datetime,@EndDate datetime
select @StartDate=''3/2/2010'', @EndDate=''3/7/2010''
DECLARE @TotalDays INT,@WorkDays INT
DECLARE @ReducedDayswithEndDate INT
DECLARE @WeekPart INT
DECLARE @DatePart INT
SET @TotalDays= DATEDIFF(day, @StartDate, @EndDate) +1
SELECT @ReducedDayswithEndDate = CASE DATENAME(weekday, @EndDate)
WHEN ''Saturday'' THEN 1
WHEN ''Sunday'' THEN 2
ELSE 0 END
SET @TotalDays=@TotalDays-@ReducedDayswithEndDate
SET @WeekPart=@TotalDays/7;
SET @DatePart=@TotalDays%7;
SET @WorkDays=(@WeekPart*5)+@DatePart
SELECT @WorkDays