How to calculate age based on current date - sql

I am trying to calculate the age of an individual.
I would like the age to be calculated to the exact as possible. However, if the age is on the same date, it should calculate the age assume it's already his birthday.
EXAMPLE: Today date: Dec 15, 2020
user #1: DOB: Dec 14, 2005 (AGE: 15)
user #2: DOB: Dec 16, 2005 (AGE: 14)
user #3: DOB: Dec 15, 2005 (AGE: 15)
I have this query, but for user #3, it is giving me age 14 when it should be 15.
SELECT
USER,
DATE_OF_BIRTH,
FLOOR(DATEDIFF(DAY, B.DATE_OF_BIRTH, GETDATE())/365.25) AS CALCULATED_AGE
FROM TBL_A

I think you could do it using the MONTHS_BETWEEN function.
select floor(months_between(getdate(), date_of_birth) / 12) from tbl_a

I have to assume this question is not being done in Snowflake, as the results are off by one.
with mytable(user, dob, age_expected) as (
select * from values
(1, '2005-12-14'::date, 15),
(2, '2005-12-16'::date, 14),
(3, '2005-12-15'::date, 15)
)
SELECT
user
,dob
,'2022-12-15'::date as todays_date
,DATEDIFF(DAY, dob, todays_date) as gap_days
,gap_days/365.25 as d365
,floor(d365) as f0
,FLOOR(DATEDIFF(DAY, dob, todays_date)/365.25) AS CALCULATED_AGE
FROM mytable
USER
DOB
TODAYS_DATE
GAP_DAYS
D365
F0
CALCULATED_AGE
1
2005-12-14
2022-12-15
6,210
17.002053
17
17
2
2005-12-16
2022-12-15
6,208
16.996578
16
16
3
2005-12-15
2022-12-15
6,209
16.999316
16
16
So if we deconstruct what is happening, move the month of interest back to november so the before after months case can be seen, and move the "current date" back 2 years so we get the stated expected results 14/15:
with mytable(user, dob, age_expected) as (
select * from values
(1, '2005-10-15'::date, 15),
(2, '2005-11-14'::date, 15),
(3, '2005-11-16'::date, 14),
(4, '2005-11-15'::date, 15),
(5, '2005-12-15'::date, 14)
)
SELECT
user
,dob
,age_expected
,'2020-11-15'::date as todays_date
,year(dob) as dob_y
,month(dob) as dob_m
,day(dob) as dob_d
,year(todays_date) as today_y
,month(todays_date) as today_m
,day(todays_date) as today_d
,DATEDIFF(DAY, dob, todays_date)/365.25 as float_val
,today_y - dob_y as y_dif
,case
when today_m < dob_m then 'a'
when today_m > dob_m then 'b'
when today_d < dob_d then 'c'
when today_d >= dob_d then 'd'
end as pick
,case
when today_m < dob_m then y_dif -1
when today_m > dob_m then y_dif
when today_d < dob_d then y_dif -1
when today_d >= dob_d then y_dif
end as result
FROM mytable
order by dob
USER
DOB
AGE_EXPECTED
TODAYS_DATE
DOB_Y
DOB_M
DOB_D
TODAY_Y
TODAY_M
TODAY_D
FLOAT_VAL
Y_DIF
PICK
RESULT
1
2005-10-15
15
2020-11-15
2,005
10
15
2,020
11
15
15.085558
15
b
15
2
2005-11-14
15
2020-11-15
2,005
11
14
2,020
11
15
15.003422
15
d
15
4
2005-11-15
15
2020-11-15
2,005
11
15
2,020
11
15
15.000684
15
d
15
3
2005-11-16
14
2020-11-15
2,005
11
16
2,020
11
15
14.997947
15
c
14
5
2005-12-15
14
2020-11-15
2,005
12
15
2,020
11
15
14.918549
15
a
14
So using the numbers you state, would would, but it we move the current date back to 2022 those numbers don't work, due to the stride of actual number of daylight days. so you just stick to the partial math method which can be simplefied to:
with mytable(user, dob, age_expected) as (
select * from values
(1, '2005-11-15'::date, 15),
(2, '2005-12-14'::date, 15),
(3, '2005-12-16'::date, 14),
(4, '2005-12-15'::date, 15),
(5, '2006-01-15'::date, 14)
)
SELECT
user
,dob
,age_expected
,CURRENT_DATE
,case
when month(CURRENT_DATE) < month(dob) then year(CURRENT_DATE) - year(dob) -1
when month(CURRENT_DATE) > month(dob) then year(CURRENT_DATE) - year(dob)
when day(CURRENT_DATE) < day(dob) then year(CURRENT_DATE) - year(dob) -1
when day(CURRENT_DATE) >= day(dob) then year(CURRENT_DATE) - year(dob)
end as result
FROM mytable
order by dob
gives:
USER
DOB
AGE_EXPECTED
CURRENT_DATE
RESULT
1
2005-11-15
15
2022-12-15
17
2
2005-12-14
15
2022-12-15
17
4
2005-12-15
15
2022-12-15
17
3
2005-12-16
14
2022-12-15
16
5
2006-01-15
14
2022-12-15
16

