Votre pipeline ETL n'est pas prêt pour les coordonnées
Dans presque tous les flux GPS se cache quelque part une grappe d'expéditions posées dans le golfe de Guinée, à environ 570 km au large de la côte d'Afrique de l'Ouest, exactement à la latitude 0, longitude 0. Aucun chauffeur n'y est allé. Un boîtier GPS qui n'a pas encore reçu son premier fix renvoie zéro par défaut au démarrage au lieu de lever une erreur, et le pipeline le croit sur parole.
Une coordonnée ressemble à une valeur et se comporte comme une affirmation. Une ligne porte, ou ne porte pas, le poids d'une commande. Une coordonnée, elle, porte un énoncé sur le monde qui peut se tromper en silence. Un ETL standard traite chaque champ comme une valeur, et la plupart de ses hypothèses se cassent sous cette lecture. Les remèdes sont des décisions de conception, pas des mises à niveau d'outils. Deux comptent plus que les autres : choisir une projection par pipeline, et partitionner avant de calculer. Toutes deux coûtent presque rien le premier jour et ruinent tout ensuite.
Une coordonnée est une affirmation, pas une valeur
Les relations spatiales sont la première chose que le SQL brut ne sait pas exprimer. La proximité, l'inclusion et l'intersection dépendent toutes d'une fonction de distance entre deux points qui n'existe pas dans le SQL standard. La requête « quelles livraisons sont tombées à moins de 500 mètres du dépôt » n'est pas une requête lente sans PostGIS. C'est une requête que vous ne pouvez pas écrire.
Les systèmes de coordonnées viennent ensuite, et ils mordent en silence. WGS84, Web Mercator et les projections locales échangent chacun de la précision contre de la performance, si bien que deux équipes calculant une « distance » dans des projections différentes produisent des chiffres incomparables. Choisissez une projection par pipeline, documentez-la, et le désaccord disparaît de vos rapports.
Puis la plus dure. « Où était le véhicule X à l'instant Y ? » assemble l'espace et le temps à la fois, et un index sur l'une des deux dimensions seul ne vous sauvera pas : filtrer sur l'une n'économise rien sur l'autre.
Les pannes que l'on peut prévoir dès le premier jour
Les données géo cassent d'un petit nombre de manières prévisibles, et c'est une bonne nouvelle. Une panne que l'on sait nommer est une panne que l'on peut refuser à la porte :
| Mode de panne | Détection | Remède |
|---|---|---|
| Inversion lat/lng | Valider les bornes : latitude dans [-90, 90] | Validation de schéma à l'ingestion |
| Null Island (0, 0) | Vérification explicite des coordonnées (0, 0) | Rejeter ou marquer pour revue manuelle |
| Dérive GPS | Signaler des vitesses dépassant 500 km/h entre points | Filtre de vitesse maximale |
| Erreurs de fuseau horaire | Détecter les horodatages qui traversent une frontière DST | Stocker tous les horodatages en UTC |
# PySpark validation
def validate_geo(df):
return (
df
.withColumn("lat_valid", col("lat").between(-90, 90))
.withColumn("lng_valid", col("lng").between(-180, 180))
.withColumn("not_null_island", ~(col("lat") == 0) | ~(col("lng") == 0))
.filter(col("lat_valid") & col("lng_valid") & col("not_null_island"))
)
Le contrôle des bornes n'est pas de la paranoïa. L'inversion entre latitude et longitude est le défaut de coordonnée le plus répandu dans n'importe quel flux réel, et une latitude de 130 vous dit instantanément que quelqu'un a transposé deux colonnes.
Partitionnez avant de calculer
Notre première clé de partitionnement fut la seule date de livraison, et elle n'éliminait rien : chaque question opérationnelle était spatiale, et filtrer par date balayait quand même chaque district du jour en question. La dimension spatiale devait être dans la partition, pas à côté. Les schémas en étoile traditionnels s'effondrent avec les données géo. Le remède est le géohash, qui convertit des coordonnées à deux dimensions en préfixes de chaîne préservant la localité spatiale. Des points proches partagent des préfixes proches, ce qui rend le partitionnement et l'élagage presque gratuits.
-- Slow: a distance function on every row of the table
SELECT * FROM deliveries
WHERE ST_Distance(location, ST_Point(-120, 39)) < 10000;
-- Fast: prune the partition first, trust PostGIS for precision
-- single cell for the example; a radius query adds the eight neighbor cells (see below)
SELECT * FROM deliveries
WHERE geohash_prefix = '9qfwvc'
AND delivery_date = '2025-12-01';
Une cellule de géohash à 6 caractères fait environ un kilomètre d'est en ouest et un demi-kilomètre du nord au sud aux latitudes moyennes, et elle se resserre d'est en ouest à mesure qu'on se rapproche d'un pôle. Assez grossière pour partitionner dessus, assez fine pour la plupart des questions métier. Ajoutez le préfixe au moment de l'ingestion, pas des mois plus tard. Greffer ce préfixe après coup sur un historique existant, c'est réécrire la table entière, pour ne rien obtenir que vous n'auriez pu avoir avec une colonne dérivée dès le premier jour.
Deux moteurs, et choisissez en connaissance de cause
Quand il vous faut aujourd'hui des tableaux de bord en direct et, du même système, l'analyse du trimestre dernier, une architecture Lambda partage le travail proprement. Flink ou Spark Streaming traite le flux en direct et écrit dans Redis ou Druid avec des index spatiaux, pour des requêtes servies en moins de 100 millisecondes. Spark traite la couche historique et écrit dans PostgreSQL avec PostGIS, pour les agrégations et le calcul de caractéristiques ML.
Servez-vous de ce motif quand vos opérateurs doivent voir les positions tout de suite et les rapports d'efficacité des itinéraires du mois dernier. Les deux publics méritent de bonnes réponses.
| Critère | Spark | Flink |
|---|---|---|
| Traitement par lots | Excellent | Possible, mais pas idéal |
| Streaming | Latence de micro-batch | Vrai streaming, faible latence |
| Fonctions géo | Via Sedona (anciennement GeoSpark) | Limitées, nécessitent des UDF |
| Gestion d'état | Surcoût de checkpointing | Backends d'état natifs |
PostGIS est un analyste, pas un ouvrier d'entrepôt
PostGIS est la référence du SQL spatial, et cette réputation est méritée. Utilisez-la pour les requêtes spatiales complexes, les jointures et l'analytique. N'en faites pas le moteur ETL principal pour l'ingestion à haut volume ou les mises à jour temps réel. C'est un analyste brillant, pas un ouvrier d'entrepôt.
Une règle à l'intérieur de PostGIS compte plus que les autres : préférez le type GEOGRAPHY au type GEOMETRY. GEOGRAPHY mesure en mètres et tient compte de la courbure de la Terre. GEOMETRY est l'alternative planaire, utilisable seulement avec la projection CRS adaptée à votre région. Et si vous vous trompez, un rayon de « 5 000 mètres » dérive discrètement avec la latitude, plus étendu dans certaines régions de votre carte que dans d'autres.
Le motif qui marche : Flink ou Spark traite les données en flux, puis écrit les résultats dans PostgreSQL avec PostGIS pour les requêtes en aval. Le calcul de distance exact a lieu là où le volume est déjà maigre.
Une question à 500 mètres, pré-filtrée
Dites que la question tombe un mardi : quelles livraisons des 30 derniers jours se sont produites à moins de 500 mètres d'un site concurrent ? Avec plus d'un million de lignes de livraison par jour, la requête naïve lit chaque ligne et calcule une fonction de distance sur toutes. Voici la forme qui permet d'y répondre pendant une réunion plutôt que du jour au lendemain :
- Ajoutez les lignes de livraison à Flink via Kafka.
- Enrichissez chaque ligne d'un préfixe de géohash à 6 caractères.
- Stockez dans PostgreSQL, partitionné par géohash et par date.
- Requêtez avec PostGIS pour la distance exacte sur l'ensemble pré-filtré.
WITH candidate_deliveries AS (
SELECT * FROM deliveries
WHERE geohash_prefix IN (
-- the cell containing the point, plus the eight neighbors
-- a 500 m radius can touch
'9qfwvc', '9qfwvb', '9qfwvf', '9qfwv9', '9qfwy1',
'9qfwv8', '9qfwvd', '9qfwy0', '9qfwy4'
)
AND delivery_date >= now() - interval '30 days'
)
SELECT * FROM candidate_deliveries
WHERE ST_Distance(
location,
ST_GeogFromText('POINT(-119.977 39.162)') -- North Lake Tahoe
) < 500;
Neuf cellules contiennent quelques milliers de lignes sur un jeu de données de livraison urbaine, à côté du million et plus que la requête naïve lirait pour une seule journée. PostGIS calcule ensuite les distances exactes sur ces seuls candidats, et c'est pour cela que les 500 mètres restent dignes de confiance. L'élagage n'a jamais touché la mesure. Il a seulement décidé qui allait être mesuré.
Là où le géohash se trompe
Les limites honnêtes, parce que chacune finit par se pointer en production. Des points à quelques mètres d'écart peuvent tomber de part et d'autre d'un bord de cellule et atterrir dans des préfixes différents, si bien que tester l'égalité sur un seul préfixe écarte discrètement de vrais candidats. Calculez les préfixes voisins au moment de la requête et incluez chaque cellule que votre rayon peut toucher, comme le fait la requête ci-dessus. Les cellules ne sont pas d'égale surface, donc un préfixe est une clé de partitionnement, jamais une distance. Et la ligne internationale de changement de date, comme l'antiméridien, brisera les implémentations spatiales naïves. Les régions polaires aussi, où chaque projection dérive. Testez ces coordonnées dès le premier sprint, parce qu'elles ne paraissent pas exotiques avant de devenir vos données.
Si vos requêtes spatiales recalculent une fonction de distance sur chaque ligne, vous payez une base de données pour relire le million de lignes encore et encore. Calculez le préfixe à l'ingestion, élaguez la partition, et laissez PostGIS ne voir que les candidats qui méritent une mesure.
Le préfixe fait l'élagage. PostGIS fait la vérité.
AngoraTek construit des pipelines qui fusionnent données géo et données métier. [Parlons-en](/fr/#contact) si vous combinez la localisation avec le reste de vos enregistrements.Merci d'avoir lu jusqu'ici. Si cela ressemble au problème sur lequel vous butez, parlez-nous-en. Un fondateur lit chaque message.
Engager la conversation