J'ai un tableau qui comprend une colonne de valeurs décimales, comme celle-ci:
id value size
-- ----- ----
1 100 .02
2 99 .38
3 98 .13
4 97 .35
5 96 .15
6 95 .57
7 94 .25
8 93 .15
Ce que je dois accomplir est un peu difficile à décrire, alors soyez indulgent avec moi. Ce que j'essaie de faire est de créer une valeur agrégée de la size
colonne qui augmente de 1 à chaque fois que les lignes précédentes totalisent 1, en ordre décroissant selon value
. Le résultat ressemblerait à ceci:
id value size bucket
-- ----- ---- ------
1 100 .02 1
2 99 .38 1
3 98 .13 1
4 97 .35 1
5 96 .15 2
6 95 .57 2
7 94 .25 2
8 93 .15 3
Ma première tentative naïve a été de conserver une valeur en cours SUM
, puis CEILING
cette valeur, mais elle ne gère pas le cas où certains enregistrements size
finissent par contribuer au total de deux compartiments distincts. L'exemple ci-dessous pourrait clarifier ceci:
id value size crude_sum crude_bucket distinct_sum bucket
-- ----- ---- --------- ------------ ------------ ------
1 100 .02 .02 1 .02 1
2 99 .38 .40 1 .40 1
3 98 .13 .53 1 .53 1
4 97 .35 .88 1 .88 1
5 96 .15 1.03 2 .15 2
6 95 .57 1.60 2 .72 2
7 94 .25 1.85 2 .97 2
8 93 .15 2.00 2 .15 3
Comme vous pouvez le voir, si je devais simplement utiliser CEILING
sur l' crude_sum
enregistrement # 8 serait affecté au compartiment 2. Cela est dû au fait que les size
enregistrements # 5 et # 8 sont répartis sur deux compartiments. Au lieu de cela, la solution idéale consiste à réinitialiser la somme chaque fois qu'elle atteint 1, ce qui incrémente ensuite la bucket
colonne et commence une nouvelle SUM
opération à partir de la size
valeur de l'enregistrement en cours. Étant donné que l'ordre des enregistrements est important pour cette opération, j'ai inclus la value
colonne, qui est destinée à être triée par ordre décroissant.
Mes premières tentatives ont consisté à effectuer plusieurs passages sur les données, une fois pour effectuer l' SUM
opération, une fois de plus pour CEILING
cela, etc. Voici un exemple de ce que j'ai fait pour créer la crude_sum
colonne:
SELECT
id,
value,
size,
(SELECT TOP 1 SUM(size) FROM table t2 WHERE t2.value<=t1.value) as crude_sum
FROM
table t1
Qui a été utilisé dans une UPDATE
opération pour insérer la valeur dans une table pour travailler plus tard.
Edit: Je voudrais essayer de nouveau d'expliquer cela, alors voici. Imaginez que chaque enregistrement soit un élément physique. Cet élément a une valeur qui lui est associée et une taille physique inférieure à un. J'ai une série de seaux d'une capacité volumique d'exactement 1, et je dois déterminer combien de ces seaux j'aurai besoin et dans quel seau chaque article va en fonction de la valeur de l'article, trié du plus élevé au plus bas.
Un élément physique ne peut pas exister à deux endroits à la fois, il doit donc être dans un seau ou dans l'autre. C'est pourquoi je ne peux pas faire une CEILING
solution total + en cours d'exécution , car cela permettrait aux enregistrements de contribuer leur taille à deux compartiments.
distinct_count
complique les choses. Aaron Bertrand a un excellent résumé de vos options sur SQL Server pour ce type de travail de fenêtrage. J'ai utilisé la méthode de "mise à jour décalée" pour calculerdistinct_sum
, que vous pouvez voir ici sur SQL Fiddle , mais ce n'est pas fiable.Réponses:
Je ne sais pas quel type de performance vous recherchez, mais si CLR ou une application externe n'est pas une option, un curseur est tout ce qui reste. Sur mon vieux portable, j'obtiens 1 000 000 de lignes en environ 100 secondes en utilisant la solution suivante. La bonne chose à ce sujet est qu'il évolue linéairement, donc je regarderais un peu environ 20 minutes pour parcourir toute la chose. Avec un serveur décent, vous serez plus rapide, mais pas d'un ordre de grandeur, il faudra donc plusieurs minutes pour terminer. S'il s'agit d'un processus ponctuel, vous pouvez probablement vous permettre la lenteur. Si vous devez l'exécuter régulièrement sous forme de rapport ou similaire, vous souhaiterez peut-être stocker les valeurs dans le même tableau et les mettre à jour à mesure que de nouvelles lignes seront ajoutées, par exemple dans un déclencheur.
Bref, voici le code:
Il supprime et recrée la table MyTable, la remplit avec 1000000 lignes, puis se met au travail.
Le curseur copie chaque ligne dans une table temporaire lors de l'exécution des calculs. À la fin, la sélection renvoie les résultats calculés. Vous pourriez être un peu plus rapide si vous ne copiez pas les données mais effectuez une mise à jour sur place à la place.
Si vous avez une option de mise à niveau vers SQL 2012, vous pouvez consulter les nouveaux agrégats de fenêtres mobiles pris en charge par la bobine de fenêtre, qui devraient vous donner de meilleures performances.
Sur une note latérale, si vous avez un assembly installé avec permission_set = safe, vous pouvez faire plus de mauvaises choses à un serveur avec T-SQL standard qu'avec l'assembly, donc je continuerais à travailler sur la suppression de cette barrière - Vous avez une bonne utilisation cas ici où CLR vous aiderait vraiment.
la source
En l'absence des nouvelles fonctions de fenêtrage dans SQL Server 2012, le fenêtrage complexe peut être accompli avec l'utilisation de CTE récursifs. Je me demande dans quelle mesure cela fonctionnera contre des millions de lignes.
La solution suivante couvre tous les cas que vous avez décrits. Vous pouvez le voir en action ici sur SQL Fiddle .
Maintenant, respirez profondément. Il y a ici deux CTE clés, chacun précédé d'un bref commentaire. Les autres ne sont que des CTE de «nettoyage», par exemple, pour tirer les bonnes lignes après les avoir classés.
Cette solution suppose qu'il
id
s'agit d'une séquence sans espace. Sinon, vous devrez générer votre propre séquence sans espace en ajoutant un CTE supplémentaire au début qui numérote les lignesROW_NUMBER()
selon l'ordre souhaité (par exempleROW_NUMBER() OVER (ORDER BY value DESC)
).Franchement, c'est assez verbeux.
la source
crude_sum
avecdistinct_sum
et leursbucket
colonnes associées pour voir ce que je veux dire.Cela semble être une solution stupide, et elle ne sera probablement pas bien adaptée, alors testez soigneusement si vous l'utilisez. Étant donné que le problème principal vient de «l'espace» laissé dans le compartiment, j'ai d'abord dû créer un enregistrement de remplissage à associer aux données.
http://sqlfiddle.com/#!3/72ad4/14/0
la source
Ce qui suit est une autre solution CTE récursive, bien que je dirais que c'est plus simple que la suggestion de @ Nick . Il est en fait plus proche du curseur de @ Sebastian , seulement j'ai utilisé des différences courantes au lieu de cumuler des totaux. (Au début, je pensais même que la réponse de @ Nick allait être dans le sens de ce que je suggère ici, et c'est après avoir appris qu'il s'agissait en fait d'une requête très différente que j'ai décidé de proposer à la mienne.)
Remarque: cette requête suppose que la
value
colonne se compose de valeurs uniques sans espaces. Si ce n'est pas le cas, vous devrez introduire une colonne de classement calculée basée sur l'ordre décroissant devalue
et l'utiliser dans le CTE récursif au lieu devalue
joindre la partie récursive à l'ancre.Une démo SQL Fiddle pour cette requête peut être trouvée ici .
la source
size
avecroom_left
) plutôt que de comparer une seule valeur avec une expression (1
avecrunning_size
+size
). Je n'ai pas utilisé deis_new_bucket
drapeau au début, mais plusieurs à laCASE WHEN t.size > r.room_left ...
place ("plusieurs" parce que je calculais également (et que je rendais) la taille totale, mais y ai ensuite pensé par souci de simplicité), donc j'ai pensé que ce serait plus élégant de cette façon.