Here is an overloaded UDF you can use. The one that takes a single parameter says you want a person's age as of today. The one that takes two parameters says you want a person's age as of the date specified in the second parameter.
create or replace function age(DATE_OF_BIRTH date)
returns integer
language sql
as
$$
year(current_date) - year(DATE_OF_BIRTH) - iff(month(current_date) <= month(DATE_OF_BIRTH) and day(current_date) < day(DATE_OF_BIRTH), 1, 0)
$$;
create or replace function age(DATE_OF_BIRTH date, AS_OF_DATE date)
returns integer
language sql
as
$$
year(AS_OF_DATE) - year(DATE_OF_BIRTH) - iff(month(AS_OF_DATE) <= month(DATE_OF_BIRTH) and day(AS_OF_DATE) < day(DATE_OF_BIRTH), 1, 0)
$$;
Tests with data set for the current date:
with PEOPLE as
(
select COLUMN1::date DOB from values
('2000-01-01'),
('2000-01-02'),
('2000-12-14'),
('2000-12-15'),
('2000-12-16'),
('2000-12-17')
)
select age(DOB) from PEOPLE
;
AGE(DOB)
22
22
22
22
22
21
This test is using an as of date to check people's ages:
with PEOPLE as
(
select COLUMN1::date DOB from values
('2000-01-01'),
('2000-01-02'),
('2000-12-14'),
('2000-12-15'),
('2000-12-16'),
('2000-12-17')
)
select age(DOB, '2023-01-01') from PEOPLE
;
AGE(DOB, '2023-01-01')
23
22
22
22
22
22

Related

how to calculate age from Date of birth and how to group the records by age ranges

I have a table with a column 'DOB', I need to group the records by age ranges. but i don't know how to calculate age using DOB and use case along with it. I got that code from this site.
select
case
when age_c <1 then 'Under 1,
when age_c between 1 and 5 then '1-5',
when age_c between 5 and 15 then '5-15',
when age_c between 15 and 35 then '15-35'
What I want is:
Under 1 1
1-5 15
15-35 54
You presumably want some kind of aggregation query here:
SELECT CASE WHEN age_c < 1 THEN 'Under 1'
WHEN age_c < 5 THEN '1 - 5'
WHEN age_c < 15 THEN '5 - 15'
WHEN age_c < 35 THEN '15 - 35' END AS age_range,
COUNT(*) AS cnt
FROM yourTable
GROUP BY 1;
WITH AgeData as
(
SELECT
cast(DATEDIFF(DAY,DOB,GETDATE())/365.25 as int) AS [AGE]
FROM your_table
),
GroupAge AS
(
SELECT [Age],
CASE
WHEN AGE < 1 THEN 'Under 1',
WHEN AGE BETWEEN 1 AND 5 THEN '1-5',
WHEN AGE BETWEEN 5 AND 15 THEN '5-15',
WHEN AGE BETWEEN 15 AND 35 THEN '15-35'
WHEN AGE > 35 THEN 'Over 35'
END AS [Age Groups]
FROM AgeData
)
SELECT COUNT(*) AS [callGrpCount],
[Age Groups] FROM GroupAge
GROUP BY [Age Groups] order by [Age Groups]
Assume: DOB in yyyymmdd format

