← Torna ai progetti

SQL Banking Customer Analytics

2026-03-29 · Data Engineering

Cos'è il progetto

SQL Banking Customer Analytics è un progetto di analisi SQL su un modello bancario relazionale. Il lavoro costruisce una vista cliente completa partendo da transazioni, conti e anagrafiche, usando join, CTE, aggregazioni condizionali, ranking e segmentazione con window function.

Contesto tecnico

I dati di un sistema bancario sono distribuiti tra clienti, conti, tipologie e transazioni. Le singole righe descrivono movimenti elementari, ma non permettono di capire direttamente quanti conti possiede un cliente, quanto denaro ha movimentato o come si posiziona rispetto al resto del portafoglio.

Il problema analitico consisteva nel trasformare un modello relazionale normalizzato in una vista sintetica per cliente, mantenendo anche i soggetti senza conti o senza transazioni. Una seconda esigenza era passare dalle sole metriche assolute a un confronto relativo basato su ranking, media globale e segmentazione.

Obiettivo

L'obiettivo era costruire un progetto SQL riproducibile su MySQL 8+ capace di:

  • creare un database bancario di test con relazioni e vincoli espliciti;
  • produrre una riga di bilancio per ciascun cliente;
  • separare entrate e uscite complessive e per tipologia di conto;
  • contare correttamente i conti senza duplicarli durante i join;
  • classificare i clienti per entrate e saldo netto dei movimenti;
  • confrontare ogni cliente con la media del portafoglio;
  • assegnare quattro segmenti descrittivi tramite quartili;
  • validare i calcoli su casi noti e predisporre gli indici per i join.

Il risultato è una mini-analisi clienti eseguita interamente in SQL, dalla costruzione del dato di test fino alla vista comparativa finale.

Modello dati

Il database banca contiene cinque tabelle collegate:

  • cliente conserva anagrafica e data di nascita;
  • conto associa uno o più conti a ciascun cliente;
  • tipo_conto distingue conti Base, Business, Privati e Famiglie;
  • transazioni registra conto, tipo e importo di ogni movimento;
  • tipo_transazione identifica entrate e uscite attraverso il campo segno.

Le relazioni principali sono cliente-conto e conto-transazione, entrambe uno-a-molti. Il dataset sintetico include 15 clienti, 23 conti e 359 transazioni, oltre a due casi limite: un cliente senza conti e un cliente con un conto privo di movimenti.

Analisi di bilancio per cliente

La query principale unisce le tabelle con LEFT JOIN e produce una riga per ogni cliente. Questa scelta conserva anche i clienti non ancora attivi, che verrebbero esclusi utilizzando INNER JOIN.

Le metriche vengono calcolate attraverso aggregazioni condizionali:

  • numero e importo totale delle entrate;
  • numero e importo totale delle uscite;
  • numero di conti distinti per cliente;
  • conteggi e importi suddivisi tra Base, Business, Privati e Famiglie;
  • età calcolata con TIMESTAMPDIFF.

Il pattern SUM(CASE WHEN ... THEN ... ELSE 0 END) permette di trasformare categorie memorizzate per riga in metriche affiancate. COUNT(DISTINCT) evita invece di contare più volte lo stesso conto dopo il join con le transazioni.

Ranking e segmentazione

La seconda query separa il calcolo delle metriche dalla classificazione attraverso la CTE bilancio_per_cliente. Su questo risultato applica quattro analisi comparative:

  • RANK() OVER per ordinare i clienti in base alle entrate totali;
  • RANK() OVER per ordinare il saldo netto dei movimenti;
  • AVG() OVER () per confrontare ogni cliente con la media globale;
  • NTILE(4) per suddividere il portafoglio in fasce Bassa, Media-Bassa, Media-Alta e Alta.

Le window function mantengono una riga per cliente e aggiungono informazioni relative all'intero portafoglio, senza richiedere subquery correlate per ogni record.

Esempio di output

