SuperAPI AI 工具集

bucket-local-time-in-code-not-in-sql

数据与分析

查看项目

更新于 10-17 16:17

技能介绍

用途

该技能面向 Kotlin 应用中需要按“本地时间”对事件分组的统计场景,例如本地收听分析中的小时直方图、最忙的一天、每日计数和总量。它主张:本地时间分桶应在应用代码中完成,而不是在 SQL 里用引擎的本地时间修饰符完成。

主要能力

  • 用一次范围查询返回原始行(时间戳与数值),而不是让 SQL 返回聚合数字。
  • 在代码中一次遍历同时填充多个本地时间桶:小时直方图、每日计数、总量、最忙日期。
  • 避免多次扫描之间因写入导致结果不一致。
  • 避免依赖进程时区(如 SQLite 的 'localtime' 及类似修饰符),因为其解析依据是操作系统交给进程的时区,可能是 TZ 环境变量、服务账号时区或容器默认的 UTC,并不等于用户时区。

使用方式

资料给出的示例查询返回原始样本:

@Query("SELECT timestamp, listenedSecond FROM activity_event" +
       " WHERE timestamp BETWEEN :startTimestamp AND :endTimestamp")
suspend fun getSamplesInRange(startTimestamp: LocalDateTime, endTimestamp: LocalDateTime): List<Sample>

随后在代码中一次遍历完成四个统计:

val hours = IntArray(24)
val perDay = mutableMapOf<LocalDate, Int>()
var listened = 0L
samples.forEach { s ->
    hours[s.timestamp.hour]++
    perDay[s.timestamp.date] = (perDay[s.timestamp.date] ?: 0) + 1
    listened += s.listenedSecond
}
val busiest = perDay.maxByOrNull { it.value }

适用场景与边界

  • 只有分组键是本地字段时才把分桶移到代码中。
  • 去重计数、Top-N 列表、首次最小值、连接、占比计数等没有本地时间成分的聚合仍应留在 SQL 引擎中,因为它们按 id 分组或按同一尺度比较时间戳,返回一行结果而非每行数据。
  • 原始扫描返回行数随窗口增长;直方图查询无论周期多长都只返回 24 个数字。应只投影推导真正需要的列,并在发布更宽窗口前了解最宽支持窗口的成本。若周期可能无界,这种形态不合适。
  • 原始扫描也可作为空检查:先读取它,空周期可提前返回,避免发出后续七个聚合查询,把八次往返减少为一次。
  • 样本类型必须让 ORM 转换器仍能运行。不要把时间戳字段声明为原始数字以“保持投影廉价”,否则返回的编码需要手工解释,而本地小时恰恰是解释错误时最容易出错的数字。应请求日期时间类型,小时和日期可直接读取,路径中无需任何时区运算。
  • 不要把本地问题推入索引:对计算出的本地字段建索引会把某个时区固化进存储字节,对其他时区的用户静默错误,规则变更后对原用户也会过期。时区应放在边缘,在代码中应用一次。
  • 每新增一个本地桶应是现有循环中的一行,而不是新查询。若分组键来自本地日期时间,就应写成 forEach 中的一行。
  • 应在声明处说明原因:一个返回原始行的查询与十一个返回数字的查询并列时,容易被误认为未学会聚合。用一条注释说明原因(本地分组、一次扫描、与时区无关),可防止日后被“优化”成四个 GROUP BY。

验证方式

检查所有查询字符串中的本地时间函数,正确结果是唯一命中来自一条说明为何不使用的注释:

grep -rn --include='*.kt' -E "localtime|strftime\(" . | grep -v '/build/'

查看范围查询族的形态——多少返回行、多少返回数字:

grep -rn --include='*Dao*.kt' -E "suspend fun (get|query)[A-Za-z]*InRange" . | grep -v '/build/'

应恰好有一个返回原始样本列表,且它应是本地桶推导所依据的查询。同一族中出现第二个返回行的查询,通常意味着同一扫描被支付了两次。

最后,在同一数据上分别用代码和 SQL 的本地时间修饰符计算小时直方图并比较。两者仅在进程时区恰好等于用户时区时一致,因此可分别在 TZ=UTC 和 TZ=Asia/Ho_Chi_Minh 环境下运行:代码答案不应变化,SQL 答案会变化。