is there good solution on select case? My select query looks very stupid. :))

select
code,
max(age) age_level,
max(age_interval) age_interval,
sum(total) total
from
(select
(case
when floor(months_between(sysdate, u.birth_date)/12) between 13 and 18 then 'Өсвөр үе'
when floor(months_between(sysdate, u.birth_date)/12) between 19 and 25 then 'Залуу үе'
when floor(months_between(sysdate, u.birth_date)/12) between 26 and 35 then 'Идэр үе'
when floor(months_between(sysdate, u.birth_date)/12) between 36 and 45 then 'Хижээл үе'
when floor(months_between(sysdate, u.birth_date)/12) between 46 and 60 then 'Өтөл үе'
when floor(months_between(sysdate, u.birth_date)/12) between 60 and 100 then 'Өндөр үе'
else 'other'
end) age,
(case
when floor(months_between(sysdate, u.birth_date)/12) between 13 and 18 then 'kid'
when floor(months_between(sysdate, u.birth_date)/12) between 19 and 25 then 'bigger_kid'
when floor(months_between(sysdate, u.birth_date)/12) between 26 and 35 then 'adult'
when floor(months_between(sysdate, u.birth_date)/12) between 36 and 45 then 'bigger_adult'
when floor(months_between(sysdate, u.birth_date)/12) between 46 and 60 then 'big_adult'
when floor(months_between(sysdate, u.birth_date)/12) between 60 and 100 then 'caption'
else 'other'
end) code,
(case
when floor(months_between(sysdate, u.birth_date)/12) between 13 and 18 then '13-18'
when floor(months_between(sysdate, u.birth_date)/12) between 19 and 25 then '19-25'
when floor(months_between(sysdate, u.birth_date)/12) between 26 and 35 then '26-35'
when floor(months_between(sysdate, u.birth_date)/12) between 36 and 45 then '36-45'
when floor(months_between(sysdate, u.birth_date)/12) between 46 and 60 then '46-60'
when floor(months_between(sysdate, u.birth_date)/12) between 60 and 100 then '60-100'
else 'other'
end) age_interval,
count(u.id) total
from sec_survey_users su
left join sec_users u on su.user_id = u.id
where su.survey_id = 'D3A21B1C2D8334C9E055824F7FFC5DF4' group by u.birth_date) mtable group by mtable.code;
Something like this could do it with less repetition (or define the age ranges in a permanent table and skip the with clause):
with age_classes (min_age, max_age, age_interval, code, age) as
( select 13, 18, '13-18', 'teen', 'Teenager' from dual union all
select 19, 25, '19-25', 'young', 'Young adult' from dual union all
select 26, 35, '26-35', 'still_young', 'It won''t last' from dual union all
select 36, 45, '36-45', 'slightly_less_young', 'Middle aged' from dual union all
select 46, 50, '46-60', 'still_got_it', 'Still got it' from dual union all
select 60, 100, '60-100', 'exceeding_expectations', 'Free bus pass' from dual
)
select nvl(ac.code,'other') as code
, nvl(ac.age,'other') as age
, nvl(ac.age_interval,'other') as age_interval
, count(*) total
from sec_survey_users su
left join sec_users u on su.user_id = u.id
left join age_classes ac on floor(months_between(sysdate, u.birth_date)/12) between ac.min_age and ac.max_age
group by
ac.code
, ac.age
, ac.age_interval;
With a permanent table, you could define the age_interval label as a virtual column that concatenated the min/max age columns to avoid some redundancy. The table would also be convenient for dropdown lists in reporting applications etc.
You can rewrite the query by using CTE in order to prevent repeatedly writing
FLOOR(MONTHS_BETWEEN(sysdate, u.birth_date) / 12) which and age aliased form of it will be used within the GROUP BY lists such as
WITH u0 AS
(SELECT FLOOR(MONTHS_BETWEEN(sysdate, birth_date) / 12) AS age, COUNT(id) AS total
FROM sec_users
GROUP BY FLOOR(MONTHS_BETWEEN(sysdate, birth_date) / 12)),
u2 AS
(SELECT (CASE
WHEN age BETWEEN 13 AND 18 THEN
'Өсвөр үе'
WHEN age BETWEEN 19 AND 25 THEN
'Залуу үе'
WHEN age BETWEEN 26 AND 35 THEN
'Идэр үе'
WHEN age BETWEEN 36 AND 45 THEN
'Хижээл үе'
WHEN age BETWEEN 46 AND 60 THEN
'Өтөл үе'
WHEN age BETWEEN 61 AND 100 THEN
'Өндөр үе'
ELSE
'other'
END) age,
(CASE
WHEN age BETWEEN 13 AND 18 THEN
'kid'
WHEN age BETWEEN 19 AND 25 THEN
'bigger_kid'
WHEN age BETWEEN 26 AND 35 THEN
'adult'
WHEN age BETWEEN 36 AND 45 THEN
'bigger_adult'
WHEN age BETWEEN 46 AND 60 THEN
'big_adult'
WHEN age BETWEEN 61 AND 100 THEN
'caption'
ELSE
'other'
END) code,
(CASE
WHEN age BETWEEN 13 AND 18 THEN
'13-18'
WHEN age BETWEEN 19 AND 25 THEN
'19-25'
WHEN age BETWEEN 26 AND 35 THEN
'26-35'
WHEN age BETWEEN 36 AND 45 THEN
'36-45'
WHEN age BETWEEN 46 AND 60 THEN
'46-60'
WHEN age BETWEEN 61 AND 100 THEN
'61-100'
ELSE
'other'
END) age_interval,
SUM(total) AS total
FROM u
GROUP BY u.age)
SELECT code,
MAX(age) AS age_level,
MAX(age_interval) AS age_interval,
SUM(total) AS total
FROM sec_survey_users su
LEFT JOIN u
ON su.user_id = u.id
WHERE su.survey_id = 'D3A21B1C2D8334C9E055824F7FFC5DF4'
GROUP BY code

cumulative using case statement in Oracle's SQL

I have a simple data
Date Count by english count by chinese
08-Mar-19 12 54
09-Mar-19 15 66
10-Mar-19 45 32
11-Mar-19 21 70
12-Mar-19 57 64
29-Mar-19 43 53
30-Mar-19 67 21
I want to group this data by week and the sum should be cumulative.The date starts from 8 march so the week should be calculated that way only. So the result should be
count by english count by chinese
08-MAR-19-14-MAR-19 150 286
15-MAR-19-22-MAR-19 150 286 (no data so same as above)
23-MAR-19-30-MAR-19 260 360
Tried using cumulative and sum but not able to achieve it
You can generate your week ranges, then use an outer join to see which data fits in each week, and use an analytic sum to get the result you want;
with week_ranges (date_from, date_to) as (
select min_date + ((level - 1) * 7), min_date + (level * 7)
from (
select min(some_date) as min_date, ceil((max(some_date) - min(some_date)) / 7) as weeks
from your_table
)
connect by level <= weeks
)
select distinct wr.date_from, wr.date_to - 1 as date_to,
sum(count_english) over (order by wr.date_from) as count_english,
sum(count_chinese) over (order by wr.date_from) as count_chinese
from week_ranges wr
left join your_table yt
on yt.some_date >= wr.date_from
and yt.some_date < wr.date_to
order by date_from;
which with your sample data gets:
DATE_FROM DATE_TO COUNT_ENGLISH COUNT_CHINESE
---------- ---------- ------------- -------------
2019-03-08 2019-03-14 150 286
2019-03-15 2019-03-21 150 286
2019-03-22 2019-03-28 150 286
2019-03-29 2019-04-04 260 360
Note this is splitting it up into four 7-days weeks, rather than one of 7 days and two of 8 days...
db<>fiddle
Here's one option; note that "my weeks" are different than yours because - your data is somewhat inconsistent as they vary from 6 to 7 days. That's also why the final result is different, but the general idea should be OK.
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> with test (datum, cbe) as
2 -- sample data
3 (select date '2019-03-08', 12 from dual union all
4 select date '2019-03-09', 15 from dual union all
5 select date '2019-03-10', 45 from dual union all
6 select date '2019-03-11', 21 from dual union all
7 select date '2019-03-12', 57 from dual union all
8 select date '2019-03-29', 43 from dual union all
9 select date '2019-03-30', 67 from dual
10 ),
11 span as
12 -- min and max date value, so that we could create a "calendar"
13 (select min(datum) mindat,
14 max(datum) maxdat
15 from test
16 ),
17 periods as
18 -- "calendar" whose periods are weeks
19 (select s.mindat + (level - 1) * 7 datum_from,
20 (s.mindat + level * 7) - 1 datum_to
21 from span s
22 connect by level <= (s.maxdat - s.mindat) / 7 + 1
23 )
24 -- running sum per weeks
25 select distinct
26 p.datum_from,
27 p.datum_to,
28 sum(t.cbe) over (order by p.datum_from) sum_cbe
29 from test t full outer join periods p on t.datum between p.datum_from and p.datum_to
30 order by p.datum_from;
DATUM_FROM DATUM_TO SUM_CBE
---------- ---------- ----------
08.03.2019 14.03.2019 150
15.03.2019 21.03.2019 150
22.03.2019 28.03.2019 150
29.03.2019 04.04.2019 260
SQL>

