BlogLecciones

La llave equivocada

Reconocíamos cada venta por su fila en la hoja y no por la cámara vendida, y así se colaban los duplicados

Nicolás Biondi

Silla vacía de un salón con un documento de identidad sobre el asiento, iluminada entre sillas difusas en la oscuridad.

El dashboard mostraba más unidades de un modelo de cámara que las registradas en los comprobantes. Conté a mano y ganaron los comprobantes: el dashboard contaba la misma cámara dos veces, y lo hacía con total convicción.

Un número inflado engaña justo a quien más lo consulta. Nadie había tocado ese código en meses, y los duplicados se fueron sumando sin hacer ruido.

El índice único nunca rechazó una fila

Mi primera teoría fue la obvia: que la restricción de unicidad, la regla que impide guardar dos veces la misma fila, se había caído en alguna migración. Fui a verificarla y estaba activa. No había rechazado una sola fila en todos esos meses, y por eso nadie sospechaba de ella.

Los registros viven en una hoja de cálculo que un script sincroniza con la base de datos, y ese script armaba la llave de cada venta con la posición de la fila dentro de la hoja.

Cuando un bloque de ventas se volvía a pegar más abajo, porque se movió el orden o porque se copió de otra pestaña, esas filas recibían posiciones nuevas. Con posiciones nuevas el script calculaba llaves nuevas, el índice único comparaba llaves distintas y las dejaba pasar sin un error en los logs.

Hoja de ventas

Bloque pegado más abajo

Llave = posición de fila

Llaves nuevas

Índice único no detecta

Filas duplicadas

El dashboard cuenta doble

Cómo un bloque repegado se convertía en ventas nuevas

Es como contar a los invitados de un evento anotando la silla donde se sentó cada uno: si alguien se levanta y se cambia de sitio, lo cuentas dos veces, porque la silla no te dice quién estuvo ahí. Para contar bien hay que pedirle el documento a cada invitado.

Meme Always Has Been: Espera, ¿la llave era la posición de la fila? / Siempre lo fue Espera, ¿la llave era la posición de la fila? Siempre lo fue

El documento de identidad de una venta

Lo único que identifica a una cámara física es su número de serie. Una misma cámara no puede aparecer dos veces en el mismo comprobante, así que la llave pasó a ser un índice único sobre serie más comprobante.

Lo hice parcial a propósito: las ventas sin número de serie no colisionan entre sí, y una cámara que vuelve por un cambio y se revende con otro comprobante sigue siendo una venta válida. Con la serie sola habría bloqueado casos legítimos.

Las correas no tienen serie

Baterías, filtros y correas entran a la hoja sin número de serie, así que necesitaban otra regla. Probé primero la amplia: si una fila aparecía repetida más abajo, y sus vecinas se repetían a la misma distancia, lo trataba como bloque pegado y lo borraba. La corrí en seco, sin borrar nada, solo contando, y habría borrado ventas legítimas, comprobantes donde alguien compró de verdad tres correas iguales. Fue la regla de borrado más prolija que escribí y también la más equivocada.

La cambié por un corte conservador: borrar un bloque repetido solo si las filas son contiguas y abarcan al menos 2 comprobantes distintos. Que un comprobante tenga tres correas es normal; que varios comprobantes seguidos se repitan idénticos y en el mismo orden es un bloque repegado. Prefiero dejar un duplicado dudoso en la tabla antes que borrar una venta buena.

Una limpieza revisada a mano y un auditor que no borra

Los duplicados salieron en una sola limpieza, revisados uno por uno antes del borrado y sin un falso positivo. El dashboard volvió a cuadrar con los comprobantes.

El script auditor quedó corriendo: recorre la tabla, reporta duplicados candidatos y no borra nada por su cuenta, porque el borrado exige una confirmación escrita a mano. La regla de bloques contiguos es un criterio aproximado, y un criterio aproximado no merece el botón de borrar.

El respaldo con cp salió incompleto

Antes de borrar nada copié la base SQLite con cp, como copio cualquier archivo. SQLite en modo WAL (write-ahead log: un archivo aparte donde la base anota los cambios recientes antes de integrarlos) mantiene las últimas transacciones fuera del archivo principal, así que el respaldo salió sin justo lo que me importaba. Ahora saco los respaldos con VACUUM INTO, el comando de la propia base.

Hoy toda regla de borrado se corre primero en seco, y antes de aceptar una deduplicación reviso si la llave describe el objeto del mundo real o el lugar donde estaba anotado. El dashboard sigue contando con la misma convicción, pero ahora le pide su documento de identidad a cada cámara antes de sumarla: el número de serie.

data-quality deduplication reconciliation sqlite postmortem