SQL: solution de tĂąche de temps de travail

Bonjour, Radio SQL est à nouveau diffusée! Aujourd'hui, nous avons une solution au problÚme que nous avions transmis lors de notre précédente émission et que nous avions promis de distinguer la prochaine fois. Et cette prochaine fois est venue.


La tĂąche a suscitĂ© une rĂ©ponse vive parmi les humanoĂŻdes de la galaxie de la Voie lactĂ©e (et sans surprise, avec leur esclavage de travail, qu'ils respectent toujours au profit de la civilisation). Malheureusement, sur la troisiĂšme planĂšte, le lancement de l'observatoire spatial Spektr-RG a Ă©tĂ© reportĂ© fin juillet 2019, la RC (chronologie locale), Ă  ​​l'aide de laquelle il Ă©tait prĂ©vu de diffuser cette Ă©mission. J'ai dĂ» chercher des voies de transmission alternatives, ce qui a entraĂźnĂ© un lĂ©ger retard dans le signal. Mais tout va bien qui se termine bien.



Je dois dire tout de suite qu'il n'y aura pas de magie dans l'analyse de la tĂąche, il n'est pas nĂ©cessaire de chercher des rĂ©vĂ©lations ici ou d'attendre une mise en Ɠuvre particuliĂšrement efficace (ou surtout certaines dans un autre sens). Ce n'est qu'une tĂąche d'analyse. Ceux qui ne savent pas comment aborder la solution de ces problĂšmes pourront voir comment les rĂ©soudre. De plus, il n'y a rien de terrible ici.


Permettez-moi de vous rappeler la condition.

Il y a plusieurs intervalles de temps spécifiés par la date-heure de son début et de sa fin (un exemple dans la syntaxe PostgreSQL):


with periods(id, start_time, stop_time) as ( values(1, '2019-03-29 07:00:00'::timestamp, '2019-04-08 14:00:00'::timestamp), (2, '2019-04-10 07:00:00'::timestamp, '2019-04-10 20:00:00'::timestamp), (3, '2019-04-11 12:00:00'::timestamp, '2019-04-12 16:07:12'::timestamp), (4, '2018-12-28 12:00:00'::timestamp, '2019-01-16 16:00:00'::timestamp) ) 

Il est requis dans une requĂȘte SQL (c) pour calculer la durĂ©e de chaque intervalle en heures de travail. Nous pensons que nous travaillons en semaine du lundi au vendredi, les heures de travail sont toujours de 10h00 Ă  19h00. En outre, conformĂ©ment au calendrier de production de la FĂ©dĂ©ration de Russie, un certain nombre de jours fĂ©riĂ©s officiels ne sont pas des jours ouvrables, et certains jours de congĂ©, au contraire, sont des jours ouvrables en raison du report de ces mĂȘmes jours fĂ©riĂ©s. Le raccourcissement des jours de prĂ©-vacances n'est pas nĂ©cessaire, nous les considĂ©rons comme complets. Étant donnĂ© que les vacances varient d'une annĂ©e Ă  l'autre, c'est-Ă -dire qu'elles sont fixĂ©es par une liste explicite, nous nous limiterons aux dates uniquement de 2018 et 2019. Je suis sĂ»r que, si nĂ©cessaire, la solution peut ĂȘtre facilement complĂ©tĂ©e.


Il est nécessaire d'ajouter une colonne avec la durée en heures de travail aux périodes initiales des périodes . Voici le résultat:


  id | start_time | stop_time | work_hrs ----+---------------------+---------------------+---------- 1 | 2019-03-29 07:00:00 | 2019-04-08 14:00:00 | 58:00:00 2 | 2019-04-10 07:00:00 | 2019-04-10 20:00:00 | 09:00:00 3 | 2019-04-11 12:00:00 | 2019-04-12 16:07:12 | 13:07:12 4 | 2018-12-28 12:00:00 | 2019-01-16 16:00:00 | 67:00:00 

Nous ne vérifions pas l'exactitude des données initiales; nous considérons toujours start_time <= stop_time .


La fin de la condition, l'original est ici: https://habr.com/en/company/postgrespro/blog/448368/ .


La tĂąche donne un lĂ©ger piquant au fait que j'ai donnĂ© consciemment une bonne moitiĂ© de la condition sous une forme descriptive (comme c'est gĂ©nĂ©ralement le cas dans la vie rĂ©elle), laissant Ă  la discrĂ©tion de la mise en Ɠuvre technique la façon dont le calendrier de travail doit ĂȘtre fixĂ©. D'une part, cela nĂ©cessite certaines compĂ©tences de rĂ©flexion architecturale. Et d'autre part, le format prĂȘt Ă  l'emploi de ce calendrier aurait incitĂ© une certaine utilisation de modĂšle de celui-ci. Et si vous omettez, alors la pensĂ©e et la fantaisie fonctionneront plus pleinement. L'accueil a portĂ© ses fruits, ce qui m'a permis de trouver Ă©galement des approches intĂ©ressantes dans les solutions publiĂ©es.


Ainsi, pour rĂ©soudre le problĂšme d'origine de cette maniĂšre, deux sous-tĂąches devront ĂȘtre rĂ©solues:


  1. DĂ©terminez comment dĂ©finir le plus rapidement possible un horaire de travail, et mĂȘme de maniĂšre Ă  ce qu'il soit pratique Ă  utiliser pour une solution.
  2. Calculez réellement la durée de chaque période source en heures de travail en fonction de l'horaire de travail de la sous-tùche précédente.

Et il vaut mieux commencer par le second, afin de comprendre sous quelle forme il faut résoudre le premier. Résolvez ensuite le premier et revenez de nouveau au second afin d'obtenir le résultat final.
Nous collecterons le rĂ©sultat progressivement, en utilisant la syntaxe CTE, qui nous permet de mettre tous les Ă©chantillons de donnĂ©es nĂ©cessaires dans des sous-requĂȘtes nommĂ©es distinctes, puis de tout lier ensemble.


Eh bien, allons-y.


Calculez la durée en heures de travail


Pour calculer la durée de chacune des périodes en heures de travail sur le front, vous devez croiser la période initiale (couleur verte sur le diagramme) avec les intervalles qui décrivent le temps de travail (orange). Les intervalles d'heures de travail sont les lundis de 10h00 à 19h00, les mardis de 10h00 à 19h00 et ainsi de suite. Le résultat est affiché en bleu:


image


Soit dit en passant, afin de devenir moins confus, je continuerai à appeler les périodes initiales des périodes initiales, et j'appellerai des intervalles d'heures de travail.


La procĂ©dure doit ĂȘtre rĂ©pĂ©tĂ©e pour chaque pĂ©riode initiale. Les pĂ©riodes initiales pour nous sont dĂ©jĂ  dĂ©finies dans l' onglet pĂ©riodes (start_time, stop_time) , nous reprĂ©senterons les heures de travail sous la forme d'un tableau, disons, horaire (strat_time, stop_time) , oĂč chaque jour ouvrable est prĂ©sent. Le rĂ©sultat est un produit cartĂ©sien complet de toutes les pĂ©riodes et intervalles initiaux du temps de travail.


Les intersections peuvent ĂȘtre comptĂ©es de maniĂšre classique, aprĂšs avoir considĂ©rĂ© toutes les options possibles pour les intervalles d'intersection - nous intersectons le vert avec l'orange, le rĂ©sultat est le bleu:


image


et en prenant dans chaque cas la valeur souhaitée pour le début et la fin du résultat:


  select s.start_time, s.stop_time -- case #1 from periods p, schedule s where p.start_time <= s.start_time and p.stop_time > s.stop_time union all select p.start_time, s.stop_time -- case #2 from periods p, schedule s where p.start_time >= s.start_time and p.stop_time > s.stop_time and p.start_time < s.stop_time union all select s.start_time, p.stop_time -- case #3 from periods p, schedule s where p.start_time <= s.start_time and p.stop_time < s.stop_time and p.stop_time > s.start_time union all select p.start_time, p.stop_time -- case #4 from periods p, schedule s where p.start_time >= s.start_time and p.stop_time < s.stop_time 