SQL query- turning daily data to average for month, cumulative sum until end of month?

SQL - turning daily data to average for month, cumulative sum until end of month
I have daily data coming from a database.
It has a table that contains many columns - Date, Entity Name, Value1, value2, value3, ..
I want to work out the average value per month for each entity. Also for certain values, i need the cumulative sum value from the beginning to until end of the month (cumulative sum by end of the month).
Example:
Date Entity Value1 Value2 Value3 Value4
01/01/2017 'ZZ-01' 5 10 25 10
01/01/2017 'BB-01' 5 10 25 10
02/01/2017 'ZZ-01' 2 1 5 0
02/01/2017 'BB-01' 2 1 5 0
03/01/2017 'ZZ-01' 5 10 25 10
03/01/2017 'BB-01' 5 10 25 10
.....
.....
31/01/2017 'ZZ-01' 5 10 25 10
31/01/2017 'BB-01' 5 10 25 5
01/02/2017 'ZZ-01' 5 10 25 15
01/02/2017 'BB-01' 5 10 25 11
02/02/2017 'ZZ-01' 5 10 25 15
.......
28/02/2017 'ZZ-01' 5 10 25 10
28/02/2017 'BB-01' 5 10 25 10
....
and so on
What I would like to create is a table that looks like:
Month Entity AvgValue1 AvgValue2 CumValue2 AvgValue3 AvgValue4
Jan-2017 'ZZ-01' Avg1 Avg2 CumV2 Avg3 Avg4
Jan-2017 'BB-01'
Feb-2017 'ZZ-01'
Feb-2017 'BB-01'
Mar-2017 'ZZ-01'
...
Average for the given month for each values. For some values, i need cumulative sum until end of month since the beginning of the data in the table.
from searching the stackoverflow, I found out that i can use the below to get the sum and cumulative
SELECT Date, Name, Sum, Avg,
(SELECT SUM(Value1)
FROM mytable as t2
Where t2.Date <=t1.Date) as CumVal
from mytable as t1
order by t1.DateO ASC, Name
and below for average for month
SELECT
dateadd(month, datediff(month, 0, t2.Date), 0) as Month,
AVG(t2.Value1) as AvgValue1
FROM mytable as t2
group by dateadd(month, datediff(month, 0, t2.Date), 0)
But I am not able to combine these things to make one single query that helps in creating the table. your help is appreciated.
I tried to create small test code with one value column:
IF OBJECT_ID('dbo.mytable', 'U') IS NOT NULL
BEGIN
DROP TABLE dbo.mytable
END
CREATE TABLE mytable
([DateOn] datetime, [Name] varchar(10), [Rate1] float, [Rate2] float,
[Rate3] float)
;
INSERT INTO mytable
([DateOn], [Name], [Rate1])
VALUES
-- MM/DD/YYYY format
-- value for first 4 days is shown. assume that the rest days are zero
-- average will be (sum /31) in January
('01/01/2017' , 'AA-01', 10),
('01/01/2017' , 'BB-01', 100),
('01/02/2017' , 'AA-01', 15),
('01/02/2017' , 'BB-01', 200),
('01/03/2017' , 'AA-01', 20),
('01/03/2017' , 'BB-01', 300),
('01/04/2017' , 'AA-01', 25),
('01/04/2017' , 'BB-01', 400),
('02/01/2017' , 'AA-01', 10),
('02/01/2017' , 'BB-01', 100),
('02/02/2017' , 'AA-01', 15),
('02/02/2017' , 'BB-01', 200),
('02/03/2017' , 'AA-01', 20),
('02/03/2017' , 'BB-01', 300),
('02/04/2017' , 'AA-01', 25),
('02/04/2017' , 'BB-01', 400)
;
Select dateadd(month, datediff(month, 0, t1.DateOn), 0) as Month, Name,
AVG(t1.Rate1) as AvgRate1,
(
SELECT SUM(Rate1)
FROM mytable as t2
Where t2.DateOn <=t1.DateOn
)
as CumRate
from mytable as t1
--order by t1.DateOn ASC,Name
group by dateadd(month, datediff(month, 0, t1.DateOn), 0)
DROP TABLE dbo.mytable
I am running this test on
https://rextester.com/CFHEW85984
but getting an error:
Error(s), warning(s):
Column 'mytable.DateOn' is invalid in the select list because it is not
contained in either an aggregate function or the GROUP BY clause.
Column 'mytable.Name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
expected result table format:
Month Name AvgRate CumRate
Jan-17 AA-01 2.258 70
Jan-17 BB-01 32.258 1000
Feb-17 AA-01 2.5 140
Feb-27 BB-01 35.714 2000
so for Jan-17 for name AA-01, i add up the daily figures (only 4 days in this example, rest assumed 0) and divide by 31 to get avg for the month. I then sum them up to get cumulative. For February, average is done same way w, but cumulative is cum_jan+cum_feb. so on
I am struggling how to combine this with join also.
Thanks

