0

I’d like to generate a dynamic Table with the start date of a month as a column and the end date of the month as another column.

Ideally, I’d like to provide two years, f.e. 2016 and 2021. The output I’d like to have when providing these two years is the following:

Begin_of_Month End_of_Month
2016-01-01     2016-01-31
2016-02-01     2016-02-29
.
.
.
2021-12-01     2021-12-31

Kindly note that I require the output for all years from 2016 to 2021. In my example above this would mean that 2017 2018 2019 2020 should be included.

I have tried to play with a time series function of Teradata but failed to get results.

The solution I tried to recreate in Teradata is this one: How to generate calendar table having begin month date and end month Date

Furthermore, I have tried the EXPAND ON PERIOD time series function of Teradata.

Eren
  • 73
  • 1
  • 2
  • 8
  • Did you want a SAS solution as well, or need a SQL based solution? This is very trivial in a SAS data step. – Reeza Aug 10 '21 at 16:05
  • 1
    Why create this at all? Why not just incorporate sys_calendar.calendar into whatever future query you are going to use that needs those dates? – Tom Aug 10 '21 at 19:52

3 Answers3

3

I'm sure there are some fancy ways of doing this, but I think just hitting up the built-in calendar table is probably the easiest:

SELECT DISTINCT 
    min(calendar_date) OVER (PARTITION BY year_of_calendar, month_of_calendar) as start_of_month, 
    max(calendar_date) OVER (PARTITION BY year_of_calendar, month_of_calendar) as end_of_month  
FROM sys_calendar.calendar
WHERE year_of_calendar BETWEEN 2016 and 2021

To do this without a table reference, it gets a little ugly. EXPAND ON seems like an obvious route, but it errors if there is no table reference in the FROM clause. UNION suffers from the same issue, but we can cheat with UNION by using a cte. EXPAND ON is more picky and to trick it we can hijack Teradata's JSON_TABLE feature:

SELECT BEGIN(dt), PRIOR(END(dt))
FROM JSON_TABLE
    ( 
        ON (SELECT 1 as id, NEW JSON('{"startdate":"2016-01-01","enddate":"2021-12-31"}') jd)
        USING 
            rowexpr('$')
            colexpr('[{"jsonpath" : "$.startdate", "type" : "DATE"},
                      {"jsonpath" : "$.enddate", "type" : "DATE"}]')
    ) as jt(id, startdate, enddate)
EXPAND ON PERIOD(startdate, enddate) as dt BY ANCHOR MONTH_BEGIN

You could also go with a recursive CTE to build out the months, which feels less hacky, but takes longer to generate.

WITH startend AS
(
    SELECT 
        DATE '2016-01-01' periodstartdate,
        DATE '2021-12-31' AS periodenddate
)
,RECURSIVE months AS
(
    SELECT periodstartdate,
        periodenddate,
        periodstartdate as monthstartdate,
        1 as monthoffset
    FROM startend
    UNION ALL
    SELECT periodstartdate,
        periodenddate,
        ADD_MONTHS(periodstartdate, monthoffset),
        monthoffset + 1
    FROM 
        months 
    WHERE monthoffset < months_between(periodenddate, periodstartdate) 
)
SELECT monthstartdate, monthstartdate + INTERVAL '1' MONTH - INTERVAL '1' DAY as monthenddate from months;

I'd be very interested if there is a more elegant way to pull this off. Without dual or sequence generation like are present in other RDBMS, the options to build data sets with no table reference are pretty limited.

JNevill
  • 46,980
  • 4
  • 38
  • 63
  • My goal is to build something that preferably is not dependent on any other table. I’ll wait for other solutions before accepting. – Eren Aug 10 '21 at 13:39
  • Why would you not want to use the built in calendar table for this? I guess you could do it with an overly complicated recursive CTE or something, but this is exactly what you asked for. – Andrew Aug 10 '21 at 14:48
  • Definitely a tall ask to go with no table reference to generate data in Teradata. It's trivial in other RDBMS's that support `dual` table reference or sequence generation, or lateral joins, but Teradata is limited a bit there. To @Andrew 's point I attempted with recursive as well as `expand on` with a `json_table` to get around errors. – JNevill Aug 10 '21 at 14:49
  • Also, I can't help but think there is something more elegant that might work here, but that JSON example is nice and snappy so it's probably a solid route. – JNevill Aug 10 '21 at 14:57
  • Amazing Solution though. Thanks. – Eren Aug 10 '21 at 14:58
  • Your 1st Select can be simplified to `GROUP BY` plus `min/max(calendar_date)` :-) – dnoeth Aug 10 '21 at 20:09
3

Normally EXPAND ON only works when a table is accessed in FROM, but applying some function like TRUNC or TO_DATE fools the optimizer:

WITH dummy AS 
 (
   SELECT 
      2016 AS yr_start
     ,2021 as yr_end
     ,TO_DATE(TRIM(yr_start) || '-01-01') AS pd_start
     ,TO_DATE(TRIM(yr_end+1) || '-01-01') AS pd_end
 ) 
SELECT 
   BEGIN(pd) AS Begin_of_Month
  ,LAST(pd)  AS End_of_Month
FROM dummy
EXPAND ON PERIOD(pd_start, pd_end) AS pd
          BY INTERVAL '1' MONTH
dnoeth
  • 59,503
  • 4
  • 39
  • 56
  • This is why I love asking how you can solve one problem multiple ways. I learn a lot from it. – Eren Aug 11 '21 at 07:37
  • `TRUNC`! I'm remembering that one! I knew there would be something prettier than slamming `JSON_TABLE()` into the mix. – JNevill Aug 11 '21 at 16:25
  • Hi Dnoeth, would you mind explaining what the calculation does exactly within the cast statement? Is there any documentation on it? – Eren Aug 11 '21 at 18:33
  • This calculation is based on the internal storage of a date (`(year - 1900) * 10000 + month * 100 + day` and returns Jan 1st. For the end year it's Jan 1st of the following year, but periods are inclusive-exclusive, i.e. start is included, but end is excluded. In fact there's a easier way which works without TRUNC, too. I'll edit my answer – dnoeth Aug 11 '21 at 19:41
2

If you are going to do this in SAS then there is no need for SQL.

data want;
  do year=2016 to 2021;
    do month=1 to 12;
      start_of_month=mdy(month,1,year);
      end_of_month=intnx('month',start_of_month,0,'e');
      output;
    end;
  end;
  format start_of_month end_of_month yymmdd10.;
  drop year month;
run;
Tom
  • 47,574
  • 2
  • 16
  • 29
  • And let’s say I want to use this table in a proc SQL query, is that possible? I guess I am asking how I can refer to this table in proc sql? – Eren Aug 10 '21 at 19:43
  • ?? `select * from want` ?? Are you asking how to upload it as a VOLATILE table in TERADATA? Just define a libname that is using that target schema and upload it. Or even just write it directly using that libref. Since it is tiny performance is no problem. Watch out for all of the teradata gotchas like PRIMARY_INDEX. – Tom Aug 10 '21 at 19:49
  • Yes. The name after DATA is the name of the dataset (aka "table") that the step is creating. – Tom Aug 10 '21 at 19:54
  • I'd bet whatever you're using this for isn't needed in SAS and could be achieved using INTNX() in the query directly or using the appropriate date formats. – Reeza Aug 10 '21 at 20:18