Étant donnĂ© que pour chaque intersection, nous ne pouvons avoir qu'une seule des quatre options, toutes sont combinĂ©es en une seule demande en utilisant union all .


Vous pouvez faire autrement en utilisant le type de plage tsrange disponible dans PostgreSQL et l'opération d'intersection déjà disponible pour lui:


  select tsrange(s.start_time, s.stop_time) * tsrange(s.start_time, s.stop_time) from periods p, schedule s 

D'accord, alors - euh - un peu plus facile. En gĂ©nĂ©ral, il y a beaucoup de petites choses aussi pratiques dans PostgreSQL, donc Ă©crire des requĂȘtes dessus est trĂšs agrĂ©able.


Générer un calendrier


Revenons maintenant Ă  la sous-tĂąche avec le calendrier des heures de travail.


Nous devons obtenir l'horaire de travail sous forme d'intervalles de temps de travail de 10h00 Ă  19h00 pour chaque jour ouvrable, quelque chose comme l' horaire (start_time, stop_time) . Comme nous l'avons compris, il sera commode de rĂ©soudre notre problĂšme. Dans la vraie vie, un tel calendrier devrait ĂȘtre Ă©tabli, pendant deux ans, il ne s'agit que d'environ 500 enregistrements, pour des raisons pratiques, il sera nĂ©cessaire de dĂ©finir mĂȘme dix ans - c'est un couple et demi mille enregistrements, de vrais dĂ©chets pour les bases de donnĂ©es modernes. Mais nous avons un problĂšme qui sera rĂ©solu en une seule demande, et lister l'ensemble de ce tableau n'est pas trĂšs pratique. Essayons de l'implĂ©menter de maniĂšre plus compacte.


Dans tous les cas, nous avons besoin de jours fériés pour les supprimer de l'horaire de base, et ici seule la liste convient:


  dates_exclude(d) as ( values('2018-01-01'::date), -- 2018 ('2018-01-02'::date), ('2018-01-03'::date), ('2018-01-04'::date), ('2018-01-05'::date), ('2018-01-08'::date), ('2018-02-23'::date), ('2018-03-08'::date), ('2018-03-09'::date), ('2018-05-01'::date), ('2018-05-02'::date), ('2018-05-09'::date), ('2018-06-11'::date), ('2018-06-12'::date), ('2018-11-05'::date), ('2018-12-31'::date), ('2019-01-01'::date), -- 2019 ('2019-01-02'::date), ('2019-01-03'::date), ('2019-01-04'::date), ('2019-01-07'::date), ('2019-01-08'::date), ('2019-03-08'::date), ('2019-05-01'::date), ('2019-05-02'::date), ('2019-05-03'::date), ('2019-05-09'::date), ('2019-05-10'::date), ('2019-06-12'::date), ('2019-11-04'::date) ) 

et jours ouvrables supplémentaires à ajouter:


  dates_include(d) as ( values --  2018,  2019  ('2018-04-28'::date), ('2018-06-09'::date), ('2018-12-29'::date) ) 

La sĂ©quence de jours ouvrables pendant deux ans peut ĂȘtre gĂ©nĂ©rĂ©e par une fonction spĂ©ciale et trĂšs appropriĂ©e generate_series () , lançant immĂ©diatement les samedis et dimanches en cours de route:


  select d from generate_series( '2018-01-01'::timestamp , '2020-01-01'::timestamp , '1 day'::interval ) as d where extract(dow from d) not in (0,6) --     

Nous obtenons les jours ouvrables en connectant tout ensemble: nous générons une séquence de tous les jours ouvrables sur deux ans, ajoutons des jours ouvrables supplémentaires à dates_include et supprimons tous les jours supplémentaires à dates_exclude :


  schedule_base as ( select d from generate_series( '2018-01-01'::timestamp , '2020-01-01'::timestamp , '1 day'::interval ) as d where extract(dow from d) not in (0,6) --     union select d from dates_include --     except select d from dates_exclude --     ) 

Et maintenant, nous obtenons les intervalles de temps dont nous avons besoin:


  schedule(start_time, stop_time) as ( select d + '10:00:00'::time, d + '19:00:00'::time from schedule_base ) 