Count of people by hour

I need some help working out how many people were on site for each hour.
The data looks like this
Id Roomid, NumPeople, Starttime, Closetime.
1 1 4 2018/10/03 09:06 2018/10/03 12:43
2 2 8 2018/10/03 10:16 2018/10/03 13:12
3 1 6 2018/10/03 13:02 2018/10/03 15:01
What I need out is the max count of people during the hour, each hour
Time | PeoplePresent
9 4
10 12
11 12
12 12
13 14
14 6
15 6
Getting the count of people as the arrived is simple enough, but I can’t think where to start to get the presence for each hour. Can anyone suggest a strategy for this. I ok with the simple SQL stuff but I’m certain this requires some advanced SQL functions.
Tested the following in SQL Server 2008 R2:
You can use a recursive CTE to build the list of hours, including the row id and NumPeople values. Then you can sum them together to get your final output. I put together the following test data based on the question.
CREATE TABLE #times
(
Id int
, Roomid INT
, NumPeople INT
, Starttime DATETIME
, Closetime DATETIME
)
INSERT INTO #times
(
Id
,Roomid
,NumPeople
,Starttime
,Closetime
)
VALUES
(1, 1, 4 , '2018/10/03 09:06', '2018/10/03 12:43')
,(2, 2, 8, '2018/10/03 10:16', '2018/10/03 13:12')
,(3, 1, 6, '2018/10/03 13:02', '2018/10/03 15:01')
;WITH recursive_CTE (id, startHour, currentHour, diff, NumPeople) AS
(
SELECT
Id
,startHour = DATEPART(HOUR, t.Starttime)
,currentHour = DATEPART(HOUR, t.Starttime)
,diff = DATEDIFF(HOUR, Starttime, Closetime)
,t.NumPeople
FROM #times t
UNION ALL
SELECT
r.id
,r.startHour
,r.currentHour + 1
,r.diff
,r.NumPeople
FROM recursive_CTE r
WHERE r.currentHour < startHour + diff
)
SELECT
Time = currentHour
,PeoplePresent = SUM(NumPeople)
FROM recursive_CTE
GROUP BY currentHour
DROP TABLE #times
Query results:
Time PeoplePresent
9 4
10 12
11 12
12 12
13 14
14 6
15 6