ET les données (j’ai omis l’ID parce qu’on s’en fiche complètement ici):
source, target, date
A, B, 2019-08-2010:00:00
A, B, 2019-08-2009:00:00
A, B, 2019-08-2008:00:00
B, C, 2019-08-2007:45:00
A, C, 2019-08-2007:30:00
A, B, 2019-08-2007:00:00
A, B, 2019-08-2006:00:00
J’aimerais obtenir un résultat dans ce genre:
source, target, date, count
A, B, 2019-08-2010:00:00, 3
B, C, 2019-08-2007:45:00, 1
A, C, 2019-08-2007:30:00, 1
A, B, 2019-08-2007:00:00, 2
Autrement dit grouper les lignes avec source et target identiques, et idéalement les compter, mais uniquement si elles sont chronologiquement consécutives.
Le tout bien évidemment globalement classé par ordre anti-chronologique.
Salut, ça faisait longtemps que j’avais pas fait de SQL.
Je suis parti de l’hypothèse que 2 lignes n’ont pas la même date.
Des lignes consécutives sont des lignes avec les mêmes données hors date et id pour lesquelles il n’existe pas de ligne avec une date entre les deux et une source ou une target différentes.
J’en viens à compter le nombre de lignes correspondant à ces critères et dont la date est inférieure pour chaque ligne de la table de façon à avoir le count le plus haut sur la ligne la plus récente de chaque groupe…
Puis j’exclus toutes les lignes qui ne sont pas une borne haute en sachant que les bornes hautes sont les lignes pour lesquelles il n’y a pas de date plus haute dans le même groupe :
SELECTsource,
target,
date,
(SELECTCOUNT(*)
FROMTest t_inf
WHERE t_inf.source = t.source
AND t_inf.target = t.target
AND t_inf.date <= t.date
ANDNOTEXISTS(SELECT1FROMTest t_between
WHERE (t_between.source <> t.source
OR t_between.target <> t.target)
AND t_inf.date < t_between.date
AND t_between.date <= t.date)) c
FROMTest t
WHERENOTEXISTS(SELECT1FROMTest t_max
WHERE t_max.source = t.source
AND t_max.target = t.target
AND t_max.date > t.date
ANDNOTEXISTS(SELECT1FROMTest t_between2
WHERE (t_between2.source <> t.source
OR t_between2.target <> t.target)
AND t.date < t_between2.date
AND t_between2.date <= t_max.date))
ORDERBYdateDESC;
Note : J’ai fait ça parce que c’est marrant mais c’est sûrement plus approprié d’utiliser une proc avec un curseur pour des lignes consécutives
Si par hasard ChinoisFurax s’est planté, voici une autre piste (pas finalisée), avec les mots clés LAG ou LEAD :
selectsource , target, date , lag(source,1,0) over (orderbydateasc) as source1,
lag(target,1,0) over (orderbydate) as target1
fromtest
A vérifier si il faut utiliser LAG ou LEAD, et order by …ASC ou DESC.
Puis :
selectsource, target, datefrom
(
selectsource , target, date , lag(source,1,0) over (orderbydateasc) as source1,
lag(target,1,0) over (orderbydate) as target1
fromtest
)
where source1 <> sourceor target1 <> target
Ca a l’air de fonctionner, mais à condition qu’il n’y ait pas plusieurs fois exactement la même date.
Sinon je me retrouve avec autant de lignes en double qu’il n’y a de dates identiques.
Problème, ce n’est pas totalement improbable que plusieurs enregistrements se fassent à la même seconde… c’est même assez vraisemblable.
Par contre c’est super lent comme requête… au moins 5 fois plus lent que le group by simple, et je n’ai pas testé avec beaucoup d’enregistrements (à peine 50).
Du coup je me demande si je ne vais pas abandonner l’idée et faire les regroupements manuellement dans le langage client.
Je les connaissais sous Oracle, j’ai vérifié que ce n’était pas spécifique à Oracle … mais je ne suis pas allé plus loin.
Si tu peux avoir des doublons sur la colonne date, tu vas avoir des grosses difficultés.
Avec ces données :
source, Target, Date
A, B, 00000
A, B, 00001
C, D, 00001
A, B, 00002
Tu peux obtenir indifféremment ce résultat :
source, Target, Date
A, B, 00000 , 1
C, D, 00001 , 1
A, B, 00002 , 2
Ou celui-ci :
source, Target, Date
A, B, 00001 , 2
C, D, 00001 , 1
A, B, 00002 , 1
Et tu n’as pas de raison valable de préférer l’un plutôt que l’autre … Sauf à dire : Si 2 enregistrements on la même date/seconde, on les ordonne en se basant sur l’ordre alphabétique sur les colonnes Source/Target.
Du code côté client, c’est à éviter au maximum.
Si tu as la possibilité de modifier le modèle de données (=Ajouter une colonne subtile dans cette table), c’est la meilleure piste.
Sinon, il faut trouver la requête magique, et c’est trouvable.
Yes, tu peux utiliser les IDs à la place des dates mais la condition est que les dates et les IDs soient strictement dans le même ordre au sens prêt, mais fonctionnellement @elegance a complètement raison, en ordonnant ensuite par source et target tu vas favoriser les regroupements pour ton dédoublonnage, je vais donc partir du principe que c’est ce qu’on veut faire pour les solutions plus bas…
Sinon oui, c’était juste parce que c’est drôle, je me doute bien que sur un bon jeu de données c’est pourri x)
J’ai pas l’habitude d’écrire des procs et encore moins pour MySQL, d’ailleurs c’est peut-être ma première pour MySQL, mais niveau perf ça devrait être très efficace :
CREATEORREPLACEPROCEDURE delete_duplicated_links()
BEGINDECLARE v_id VARCHAR(255);
DECLARE v_source VARCHAR(255);
DECLARE v_target VARCHAR(255);
DECLARE v_previous_source VARCHAR(255) DEFAULTNULL;
DECLARE v_previous_target VARCHAR(255) DEFAULTNULL;
DECLARE v_done INTDEFAULT0;
DEClARE links_cursor CURSORFORSELECT t.id, t.source, t.target
FROMTest t
ORDERBY t.date DESC, t.source DESC, t.target DESC, t.id DESC;
DECLARE CONTINUE HANDLERFORNOTFOUNDSET v_done = 1;
OPEN links_cursor;
links_loop: LOOP
FETCH links_cursor INTO v_id, v_source, v_target;
IF v_done THEN
LEAVE links_loop;
ENDIF;
IF v_source = v_previous_source AND v_target = v_previous_target THEN
DELETEFROMTestWHEREid = v_id;
ENDIF;
SET v_previous_source = v_source, v_previous_target = v_target;
ENDLOOP;
CLOSE links_cursor;
END$$
ou pour ton select :
CREATEORREPLACEPROCEDURE list_duplicated_links()
BEGINDECLARE v_id VARCHAR(255);
DECLARE v_source VARCHAR(255);
DECLARE v_target VARCHAR(255);
DECLARE v_date DATETIME;
DECLARE v_previous_source VARCHAR(255) DEFAULTNULL;
DECLARE v_previous_target VARCHAR(255) DEFAULTNULL;
DECLARE v_previous_date DATETIME;
DECLARE v_count INTDEFAULT0;
DECLARE v_done INTDEFAULT0;
DEClARE links_cursor CURSORFORSELECT t.id, t.source, t.target, t.date
FROMTest t
ORDERBY t.date, t.source, t.target, t.id;
DECLARE CONTINUE HANDLERFORNOTFOUNDSET v_done = 1;
DROPTEMPORARYTABLEIFEXISTS t_duplicated_links;
CREATETEMPORARYTABLEIFNOTEXISTS t_duplicated_links
(
sourceVARCHAR(255),
target VARCHAR(255),
date DATETIME,
countINT
);
OPEN links_cursor;
links_loop:
REPEAT
FETCH links_cursor INTO v_id, v_source, v_target, v_date;
IF v_done OR v_source <> v_previous_source OR v_target <> v_previous_target THEN
INSERTINTO t_duplicated_links (source, target, date, count)
VALUES (v_previous_source, v_previous_target, v_previous_date, v_count);
SET v_count = 1;
ELSE
SET v_count = v_count + 1;
ENDIF;
SET v_previous_source = v_source, v_previous_target = v_target, v_previous_date = v_date;
UNTIL v_done ENDREPEAT;
CLOSE links_cursor;
SELECT * FROM t_duplicated_links;
DROPTABLE t_duplicated_links;
END;
CALL list_duplicated_links;
Attention dans les deux je ne trie pas dans le même ordre car les lignes que je garde dans le select ne sont pas celles que je veux supprimer dans la première proc.
Je ne connaissais pas LAG et LEAD, ça fait un truc assez pratique comme base pour un DELETE :
WITH t_nexts AS (
SELECT t.id,
t.source,
t.target,
t.date,
LEAD(t.source, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) next_source,
LEAD(t.target, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) next_target
FROMTest t)
SELECT tp.id, tp.source, tp.target, tp.date
FROM t_nexts tp
WHERE tp.source <> tp.next_source
OR tp.target <> tp.next_target
OR tp.next_source ISNULLAND tp.next_target ISNULL;
Pour le COUNT, c’est plus compliqué mais pas impossible, j’ai une solution, sûrement pas optimale, qui consiste à créer des groupes identifiés uniquement à partir des bornes inf et sup puis de compter le nombre de lignes dans chaque groupe :
WITH t_followings AS (
SELECT t.id,
t.source,
t.target,
t.date,
LAG(t.source, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) previous_source,
LAG(t.target, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) previous_target,
LEAD(t.source, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) next_source,
LEAD(t.target, 1) OVER (ORDERBY t.date, t.source, t.target, t.id) next_target
FROMTest t),
t_lasts AS (
SELECT tp.id,
tp.source,
tp.target,
tp.date,
ROW_NUMBER() OVER (ORDERBY tp.source, tp.target, tp.date, tp.id) group_nb
FROM t_followings tp
WHERE tp.source <> tp.next_source
OR tp.target <> tp.next_target
OR tp.next_source ISNULLAND tp.next_target ISNULL
),
t_firsts AS (
SELECT tp.id,
tp.source,
tp.target,
tp.date,
ROW_NUMBER() OVER (ORDERBY tp.source, tp.target, tp.date, tp.id) group_nb
FROM t_followings tp
WHERE tp.source <> tp.previous_source
OR tp.target <> tp.previous_target
OR tp.previous_source ISNULLAND tp.previous_target ISNULL
),
t_groups AS (
SELECT tf.source, tf.target, tf.date first_date, tl.date last_date
FROM t_firsts tf
INNERJOIN t_lasts tl ON tl.group_nb = tf.group_nb
)
SELECT tg.source, tg.target, tg.last_date, COUNT(*)
FROMTest t
INNERJOIN t_groups tg
ON t.date BETWEEN tg.first_date AND tg.last_date AND t.source = tg.source AND t.target = tg.target
GROUPBY tg.source, tg.target, tg.last_date
ORDERBY tg.last_date;
Sans faire de benchmark, je pense que la meilleure solution est celle de la proc, qui devrait être sauf erreur de ma part la plus performante, surtout qu’elle n’a qu’à lire une fois le contenu d’un simple select.
Voici une requête qui devrait également faire le job.
Je l’ai tapée dans notepad, sans le moindre test, donc il peut y avoir des ratés.
withdataas
(
selectsource , target, datefromtestorderbydate , source, target
),
data_number as
(
selectsource, target, date, rownumasnumfromdata
),
last_per_block as
(
select a.source, a.target , a.date , a.num
from data_number a, data_number b
where b.num(+) = a.num+1and ( nvl(b.source, 'XXX' ) <> a.source or nvl(b.target, 'XXX') <> a.target )
orderby a.num
) ,
blocks as (
select c.source , c.target, c.date, c.num, rownumas num2
from last_per_block c
)
select c.source, c.target, c.date, c.num - nvl(d.num,0) as nbr
from blocks c, blocks d
where d.num2 (+)= c.num2 -1
En terme de performance, je serais très curieux de savoir si cette requête est plus ou moins performante par rapport à la procédure proposée par chinoisfurax…. pour une table test qui aurait 1 Million de lignes par exemple.
Si la solution de chinoisfurax est plus performante, ça m’ouvre des horizons nouveaux
PS : j’ai finalement testé cette requête sur une table de 3 Millions de lignes, sans index particulier, sur un serveur puissant, et la réponse est arrivée sous 20 secondes
PS2 : J’ai aussi testé la procédure de chinoisfurax. Performances comparables
Sauf que la procédure zappe la dernière ligne. Il manque un insert après la ligne 47.
Pourtant dans mon test elle la prend bien en compte Tu es sûr ? Je l’ai testée sur MariaDB au départ et MySQL ensuite (d’ailleurs il faut enlever les OR REPLACE sur MySQL en fait), d’ailleurs ça renvoie la même chose que ta requête réécrite en-dessous.
Ton code n’était pas du code MySQL mais du Oracle.
J’ai dû le traduire pour le faire fonctionner, j’en ai profité pour donner des alias un peu plus explicites. C’est bien vu de se baser sur la numérotation d’une seule ligne par groupe pour éviter de compter les lignes !
WITH test_numbered AS (
SELECTsource, target, date, ROW_NUMBER() OVER (ORDERBYdate, source, target) numFROMTest
),
last_per_block AS (
SELECT n.source, n.target, n.date, n.num
FROM test_numbered n
LEFTJOIN test_numbered n_next
ON n_next.num = n.num + 1WHERE (IFNULL(n_next.source, 'XXX') <> n.source ORIFNULL(n_next.target, 'XXX') <> n.target)
),
blocks AS (
SELECT l.source, l.target, l.date, l.num, ROW_NUMBER() OVER (ORDERBY l.num) AS num_block
FROM last_per_block l
)
SELECT b.source, b.target, b.date, b.num - IFNULL(b_prev.num, 0) AS nbr
FROM blocks b
LEFTJOIN blocks b_prev
ON b_prev.num_block = b.num_block - 1
Ta requête est meilleure que la proc sur la dernière version de MySQL, le problème de la proc est probablement sa manipulation de la table tempo comparativement. À noter que le IFNULL(x, 'XXX') / NVL est indispensable pour avoir de bonnes perfs mais il laisse toujours passer un cas foireux si on veut vraiment être carré, ça donne toujours l’impression de bidouiller
Merci pour vos dernières réponses, ça fonctionne bien avec la procédure stockée ou la vue. Je passe donc le sujet en résolu.
Entre les deux les performances sont comparables (les différences ne me paraissent pas significatives).
Ca ne me surprend pas, dans le sens où ni l’un ni l’autre ne font l’objet d’optimisations spécifiques dans MySQL/MariaDB.
Je suppose que la vue doit être un peu plus rapide que la procédure avec Oracle ?
Yes, tu peux utiliser les IDs à la place des dates mais la condition est que les dates et les IDs soient strictement dans le même ordre au sens prêt, mais fonctionnellement
C’est le cas, les ID sont chronologiquement strictement croissants.
Si tu peux avoir des doublons sur la colonne date, tu vas avoir des grosses difficultés. […] Et tu n’as pas de raison valable de préférer l’un plutôt que l’autre … Sauf à dire : Si 2 enregistrements on la même date/seconde, on les ordonne en se
basant sur l’ordre alphabétique sur les colonnes Source/Target.
Tu as raison, mais en pratique ça n’a pas beaucoup d’importance si j’obtiens un résultat plutôt que l’autre, les deux conviennent très bien.
Ces résultats ne sont pas remachinés par la suite, ils sont seulement affichés comme une sorte de log.
Mais bon, en fonctionnant avec les ID on élimine ce problème de toute façon; et sans effet secondaire.
Connectez-vous pour pouvoir poster un message.
Connexion
Pas encore membre ?
Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte