【问题标题】:Use T-SQL window functions to retrieve 5-minute averages from 1-minute data使用 T-SQL 窗口函数从 1 分钟数据中检索 5 分钟平均值
【发布时间】:2020-02-29 12:27:26
【问题描述】:

我有一个数据库表,其中包含一分钟的开盘价、收盘价、最高价、最低价、交易量值。我使用的是 SQL Server 2017,但可以选择 2019 RC。

我正在尝试找到一个高效的 SQL Server 查询,可以将这些汇总到 5 分钟的窗口中,其中:

  • Open = 窗口的第一个 Open 值
  • 关闭 = 窗口的最后关闭值
  • High = 窗口的最大 High 值
  • Lo​​w = min 窗口的最小值
  • 音量 = 窗口内的平均音量

理想情况下,此查询将考虑数据中的空白,即基于日期计算而不是计算前后行。

例如说我有(这里是 6 分钟的数据):

|时间 |打开 |关闭 |高 |低 |体积 | |-----------------|------|--------|------|-----|-- ------| | 2019-10-30 09:30 | 5 | 10 | 15 | 1 | 125000 | | 2019-10-30 09:31 | 10 | 15 | 20 | 5 | 100000 | | 2019-10-30 09:32 | 15 | 20 | 25 | 10 | 120000 | | 2019-10-30 09:33 | 20 | 25 | 30 | 15 | 10000 | | 2019-10-30 09:34 | 20 | 22 | 40 | 2 | 13122 | | 2019-10-30 09:35 | 22 | 30 | 35 | 4 | 15000 |未考虑在内,因为这将是下一个 5 分钟窗口的第一行

我正在尝试编写一个可以给我的查询(这是 5 分钟聚合的第一个示例):

|时间 |打开 |关闭 |高 |低 |体积 | |-----------------|------|--------|------|-----|-- --------| | 2019-10-30 09:30 | 5 | 30 | 40 | 1 | 50224.4 |

有什么建议吗?我用 OVER 子句及其 PARTITION / RANGE 选项将我的头撞到墙上

【问题讨论】:

  • 如果您共享脚本以生成示例数据,那将是一个加分点!
  • 请分享您的尝试。

标签: sql sql-server window-functions algorithmic-trading stockquotes


【解决方案1】:

您希望以 5 分钟为间隔分析数据。您可以使用带有以下分区子句的窗口函数:

partition by datepart(year, t.[time]),
    datepart(month, t.[time]),
    datepart(day, t.[time]),
    datepart(hour, t.[time]),
    (datepart(minute, t.[time]) / 5)

查询:

select *
from (
    select  
        t.time,
        row_number() over(
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
            order by [time]
        ) [rn],
        first_value([open]) over(
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
            order by [time]
        ) [open],
        last_value([close]) over(
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
            order by [time]
        ) [close],
        max([high]) over (
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
        ) [high],
        min([low]) over (
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
        ) [low],
        avg([volume]) over (
            partition by datepart(year, [time]),
                datepart(month, [time]),
                datepart(day, [time]),
                datepart(hour, [time]),
                (datepart(minute, [time]) / 5)
        ) [volume]
    from mytable t
) t
where rn = 1

【讨论】:

  • 非常感谢!我也试过你的版本,它确实帮助我理解了 over 子句的分区逻辑。它也产生了很好的结果,尽管接受的答案似乎执行起来稍微快一些并且更短。艰难的选择!
  • 我认为您的选择是正确的,我发现@SalmanA 的解决方案实际上比我的要好,因为分区的定义更简洁。
【解决方案2】:

你可以试试这个。

  SELECT
      MIN([Time]) [Time], 
      Min([Open]) [Open],
      LEAD(Min([Open])) OVER (ORDER BY MIN([Time])) AS [Close],
      Max([High]) [High], 
      Min([Low]) [Low], 
      Avg(Volume) Volume
  FROM SampleData
  GROUP BY DATEADD(Minute, -1* DATEPART(Minute, Time) %5, Time)

sql fiddle

【讨论】:

    【解决方案3】:

    问题的要点是将日期时间值四舍五入到 5 分钟边界(假设数据类型为 datetime)可以使用 DATEADD(MINUTE, DATEDIFF(MINUTE, 0, time) / 5 * 5, 0) 完成。休息是基本的分组/窗口功能:

    WITH cte AS (
      SELECT clamped_time
           , [Open]
           , [Close]
           , [High]
           , [Low]
           , [Volume]
           , rn1 = ROW_NUMBER() OVER (PARTITION BY clamped_time ORDER BY [Time])
           , rn2 = ROW_NUMBER() OVER (PARTITION BY clamped_time ORDER BY [Time] DESC)
      FROM t
      CROSS APPLY (
          SELECT DATEADD(MINUTE, DATEDIFF(MINUTE, 0, time) / 5 * 5, 0)
      ) AS x(clamped_time)
    )
    SELECT clamped_time
         , MIN(CASE WHEN rn1 = 1 THEN [Open] END) AS [Open]
         , MIN(CASE WHEN rn2 = 1 THEN [Close] END) AS [Close]
         , MAX([High]) AS [High]
         , MIN([Low]) AS [Low]
         , AVG([Volume])
    FROM cte
    GROUP BY clamped_time
    

    Demo on db<>fiddle

    【讨论】:

    • 非常感谢!它是如此之快!我从 2015 年开始就开始工作了,但是对于一个安全 sqlfingers.com/2015/06/…,它需要 3 秒跨越几千行 - 您的解决方案在 2 秒内跨越 500k 行(或每个安全 100 毫秒,这就是它的运行方式)生成等效值。谢谢!
    • OOC 您编辑它以包含用于计算打开/关闭的 rn1/rn2 的原因是什么?这样我才能理解。我尝试通过使用此语句而不是 CASE 语句来进行快捷方式:LAST_VALUE([Close]) OVER (PARTITION BY clamped_time ORDER BY t.PeriodOpen DESC) 但我假设您有理由不这样做?
    • @user12319070 使用 row_number 要简单得多。对于LAST_VALUE,您需要一个ROWS BETWEEN 子句,这是我不熟悉的。但是,如果您制定了更好的计划,欢迎您使用它。重新速度:如果您想要更快,我建议创建一个持久的计算列来存储 5 分钟边界并在 (x_time, time) include (open, close, ...) 上创建索引。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-17
    • 1970-01-01
    • 2022-12-17
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    • 2020-04-25
    相关资源
    最近更新 更多