← All posts
by Arif Aslam 4 min read

Calculating Age from a Date of Birth Column

Your HR export has a date_of_birth column. Your customer database has birthdate. Neither of them has an age field. You need ages for demographic analysis, eligibility checks, or to bucket people into age groups for a report. Every time you open the file, the ages should reflect today's date, not some hardcoded snapshot from when the data was last updated.

Here's how to calculate a live age column from a birthdate in ExploreMyData, then segment it into age groups.

Make sure the column is a date type

Before calculating anything, the birthdate column needs to actually be a DATE type, not a string. If your CSV imported it as VARCHAR (common with formats like "03/15/1990" or "1990-03-15"), add a Convert Type step first. Select the column, choose date as the target type, and leave Auto-detect date format on. It tries ISO, US, European and named-month patterns per value, so most columns need nothing else. If yours uses an unusual format, turn auto-detect off and give the exact pattern in Source date format.

You can check the current type in the column header. If it shows VARCHAR, convert it. If it already shows DATE, you're good.

Step 1: give every row a "today" column

Here is the part that trips people up. Date Difference compares two date columns. It has a "First date column" dropdown and a "Second date column" dropdown, and both of them only list columns that already exist. There is nowhere to type CURRENT_DATE, and no "today" option hiding in a menu.

So you make today into a column first. Click the green + in the Pipeline panel and select Add Column from the Columns group. Name it today, put CURRENT_DATE in the Expression box, and set the output type to date.

Add Column configuration:

New column name today
Expression CURRENT_DATE
Type date

Generated SQL: SELECT *, TRY_CAST(CURRENT_DATE AS TIMESTAMP) AS "today" FROM employees

Every row now carries the same value, and it is a real date column that the Date operations will accept. The cast to TIMESTAMP is what the date output type does; DATEDIFF is happy to compare a TIMESTAMP against a DATE, so nothing else is needed.

Step 2: Date Difference between the two columns

Add a second step: Date Difference from the Date group.

  1. First date column: date_of_birth.
  2. Second date column: today.
  3. Unit: year.
  4. Under "Apply results into", choose New Column and name it age_years. Leave it blank and you get a column called date_diff.

The generated SQL: DATEDIFF('year', "date_of_birth", "today") AS "age_years"

Running that over a small HR extract on 20 January 2026 gives this. Your own numbers will differ, because the column is recomputed against the real current date every time the file loads.

name date_of_birth age_years
Sarah Chen1990-03-1536
James Wright1985-11-2241
Maria Garcia1998-07-0428
David Kim2001-01-3025
Tom Becker(blank)NULL

Read the caveat before you ship this

Every one of those four numbers is one too high. DuckDB's DATEDIFF('year', ...) counts calendar year boundaries crossed, not completed years. Sarah Chen was born in 1990 and it is 2026, so DATEDIFF says 36. She does not actually turn 36 until 15 March.

In plain English: the year unit gives you the age each person turns during the current year. That is genuinely what you want for things like "who turns 40 this year", and it is stable for a whole calendar year, which makes it convenient for annual reporting. It is the wrong number for eligibility checks and anything that has to be true today. January is the worst case, because almost nobody has had their birthday yet.

Step 3: the exact age

For completed years, add one more Add Column step. Name it age, set the type to numeric, and put this in the Expression box:

EXTRACT(year FROM AGE("today", "date_of_birth"))

AGE() returns a calendar interval rather than a raw day count, so pulling the year part out of it gives whole years lived, birthday included. No leap-year fudge factor, no dividing by 365.25 and hoping. Compare the two columns side by side and you can see exactly who has not had their birthday yet:

name date_of_birth age_years age
Sarah Chen1990-03-153635
James Wright1985-11-224140
Maria Garcia1998-07-042827
David Kim2001-01-302524
Tom Becker(blank)NULLNULL

David Kim is ten days short of his 25th birthday on this run, which is exactly the case that would have quietly failed an "is this person 25 or over" check. Once you are happy with age, delete age_years with a Delete Columns step so nobody downstream picks the wrong one.

Adding age group buckets

Raw ages are useful, but reports usually want groups: "18-25", "26-35", "36-45", and so on. One more Add Column step does it. Name the column age_group, set the type to text, and paste a CASE expression into the Expression box:

CASE
  WHEN "age" IS NULL THEN 'Unknown'
  WHEN "age" < 18   THEN 'Under 18'
  WHEN "age" <= 25  THEN '18-25'
  WHEN "age" <= 35  THEN '26-35'
  WHEN "age" <= 45  THEN '36-45'
  WHEN "age" <= 55  THEN '46-55'
  WHEN "age" <= 65  THEN '56-65'
  ELSE '65+'
END

Branches are evaluated top to bottom and the first match wins, which is why each one only needs an upper bound. The NULL branch has to come first: a NULL age fails every comparison silently, so without it Tom Becker lands in the "65+" bucket and nobody notices until someone asks why the pensioner count looks wrong.

Branch age_group value
age IS NULLUnknown
age < 18Under 18
age <= 2518-25
age <= 3526-35
age <= 4536-45
age <= 5546-55
age <= 6556-65
Default (all other rows)65+

Eight branches, seven buckets plus the fallback. Every age lands in exactly one of them.

The result:

name age age_group
Sarah Chen3526-35
James Wright4036-45
Maria Garcia2726-35
David Kim2418-25
Tom BeckerNULLUnknown

Sarah Chen sits in "26-35" rather than "36-45" precisely because we used the exact age. Had we shipped the DATEDIFF year column she would have been bucketed a decade too old for eight more weeks. That is the kind of error nobody catches in a review, so it is worth the extra step.

The age updates every time you open the file

Because the today column is CURRENT_DATE rather than a literal, every step downstream of it recomputes when the data loads. Open the same file in January and again in June and anyone with a birthday in between moves up a year, and out of one bucket into the next if that is where they sat. Nothing to remember, nothing to re-run by hand.

The flip side is worth saying out loud: an export of this table is a snapshot. If you send the CSV to someone in March they get March's ages, frozen. Send them the source file and the pipeline instead and the numbers stay honest.

Calculate ages in ExploreMyData →

AA

Arif Aslam

Staff engineer in Bangalore. By day at Mammoth Analytics; building ExploreMyData on the side. More on my author page or LinkedIn.

Try it yourself

No sign-up, no upload, no tracking.

Open ExploreMyData