L'output evidenzia che volume in entrata e saldo netto non raccontano la stessa storia:

  • Valentina Galli è prima per saldo netto, con 6.950 EUR di entrate, 3.595 EUR di uscite e un risultato di +3.355 EUR;
  • Marco Ricci è secondo per saldo, con un risultato di +2.984 EUR;
  • Stefano Marini è primo per entrate con 32.250 EUR, ma ultimo per saldo netto: 40.108 EUR di uscite portano il risultato a -7.858 EUR;
  • la media delle entrate per cliente, includendo i soggetti inattivi, è pari a 5.882 EUR.

Questo confronto mostra perché un ranking basato soltanto sul volume sarebbe incompleto: la stessa query rende visibili intensità dei movimenti, risultato netto e posizione relativa.

Validazione dei calcoli

Il progetto include test_singolo_cliente.sql, usato per verificare la logica su un caso controllabile. Il cliente 15 possiede tre conti e registra esattamente 184 movimenti: 159 uscite e 25 entrate.

Il test confronta inoltre gli importi complessivi con la somma degli importi suddivisi per tipologia di conto. Questo sviluppo incrementale riduce il rischio di accettare aggregazioni formalmente valide ma numericamente errate.

Performance

Il setup definisce primary key su tutte le tabelle e indici sulle chiavi esterne più utilizzate:

  • conto.id_cliente per il join tra clienti e conti;
  • transazioni.id_conto per il join tra conti e movimenti;
  • transazioni.id_tipo_trans per la classificazione del segno.

Su 359 transazioni il beneficio non è misurabile in modo significativo, ma la struttura evita che il progetto ignori il costo dei join quando il volume cresce. Gli indici sono quindi una scelta di design, non un benchmark prestazionale dichiarato.

Risultati

  • 15 clienti conservati nell'analisi, inclusi i casi senza attività;
  • 23 conti distribuiti tra quattro tipologie;
  • 359 transazioni, composte da 71 entrate e 288 uscite;
  • 88.230 EUR di entrate e 71.275,50 EUR di uscite nel dataset;
  • due ranking indipendenti per entrate e saldo netto;
  • confronto percentuale con una media di 5.882 EUR;
  • quattro segmenti ottenuti con NTILE.

Evoluzioni future

Il progetto usa dati sintetici per concentrarsi sulla logica SQL, sulla modellazione relazionale e sulla costruzione di viste analitiche. La stessa struttura può essere estesa verso scenari bancari più completi aggiungendo dimensioni temporali, saldo iniziale, valuta e riconciliazioni contabili.

La segmentazione in quartili è pensata come lettura analytics del portafoglio clienti. Le evoluzioni principali sarebbero:

  • aggiungere timestamp per analisi mensili, trend e rolling metrics;
  • modellare saldo iniziale e saldo corrente per conto;
  • introdurre controlli di qualità su importi, segni e chiavi orfane;
  • creare view riutilizzabili per bilancio e segmentazione;
  • verificare i piani di esecuzione con EXPLAIN ANALYZE su volumi maggiori;
  • esporre i risultati in una dashboard BI;
  • aggiungere test SQL automatici per conteggi, null e riconciliazione degli importi.

FAQ tecniche

Perché usare LEFT JOIN?

Per mantenere nel risultato tutti i clienti, inclusi quelli senza conti e quelli con un conto ma senza transazioni. Un join interno li eliminerebbe dall'analisi.

Perché COUNT(DISTINCT) è necessario?

Dopo il join, lo stesso conto appare una volta per ciascuna transazione. Contare gli identificativi senza DISTINCT produrrebbe quindi un numero di conti sovrastimato.

Saldo netto e saldo bancario sono la stessa cosa?

No. Nel progetto il saldo netto è la differenza tra entrate e uscite incluse nel dataset. Senza un saldo iniziale e una storia completa dei movimenti non rappresenta la disponibilità reale del conto.

I segmenti indicano il rischio del cliente?

No. NTILE(4) crea quartili descrittivi ordinati per saldo netto. Per stimare il rischio servirebbero variabili, regole e validazioni dedicate.

Perché è richiesto MySQL 8+?

La query di ranking utilizza CTE e window function, funzionalità supportate da MySQL a partire dalla versione 8.0.

Read in English