Donc, nous avons le calendrier.


Tout mettre ensemble


Nous allons maintenant obtenir les intersections:


  select p.* , tsrange(p.start_time, p.stop_time) * tsrange(s.start_time, s.stop_time) as wrkh from periods p join schedule s on tsrange(p.start_time, p.stop_time) && tsrange(s.start_time, s.stop_time) 

Faites attention Ă  la condition de jointure ON , elle ne correspond pas Ă  deux enregistrements correspondants des tables jointes, il n'y a pas une telle correspondance, mais une optimisation est introduite qui coupe les intervalles de temps de travail avec lesquels notre pĂ©riode initiale ne se coupe pas. Cela se fait Ă  l'aide de l'opĂ©rateur && , qui vĂ©rifie l'intersection des intervalles tsrange . Cela supprime beaucoup d'intersections vides afin de ne pas gĂȘner les yeux, mais, d'autre part, supprime les informations sur les pĂ©riodes initiales qui tombent entiĂšrement en dehors des heures. Nous admirons donc que notre approche fonctionne et réécrivons la demande comme suit:


  periods_wrk as ( select p.* , tsrange(p.start_time, p.stop_time) * tsrange(s.start_time, s.stop_time) as wrkh from periods p , schedule s ) select id, start_time, stop_time , sum(upper(wrkh)-lower(wrkh)) from periods_wrk group by id, start_time, stop_time 

Dans périodes_wrk, nous décomposons chaque période source en intervalles de travail, puis nous considérons leur durée totale. Le résultat a été un produit cartésien complet de toutes les périodes et intervalles, mais pas une seule période n'a été perdue.


Tout, le rĂ©sultat est reçu. Je n'aimais pas les valeurs NULL pour les intervalles vides, laissez la requĂȘte afficher un intervalle de longueur nulle mieux. Enveloppez le montant en fusion () :


 select id, start_time, stop_time , coalesce(sum(upper(wrkh)-lower(wrkh)), '0 sec'::interval) from periods_wrk group by id, start_time, stop_time 

Tous ensemble donne le résultat final:


 with periods(id, start_time, stop_time) as ( values(1, '2019-03-29 07:00:00'::timestamp, '2019-04-08 14:00:00'::timestamp) , (2, '2019-04-10 07:00:00'::timestamp, '2019-04-10 20:00:00'::timestamp) , (3, '2019-04-11 12:00:00'::timestamp, '2019-04-12 16:00:00'::timestamp) , (4, '2018-12-28 12:00:00'::timestamp, '2019-01-16 16:00:00'::timestamp) ), dates_exclude(d) as ( values('2018-01-01'::date), -- 2018 ('2018-01-02'::date), ('2018-01-03'::date), ('2018-01-04'::date), ('2018-01-05'::date), ('2018-01-08'::date), ('2018-02-23'::date), ('2018-03-08'::date), ('2018-03-09'::date), ('2018-05-01'::date), ('2018-05-02'::date), ('2018-05-09'::date), ('2018-06-11'::date), ('2018-06-12'::date), ('2018-11-05'::date), ('2018-12-31'::date), ('2019-01-01'::date), -- 2019 ('2019-01-02'::date), ('2019-01-03'::date), ('2019-01-04'::date), ('2019-01-07'::date), ('2019-01-08'::date), ('2019-03-08'::date), ('2019-05-01'::date), ('2019-05-02'::date), ('2019-05-03'::date), ('2019-05-09'::date), ('2019-05-10'::date), ('2019-06-12'::date), ('2019-11-04'::date) ), dates_include(start_time, stop_time) as ( values --  2018,  2019  ('2018-04-28 10:00:00'::timestamp, '2018-04-28 19:00:00'::timestamp), ('2018-06-09 10:00:00'::timestamp, '2018-06-09 19:00:00'::timestamp), ('2018-12-29 10:00:00'::timestamp, '2018-12-29 19:00:00'::timestamp) ) ), schedule_base(start_time, stop_time) as ( select d::timestamp + '10:00:00', d::timestamp + '19:00:00' from generate_series( (select min(start_time) from periods)::date::timestamp , (select max(stop_time) from periods)::date::timestamp , '1 day'::interval ) as days(d) where extract(dow from d) not in (0,6) ), schedule as ( select * from schedule_base where start_time::date not in (select d from dates_exclude) union select * from dates_include ), periods_wrk as ( select p.* , tsrange(p.start_time, p.stop_time) * tsrange(s.start_time, s.stop_time) as wrkh from periods p , schedule s ) select id, start_time, stop_time , sum(coalesce(upper(wrkh)-lower(wrkh), '0 sec'::interval)) from periods_wrk group by id, start_time, stop_time 

