Cómo funciona datespine en dbt (dbt utils package)

date spine dbt utils package

Una de las grandes ventajas de dbt es que existen múltiples paquetes con soluciones a problemas comunes, mismos que puedes aprovechar en tus propios proyectos. En el paquete de dbt-utils existen distintas macros que te pueden ayudar con el día día en la resolución de problemas comunes en SQL. Una de las macros es date_spine, misma que genera una columna con una lista de fechas consecutivas en un período determinado. Por ejemplo, si quisieras generar la serie de fechas de todo 2020, la macro quedaría así:

{{ dbt_utils.date_spine(
    datepart="day",
    start_date="cast('2020-01-01' as date)",
    end_date="cast('2021-01-01' as date)"
   )
}}

De acuerdo a la documentación, el argumento que pones como “end_date” no está incluido en la lista de fechas. De hecho, “end_date” es el día posterior. Usando esa macro, obtienes esta lista:

dbt date spine macro result

Funcionamiento de la macro datespine

La lógica para esta macro está disponible aquí: https://github.com/dbt-labs/dbt-utils/blob/main/macros/sql/date_spine.sql. Traducido para este ejemplo en particular queda así:

with
    rawdata as (
        with
            p as (
                select
                    0 as generated_number
                union all
                select
                    1
            ),
            unioned as (
                select
                            p0.generated_number * power(2, 0)
                        +
                            p1.generated_number * power(2, 1)
                        +
                            p2.generated_number * power(2, 2)
                        +
                            p3.generated_number * power(2, 3)
                        +
                            p4.generated_number * power(2, 4)
                        +
                            p5.generated_number * power(2, 5)
                        +
                            p6.generated_number * power(2, 6)
                        +
                            p7.generated_number * power(2, 7)
                        +
                            p8.generated_number * power(2, 8)
                        + 1
                        as generated_number

                from p as p0
                     cross join
                     p as p1
                     cross join
                     p as p2
                     cross join
                     p as p3
                     cross join
                     p as p4
                     cross join
                     p as p5
                     cross join
                     p as p6
                     cross join
                     p as p7
                     cross join
                     p as p8
            )

        select *
        from unioned
        where
            generated_number <= 365
        order by generated_number
    ),

    all_periods as (
        select
            (
                datetime_add(
                        cast(cast('2020-01-01' as date) as datetime),
                        interval row_number() over (order by 1) - 1 day
                    )
                ) as date_day
        from rawdata
    ),

    filtered as (
        select *
        from all_periods
        where
            date_day <= cast('2021-01-01' as date)
    )

select *
from filtered

Y funciona de la siguiente manera.

Crear punto de partida “p”

La primera parte del query genera un punto de partida, que en el query está designada como “p”. Está lógica, de hecho está generada por otra macro “generate_series” dentro del mismo paquete de dbt (ver referencia).

with
    rawdata as (
        with
            p as (
                select
                    0 as generated_number
                union all
                select
                    1
            )
        SELECT * FROM p
    )
select *
from rawdata

¿Por qué un 0 y un 1? Para crear una plantilla que nos permita contar en binario. El siguiente paso es cruzar esta plantilla consigo misma para generar un “tablero” que nos permita hacer los conteos en binario:

with
    rawdata as (
        with
            p as (
                select
                    0 as generated_number
                union all
                select
                    1
            ),
            unioned as (
                select *
                from p as p0
                     cross join
                     p as p1
                     cross join
                     p as p2
                     cross join
                     p as p3
                     cross join
                     p as p4
                     cross join
                     p as p5
                     cross join
                     p as p6
                     cross join
                     p as p7
                     cross join
                     p as p8
            )

        select *
        from unioned

    )

select *
from rawdata

El tablero se ve así en una hoja de cálculo: https://docs.google.com/spreadsheets/d/1W5-GW5MOhGJraaYJ4X74-5P0LHAJxeuP-_1P6FlzuSs/edit#gid=176921942&range=A1.Visto de manera simplificada, si solo se cruzara el query consigo mismo una vez, quedaría así

with
    rawdata as (
        with
            p as (
                select
                    0 as generated_number
                union all
                select
                    1
            ),
            unioned as (
                select *
                from p as p0
                     cross join
                     p as p1

            )

        select *
        from unioned

    )

select *
from rawdata

Con este cruce podríamos contar hasta desde 0 hasta 3.

Cruzando el query 8 veces (como ocurre en el query que genera la plantilla), podemos llegar hasta el 511:

511 = 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28

Esto es visible en el siguiente paso del query:

with
    rawdata as (
        with
            p as (
                select
                    0 as generated_number
                union all
                select
                    1
            ),
            unioned as (
                select
                            p0.generated_number * power(2, 0)
                        +
                            p1.generated_number * power(2, 1)
                        +
                            p2.generated_number * power(2, 2)
                        +
                            p3.generated_number * power(2, 3)
                        +
                            p4.generated_number * power(2, 4)
                        +
                            p5.generated_number * power(2, 5)
                        +
                            p6.generated_number * power(2, 6)
                        +
                            p7.generated_number * power(2, 7)
                        +
                            p8.generated_number * power(2, 8)
                        + 1
                        as generated_number

                from p as p0
                     cross join
                     p as p1
                     cross join
                     p as p2
                     cross join
                     p as p3
                     cross join
                     p as p4
                     cross join
                     p as p5
                     cross join
                     p as p6
                     cross join
                     p as p7
                     cross join
                     p as p8
            )

        select *
        from unioned
        where
            generated_number <= 365
        order by generated_number
    )
select *
from rawdata

Esta operación es la misma que en esta hoja de cálculo: https://docs.google.com/spreadsheets/d/1W5-GW5MOhGJraaYJ4X74-5P0LHAJxeuP-_1P6FlzuSs/edit#gid=1546948972&range=T4. Y aplicando el filtro (“where”) y ordenando, se obtiene una lista del 1 al 365. Cuando el rango es extendido, la macro para generar la serie extiende esta lista para admitir fechas más allá del mismo año.

Con esta lista como base (rawdata), se calculan las fechas (partiendo de la fecha inicial), y se limita por la fecha final:

...
all_periods as (
        select
            (
                datetime_add(
                        cast(cast('2019-01-01' as date) as datetime),
                        interval row_number() over (order by 1) - 1 day
                    )
                ) as date_day
        from rawdata
    ),

    filtered as (
        select *
        from all_periods
        where
            date_day <= cast('2021-01-01' as date)
    )

select *
from filtered

Y así obtienes el producto final:

Date spine result dbt

Copyright © Todos los derechos reservados | FDI Tecnología y Finanzas, SAS de CV. De no existir previa autorización, queda expresamente prohibida la Publicación, retransmisión, edición y cualquier otro uso de los contenidos.