I was fiddling around with some data, looking at making a quick aging report. I probably should have reached for Power BI Desktop, but sometimes I can be stubborn, and I wanted to get my T-SQL to work–if nothing else to figure out how to make it work.

## My Data

My data was essentially a really long list of widgets, with dates–really simple & straightforward:

Widget Name | Widget Date |
---|---|

Widget A | 2019-06-19 |

Widget B | 2019-07-14 |

Widget C | 2018-11-04 |

… | … |

Widget D | 2015-11-30 |

## My reporting requirements

I wanted to count widgets based on how old they were. The data spanned several years, so I wanted to be able to put them into 3 or 6 month buckets. Essentially, I wanted to use N-month sized buckets to make my report to look something like this:

Widget Age | Widget Count |
---|---|

0 – 6 Months Old | 8752 |

6 – 12 Months Old | 9836 |

12 – 18 Months old | 7852 |

… | … |

60 – 66 Months old | 5872 |

## Let’s write the report

First, we need some test data we can fool around with. I’m going to create a temp table with one row per day, using the Numbers table from my DBA Database. For this test data, that will make the numbers per bucket pretty similar, but that makes for easy validation!

SELECT WidgetName = 'Widget ' + LEFT(NEWID(),3), WidgetDate = CONVERT(DATE,DATEADD(DAY,-1*[Number],GETDATE())) INTO #Widgets FROM DBA.dbo.Numbers WHERE [Number] < 75000;

Now, it’s pretty easy to calculate the age of these rows–we just use `DATEDIFF`

to compute the number of days/weeks/months/years between the `WidgetDate`

and today. But that doesn’t help me with my report. I could do a `GROUP BY MonthsAgo`

to count per month… but I really wanted to put these into N-month buckets.

SELECT WidgetDate, DaysOld = DATEDIFF(DAY,WidgetDate,GETDATE()), MonthsOld = DATEDIFF(MONTH,WidgetDate,GETDATE()) FROM #Widgets;

Next, I want to put those “Months Ago” numbers into buckets–and I want to be able to easily adjust the size of those buckets as I evaluate the data & work on how I want to present it. Each bucket has a “bottom end” and “top end” value–referred to as a floor & ceiling.

Here’s where my math nerd-iness comes out… There’s a little trick for “bucketizing” numbers (in this case, turning “Months” into “Month Buckets”):

- Take a number
- Divide it by your bucket size
- Round that number down to a whole number–We’ll call this the “divided number”
- Multiply the “divided number” by the bucket size–This is your bucket floor
- Add one to the “divided number” and then multiply
*that*by your bucket size–This is your bucket ceiling

Converting that into T-SQL, we can even save a step: Our `MonthsOld`

column is always a whole number (ie, an `int`

). If our bucket size is also an `int`

, then dividing the two will always result in an `int`

, which has been rounded down–this saves me from having to manually do any rounding! (Though, I could totally throw in a call to the `FLOOR()`

function if I wanted to be explicit.)

Now I can convert that into T-SQL and continue to add on to our last query, and spot-check the results:

DECLARE @BucketSize int = 6; SELECT WidgetDate, DaysOld = DATEDIFF(DAY,WidgetDate,GETDATE()), MonthsOld = DATEDIFF(MONTH,WidgetDate,GETDATE()), MonthBucketFloor = DATEDIFF(MONTH,WidgetDate,GETDATE()) / @BucketSize * @BucketSize, MonthBucketCeiling = (DATEDIFF(MONTH,WidgetDate,GETDATE()) / @BucketSize + 1) * @BucketSize FROM #Widgets;

I know there’s someone else out there shouting “No, Andy! That formula is too complicated! There’s another way to do that with fewer steps!” Yes, there is another way. This option might be easier to understand why it works, but I find the *code* harder to read.

- Take a number
- Perform a modulo with the bucket size as the divisor to get the remainder
- Subtract the remainder from the original number–This is your bucket floor
- Take your bucket floor and add your bucket size–This is your bucket ceiling

OK, so technically, that’s only 4 steps. But I don’t think that makes it any simpler. Here’s what this alternate version looks like when you convert it into T-SQL:

--Alternate to my prior query DECLARE @BucketSize int = 6; SELECT WidgetDate, DaysOld = DATEDIFF(DAY,WidgetDate,GETDATE()), MonthsOld = DATEDIFF(MONTH,WidgetDate,GETDATE()), MonthBucketFloor = DATEDIFF(MONTH,WidgetDate,GETDATE()) - (DATEDIFF(MONTH,WidgetDate,GETDATE()) % @BucketSize), MonthBucketCeiling = DATEDIFF(MONTH,WidgetDate,GETDATE()) - (DATEDIFF(MONTH,WidgetDate,GETDATE()) % @BucketSize) + @BucketSize FROM #Widgets;

I’m not going to use this version, but know that it’s 100% logically equivalent to the prior version. Maybe some of my fellow math nerds will slide into the comments with the mathematical proof that these two queries are logically equivalent.

My last step is simply to do some counts grouped by the buckets–and to convert the floor/ceiling numbers into a pretty label:

DECLARE @BucketSize int = 6; WITH WidgetsWithAge AS ( SELECT WidgetDate, DaysOld = DATEDIFF(DAY,WidgetDate,GETDATE()), MonthsOld = DATEDIFF(MONTH,WidgetDate,GETDATE()), MonthBucketFloor = FLOOR(DATEDIFF(MONTH,WidgetDate,GETDATE())/@BucketSize) * @BucketSize, MonthBucketCeiling = (FLOOR(DATEDIFF(MONTH,WidgetDate,GETDATE())/@BucketSize) + 1) * @BucketSize FROM #Widgets ) SELECT WidgetAge = CONVERT(varchar(100), MonthBucketFloor) + ' - ' + CONVERT(varchar(100), MonthBucketCeiling) + ' Months Old', WidgetCount = COUNT(*) FROM WidgetsWithAge GROUP BY MonthBucketFloor, MonthBucketCeiling;

Boom! That’s it. That’s my report. And since I sneaked in that `@BucketSize`

as a parameter, it’s pretty easy for me to adjust my bucket size for different data. I can easily switch that to a 36-month bucket with the flick of a keystroke:

DECLARE @BucketSize int = 36; WITH WidgetsWithAge AS ( SELECT WidgetDate, DaysOld = DATEDIFF(DAY,WidgetDate,GETDATE()), MonthsOld = DATEDIFF(MONTH,WidgetDate,GETDATE()), MonthBucketFloor = FLOOR(DATEDIFF(MONTH,WidgetDate,GETDATE())/@BucketSize) * @BucketSize, MonthBucketCeiling = (FLOOR(DATEDIFF(MONTH,WidgetDate,GETDATE())/@BucketSize) + 1) * @BucketSize FROM #Widgets ) SELECT WidgetAge = CONVERT(varchar(100), MonthBucketFloor) + ' - ' + CONVERT(varchar(100), MonthBucketCeiling) + ' Months Old', WidgetCount = COUNT(*) FROM WidgetsWithAge GROUP BY MonthBucketFloor, MonthBucketCeiling;