Creazione di un database IP-to-Country
In un progetto di lead generation per il mercato europeo mi è capitato di ereditare una funzione di geolocalizzazione che decideva la lingua del sito sulla base del paese dell'utente. Funzionava per metà dei visitatori e per l'altra metà no, in modo apparentemente casuale. La causa, scoperta dopo un po' di indagine, era duplice: il database conosceva solo gli indirizzi IPv4, mentre una quota crescente di traffico arrivava ormai su IPv6 e cadeva in un ramo di default; e la query di lookup aveva un difetto logico che, per certi indirizzi, restituiva il paese sbagliato invece di nessun risultato. Era costruita esattamente come tanti tutorial di quindici anni fa insegnavano, ed è il motivo per cui vale la pena rivedere da capo come si crea e si usa un database IP-to-country nel 2026.
Il principio di base non è cambiato: si parte da una fonte che mappa intervalli di indirizzi IP a un codice paese, si caricano quegli intervalli in una struttura interrogabile, e dato un IP si cerca l'intervallo che lo contiene. Quello che è cambiato sono tutti i dettagli che fanno la differenza fra una soluzione che funziona e una che funziona a metà: la gestione di IPv6, la correttezza del lookup, la scelta della fonte, e una dimensione che nel 2011 nessuno considerava, cioè la conformità.
TL;DR
- Prima domanda: ti serve davvero un DB tuo? Se sei dietro un CDN, il paese ti arriva gratis nell'header (es.
CF-IPCountry).- Fonte dati: GeoLite2, DB-IP Lite o IP2Location LITE, tutte gratuite e con IPv4 e IPv6 (DB-IP Lite non richiede registrazione).
- Schema: memorizza i range come
VARBINARY(16)conINET6_ATON(), non come interi: gli interi non reggono IPv6.- Lookup corretto:
WHERE ip_start <= INET6_ATON(?) AND ip_end >= INET6_ATON(?), con prepared statement (controlla entrambi gli estremi).- Via più semplice: leggi il file binario MMDB con
geoip2/geoip2, senza tabella. E ricorda: un indirizzo IP è un dato personale sotto GDPR.
Ti serve davvero un database IP-to-country tuo?
È la prima domanda, e va fatta prima di scrivere una riga di codice, perché spesso la risposta è no e ci si risparmia un componente da mantenere. Ci sono tre alternative al costruirsi un database in proprio, e ognuna copre un caso d'uso diverso.
La prima: se il tuo sito è già dietro un CDN come Cloudflare, il paese dell'utente ti arriva gratis in un header HTTP (CF-IPCountry) calcolato a monte, senza che tu debba gestire alcun dato di geolocalizzazione. Per il caso d'uso classico, scegliere la lingua, è quasi sempre la soluzione più semplice e accurata. La seconda: se ti serve la geolocalizzazione solo per poche richieste, una web API di geolocalizzazione interrogata al volo evita di mantenere un database locale, al costo di una dipendenza esterna e di un limite di chiamate. La terza, ed è quella che giustifica il database in proprio: se devi geolocalizzare grandi volumi (ogni richiesta, milioni al giorno), o devi farlo offline su log o dataset, o non vuoi che gli indirizzi dei tuoi utenti escano verso un servizio terzo (un punto non banale proprio per la privacy), allora avere il database in casa è la scelta giusta. Il resto dell'articolo riguarda quest'ultimo caso.
Le fonti dati gratuite nel 2026, e perché IPv6 non è opzionale
Il database lo si popola da una fonte che pubblica gli intervalli IP-paese. Le tre gratuite di riferimento oggi sono tutte aggiornate e tutte coprono sia IPv4 sia IPv6, e la scelta dipende soprattutto dall'attrito di registrazione e dalla frequenza di aggiornamento. MaxMind GeoLite2 è la più nota: richiede un account gratuito, si aggiorna settimanalmente (ogni martedì), è distribuita sia in CSV sia in formato binario MMDB, ed è sotto licenza Creative Commons con obbligo di attribuzione. DB-IP Lite è la più frictionless, perché non richiede alcuna registrazione, si aggiorna mensilmente ed è disponibile in CSV e MMDB. IP2Location LITE, che era la fonte usata nell'approccio originale, oggi richiede un account gratuito senza carta di credito, si aggiorna mensilmente e documenta esplicitamente il setup per IPv6.
Il punto su IPv6 è dirimente e va capito bene, perché è la causa più comune di geolocalizzazione che "funziona a metà". Lo spazio IPv4 è esaurito da anni, e una frazione importante e crescente del traffico reale arriva ormai su IPv6, soprattutto da reti mobili. Un database costruito solo su IPv4, come quasi tutti quelli derivati dai tutorial dell'epoca, semplicemente non sa cosa rispondere a un indirizzo IPv6 e finisce nel ramo di default. Tutte e tre le fonti pubblicano due dataset separati, uno per gli intervalli IPv4 e uno per gli IPv6, e il database va popolato con entrambi. Questo ha una conseguenza tecnica importante: un indirizzo IPv6 non entra in un intero a 32 bit, quindi tutta l'impostazione "salvo l'IP come INT con ip2long" dell'approccio classico è strutturalmente incompatibile con IPv6 e va abbandonata.
Lo schema corretto: range su colonne binarie e il lookup che non sbaglia
Per gestire IPv4 e IPv6 nello stesso schema, la chiave è memorizzare gli estremi degli intervalli come dati binari a lunghezza fissa anziché come interi. MySQL offre da anni le funzioni INET6_ATON() e INET6_NTOA(), documentate fra le funzioni varie del manuale MySQL, che convertono un indirizzo (sia IPv4 sia IPv6) in una rappresentazione binaria VARBINARY(16) e viceversa. Salvando inizio e fine di ogni intervallo in due colonne VARBINARY(16), gli intervalli di entrambi i protocolli convivono nella stessa tabella e si ordinano e confrontano correttamente con gli operatori binari.
CREATE TABLE ip_to_country (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
ip_start VARBINARY(16) NOT NULL,
ip_end VARBINARY(16) NOT NULL,
country_code CHAR(2) NOT NULL,
PRIMARY KEY (id),
KEY idx_ip_start (ip_start),
KEY idx_ip_end (ip_end)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Qui sta il difetto logico dell'approccio classico, quello che nel progetto da cui sono partito restituiva il paese sbagliato. La query originale cercava l'intervallo il cui inizio fosse minore o uguale all'IP, ordinava per inizio decrescente e prendeva il primo. Il problema è che non verificava mai che l'IP fosse anche minore o uguale alla fine di quell'intervallo. Se un indirizzo cade in un buco fra due intervalli allocati (e i buchi esistono, perché non tutto lo spazio IP è assegnato), questa query restituisce comunque l'intervallo precedente, attribuendo all'IP un paese che non gli appartiene. Il lookup corretto controlla entrambi gli estremi:
SELECT country_code
FROM ip_to_country
WHERE ip_start <= INET6_ATON(?) AND ip_end >= INET6_ATON(?)
LIMIT 1;Due cose, oltre alla correttezza logica. La prima: l'IP entra come parametro di un prepared statement, non concatenato nella stringa SQL come faceva il codice originale, che era una SQL injection in attesa di accadere. La seconda: su una tabella con milioni di intervalli, la performance di questa query dipende interamente dagli indici e da come l'ottimizzatore li usa per la condizione di range; è un caso da verificare con EXPLAIN e, su volumi importanti, da affrontare con tecniche di indicizzazione mirate, un tema che approfondisco nell'articolo sull'ottimizzazione di performance e sicurezza dei database. Se la geolocalizzazione gira a ogni richiesta, una query lenta qui si paga su ogni pagina servita.
Se stai costruendo qualcosa del genere e queste scelte di schema e di indicizzazione ti sembrano un terreno scivoloso, è esattamente il tipo di lavoro su cui intervengo: nel mio profilo professionale trovi l'esperienza concreta su architetture dati, tuning di query su grandi volumi e integrazione di dataset esterni in stack PHP e MySQL.
Come si popola la tabella senza metterci ore
Il modo in cui si caricano i dati nella tabella è il punto in cui l'approccio classico mostrava il suo limite più grave di performance. Lo script originale ciclava sulle righe del file CSV e per ognuna eseguiva una INSERT singola; peggio, per ogni paese mai visto prima faceva una chiamata HTTP a un servizio di traduzione per ottenere il nome in italiano. Su un dataset di milioni di intervalli, questo significa milioni di insert non transazionali e una dipendenza di rete dentro il ciclo: un caricamento che può durare ore ed è fragile a ogni singolo errore di rete.
L'approccio corretto è duplice. Per i nomi dei paesi non serve alcun servizio esterno: i codici ISO 3166 a due lettere e i loro nomi sono un elenco statico e stabile di poco più di duecento voci, che si tiene in una tabella di lookup popolata una volta sola o direttamente in un array nel codice. Geolocalizzare restituisce un codice paese (IT, FR, DE), e la traduzione del nome è una semplice corrispondenza in memoria, non una chiamata di rete per riga. Per gli intervalli, invece di insert singole si usa il caricamento massivo: LOAD DATA LOCAL INFILE importa l'intero CSV in un colpo a velocità di ordini di grandezza superiore, oppure, se si preferisce passare dal codice, si raggruppano le righe in insert multi-valore dentro una transazione, facendo commit ogni qualche migliaio di righe.
C'è infine un dettaglio operativo che evita disservizi: l'aggiornamento periodico del database non va fatto svuotando la tabella in produzione e ricaricandola, perché nella finestra di ricaricamento la geolocalizzazione restituirebbe risultati vuoti. Meglio caricare i nuovi dati in una tabella di staging e poi sostituirla a quella attiva con una RENAME TABLE atomica, così il passaggio è istantaneo e i lookup non vedono mai uno stato incompleto. È lo stesso principio di prudenza che applico a ogni aggiornamento di dati di riferimento serviti in produzione: la sostituzione deve essere atomica, mai un buco temporale.
La via più semplice: leggere il database binario senza una tabella SQL
C'è una scorciatoia che vale la pena conoscere, perché in molti casi rende superfluo l'intero schema SQL. Le fonti come MaxMind e DB-IP distribuiscono il database anche in formato binario MMDB, una struttura ottimizzata proprio per il lookup di intervalli IP che si interroga direttamente da file, senza importarla in alcun database relazionale. In PHP la si legge con la libreria ufficiale geoip2/geoip2 (o il lettore maxminddb) installabile via Composer, e il lookup è una singola chiamata che gestisce IPv4 e IPv6 in modo trasparente.
use GeoIp2\Database\Reader;
$reader = new Reader('/percorso/GeoLite2-Country.mmdb');
$record = $reader->country($ip); // gestisce IPv4 e IPv6
$countryCode = $record->country->isoCode; // es. "IT"Questo approccio aggiorna i dati semplicemente sostituendo il file MMDB (operazione che si automatizza con un cronjob settimanale o mensile a seconda della fonte), non richiede una tabella né manutenzione di schema, ed è più veloce di una query SQL per il singolo lookup. La tabella SQL custom resta giustificata quando devi fare join fra la geolocalizzazione e altri tuoi dati direttamente nel database, o eseguire analisi aggregate su grandi dataset di IP; per il caso "dato un IP, dimmi il paese" il lettore MMDB è quasi sempre la scelta migliore. Confrontare le due strade, tabella propria contro lettura del file binario, in base al caso d'uso reale è il tipo di decisione architetturale che evita di costruire e mantenere infrastruttura che non serve.
Un indirizzo IP è un dato personale: la dimensione che nel 2011 non esisteva
C'è un aspetto che un tempo non era una preoccupazione concreta e che oggi è centrale: un indirizzo IP, secondo la giurisprudenza europea e l'orientamento delle autorità, è un dato personale ai sensi del GDPR, perché può concorrere a identificare una persona. Questo cambia la natura dell'operazione: geolocalizzare gli IP dei tuoi utenti è un trattamento di dati personali, e come tale richiede una base giuridica, una finalità dichiarata e il rispetto del principio di minimizzazione. Tradotto in pratica, significa che geolocalizzare per servire la lingua giusta è legittimo e proporzionato, ma conservare a tempo indeterminato i log degli IP geolocalizzati di ogni visitatore, o incrociarli per profilare gli utenti, entra in un territorio dove servono attenzione, trasparenza nell'informativa e probabilmente una valutazione più seria.
C'è anche un argomento tecnico che si lega a quello giuridico, ed è il motivo per cui avere il database in casa è preferibile a interrogare una web API esterna quando i volumi lo giustificano: interrogare un servizio terzo significa inviargli gli indirizzi IP dei tuoi utenti, cioè trasferire dati personali a un fornitore, con tutte le implicazioni del caso, spesso verso paesi extra-UE. Tenere la geolocalizzazione locale mantiene quei dati dentro il tuo perimetro. Su come questi obblighi si traducono in scelte tecniche concrete per una PMI ho scritto in dettaglio nell'articolo sulla conformità GDPR e NIS2 e i rischi di sanzione, perché la compliance, su questi temi, è fatta di decisioni di architettura prima che di documenti.
Vale anche la pena essere onesti sui limiti di accuratezza, perché evitano usi sbagliati. La geolocalizzazione a livello di paese è generalmente affidabile; quella a livello di città molto meno, ed è facile prendere abbagli. VPN, proxy, reti mobili che instradano il traffico da un punto centrale e l'assegnazione dinamica degli indirizzi rendono la mappatura imperfetta per definizione. Per questo un dato di geolocalizzazione va usato per adattare l'esperienza (la lingua, una valuta predefinita), non come unico fattore per decisioni di sicurezza o di accesso, dove un IP geolocalizzato male può escludere un utente legittimo o far passare uno indesiderato.
Costruire un database IP-to-country nel 2026 non è più la passeggiata di quindici anni fa, e proprio per questo è l'occasione per farlo bene: scegliere una fonte aggiornata che copra IPv6 oltre a IPv4, memorizzare gli intervalli in colonne binarie con INET6_ATON invece che in interi che reggono solo IPv4, scrivere un lookup che verifica entrambi gli estremi del range con un prepared statement, e valutare onestamente se non convenga leggere direttamente il file binario MMDB o appoggiarsi all'header di un CDN che già hai. E sopra tutto questo, tenere presente che stai trattando dati personali, con tutto ciò che il GDPR comporta. Se hai una geolocalizzazione che funziona a metà, magari ferma a IPv4 o con risultati incoerenti come quella da cui sono partito, contattami per un confronto diretto: spesso il problema non è la fonte dati, ma un dettaglio di schema o di query che si corregge una volta sola e smette di costarti utenti serviti nella lingua sbagliata.