web-dev-qa-db-fra.com

Utilisation de GROUP BY avec FIRST_VALUE et LAST_VALUE

Je travaille avec des données actuellement stockées à des intervalles d'une minute qui ressemblent à ceci:

CREATE TABLE #MinuteData
    (
      [Id] INT ,
      [MinuteBar] DATETIME ,
      [Open] NUMERIC(12, 6) ,
      [High] NUMERIC(12, 6) ,
      [Low] NUMERIC(12, 6) ,
      [Close] NUMERIC(12, 6)
    );

INSERT  INTO #MinuteData
        ( [Id], [MinuteBar], [Open], [High], [Low], [Close] )
VALUES  ( 1, '2015-01-01 17:00:00', 1.557870, 1.557880, 1.557870, 1.557880 ),
        ( 2, '2015-01-01 17:01:00', 1.557900, 1.557900, 1.557880, 1.557880 ),
        ( 3, '2015-01-01 17:02:00', 1.557960, 1.558070, 1.557960, 1.558040 ),
        ( 4, '2015-01-01 17:03:00', 1.558080, 1.558100, 1.558040, 1.558050 ),
        ( 5, '2015-01-01 17:04:00', 1.558050, 1.558100, 1.558020, 1.558030 ),
        ( 6, '2015-01-01 17:05:00', 1.558580, 1.558710, 1.557870, 1.557950 ),
        ( 7, '2015-01-01 17:06:00', 1.557910, 1.558120, 1.557910, 1.557990 ),
        ( 8, '2015-01-01 17:07:00', 1.557940, 1.558250, 1.557940, 1.558170 ),
        ( 9, '2015-01-01 17:08:00', 1.558140, 1.558200, 1.558080, 1.558120 ),
        ( 10, '2015-01-01 17:09:00', 1.558110, 1.558140, 1.557970, 1.557970 );

SELECT  *
FROM    #MinuteData;

DROP TABLE #MinuteData;

Les valeurs suivent les taux de change, donc pour chaque intervalle de minute (barre), il y a le prix Open au début de la minute et un prix Close pour la fin de la minute. Les valeurs High et Low représentent le taux le plus élevé et le plus bas au cours de chaque minute individuelle.

Sortie souhaitée

Je cherche à reformater ces données en intervalles de 5 minutes pour produire la sortie suivante:

MinuteBar                Open       Close       Low         High
2015-01-01 17:00:00.000  1.557870   1.558030    1.557870    1.558100
2015-01-01 17:05:00.000  1.558580   1.557970    1.557870    1.558710

Cela prend la valeur Open de la première minute du 5, la valeur Close de la dernière minute du 5. Les valeurs High et Low représentent les taux high et low les plus élevés sur la période de 5 minutes.

Solution actuelle

J'ai une solution qui fait cela (ci-dessous), mais elle semble inélégante car elle repose sur les valeurs id et les auto-jointures. En outre, j'ai l'intention de l'exécuter sur des ensembles de données beaucoup plus importants, donc je cherchais à le faire de manière plus efficace si possible:

-- Create a column to allow grouping in 5 minute Intervals
SELECT  Id, MinuteBar, [Open], High, Low, [Close], 
DATEDIFF(MINUTE, '2015-01-01T00:00:00', MinuteBar)/5 AS Interval
INTO    #5MinuteData
FROM    #MinuteData
ORDER BY minutebar

-- Group by inteval and aggregate prior to self join
SELECT  Interval ,
        MIN(MinuteBar) AS MinuteBar ,
        MIN(Id) AS OpenId ,
        MAX(Id) AS CloseId ,
        MIN(Low) AS Low ,
        MAX(High) AS High
INTO    #DataMinMax
FROM    #5MinuteData
GROUP BY Interval;

-- Self join to get the Open and Close values
SELECT  t1.Interval ,
        t1.MinuteBar ,
        tOpen.[Open] ,
        tClose.[Close] ,
        t1.Low ,
        t1.High
FROM    #DataMinMax t1
        INNER JOIN #5MinuteData tOpen ON tOpen.Id = OpenId
        INNER JOIN #5MinuteData tClose ON tClose.Id = CloseId;

DROP TABLE #DataMinMax
DROP TABLE #5MinuteData

Tentative de reprise

Au lieu des requêtes ci-dessus, j'ai cherché à utiliser FIRST_VALUE et LAST_VALUE , car cela semble être ce que je recherche, mais je ne peux pas vraiment l'obtenir travailler avec le groupement que je fais. Il pourrait y avoir une meilleure solution que ce que j'essaie de faire, donc je suis ouvert aux suggestions. Actuellement j'essaye de faire ceci:

SELECT  MIN(MinuteBar) MinuteBar5 ,
        FIRST_VALUE([Open]) OVER (ORDER BY MinuteBar) AS Opening,
        MAX(High) AS High ,
        MIN(Low) AS Low ,
        LAST_VALUE([Close]) OVER (ORDER BY MinuteBar) AS Closing ,
        DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5 AS Interval
FROM    #MinuteData
GROUP BY DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5

Cela me donne l'erreur ci-dessous, qui est liée au FIRST_VALUE et LAST_VALUE lorsque la requête s'exécute si je supprime ces lignes:

La colonne '# MinuteData.MinuteBar' n'est pas valide dans la liste de sélection car elle n'est contenue ni dans une fonction d'agrégation ni dans la clause GROUP BY.

13
Tanner
SELECT 
    MIN(MinuteBar) AS MinuteBar5,
    Opening,
    MAX(High) AS High,
    MIN(Low) AS Low,
    Closing,
    Interval
FROM 
(
    SELECT FIRST_VALUE([Open]) OVER (PARTITION BY DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5 ORDER BY MinuteBar) AS Opening,
           FIRST_VALUE([Close]) OVER (PARTITION BY DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5 ORDER BY MinuteBar DESC) AS Closing,
           DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5 AS Interval,
           *
    FROM #MinuteData
) AS T
GROUP BY Interval, Opening, Closing

Une solution proche de la vôtre. Il y a deux endroits où vous vous êtes trompé.

  1. FIRST_VALUE ET LAST_VALUE sont Fonctions analytiques , qui fonctionnent sur une fenêtre ou une partition, au lieu d'un groupe. Vous pouvez exécuter la requête imbriquée seule et voir son résultat.
  2. LAST_VALUE est la dernière valeur de la fenêtre actuelle, qui n'est pas spécifiée dans votre requête, et une fenêtre par défaut est des lignes de la première ligne de la partition actuelle à ligne actuelle. Vous pouvez utiliser FIRST_VALUE avec l'ordre de désensemencement ou spécifier une fenêtre

    LAST_VALUE([Close]) OVER (PARTITION BY DATEDIFF(MINUTE, '2015-01-01 00:00:00', MinuteBar) / 5 
                ORDER BY MinuteBar 
                ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS Closing,
    
15
qxg

Voici une façon de le faire sans tables temporaires:

;WITH CTEInterval AS 
(  -- This replaces your first temporary table (#5MinuteData)
    SELECT  [Id], 
            [MinuteBar], 
            [Open], 
            [High], 
            [Low], 
            [Close],
            DATEPART(MINUTE, MinuteBar)/5 AS Interval
    FROM #MinuteData
), CTEOpenClose as 
( -- this is instead of your second temporary table (#DataMinMax)
    SELECT  [Id], 
            [MinuteBar], 
            FIRST_VALUE([Open]) OVER (PARTITION BY Interval ORDER BY MinuteBar) As [Open],
            [High],
            [Low], 
            FIRST_VALUE([Close]) OVER (PARTITION BY Interval ORDER BY MinuteBar DESC) As [Close],
            Interval
    FROM CTEInterval
)

-- This is the final select
SELECT  MIN([MinuteBar]) as [MinuteBar], 
        AVG([Open]) as [Open], -- All values of [Open] in the same interval are the same...
        AVG([Close]) as [Close],  -- All values of [Close] in the same interval are the same...
        MIN([Low]) as [Low], 
        MAX([High]) as [High]
FROM CTEOpenClose
GROUP BY Interval

Résultats:

MinuteBar                Open       Close       Low         High
2015-01-01 17:00:00.000  1.557870   1.558030    1.557870    1.558100
2015-01-01 17:05:00.000  1.558580   1.557970    1.557870    1.558710
3
Zohar Peled

Demo here

;with cte
as
(--this can be your permanent table with intervals ,rather than generating on fly
select cast('2015-01-01 17:00:00.000' as datetime) as interval,dateadd(mi,5,'2015-01-01 17:00:00.000') as nxtinterval
union all
select dateadd(mi,5,interval),dateadd(mi,5,nxtinterval) from cte
where interval<='2015-01-01 17:45:00.000'

)
,finalcte
as
(select minutebar,
low,high,
dense_rank() over (order by  interval,nxtinterval) as grpd,
last_value([close]) over ( partition by interval,nxtinterval order by interval,nxtinterval) as [close],
first_value([open]) over (partition by interval,nxtinterval order by interval,nxtinterval) as [open]
 from cte c
join
#minutedata m
on m.minutebar between interval and nxtinterval
)
select 
min(minutebar) as minutebar,
min(low) as 'low',
max(high) as 'High',
max([open]) as 'open',
max([close]) as 'close'
 from finalcte
 group by grpd
2
TheGameiswar