Eseguire calcoli con Postgres in Django

Pubblicatodi il Ago 28, 2018 in Programmazione
Un commento

Il framework che più di tutti prediligo per lo sviluppo di web app è sicuramente Django.

Ci sono innumerevoli motivi per cui ho scelto di affidarmi a questa tecnologia ma non ne discuterò in questo post.

Un problema che mi sono recentemente trovato ad affrontare è quello di inserire all’interno di uno dei miei software una pagina dedicata alle statistiche.

Questo software è quello che gestisce e automatizza la maggior parte delle operazione all’interno della nostra azienda.

Il 98% del nostro lavoro avviene sul web e passa per uno dei nostri siti.

Chiunque lavori in questo ambito sa quanto sia fondamentale avere statistiche il più possibile dettagliate che ti permettano, in qualsiasi momento, di avere il polso della situazione.

Tralasciando a questo punto tutta una serie di dettagli, quello che ho cercato di realizzare era una pagina che mi permettesse di avere a colpo d’occhio tutte le statistiche di cui ho bisogno per capire se stiamo lavorando nel modo corretto o meno ed eventualmente capire cosa non sta funzionando (o, al contrario, cosa sta funzionando molto bene).

Queste statistiche che includono numeri di click, di compilazioni di form, conversion rate, abbandon rate e tutto uno storico per avere dati di riferimento, vengono calcolate in real time.

Questa, dal punto di vista delle performance, può non essere la soluzione ottimale considerando anche che tutti questi calcoli vengono eseguiti su decine di migliaia di record.

Ho voluto quindi mettermi alla prova e verificare se ero in grado di realizzare codice in grado di performare comunque.

<spoiler>Ci sono riuscito</spoiler>

Come faccio quindi a fare in modo che decine di migliaia di calcoli vengano eseguiti in real time e restituiti all’utente in meno di 2 secondi (è comunque una pagina solo per gli addetti ai lavori, non è una pagina pubblica)? Semplice. Ho delegato molto del lavoro al DBMS.

Inizialmente la sezione delle statistiche aveva un tempo di caricamento medio di 9s. Inaccettabile anche se si trattava di una pagina riservata allo staff.

Il problema stava nel fatto che io andavo a selezionare un numero elevato di record dal database (il tutto ovviamente usando l’ORM integrato in Django) e andavo poi a “eseguire calcoli” su ogni singolo record utilizzando banalmente un ciclo for per scorrere l’intero queryset.

Django è realizzato in python. Python è linguaggio performante ma essendo comunque un linguaggio interpretato (all’interprete non viene passato direttamente il codice sorgente ma viene creato prima un bytecode intermedio) in certe situazione può soffrire di problemi di performance.

La soluzione quindi è sempre quella di eseguire query che restituiscano dati già pronti all’uso che non necessitano di lavoro aggiuntivo per essere utilizzati.

Un DMBS sarà sempre più veloce di un linguaggio interpretato per due motivi fondamentali:

  • E’ scritto in un linguaggio a basso livello
  • Il suo mestiere è quello di gestire record

Per riuscire a ridurre di ben 7s di media il tempo di apertura ho dovuto quindi lavorare sulle query per riuscire a ottenere direttamente dal DMBS informazioni già “pronte all’uso”.

Quello di cui vorrei parlare in questo post è un aspetto molto interessante che permette di evitare di scrivere molto codice e di ottenere dal database l’informazione di cui abbiamo bisogno “chiavi in mano”.

Con postgres possiamo eseguire calcoli direttamente nelle query.

Uno dei calcoli che faccio è quello del numero di click medi in un determinato periodo di tempo.

Grazie a postgres e al completissimo ORM di Django questo è possibile farlo direttamente con una query e avere già a disposizione l’informazione pronta all’uso.

Supponiamo io voglia calcolare il numero medio di click ricevuti al nostro sito suddivisi per mese. Per numero medio si intende la media rispetto a tutti gli anni di cui abbiamo dati.

Quello che dovrò fare sarà quindi andare a prendere tutti i click di tutti gli anni, suddividerli per mese, sommarli e poi calcolarne la media.

Come si scrive una query del genere in Django?

Così:

Click.objects.annotate(month=ExtractMonth('day')).values('month').annotate(c=Sum('unique_visit')).values('month', 'c').annotate(tot=ExpressionWrapper(1.0*F('c') / delta, output_field=FloatField())).order_by('month', 'c', 'tot')

Con questa riga di codice eseguo 5 operazioni distinte.

  • Dal model Click prendo tutti i record ed estraggo per ognuno di essi il mese di appartenenza usando la funzione ExtractMonth integrata in Django, specifica per postgres, per estrarre da un campo data (nel mio model il campo in questione è day) il mese.
  • Annoto della query il campo che conterrà la somma di tutti i campi unique_visit che indica il numero di visite uniche. In questo modo ottengo la somma dei click per ogni mese.
  • A questo punto mi appoggio alle funzioni EspressionWrapper F per tradurre l’operazione di divisione (divido il totale dei click per la variabile delta precedentemente calcolata e che contiene il numero di anni di cui abbiamo storico) in una query per postgres. ExpressionWrapper serve per l’operazione aritmetica mentre per poter utilizzare il valore di un campo del model come fosse una variabile normale. Indico che voglio che il risultato della query venga tradotto in un campo di tipo float.
  • Ordino il risultato per il campo month.

In questo modo l’ORM mi genera una query che mi restituirà il valore già pronto all’uso senza la necessità di eseguire ulteriori calcoli utilizzando python.

Il risultato sarà qualcosa del tipo:

({'month': 1, 'c': 100, 'tot': 54.2}, {'month': 2, 'c': 130, 'tot': 63.2}, {...})

Utilizzando questa strategia ho potuto risparmiare numerose linee di codice e preziosi secondi per il calcolo delle informazioni di cui avevo bisogno.

Conclusioni

Per quanto il vostro codice python sia efficiente non sarà mai rapido come una buona query 😁

# # #

Un pensiero

  1. […] dal precedente post in cui ho spiegato come ho fatto a migliorare drasticamente le performance della sezione […]

    Rispondi

Rispondi