I have to create some random data and so far with the help of this forum I have done 80% of it but I am now stuck (again)
What I need is this I have a column called Requested Date, this date should randomised but be between 1 and 10 days later than the Order Date. Can this be done?
DECLARE #OrderNumber varchar (30)
DECLARE #OrderDate int
DECLARE #OrderLineNumber varchar(50)
DECLARE #CustomerSkey int
DECLARE #ProductSkey int
DECLARE #OrderMethodSkey int
DECLARE #Quantity int
DECLARE #Cost Decimal(18,3)
DECLARE #RequestedDate int
SET #OrderNumber = 1
SET #OrderDate = 0
SET #OrderLineNumber = 1
SET #CustomerSkey = 1
SET #ProductSkey = 1
SET #OrderMethodSkey = 1
SET #Quantity = 1
SET #Cost = 1
SET #RequestedDate = 0
WHILE #OrderNumber <= 100
WHILE #OrderDate <= 100
WHILE #OrderLineNumber <= 100
WHILE #CustomerSkey <= 100
WHILE #ProductSkey <= 100
WHILE #OrderMethodSkey <= 100
WHILE #Quantity <= 100
WHILE #Cost <= 100
WHILE #RequestedDate <= 100
BEGIN
INSERT INTO Orders
(OrderNumber
, OrderDate
, OrderLineNumber
, CustomerSkey
, ProductSkey
, OrderMethodSkey
, OrderTime
, Quantity
, Cost
, RequestedDate)
SELECT
'ORD' + Right ('000000' + CAST (#OrderNumber AS varchar (30)), 6)
,DATEADD (day, CAST (RAND () * 1500 as int), '2008-1-1')
,(Right ('0' + CAST (#OrderLineNumber AS varchar (30)), 6))
,(99 * RAND()) + 1
,(99 * RAND()) + 1
,(2 * RAND()) + 1
,DATEADD(ms, cast(86400000 * RAND() as int), convert(time, '00:00'))
,(190 * RAND()) + 10
,(40 * RAND()) + 10
,DATEADD (day, CAST (RAND () * 10 as int), #RequestedDate)
SET #OrderNumber = #OrderNumber + 1
SET #OrderDate = #OrderDate + 1
SET #OrderLineNumber = #OrderLineNumber + 1
SET #CustomerSkey = #CustomerSkey + 1
SET #ProductSkey = #ProductSkey + 1
SET #OrderMethodSkey = #OrderMethodSkey + 1
SET #Quantity = #Quantity + 1
SET #Cost = #Cost + 1
SET #RequestedDate = #RequestedDate + 1
END
In PostgreSQL you can do:
with t as (select '2013-01-01'::date as order_date)
select
order_date,
(order_date + round(.5 + 10*random()) * interval '1 day')::date as requested_date,
(order_date + round(.5 + 10*random()) * interval '1 day')::date - order_date as difference from t
0) random() function returns random number between 0 and 1
1) round(.5 + 10*random() will give you number between 1 and 10
2) random number is added to date as an interval in days
3) result is casted to date (because step 2 will return timestamp)
To prove that round(.5 + 10*random() gets random number between 1 and 10:
with t as (select round(.5 + 10*random()) rnd from generate_series(1,1000000,1))
select rnd, count(*) cnt from t group by rnd order by rnd
rnd cnt
1 100249
2 100817
3 99550
4 99813
5 100065
6 99468
7 100089
8 99652
9 99889
10 100408
you can calculate the value of order_date before the insert statement
add a new variable with the other declares
declare #od datetime
inside the while loops but before the insert statement set up the order date
select #od = DATEADD (day, CAST (RAND () * 1500 as int), '2008-1-1')
use this variable in the insert statement so
,DATEADD (day, CAST (RAND () * 1500 as int), '2008-1-1')
becomes
,#od
and use this value to calculate the request date, so
becomes
,DATEADD (day, CAST (RAND () * 10 as int), #RequestedDate)
and
,DATEADD (day, CAST (RAND () * 10 as int), #od)
sql fiddle
Related
Generate the following output and where the timestamp is at 5 minutes interval starting from a start date and will end at an end date and the placeID needs to be random.(between 1 to 4) using SQL server.
--Creating data base ; name of the database PARKINGDATA for better understanding
Create database parkingdata;
use parkingdata;
CREATE Table marketplace
(
MarketPlaceId nvarchar,
DateTimeStamp datetime,
)
--drop table marketplace;
SELECT * from marketplace;
Declare #RandomMarketPlaceID int;
Declare #RandomDateTimeStamp datetime;
Declare #IdLowerLimitNumber int;
Declare #IdUpperLimitNumber int;
Set #IdLowerLimitNumber = 1
Set #IdUpperLimitNumber = 4
DECLARE #start DATETIME = '2015-01-01 00:00:00';
DECLARE #end DATETIME = '2015-03-05 00:00:00';
DECLARE #minuteInterval INT = 5;
DECLARE #countNum INT = DATEDIFF(MINUTE, #start, #end) / #minuteInterval + 1;
Declare #count int
Set #count = 1
While #count <= 20000
Begin
Select #RandomMarketPlaceID = Round(((#IdUpperLimitNumber - #IdLowerLimitNumber) * Rand()) +
#IdLowerLimitNumber, 0)
WITH N1 AS (SELECT N = 1 UNION ALL SELECT 1), -- 2
N2 AS (SELECT N = 1 FROM N1 CROSS JOIN N1 AS N), -- 4
N3 AS (SELECT N = 1 FROM N2 CROSS JOIN N2 AS N), -- 16
N4 AS (SELECT N = 1 FROM N3 CROSS JOIN N3 AS N), -- 256
N5 AS (SELECT N = 1 FROM N4 CROSS JOIN N4 AS N), -- 65536
Numbers AS (SELECT Number = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM N5)
SELECT #RandomDateTimeStamp= DATEADD(MINUTE, #minuteInterval * (Numbers.Number - 1), #start)
FROM Numbers
WHERE Numbers.Number <= #countNum;
Insert Into marketplace values (#RandomMarketPlaceID, #RandomDateTimeStamp)
Print #count
Set #count = #count + 1
End
Required OUTPUT Should be:
MarketPlaceID DateTimestamp
3 2015-01-01 00:00:00
2 2015-01-01 00:05:00
4 2015-01-01 00:10:00
1 2015-01-01 00:15:00
2 2015-01-01 00:20:00
.
.
.
.till the end date.
Generate the following output and where the timestamp is at 5 minutes interval starting from a start date and will end at an end date and the placeID needs to be random.(between 1 to 4)
Do we really need the table Number because we can achieve the desired results by using #countNum in the loop?
try the following:
While #count <= #countNum
Begin
Select #RandomMarketPlaceID = Round(((#IdUpperLimitNumber - #IdLowerLimitNumber) * Rand()) + #IdLowerLimitNumber, 0)
SELECT #RandomDateTimeStamp = DATEADD(MINUTE, #minuteInterval * #count, #start);
Insert Into marketplace values (#RandomMarketPlaceID, #RandomDateTimeStamp)
Set #count = #count + 1
End
Please find the demo here.
I have a Price list table in SQL Server 2008R2 and want to calculate the Price for a Service according to the time where the Service has been made.
timefrom |timeto |Price
-------- |------ |-----
1900-01-01 00:00:00|1900-01-01 07:00:00|20.00
1900-01-01 07:00:00|1900-01-01 19:00:00|15.00
1900-01-01 19:00:00|1900-01-02 00:00:00|20.00
This pricelist Shows different Price during night time starting at 19:00 and Lasting until 07.00 AM and daytime from 07:00 to 19:00.
Minutes have to be rounded up and down to quarter hours.
There is some more to take care of, such as Minimum notice Periode (#Vorlaufzeit) and if weekday of Weekend. Both conditions are met and not the Problem. My Problem is the first and the last record, where I have to round up and/or round down, which are incorrect. Both lines have 1 in the loop and should be corrected correctly in the both updates, but does not.
So, for example a Service from 2016-11-04 10:50 (rounding down to 10:45 which is 0.25 hours) to 2016-11-04 19:25 (rounded up to 19:30 which is 0.5 hours) is 0.25+8+0.5= 8.75 hours and costs 8.25*15 + 0.5*20 = 133.75.
I tried with this code, but it does not bring me the correct result. It is only the first and the last record where I have to round up or down. It is only correct, when there are full hours.
DECLARE #Dauer int
DECLARE #X int --Loopcounter für Stunden
declare #Y int --Loopcounter für Tageszahler
declare #Anfangszeit datetime
declare #Anfangsstunde datetime
declare #Endzeit datetime
declare #Vorlaufzeit int --in Minuten
declare #ErsteZeitvon datetime
declare #SummeAnzStunden decimal(8,2)
declare #MinimumZeit int
declare #ZeitvonVolleStunde int -- aus 07:25 mach 7 Uhr
declare #ZeitbisVolleStunde int
declare #AnfangsDatumZeit as datetime
declare #EndDatumZeit as datetime
declare #AnfangsDatumZeitLoop as datetime
declare #AnfangsZeitLoop as datetime
declare #TagesZaehler int
set #AnfangsDatumZeit = #Datumvon+#Zeitvon
set #EndDatumZeit = #Datumbis+#Zeitbis
set #Tageszaehler=datediff(day,#AnfangsDatumZeit, #EndDatumZeit)
declare #t1 table ( PreisID int, AnzStunden decimal(5,2), Preis decimal(8,2), Anfangszeit datetime, Prüfzeit datetime, startzeit datetime, endezeit datetime, Vorlaufzeit int, Dauer int, PreisFT decimal(8,2), DatZeitvon datetime, DatZeitbis datetime, Tageszaehler int )
-- Insert statements for procedure here
set #ZeitvonVolleStunde=Datediff(hour, '00:00:00', #Zeitvon)
set #ZeitbisVolleStunde=Datediff(minute, '00:00:00', #Zeitbis)
set #Dauer=ceiling(Datediff(minute, #AnfangsDatumZeit, #EndDatumZeit)/60.00)
set #Vorlaufzeit=datediff(minute,#Bestelldatum, #AnfangsDatumZeit)
SET #X = 0
if Datediff(minute, #AnfangsDatumZeit, #EndDatumZeit) > 360
begin
WHILE (#X <=#Dauer) --z.b. 13
begin
--set #Y = datediff(day,#AnfangsDatumZeit,#AnfangsDatumZeitLoop)
set #Y = datediff(day,#AnfangsDatumZeit,dateadd(hour,#X, #AnfangsDatumZeit))
set #AnfangsDatumZeitLoop=dateadd(hour,#X, #AnfangsDatumZeit)
set #AnfangsZeitLoop=dateadd(hour,#X, #Zeitvon)
insert into #t1 ( PreisID, AnzStunden, Preis , Anfangszeit, Prüfzeit, DatZeitvon , DatZeitbis )
SELECT top 1 preisID, 1, Preis, #AnfangsZeitLoop, #AnfangsDatumZeitLoop, Zeitvon, Zeitbis
FROM dbo.Mypricetable
where SdlID=#Leistungsart --SdlID
and Wochentag=case when DATEPART(dw,#AnfangsDatumZeitLoop) < 6 then 'W' else 'S' end --Wochentag
and #Vorlaufzeit BETWEEN Vorlaufzeitvon and Vorlaufzeitbis --Vorlaufzeit in Minuten
AND #Dauer*60 BETWEEN Dauervon AND Dauerbis --DauerInMinuten
and #AnfangsZeitLoop between Zeitvon and Zeitbis --sucht die von/bis Zeitgruppe
order by zeitvon
SET #X = #X + 1
end
--check and udate of the first record in #t1 rounding down to 15 minutes
update #t1 set Anzstunden= Anzstunden + CONVERT(DECIMAL(6, 2), ROUND(((datediff(minute,[dbo].[sfRoundToHourParts](#AnfangsDatumZeit,1), [dbo].[sfRoundToHourParts](#AnfangsDatumZeit,4)) + 7) / 60.00) / 25, 2) * 25)
from #t1 c where c.Prüfzeit=(SELECT Top 1 Prüfzeit from #t1 order by Prüfzeit)
--check and udate of the last record in #t1 rounding up to 15 minutes
update #t1 set Anzstunden= round(convert(decimal(5,2),datepart(minute,#EndDatumZeit)+7)/60/25,2)*25
from #t1 c where c.Prüfzeit=(SELECT Top 1 Prüfzeit from #t1 order by Prüfzeit DESC)
end
select * from #t1 order by Prüfzeit
Thanks your help!
Michael
This code is a bit verbose, but wanted to share it with you as it produces the desired result of 133.75
DECLARE #x table (
timefrom datetime
, timeto datetime
, price decimal(14,4)
);
INSERT INTO #x (timefrom, timeto, price)
VALUES ('1900-01-01T00:00:00', '1900-01-01T07:00:00', 20.00)
, ('1900-01-01T07:00:00', '1900-01-01T19:00:00', 15.00)
, ('1900-01-01T19:00:00', '1900-01-02T00:00:00', 20.00)
;
-- You should have your own, physical tally table!
DECLARE #numbers table (
number tinyint
);
INSERT INTO #numbers (number)
SELECT DISTINCT
number
FROM master.dbo.spt_values
WHERE number BETWEEN 0 AND 255
;
DECLARE #start datetime = '2016-11-04T10:50:00'
, #end datetime = '2016-11-04T19:25:00'
;
-- first, let's do some rounding of our inputs
DECLARE #rounded_start_time time
, #rounded_end_time time
;
-- Illustrate the steps to round the time to quarters... this might not be the simplest method; but it works!
/*
SELECT #start AS start
, DateAdd(hh, DateDiff(hh, 0, #start), 0) AS truncate_hour
, Round(DatePart(mi, #start) / 15.0, 0) * 15 AS rounded_mins
, DateAdd(mi, Round(DatePart(mi, #start) / 15.0, 0) * 15, DateAdd(hh, DateDiff(hh, 0, #start), 0)) AS truncate_hour_then_add_mins
;
*/
SET #rounded_start_time = DateAdd(mi, Round(DatePart(mi, #start) / 15.0, 0) * 15, DateAdd(hh, DateDiff(hh, 0, #start), 0));
SET #rounded_end_time = DateAdd(mi, Round(DatePart(mi, #end ) / 15.0, 0) * 15, DateAdd(hh, DateDiff(hh, 0, #end ), 0));
PRINT 'Start: ' + Format(#rounded_start_time, 'HH:mm');
PRINT 'End: ' + Format(#rounded_end_time , 'HH:mm');
--SELECT *
--FROM #x
--;
; WITH intervals AS (
SELECT number * 15 AS minute_increments
, DateAdd(mi, number * 15, 0) AS interval_start
, DateAdd(mi, (number + 1) * 15, 0) AS interval_end
FROM #numbers
WHERE number >= 0
AND number < 24 * 4 --number of 15 minute increments in a day
)
, costed_intervals AS (
SELECT intervals.interval_start
, intervals.interval_end
, Cast(intervals.interval_start AS time) As interval_start_time
, Cast(intervals.interval_end AS time) As interval_end_time
, x.price / 4.0 AS interval_price
FROM #x AS x
INNER
JOIN intervals
ON intervals.interval_end <= x.timeto
AND intervals.interval_start >= x.timefrom
)
, applicable_intervals AS (
SELECT interval_start
, interval_end
, interval_start_time
, interval_end_time
, interval_price
FROM costed_intervals
WHERE interval_start_time < #rounded_end_time
AND interval_end_time > #rounded_start_time
)
SELECT Sum(interval_price) AS total_price
FROM applicable_intervals
;
This could use a lot of cleaning up and optimising.
It also only works when the start and end times are within the same day, among other bugs and fun stuff.
thanks to everyones contribution I could find my way and corrected my Duration and the two updates.
Duration:
set #Dauer=datediff(hh, DateAdd(hh, DateDiff(hh, 0, #Datumvon+#Zeitvon), 0),DateAdd(hh, DateDiff(hh, 0, #datumbis+#Zeitbis), 0))
Update the first record
update #t1 set Anzstunden =Anzstunden + (datediff(mi,DateAdd(mi, Round((DatePart(mi, #Zeitvon)-7) / 15.0, 0) * 15, DateAdd(hh, DateDiff(hh, 0, #Zeitvon), 0)),DateAdd(hh, DateDiff(hh, 0, #Zeitvon), 0))/60.00)
-- case when CONVERT(DECIMAL(6, 2), ROUND(((datediff(minute,[dbo].[sfRoundToHourParts](#AnfangsDatumZeit,1), [dbo].[sfRoundToHourParts](#AnfangsDatumZeit,4)) + 7) / 60.00) / 25, 2) * 25) *-1 > 0 then
-- CONVERT(DECIMAL(6, 2), ROUND(((datediff(minute,[dbo].[sfRoundToHourParts](#AnfangsDatumZeit,1), [dbo].[sfRoundToHourParts](#AnfangsDatumZeit,4)) + 7) / 60.00) / 25, 2) * 25) *-1 else Anzstunden end
from #t1 c where c.Prüfzeit=(SELECT Top 1 Prüfzeit from #t1 order by Prüfzeit)
Update the last record
--Prüft und korrigiert den LETZTEN Datensatz der #t1 auf 15 Minuten-Takt
update #t1 set Anzstunden= Anzstunden + ((datediff(mi,DateAdd(mi, Round((DatePart(mi, #Zeitbis)+7) / 15.0, 0) * 15, DateAdd(hh, DateDiff(hh, 0, #Zeitbis), 0)),DateAdd(hh, DateDiff(hh, 0, #Zeitbis), 0))/60.00)*-1)
from #t1 c where c.Prüfzeit=(SELECT Top 1 Prüfzeit from #t1 order by Prüfzeit DESC)
now everything is correct.
Thx to everyone.
Michael
We keep our dates in the DWH in int format.
I would like to do some simple Dateadd calculations (adding and subtract months from a date) without having to convert to date and convert back to int.
I have been able to reach something closing to working using 2 methods:
1. Using Modulo and Absolute functions:
DECLARE #EffectiveDate INT = 20121003,
#Diff INT = -12
SELECT #EffectiveDate + ( Abs(#Diff) / #diff ) * ( Abs(#Diff) + mnt ) / 12 *
10000 + (
Abs(#Diff) / #diff ) * ( ( Abs(#Diff) + mnt - 1 )%12 - mnt + 1 ) *
100
FROM (SELECT #EffectiveDate / 100%100 Mnt)T
2.By calculating the year in months, adding/subtracting the required months and dividing by 12:
DECLARE #EffectiveDate INT = 20121003,
#Diff INT = -12
SELECT ( yr * 12 + mnt + #Diff ) / 12 * 10000 +
( yr * 12 + mnt + #Diff )%12 * 100 + 1
FROM (SELECT #EffectiveDate / 100%100 Mnt,
#EffectiveDate / 10000 Yr)T
In both cases I come up with the same problem, December is represented incorrectly as 0 and effects the year outcome as well.
Any ideas?
Your logic is wrong:
SELECT ( yr * 12 + mnt + #Diff - 1) / 12 * 10000 +
(( yr * 12 + mnt + #Diff - 1) % 12 + 1) * 100 + 1
FROM (SELECT #EffectiveDate / 100%100 Mnt,
#EffectiveDate / 10000 Yr)T
Of course everything would be much easier if your store dates in DATEs :)
Can you please help:
I have an arrival date column that needs 5000 rows adding to it with random dates from the year 2010.
For example: 01/01/2010, 25/8/2010, 03/05/2010, 31/12/2010, etc
I think it would be an INSERT statement but I am unsure.
Thanks
Wayne
USE Occupancy
CREATE TABLE Book
(BookNumber varchar(30),
ArrivalDate int)
DECLARE #BookNumber varchar(30)
DECLARE #ArrivalDate int
SET #BookNumber = 1
SET #ArrivalDate = 0
WHILE #BookNumber <= 5000
WHILE #ArrivalDate <= 5000
BEGIN
INSERT INTO Book (BookNumber,ArrivalDate)
SELECT 'B' + Right ('00000000' + CAST(#BookNumber AS varchar(30)),8),
DATEADD(day, CAST(RAND() * 365 as int), '2010-1-1')
SET #BookNumber = #BookNumber + 1
SET #ArrivalDate = #ArrivalDate + 1
END
DECLARE #counter int
SET #counter = 0
WHILE #counter < 5000
BEGIN
INSERT INTO myTable (arrivalDate)
SELECT DATEADD(day, CAST(RAND() * 365 as int), '2010-1-1')
SET #counter = #counter + 1
END
And if you have a numbers table or generator, you can avoid the loop:
INSERT INTO myTable (arrivalDate)
SELECT DATEADD(day, ABS(CHECKSUM(NEWID()) % 365), '2010-1-1')
FROM ( GenerateRowNumbers(5000, 1) ) t
What is the best code (UDF) to calculate the age given birthday? Very optimized code. I can write the date related code just to make sure what best answer I get from best minds in the industry.
Would like to calculate based on days.
The following function gives very reasonable answers for all the date ranges that I've commented in this thread. You will notice that some values may be off by 1 day, but this is an artifact of how months are counted due to them being varying length. February always gives the most trouble being the shortest. But it should never be off by more than 1 day from dead reckoning (and even that comes down to semantics of how you are supposed to count the months, and is still a valid answer). Sometimes, it may give a number of days when you might have expected 1 month, but again it is still a reasonable answer.
The function, using only date math instead of string operations until the very end, should also yield very good performance when stacked up against any other function that returns calculations that are similarly accurate.
CREATE FUNCTION dbo.AgeInYMDFromDates(
#FromDate datetime,
#ToDate datetime
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (
SELECT
Convert(varchar(11), AgeYears) + 'y '
+ Convert(varchar(11), AgeMonths) + 'm '
+ Convert(varchar(11), DateDiff(day, DateAdd(month, AgeMonths,
DateAdd(year, AgeYears, #FromDate)), #ToDate)
) + 'd' Age
FROM (
SELECT
DateDiff(year, #FromDate, #ToDate)
- CASE WHEN Month(#FromDate) * 32 + Day(#FromDate)
> Month(#ToDate) * 32 + Day(#ToDate) THEN 1 ELSE 0 END AgeYears,
(DateDiff(month, #FromDate, #ToDate)
- CASE WHEN Day(#FromDate) > Day(#ToDate) THEN 1 ELSE 0 END) % 12 AgeMonths
) X
);
Use like so (SQL 2008 script):
SELECT
FromDate,
ToDate,
ExpectedYears,
ExpectedMonths,
ExpectedDays,
(SELECT TOP 1 Age FROM dbo.AgeInYMDFromDates(FromDate, ToDate)) Age
FROM
(
VALUES
(Convert(datetime, '20120201'), Convert(datetime, '20120301'), 0, 1, 0),
('20120228', '20120328', 0, 1, 0),
('20120228', '20120331', 0, 1, 3),
('20120228', '20120327', 0, 0, 27),
('20120801', '20120802', 0, 0, 1),
('20120131', '20120301', 0, 1, 2),
('19920507', '20120506', 19, 11, 29),
('19920507', '20120507', 20, 0, 0)
) X (FromDate, ToDate, ExpectedYears, ExpectedMonths, ExpectedDays)
Because it is an inline function, it can be inserted into the execution plan of the query and will perform the best possible. If you convert it to a scalar-returning function (so you don't have to use (SELECT Age FROM func) then you will get worse performance. The WITH SCHEMABINDING directive can help because it precalculates that the function makes no data access to any tables rather than having to check it at runtime.
Here are the results of the above execution:
FromDate ToDate ExpectedYears ExpectedMonths ExpectedDays Age
---------- ---------- ------------- -------------- ------------ -----------
2012-02-01 2012-03-01 0 1 0 0y 1m 0d
2012-02-28 2012-03-28 0 1 0 0y 1m 0d
2012-02-28 2012-03-31 0 1 3 0y 1m 3d
2012-02-28 2012-03-27 0 0 27 0y 0m 28d
2012-08-01 2012-08-02 0 0 1 0y 0m 1d
2012-01-31 2012-03-01 0 1 2 0y 1m 1d
1992-05-07 2012-05-06 19 11 29 19y 11m 29d
1992-05-07 2012-05-07 20 0 0 20y 0m 0d
Enjoy!
Try This.
CREATE TABLE patient(PatName varchar(100), DOB date, Age varchar(100))
INSERT INTO patient
VALUES ('d','06/02/2011',NULL)--,('b','07/10/1947',NULL),('c','12/21/1982',NULL)
;WITH CTE(PatName,DOB,years,months,days) AS
(SELECT PatName, DOB, DATEDIFF(yy,DOB,getdate()), DATEDIFF(mm,DOB,getdate()),
DATEDIFF(dd,DOB,getdate())
FROM patient)
SELECT PatName, DOB,
CAST(months/12 as varchar(5)) + ' Years' +
CAST((months % 12) as varchar(5)) + ' month/s ' +
CAST( CASE WHEN DATEADD(MM,(months % 12), DATEADD(YY,(months/12),DOB)) <= GETDATE()
THEN DATEDIFF(dd,DATEADD(MM,(months % 12), DATEADD(YY,(months/12),DOB)),GETDATE())
ELSE DAY(getdate())
END as varchar(5))+' days' AS Age
FROM CTE
Performance of scalar functions is usually not very good, to do what you ask I would write it as a table valued function. A table valued function has the benefit that it is inlined properly.
Other forms of the query will not make a huge difference as it is the calls to the function which eat up the time.
CREATE FUNCTION dbo.fn_age(#birthdate datetime, #current_date datetime)
RETURNS TABLE
WITH SCHEMABINDING
AS
return (
select CAST(DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END AS varchar(3)) + ' y, '
+ CAST(DATEDIFF(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate),
#current_date) AS varchar(2)) + ' m, '
+ CAST(DATEDIFF(day, DATEADD(month
, datediff(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
, #current_date)
, dateadd(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
)
, #current_date) AS varchar(2)) + ' d' as [Age]
)
GO
This function has to be called like this:
SELECT Age = (SELECT Age FROM dbo.fn_age(birthDate, current_timestamp))
FROM Person
Comparison with other alternatives
When writing this problem as a normal scalar function I would create something like this:
CREATE FUNCTION dbo.fn_age_slow(#birthdate datetime, #current_date datetime )
RETURNS VARCHAR(10)
WITH SCHEMABINDING
AS
begin
return CAST(DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END AS varchar(3)) + ' y, '
+ CAST(DATEDIFF(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate),
#current_date) AS varchar(2)) + ' m, '
+ CAST(DATEDIFF(day, DATEADD(month
, datediff(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
, #current_date)
, dateadd(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
)
, #current_date) AS varchar(2)) + ' d'
end
GO
As you can see it does exactly the same as the table valued function. (it is also schema bound which makes the functions faster in some cases)
When running the following script against the first function (on my pc)
declare #a varchar(10) = ''
, #d datetime = '20120101'
, #i int = 1
, #begin datetime
select #begin = CURRENT_TIMESTAMP
while #i < 1000000
begin
select #a = Age
, #d = #begin - #i%1000
, #i += 1
from dbo.fn_age(#d, #begin)
end
select CURRENT_TIMESTAMP - #begin
GO
==> 00:00:07.920
declare #a varchar(10) = ''
, #d datetime = '19500101'
, #i int = 1
, #begin datetime
select #begin = CURRENT_TIMESTAMP
while #i < 1000000
begin
select #a = dbo.fn_age_slow(#d, #begin)
, #d = #begin - #i%1000
, #i += 1
end
select CURRENT_TIMESTAMP - #begin
==> 00:00:14.023
This is in no way a proper benchmark but it should give you an idea about the performance difference.