R dplyr操作Rpostgres时转换Unix时间戳为日期报错问题
在dplyr链路中操作Rpostgres连接的数据库表时,需要将库中整数类型存储的Unix时间戳转换为日期格式,后续提取年、季度、月、日维度做分组汇总,避免全量拉取数据到本地执行collect()产生的高耗时。本地R环境可正常运行的转换逻辑,直接嵌入数据库查询代码后连续触发报错。
本地环境可正常运行的转换代码
df$timestamp <- as_datetime(df$timestamp) df$Day <- format(as.Date(df$timestamp, format="%Y/%m/%d"),"%d")
数据库查询中的报错复现
第一次尝试:传入origin、tz参数调用
lubridate::as_datetime()
代码如下:%>% mutate(date = lubridate::as_datetime(timestamp, origin='1970-01-01', tz = "GMT"))%>% select(date)触发报错:
Error in as_datetime(timestamp, origin = "1970-01-01", tz = "GMT") : unused arguments (origin = "1970-01-01", tz = "GMT")
第二次尝试:移除origin、tz参数直接调用转换函数
代码如下:%>% mutate(date = lubridate::as_datetime(timestamp))%>% select(date)触发报错:
Failed to prepare query: ERROR: cannot cast type integer to timestamp without time zone LINE 1: SELECT CAST("timestamp" AS TIMESTAMP) AS "date"
dbplyr在做R到SQL的语法翻译时,没有把lubridate的时间转换函数映射到PostgreSQL对应的时间处理逻辑,直接调用会生成两类错误语法:要么是本地R函数不识别传入的数据库端参数,要么是被翻译成整数直接强转timestamp的CAST逻辑,而PostgreSQL本身不支持整数类型直接强转为时间戳类型。
方案1:嵌入数据库原生函数(推荐,执行效率最高)
PostgreSQL原生提供to_timestamp()函数专门处理Unix整数时间戳转日期时间的需求,可通过sql()嵌入原生SQL片段,所有转换逻辑完全在数据库端执行,不会触发全量数据拉取:
# 若时间戳为毫秒级存储,需将to_timestamp(timestamp)替换为to_timestamp(timestamp / 1000) db_table %>% mutate( # 转换为GMT时区的日期时间类型 datetime = sql("to_timestamp(timestamp) AT TIME ZONE 'GMT'"), # 直接在库端提取各时间维度,无需拉取到本地计算 year = sql("EXTRACT(YEAR FROM to_timestamp(timestamp) AT TIME ZONE 'GMT')"), quarter = sql("EXTRACT(QUARTER FROM to_timestamp(timestamp) AT TIME ZONE 'GMT')"), month = sql("EXTRACT(MONTH FROM to_timestamp(timestamp) AT TIME ZONE 'GMT')"), day = sql("EXTRACT(DAY FROM to_timestamp(timestamp) AT TIME ZONE 'GMT')") ) %>% # 后续直接接分组汇总逻辑即可 group_by(year, quarter, month, day) %>% summarise(metric = sum(target_col), .groups = "drop")
方案2:使用dbplyr兼容的时间运算写法
如果不想编写原生SQL片段,可利用dbplyr已覆盖的时间间隔运算翻译规则,从Unix时间原点累加秒数得到时间类型,写法如下:
library(lubridate) db_table %>% mutate( datetime = as.POSIXct("1970-01-01", tz = "GMT") + timestamp * dseconds() )
该写法会被dbplyr正确翻译为PostgreSQL可识别的时间运算逻辑,不会触发强转报错。
- 转换前先确认Unix时间戳的精度:秒级精度直接传入转换函数即可,毫秒级精度需要先除以1000,否则会得到超出正常范围的错误时间值
- 所有转换、分组、汇总逻辑都要写在
collect()调用之前,运算会自动下推到数据库执行,只有最终汇总后的小结果集会被拉取到本地,大幅降低运行耗时 - 不要直接在Rpostgres连接的tbl对象上调用lubridate的
as_datetime()、as.Date()等转换函数,dbplyr对这类函数的翻译覆盖不全,很容易生成数据库无法识别的错误语法
内容的提问来源于stack exchange,提问作者EHL

