Déclaration SQL Select pour le calcul d'une exécution colonne moyenne
-
06-09-2019 - |
Question
Je suis en train d'avoir une colonne moyenne en cours d'exécution dans l'instruction SELECT basée sur une colonne des n lignes précédentes dans la même instruction SELECT. La moyenne j'ai besoin est basé sur les n lignes précédentes dans le resultset.
Laissez-moi vous expliquer
Id Number Average
1 1 NULL
2 3 NULL
3 2 NULL
4 4 2 <----- Average of (1, 3, 2),Numbers from previous 3 rows
5 6 3 <----- Average of (3, 2, 4),Numbers from previous 3 rows
. . .
. . .
Les 3 premières lignes de la colonne moyenne sont nuls, car il n'y a pas de lignes précédentes. La ligne 4 dans la colonne moyenne représente la moyenne de la colonne de numéro à partir des 3 dernières lignes.
J'ai besoin d'aide à essayer de construire une instruction SQL Select qui va le faire.
La solution
Cela devrait le faire:
--Test Data
CREATE TABLE RowsToAverage
(
ID int NOT NULL,
Number int NOT NULL
)
INSERT RowsToAverage(ID, Number)
SELECT 1, 1
UNION ALL
SELECT 2, 3
UNION ALL
SELECT 3, 2
UNION ALL
SELECT 4, 4
UNION ALL
SELECT 5, 6
UNION ALL
SELECT 6, 8
UNION ALL
SELECT 7, 10
--The query
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM RowsToAverage rta
)
SELECT nr.ID, nr.Number,
CASE
WHEN nr.RowNumber <=3 THEN NULL
ELSE ( SELECT avg(Number)
FROM NumberedRows
WHERE RowNumber < nr.RowNumber
AND RowNumber >= nr.RowNumber - 3
)
END AS MovingAverage
FROM NumberedRows nr
Autres conseils
Si l'on suppose que la colonne Id est séquentielle, voici une requête simplifiée pour une table nommée « MyTable »:
SELECT
b.Id,
b.Number,
(
SELECT
AVG(a.Number)
FROM
MyTable a
WHERE
a.id >= (b.Id - 3)
AND a.id < b.Id
AND b.Id > 3
) as Average
FROM
MyTable b;
Un auto simple, joindre semble faire beaucoup mieux qu'un sous-requête de référencement de ligne
Générer 10k lignes de données de test:
drop table test10k
create table test10k (Id int, Number int, constraint test10k_cpk primary key clustered (id))
;WITH digits AS (
SELECT 0 as Number
UNION SELECT 1
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
UNION SELECT 5
UNION SELECT 6
UNION SELECT 7
UNION SELECT 8
UNION SELECT 9
)
,numbers as (
SELECT
(thousands.Number * 1000)
+ (hundreds.Number * 100)
+ (tens.Number * 10)
+ ones.Number AS Number
FROM digits AS ones
CROSS JOIN digits AS tens
CROSS JOIN digits AS hundreds
CROSS JOIN digits AS thousands
)
insert test10k (Id, Number)
select Number, Number
from numbers
Je tirerais le cas particulier des 3 premières lignes de la requête principale, vous pouvez UNION tous ceux en arrière si vous le voulez vraiment dans l'ensemble de la ligne. Auto de jointure:
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM test10k rta
)
SELECT nr.ID, nr.Number,
avg(trailing.Number) as MovingAverage
FROM NumberedRows nr
join NumberedRows as trailing on trailing.RowNumber between nr.RowNumber-3 and nr.RowNumber-1
where nr.Number > 3
group by nr.id, nr.Number
Sur ma machine, cela prend environ 10 secondes, l'approche sous-requête Aaron Alton démontré prend environ 45 secondes (après avoir changé pour refléter ma table de source de test):
;WITH NumberedRows
AS
(
SELECT rta.*, row_number() OVER (ORDER BY rta.ID ASC) AS RowNumber
FROM test10k rta
)
SELECT nr.ID, nr.Number,
CASE
WHEN nr.RowNumber <=3 THEN NULL
ELSE ( SELECT avg(Number)
FROM NumberedRows
WHERE RowNumber < nr.RowNumber
AND RowNumber >= nr.RowNumber - 3
)
END AS MovingAverage
FROM NumberedRows nr
Si vous faites SET STATISTIQUES SUR LE PROFIL, vous pouvez voir l'auto se joindre a 10k sur la bobine exécute de table. La sous-requête a 10k exécute sur le filtre, des agrégats et d'autres étapes.
Edit: Je Maladroit qu'il devrait atteindre en moyenne les trois précédents records ...
Pour une moyenne de fonctionnement général, je pense que quelque chose comme ça fonctionnerait:
SELECT
id, number,
SUM(number) OVER (ORDER BY ID) /
ROW_NUMBER() OVER (ORDER BY ID) AS [RunningAverage]
FROM myTable
ORDER BY ID
Si vous voulez que ce soit vraiment performant, et arn't peur de creuser dans une zone rarement utilisée de SQL Server, vous devez envisager d'écrire une fonction personnalisée globale. SQL Server 2005 et 2008 a l'intégration du CLR à la table, y compris la possibilité d'écrire des fonctions d'agrégation de l'utilisateur. Un agrégat total serait le moyen le plus efficace en cours d'exécution sur mesure pour calculer une moyenne en cours d'exécution comme celui-ci, de loin.
Sinon, vous pouvez dénormaliser et stocker les valeurs précalculées en cours d'exécution. Décrite ici:
Performance de selects est aussi rapide que ça se passe. Bien sûr, les modifications sont plus lentes.