Hourra! .. Cela pourrait ĂȘtre terminĂ©, mais pour ĂȘtre complet, nous examinerons quelques sujets plus connexes.


Poursuite du développement du sujet


Jours de pré-vacances raccourcis, pauses déjeuner, horaires différents pour différents jours de la semaine ... En principe, tout est clair, vous devez fixer la définition de l' horaire , donnez juste quelques exemples.


Voici comment définir des heures de début et de fin différentes pour une journée de travail, selon le jour de la semaine:


  select d + case extract(dow from d) when 1 then '10:00:00'::time --  when 2 then '11:00:00'::time --  when 3 then '11:00:00'::time --  --        else '10:00:00'::time end , d + case extract(dow from d) --   19   when 5 then '14:00:00'::time --  else '19:00:00'::time end from schedule_base 

Si vous devez prendre en compte les pauses déjeuner de 13h00 à 14h00, alors au lieu d'un intervalle par jour, faites deux:


  select d + '10:00:00'::time , d + '13:00:00'::time from schedule_base union all select d + '14:00:00'::time , d + '19:00:00'::time from schedule_base 

Eh bien et ainsi de suite.


Performances


Je vais dire quelques mots sur les performances, car il y a toujours des questions à ce sujet. Je ne vais pas déjà mùcher beaucoup, c'est une section avec un astérisque.


En général, l'optimisation prématurée est mauvaise. D'aprÚs mes nombreuses années d'observation, la lisibilité du code est son avantage le plus important. Si le code est bien lu, il est plus facile à maintenir et à développer. Un code bien lisible nécessite implicitement à la fois une bonne architecture de solution, des commentaires appropriés et de bons noms de variables, une compacité sans sacrifier la lisibilité, etc., c'est-à-dire tout ce pour quoi le code est appelé bon.


Par consĂ©quent, la demande est toujours Ă©crite aussi lisible que possible, et nous commençons Ă  l'optimiser si et seulement s'il s'avĂšre que les performances sont insuffisantes. De plus, nous l'optimiserons prĂ©cisĂ©ment lĂ  oĂč les performances sont insuffisantes et exactement dans la mesure oĂč elles deviennent suffisantes. Si vous apprĂ©ciez certainement votre temps et que vous avez quelque chose Ă  faire.


Mais ne pas faire de travail inutile dans la demande est juste; vous devez toujours essayer de prendre cela en compte.


Sur cette base, nous inclurons immĂ©diatement une optimisation dans la requĂȘte - laissez chaque pĂ©riode source ne croiser qu'avec les intervalles de temps de travail avec lesquels elle a des points communs (au lieu d'une longue condition classique sur les limites de la plage, il est plus pratique d'utiliser l'opĂ©rateur && intĂ©grĂ© pour le type tsrange ). Cette optimisation est dĂ©jĂ  apparue dans la demande, mais a conduit au fait que les pĂ©riodes initiales totalement tombĂ©es en dehors des heures de travail ont disparu des rĂ©sultats.


Ramenez cette optimisation. Pour ce faire, utilisez LEFT JOIN , qui enregistrera tous les enregistrements de la table des pĂ©riodes . Maintenant, la sous-requĂȘte period_wrk ressemblera Ă  ceci:


 , periods_wrk as ( select p.* , tsrange(p.start_time, p.stop_time) * tsrange(s.start_time, s.stop_time) as wrkh from periods p left join schedule s on tsrange(p.start_time, p.stop_time) && tsrange(s.start_time, s.stop_time)) 

