I have an MS-SQL Server which keeps track on a number of clients. If the birthday is known it is stored as a datetime attribute called dayOfBirth. Now I would like to have another attribute age, which keeps track of the current age of the client. Since age can change any day I figured a script might be the best idea.
First thing I did, was to create a stored procedure which computes the age given the birthday as datetime. Here is what I came up with:
USE [MyDB]
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE CalculateAge
#dayOfBirth datetime,
#age INT OUTPUT
AS
DECLARE #today datetime, #thisYearBirthDay datetime
DECLARE #years int
SET #today = GETDATE()
SET #thisYearOfBirth = DATEADD(year, #dayOfBirth, #today), #dayOfBirth)
SET #years = DATEDIFF(year, #dayOfBirth, #today) - (CASE WHEN #thisYearBirthDay > #today THEN 1 ELSE 0 END)
SET #age = #years
Afterwards I created another script which runs through all records that have a non null dayOfBirth attribute and updates the age filled accordingly.
USE [MyDB]
GO
DECLARE #age int;
DECLARE #birth datetime;
DECLARE #id intl
DECLARE cursorQuery CURSOR FOR SELECT clientId FROM Clients WHERE dayOfBirth IS NOT NULL;
OPEN cursorQuery
FETCH NEXT FROM cursorQuery INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
SET #birth = (SELECT dayOfBirth from Kunden where clientId=#id);
EXEC dbo.CalculateAge #birth, #age OUTPUT;
UPDATE Clients SET Age = #age WHERE clientId = #id;
FETCH NEXT FROM cursorQuery INTO #id
END
CLOSE cursorQuery
DEALLOCATE cursorQuery
I would trigger the script above once per day to populate the age attribute. Thats what I have so far, but I have the feeling there is plenty of room for improvement.
** Thanks Sung Meister **
I ended up with something like this:
CREATE TABLE Client (
ID int identity(1,1) primary key,
DOB datetime null,
AGE as (case
when DOB is null then null
else DATEDIFF(YY,DOB,GETDATE()) - CASE WHEN (MONTH(GETDATE()) = MONTH(DOB) AND DAY(DOB) > DAY(GETDATE()) OR MONTH(GETDATE()) > MONTH(DOB)) THEN 1 ELSE 0 END
end
)
)
Instead of creating a trigger and do any manual updates,
easiest way to get around with this is to create a calculated field called Age.
This way, you do not have to worry about Age data being out of sync (data stays consistent) and no more trigger or manual update is required.
Calculating an age of person can be a bit hairy as you can see in the picture below.
Here is the text used
create table #Person (
ID int identity(1, 1) primary key,
DOB datetime null,
Age as
(case
when DOB is null then null
--; Born today!
when datediff(d, DOB, getdate()) = 0 then 0
--; Person is not even born yet!
when datediff(d, DOB, getdate()) < 0 then null
--; Before the person's B-day month so calculate year difference only
when cast(datepart(m, getdate()) as int) > cast(datepart(m, DOB) as int)
then datediff(year, DOB, getdate())
--; Before Person's b-day
else datediff(year, DOB, getdate()) - 1
end)
)
insert #Person select GetDate()
insert #Person select null
insert #Person select '12/31/1980'
select * from #Person
update #Person
set DOB = '01/01/1980'
where ID = 2
select * from #Person
You really shouldn't be storing the age within the database as it is easily calculated and changes on a daily basis.
I would suggest that you keep the date of birth field and just calculate the age as you need it. If you wish to have the age selected along with the other attributes then consider a view, perhaps with a user defined function to calculate the age.
The following is an example (untested) UDF that you could use
CREATE FUNCTION age
(
#userId int
)
RETURNS int
AS
BEGIN
DECLARE #Result int
SELECT #Result = DATEDIFF(year, dateofBirth, getDate())
FROM person
WHERE userId = #userId
RETURN #Result
END
GO
Then within your queries you can do something similar to the following,
SELECT *,
dbo.age(userId) as age
FROM person
In answer to your question on sorting etc, then you could create a view on the data and use that to show the data so something like this (untested)
CREATE VIEW personview(firstname, surname, dateOfBirth,age) AS
SELECT firstname,
surname,
dateOfbirth,
dbo.age(userid)
FROM person
You can then use this view to perform your queries, there could be a performance hit for filtering and sorting based on the age and if you regularly sort/filter based upon the age field then you may want to create an indexed view.
Use a view and/or function. Never store two fields that arew based on the exact same data if you can help it as they will eventually get out of sync.
Related
I'm sure there's a more elegant (or simply "correct") way to do what I'm trying to achieve. I believe I need to use a Cursor, but can't quite wrap my head around how to.
I have the code below to find the days left in a contract, but unless I put the 'where' clause (which basically selects a specific record), I get this error message:
'Subquery returned more than 1 value'
That's why I think I need a cursor; to loop through the records, and update a field with the number of days left in a contract.
Here's what I have, which works in-as-much that it returns a number.
DECLARE #TodaysDT date = GetDate()
DECLARE #ContractExpirationDT date =
(SELECT ExprDt from CONTRACTS
WHERE ID = 274);
DECLARE #DaysRemaining INT =
(SELECT DATEDIFF(dd, #ContractExpirationDT,#TodaysDT));
Print #DaysRemaining;
This returns a correct value for a specific record ID (this case, ID 274)
How do I use a cursor to step through each record, and then update a field in each record with the #DaysRemaining value?
Thank you for your time!
In my opinion you don't need a cursor; you can just run an update without where clause in order to calculate remaining days for all rows.
Here is a basic example that you can use as a starting point:
--Create a table variable to hold test data
declare #contract table (Id int, ExprDt datetime, DaysRemaining int )
--Insert sample data
insert into #contract select 1, '20200101' ,null
insert into #contract select 2, '20201231' ,null
insert into #contract select 3, '20191231' ,null
insert into #contract select 274, '20191231',null
--Save today's date inseide a variable
DECLARE #TodaysDT date = GetDate()
--Update DaysRemaining field for each record
update #contract set DaysRemaining = DATEDIFF(dd, ExprDt,#TodaysDT)
--Select records to check results
select Id, ExprDt, DaysRemaining
from #contract
Here is the output of this command:
I have 2 tables
Table Person with a column PersonID (integer)
Table CarePlanReport with columns PersonID (integer), Shift (nvarchar(5) - will hold the text values ‘day’ or ‘night’), CarePlanDate (datetime), Details (nvarchar(50))
I need a stored procedure that will for all dates in the current month, insert 2 records into the CarePlanReport table for each Person.PersonID (a day shift and a night shift) if they don't already exist. This is because each person requires both a day and night shift record for each day in the current month. The problem is a new person could be added during any day in the current month and the missing records will need to be added - so this procedure will need to be run multiple times in any given month. Existing records will remain unchanged because they could already store important details.
EXAMPLE
Person table holds:
PersonId
--------
1
2
CarePlanReport holds for current month
PersonID Shirt CarePlanDate
----------------------------------
1 day 2015/03/01
2 day 2015/03/01
1 night 2015/03/01
2 night 2015/03/01
.. and so on for each day in the month ....
1 day 2015/03/31
2 day 2015/03/31
1 night 2015/03/31
2 night 2015/03/31
I used this to return all dates for the current month (not sure if it’s the most efficient method)
declare #date datetime
set #date = cast(YEAR(GETDATE()) as nvarchar(4)) + RIGHT('00' + cast(Month(GETDATE()) as nvarchar(2)), 2) + '01';
with d as (
select #date as Date
union all
select dateadd(dd,1,Date)
from d
where month(date) = month(#Date)
)
select d.date
from d
where month(date) = month(#Date)
#tfa
It would be good if you write a trigger and that should get invoked as a new record inserted in the Person Table. Let me know if any need any help for this. You have to have few set of validations in your trigger
Use the below procedure which will insert "day" and "night" record for each personid. Just you have to invoke once in a month.
create procedure [dbo].[InsertsRecords]
as
begin
DECLARE #DATE DATETIME
declare #NoOfDays int
declare #lv_count int =1
declare #LastDateOfMonth date
declare #getdates date
declare #lv_personid int
declare #FirstDateOfMonth date
SET #DATE=convert(date,getdate())
set #LastDateOfMonth =(select DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#date)+1,0)))
set #FirstDateOfMonth=(SELECT convert(date,DATEADD(mm, DATEDIFF(mm, 0, GETDATE()), 0)))
declare rcds_cur cursor for
select personid from dbo.person where personid not in (select personid from dbo.CarePlanReport between
#FirstDateOfMonth and #LastDateOfMonth )
open rcds_cur
FETCH NEXT FROM rcds_cur into #lv_personid
while ##FETCH_STATUS=0
begin
set #NoOfDays=( Select Day(EOMONTH(#DATE)) AS [Current Month])
set #NoOfDays=#NoOfDays-1
while (#lv_count<=#NoOfDays)
begin
set #getdates=(select DATEADD(dd,-#lv_count , #LastDateOfMonth))
insert into dbo.CarePlanReport values (#lv_personid,'day',#getdates,'day shift')
insert into dbo.CarePlanReport values (#lv_personid,'night',#getdates,'night shift')
set #lv_count=#lv_count+1
end
FETCH NEXT FROM rcds_cur into #lv_personid
end
close rcds_cur
deallocate rcds_cur
end
I am trying to write a function where I can find age based on the date the record was inserted rather than getdate(). I want to filter the user who are less than 18 years when they registered.
If I query it after a year, it should still show the user as 17 based on record insert date than current date. This is what I wrote but it is still giving the age based on current date than the record insert date. Any suggestions would be really helpful.
Thank You
--InputDate as DateOfBirth
--InsertDate as date the record was inserted
CREATE FUNCTION [dbo].[FindAge] (#InputDate int, #Insertdate datetime )
RETURNS int
AS
BEGIN
DECLARE #Age as Int
DECLARE #d DATETIME
SET #d = CONVERT(DATETIME, CONVERT(VARCHAR(8), #InputDate), 112)
SELECT #Age=DATEDIFF(year, #d, #Insertdate)
- CASE WHEN DATEADD(year, DATEDIFF(year, #d, #Insertdate), #d) <= GetDate()
THEN 0 ELSE 1 END
RETURN #Age
END
---- Drop Obselete procs
GO
Update
Followed Bacon Bits suggestion and it worked out perfectly.
All DATEDIFF() does is subtract the years from the date components. It's very stupid:
select datediff(yy,'2000-12-19','2014-01-01') --14
select datediff(yy,'2000-12-19','2014-12-18') --14
select datediff(yy,'2000-12-19','2014-12-19') --14
select datediff(yy,'2000-12-19','2014-12-20') --14
select datediff(yy,'2000-12-19','2014-12-31') --14
select datediff(yy,'2000-12-19','2015-01-01') --15
select datediff(yy,'2000-12-19','2015-12-31') --15
select datediff(yy,'2000-12-19','2016-01-01') --16
select datediff(yy,'2000-12-19','2016-12-31') --16
Don't calculate the number of hours in a year with the year being 365.25 days long or something like that. It's an exercise in futility, and just guarantees that you will be wrong near every person's birthday.
Your best bet is to calculate it how humans do it. In the US (and most Western nations, I believe) it's the difference between the years, but you only count the current year when you pass your birthday:
declare #birthdate date = '2000-12-19';
declare #target date;
SELECT DATEDIFF(yy, #birthdate, #target)
- CASE
WHEN (MONTH(#birthdate) > MONTH(#target))
OR (
MONTH(#birthdate) = MONTH(#target)
AND DAY(#birthdate) > DAY(#target)
)
THEN 1
ELSE 0
END
Here's the values you'd get:
set #target = '2014-01-01' --13
set #target = '2014-12-18' --13
set #target = '2014-12-19' --14
set #target = '2014-12-20' --14
set #target = '2014-12-31' --14
set #target = '2015-01-01' --14
set #target = '2015-12-31' --15
set #target = '2016-01-01' --15
set #target = '2016-12-31' --16
Change #target to getdate() to calculate the age as of now.
If your region uses East Asian age reckoning, however, you'll need to use a completely different method to determine what age a person is since they're considered age 1 on the day they're born, and their age increases each February.
I need to create trigger prevent insert and update to table employee under age 21 and over age 67
what next on the code?
CREATE TRIGGER allowInsertUpdateemployee ON dbo.employee
AFTER UPDATE, INSERT
AS
BEGIN
DECLARE #v_ageLow INT = 21,
#v_ageHigh INT = 67,
#v_dateLow date,
#v_dateHigh date
SET #v_dateLow = DATEADD(YEAR, -1 * #v_ageLow, GETDATE())
SET #v_dateHigh = DATEADD(YEAR, -1 * #v_ageHigh, GETDATE())
END
Since the upper and lower bounds are fixed, a check constraint might be a more appropriate solution than a trigger
ALTER TABLE employee ADD CONSTRAINT ck_employee_age CHECK
(DateOfBirth BETWEEN DATEADD(YEAR,-67,GETDATE()) AND DATEADD(YEAR,-21,GETDATE()))
Use "INSTEAD OF INSERT, UPDATE" trigger;
Use INSERTED table to check new incoming values, raiserror if needed;
Use DELETED table to detect if update is processing (this can
help);
Do manual insert or update then (if needed).
INSERT INTO
dbo.employee
SELECT
*
FROM
INSERTED I
I hope this will do
This will not fetch accurate age , However if your method gets you an accurate date you can use your code with it .
OR
you can also use below code to get age:
SELECT DATEDIFF(Day,'2011-11-03' , GETDATE()) /365
SELECT DATEDIFF(Day,#Age , GETDATE())
/365
CREATE TRIGGER allowInsertUpdateemployee ON dbo.employee
INSTEAD OF Insert
AS
BEGIN
DECLARE #v_ageLow INT,
#v_ageHigh INT,
#v_date date,
--#v_dateHigh date
SET #v_ageLow = SELECT DATEDIFF(Year,#v_date , GETDATE())
SET #v_ageHigh = SELECT DATEDIFF(Year,#v_date , GETDATE())
BEGIN
if(#v_ageLow <21 AND #v_ageHigh >67)
BEGIN
RAISERROR('Cannot Insert or Update where User is not in age limit);
ROLLBACK;
END
ELSE
BEGIN
// else write your insert update query
Insert into values ()
PRINT 'Unable to Insert-- Instead Of Trigger.'
END
END
You have to put OR in the where clause. The employee can't be under 21 AND more than 67.
Create TRIGGER tr_Too_young_or_too_old
ON TableName
AFTER INSERT
AS
if exists ( select *, DATEDIFF(yy, birthdate, GETDATE()) as age
from TableName
where DATEDIFF(yy, birthdate, GETDATE()) < 21 or DATEDIFF(yy, birthdate, GETDATE()) > 67 )
begin
rollback
RAISERROR ('Cannot Insert or Update where User is not in age limit', 16, 1);
end
I have a Order Search page where user can enter any search criteria and submit.
If if clicks submit without entering any criteria we should show all orders.
If users enters orderId we should show only that respective order.
If user enters date range and status we should show orders with that selected status in selected date range.
I tried as shown.
SELECT * FROM dbo.Orders
WHERE ( OrderNumber ='' OR OrderNumber ='212' )
AND ( (OrderDate BETWEEN '2010-01-01' AND '2012-10-10')
OR (OrderDate BETWEEN '' AND '') )
You cannot check if the field has the value AND if the field does not have the value. You will always get all records regardless. You will need to pull the values out into a variable. Unless you are writing dynamic SQL; if that is the case then you would only add to the where clause if you intend to filter...
DECLARE #OrderNumber VARCHAR, #StartDate VARCHAR, #EndDate VARCHAR
SET #OrderNumber = '212'
SET #StartDate = '2010-01-01'
SET #EndDate = '2012-01-01'
SELECT *
FROM dbo.Orders
WHERE (#OrderNumber ='' OR OrderNumber = #OrderNumber)
AND (#StartDate = '' OR OrderDate >= #StartDate)
AND (#EndDate = '' OR OrderDate <= #EndDate)
Check out this complete article from Erland and in particular the heading "Umachandar's Bag of Tricks"
Also check Implementing a Dynamic WHERE Clause
Extract from the above article showing how to make use of COALESCE to achieve this:
DECLARE #Cus_Name varchar(30),
#Cus_City varchar(30),
#Cus_Country varchar(30)
SET #Cus_Name = NULL
SET #Cus_City = 'Paris'
SET #Cus_Country = NULL
SELECT Cus_Name,
Cus_City,
Cus_Country
FROM Customers
WHERE Cus_Name = COALESCE(#Cus_Name,Cus_Name) AND
Cus_City = COALESCE(#Cus_City,Cus_City) AND
Cus_Country = COALESCE(#Cus_Country,Cus_Country)
this is a very simple query
declare #ordernumber int = null
SELECT * FROM dbo.Orders
WHERE
(#ordernumber is null or (OrderNumber IS NOT NULL AND OrderNumber IN (#ordernumber)))
AND ( (OrderDate BETWEEN '2010-01-01' AND '2012-10-10') OR (OrderDate BETWEEN '' AND '') )
Although this query is simple enough but here goes the explanation : if #ordernumber is null, then it would display all the records, while for the other case it will display only the record corresponding to the passed orderid.