L'analyse de la demande montre que le temps consacrĂ© aux donnĂ©es de test a diminuĂ© d'environ la moitiĂ©. Étant donnĂ© que le temps d'exĂ©cution dĂ©pend de ce que le serveur faisait en mĂȘme temps, j'ai pris quelques mesures et donnĂ© un rĂ©sultat «typique», pas le plus grand, pas le plus petit, du milieu.


Ancienne requĂȘte:


 explain (analyse) with periods(id, start_time, stop_time) as ( ... QUERY PLAN ------------------------------------------------------------------------------------ HashAggregate (cost=334.42..338.39 rows=397 width=36) (actual time=10.724..10.731 rows=4 loops=1) ... 

Nouveau:


 explain (analyse) with periods(id, start_time, stop_time) as ( ... QUERY PLAN ------------------------------------------------------------------------------------ HashAggregate (cost=186.37..186.57 rows=20 width=36) (actual time=5.431..5.440 rows=4 loops=1) ... 

Mais le plus important est qu'une telle demande évoluera également mieux, nécessitant moins de ressources serveur, car le produit cartésien complet se développe trÚs rapidement.


Et lĂ -dessus, je m'arrĂȘterais avec des optimisations. Lorsque j'ai rĂ©solu ce problĂšme par moi-mĂȘme, j'avais suffisamment de performances mĂȘme dans une forme beaucoup plus terrible de cette demande, mais il n'y avait vraiment pas besoin d'optimiser. Pour obtenir un rapport sur mes donnĂ©es une fois par trimestre, je peux attendre dix secondes supplĂ©mentaires. L'heure supplĂ©mentaire consacrĂ©e Ă  l'optimisation dans de telles conditions ne sera jamais payante.


Mais cela s'avĂšre sans intĂ©rĂȘt; rĂ©flĂ©chissons encore Ă  la façon dont les Ă©vĂ©nements pourraient se dĂ©velopper si l'optimisation en termes de temps d'exĂ©cution Ă©tait vraiment nĂ©cessaire. Par exemple, nous voulons surveiller ce paramĂštre en temps rĂ©el pour chacun de nos enregistrements dans la base de donnĂ©es, c'est-Ă -dire que pour chaque Ă©ternuement, une telle demande sera appelĂ©e. Eh bien, ou venez avec votre propre raison, pourquoi auriez-vous besoin d'optimiser.


La premiĂšre chose qui me vient Ă  l'esprit est de compter une fois et de mettre dans la base de donnĂ©es une table avec des intervalles de travail. Il peut y avoir des contre-indications: si la base de donnĂ©es ne peut pas ĂȘtre modifiĂ©e, ou si des difficultĂ©s sont attendues avec la prise en charge des donnĂ©es pertinentes dans un tel tableau. Ensuite, vous devrez laisser la gĂ©nĂ©ration de temps de travail «à la volĂ©e» dans la demande elle-mĂȘme, car il ne s'agit pas d'une sous-requĂȘte trĂšs lourde.


L'approche suivante et la plus puissante (mais pas toujours applicable) est l'optimisation algorithmique. Certaines de ces approches ont déjà été présentées dans les commentaires de l'article avec l'état du problÚme.


J'aime celui-ci surtout. Si vous créez un tableau avec tous les jours (non seulement de travail) du calendrier et calculez le total cumulé du nombre d'heures de travail chaque jour d'une certaine «création du monde», vous pouvez obtenir le nombre d'heures de travail entre deux dates avec une opération de soustraction. Il ne reste plus qu'à prendre correctement en compte les heures de travail du premier et du dernier jour - et vous avez terminé. Voici ce que j'ai obtenu dans cette approche:


  schedule_base(d, is_working) as ( select '2018-01-01'::date, 0 union all select d+1, case when extract(dow from d+1) not in (0,6) and d+1 <> all('{2019-01-01,2019-01-02,2019-01-03,2019-01-04,2019-01-07,2019-01-08,2019-03-08,2019-05-01,2019-05-02,2019-05-03,2019-05-09,2019-05-10,2019-06-12,2019-11-04,2018-01-01,2018-01-02,2018-01-03,2018-01-04,2018-01-05,2018-01-08,2018-02-23,2018-03-08,2018-03-09,2018-04-30,2018-05-01,2018-05-02,2018-05-09,2018-06-11,2018-06-12,2018-11-05,2018-12-31}') or d+1 = any('{2018-04-28,2018-06-09,2018-12-29}') then 1 else 0 end from schedule_base where d < '2020-01-01' ), schedule(d, is_working, work_hours) as ( select d, is_working , sum(is_working*'9 hours'::interval) over (order by d range between unbounded preceding and current row) from schedule_base ) select p.* , s2.work_hours - s1.work_hours + ('19:00:00'::time - least(greatest(p.start_time::time, '10:00:00'::time), '19:00:00'::time)) * s1.is_working - ('19:00:00'::time - least(greatest(p.stop_time::time, '10:00:00'::time), '19:00:00'::time)) * s2.is_working as wrk from periods p, schedule s1, schedule s2 where s1.d = p.start_time::date and s2.d = p.stop_time::date 

Je vais expliquer briĂšvement ce qui se passe ici. Dans la sous-requĂȘte schedule_base , nous gĂ©nĂ©rons tous les jours du calendrier pendant deux ans et nous dĂ©terminons chaque jour le signe si le jour ouvrable (= 1) ou non (= 0). De plus, dans la sous-requĂȘte de planification , nous considĂ©rons la fonction de fenĂȘtre comme le nombre total cumulĂ© d'heures de travail Ă  partir du 2018-01-01. Il serait possible de tout faire dans une seule sous-requĂȘte, mais cela s'avĂ©rerait plus lourd, ce qui nuirait Ă  la lisibilitĂ©. Ensuite, dans la demande principale, nous considĂ©rons la diffĂ©rence entre le nombre d'heures de travail Ă  la fin et au dĂ©but de la pĂ©riode et, quelque peu fleuri, prenons en compte les heures de travail du premier et du dernier jour de la pĂ©riode. La floriditĂ© est associĂ©e au dĂ©calage de l'heure avant le dĂ©but de la journĂ©e de travail vers son dĂ©but et de l'heure aprĂšs la fin de la journĂ©e de travail Ă  sa fin. De plus, si la partie de la demande avec shedule_base et planning est supprimĂ©e dans une table prĂ©-calculĂ©e distincte (comme suggĂ©rĂ© prĂ©cĂ©demment), cette demande deviendra complĂštement triviale.


Comparons l'exécution sur un échantillon plus grand afin de mieux montrer l'optimisation effectuée, pour quatre périodes à partir de la condition de tùche, plus de temps est consacré à la génération d'un horaire de travail.


J'ai pris environ 3 000 rÚgles. Je ne donnerai que la ligne de résumé supérieure dans EXPLAIN, les valeurs typiques sont les suivantes.


Option d'origine:


 GroupAggregate (cost=265790.95..296098.23 rows=144320 width=36) (actual time=656.654..894.383 rows=2898 loops=1) ... 

Optimisé:


 Hash Join (cost=45.01..127.52 rows=70 width=36) (actual time=1.620..5.385 rows=2898 loops=1) ... 

Le gain de temps était de quelques ordres de grandeur. Avec l'augmentation du nombre de périodes et leur durée en années, l'écart ne fera que s'élargir.


Tout semblait aller bien, mais pourquoi, aprÚs avoir fait une telle optimisation, j'ai laissé la premiÚre version de la demande pour moi jusqu'à ce que ses performances soient suffisantes? Oui, car la version optimisée est sans aucun doute plus rapide, mais elle nécessite beaucoup plus de temps pour comprendre comment elle fonctionne, c'est-à-dire que la lisibilité s'est détériorée. Autrement dit, la prochaine fois que je devrai réécrire la demande dans mes conditions modifiées, je devrai (ou pas moi) passer beaucoup plus de temps à comprendre comment fonctionne la demande.


C'est tout pour aujourd'hui, gardez les tentacules au chaud, et je vous dis au revoir jusqu'Ă  la prochaine version de Radio SQL.

Source: https://habr.com/ru/post/fr459236/


All Articles