wandres.dev
EL CATÁLOGO DE BASES LOCALES · y cómo elegir

Un árbol de decisión honesto, tramo por tramo

De guardar cuatro preferencias a consultar un millón de filas con uniones hay cinco tramos, cada uno con una respuesta correcta y con señales inequívocas de que te has pasado de complejidad.

⏱ 19 min

Un árbol de decisión honesto tiene una propiedad que los árboles de decisión promocionales no tienen: la mayoría de sus hojas apuntan a la opción más aburrida. Si se recorre con sinceridad el espectro completo de lo que las aplicaciones guardan en el cliente, desde cuatro preferencias de interfaz hasta un millón de filas consultadas con uniones, resulta que los tramos bajos —donde vive la enorme mayoría del software real— se resuelven con herramientas que nadie presenta en una conferencia, y que la sofisticación solo empieza a pagar cuando concurren varias condiciones a la vez. Este es el cierre del nivel, y su objetivo no es que sepas elegir la herramienta más potente sino que sepas reconocer, con señales concretas y verificables, el momento exacto en que te has pasado de complejidad y el momento exacto en que te has quedado corto.

🎯 Al terminar esta lección sabrás
  • Recorrer los cinco tramos del espectro y saber cuál es la respuesta correcta en cada uno.
  • Reconocer las condiciones que hay que cumplir simultáneamente para que un salto de tramo compense.
  • Identificar las señales tempranas de exceso de complejidad y las de haberse quedado corto.
  • Planificar la rectificación en ambas direcciones sin que implique reescribir la aplicación.

Los cinco tramos

El espectro se ordena por dos magnitudes combinadas: cuántos datos tocas y qué clase de preguntas les haces. Los tramos no son una escala de calidad, son cinco regímenes con físicas distintas. Un detalle importante antes de recorrerlos: las fronteras entre tramos no están en cifras redondas de registros sino en cambios cualitativos de régimen, y por eso no las verás enunciadas aquí como umbrales exactos. Se cruza de tramo cuando la operación que antes era trivial deja de serlo, y ese momento depende del dispositivo, del tamaño de los registros y de la forma de las consultas mucho más que de un número universal que alguien pueda publicar.

🔧

Tramo 1 · Preferencias y borradores

Unas decenas de claves, ninguna consulta, ninguna relación. Un almacén de clave-valor síncrono basta y sobra, con la precaución de no bloquear el hilo con volúmenes que no son de su liga. Instalar aquí una base de datos con esquema es la forma más común de complejidad injustificada, y la más difícil de deshacer porque nadie la percibe como un error.

📇

Tramo 2 · Colecciones que crecen

Miles de registros, lecturas por clave y filtros por uno o dos campos conocidos de antemano. Es el territorio natural de IndexedDB, con un envoltorio fino si lo que te duele es la ergonomía y con una capa más gruesa si lo que te duele es no enterarte de los cambios. Aquí vive la mayoría de las aplicaciones que se creen del tramo 4.

Tramo 3 · Estado vivo y compartido

Decenas de vistas que reaccionan, varios contextos escribiendo, y un conjunto de trabajo que cabe cómodamente en memoria. Un almacén reactivo con persistencia como adaptador convierte todo el código de sincronización de interfaz en algo que no escribes. El precio, aceptado con los ojos abiertos, es un techo de volumen fijado el primer día.

🗃️

Tramo 4 · Modelo relacional de verdad

Cientos de miles de filas, entidades que se recorren en ambos sentidos y consultas que el usuario compone. Aquí sí hace falta un planificador, y un motor SQL compilado a WebAssembly sobre un sistema de ficheros virtual serio dentro de un worker es la respuesta. Es también el primer tramo donde el coste operativo deja de ser despreciable.

📊

Tramo 5 · Exploración analítica

Millones de filas homogéneas, de solo lectura, agregadas y cruzadas por un humano que decide la siguiente pregunta mirando la respuesta anterior. Motor columnar vectorizado, ingestión en formato columnar y, si procede, consulta directa de ficheros publicados. Convive con un almacén transaccional, no lo sustituye.

💡
La distribución real está sesgada hacia abajo

Los saltos entre tramos tampoco son todos iguales de caros. Del primero al segundo se sube casi sin fricción, porque el modelo mental apenas cambia. Del segundo al tercero se cambia dónde vive la verdad, que es una decisión arquitectónica pero reversible. Del tercero o el segundo al cuarto se cambia de motor, de contexto de ejecución y probablemente de modelo de datos a la vez, y ese es el salto caro del espectro. Y el quinto no es un salto sino una adición: se llega a él sumando una pieza, no sustituyendo la que había. Saber cuál de estos cuatro movimientos tienes por delante importa tanto como saber a qué tramo perteneces, porque determina si conviene anticiparlo o esperar a necesitarlo.

Si tuvieras que apostar sobre una aplicación desconocida, la apuesta racional es tramo 2. Casi todo el software que guarda datos en el cliente guarda colecciones moderadas con consultas previsibles, y casi todo el material que se escribe sobre el tema habla de los tramos 4 y 5, porque son los interesantes de contar. Ese desajuste entre lo que se publica y lo que se construye es la causa más frecuente de sobredimensionamiento, y conviene tenerlo presente al leer cualquier recomendación, incluida esta.

El árbol

flowchart TB
Q0{Habra una segunda copia sincronizada} -->|si| SYNC[Elige primero el motor de replica]
Q0 -->|no o todavia no| Q1{Cuantos registros toca una sesion}
Q1 -->|decenas| T1[Clave valor simple]
Q1 -->|miles| Q2{Las consultas son rangos sobre indices}
Q2 -->|si| T2[IndexedDB con envoltorio]
Q2 -->|no| Q3{El conjunto cabe en memoria}
Q3 -->|si| T3[Almacen reactivo con persistencia]
Q3 -->|no| T4[Motor SQL en WebAssembly en un worker]
Q1 -->|cientos de miles o mas| Q4{Escrituras frecuentes por fila}
Q4 -->|si| T4
Q4 -->|no, solo lectura y agregacion| T5[Motor columnar vectorizado]
style SYNC fill:#cba6f7,color:#11111b
style T2 fill:#a6e3a1,color:#11111b
style T4 fill:#89b4fa,color:#11111b

Un árbol así invita a recorrerse una vez y archivarse, y ese es exactamente el uso incorrecto. Su valor no está en la hoja a la que llegas hoy sino en el registro de por qué respondiste lo que respondiste en cada bifurcación, porque son esas respuestas —y no la conclusión— las que caducan. Anota junto a cada rama la cifra o la frase que la justificó y la fecha, y el árbol se convierte en un instrumento reutilizable: dentro de un año no habrá que rehacer el razonamiento entero, bastará con comprobar cuál de las respuestas ha dejado de ser cierta.

Obsérvese que la primera bifurcación no es de volumen sino de sincronización, y que su rama afirmativa sale del árbol. No es un descuido: si va a haber una segunda copia, el motor de réplica impone modelo de datos, identidad y versionado, y todas las demás preguntas quedan subordinadas a esa elección. Zero, por ejemplo, describe su diseño alrededor de un almacén cliente local y normalizado en el que las lecturas y escrituras aterrizan primero y que se sincroniza de forma continua por debajo, con lo que se sincroniza determinado por las consultas que escribes en el código en lugar de por reglas estáticas o por tablas enteras. En un planteamiento así, la pregunta qué base local uso simplemente no se formula, y responderla antes de tiempo es trabajo tirado.

Las otras dos bifurcaciones del árbol también merecen un comentario, porque son las que más se responden mal. La pregunta sobre si las consultas son rangos sobre índices se contesta casi siempre que sí, y casi siempre es falso: basta con que una sola vista de la aplicación necesite ordenar por un campo distinto del que filtra para que la respuesta correcta sea que no. Y la pregunta sobre si el conjunto cabe en memoria se contesta pensando en el total actual, cuando lo que hay que estimar es el total del usuario del percentil alto dentro de un año, en el dispositivo más modesto que soportes. Ambas preguntas tienen la misma patología: se responden con el caso cómodo en la cabeza, y el árbol solo es útil si se responden con el caso incómodo.

⚠️
La rama de sincronización se recorre una sola vez, y hacia adelante

Migrar de un almacén propio a un motor de sincronización rara vez es una migración: suele ser una reescritura de la capa de datos, porque el motor necesita controlar cómo se identifican las entidades, cómo se versionan los cambios y en qué orden se aplican, y eso no se puede injertar sobre un esquema que no lo previó. Por eso la pregunta va la primera. Y si la respuesta honesta es todavía no, pero probablemente sí, la decisión correcta no es adelantar la adopción sino mantener la capa de datos detrás de una interfaz de dominio estrecha, de modo que el día que haya que cambiar el motor, la aplicación no se entere.

Las señales de que te has pasado

El exceso de complejidad no produce errores, y esa es precisamente la razón de que sea difícil de detectar. Un sistema sobredimensionado funciona: da respuestas correctas, pasa las pruebas y sostiene el producto. Lo que produce es fricción constante que el equipo naturaliza hasta dejar de verla, del mismo modo en que se deja de oír un ventilador ruidoso. Hay cuatro señales que lo delatan y todas son observables sin instrumentar nada.

🐌

El arranque domina al trabajo

El usuario espera a que se inicialice un motor completo para luego pedirle algo que una lectura de clave-valor habría resuelto de inmediato. Cuando el tiempo de puesta en marcha es varias veces el de la primera consulta real, estás pagando una máquina cuya capacidad tu carga de trabajo no usa.

🤷

Nadie sabe explicar la elección

Preguntado por qué está lo que está, el equipo responde con una referencia a una entrada de blog, a un proyecto anterior o a lo que usaba alguien que ya no está. La ausencia de un motivo formulable en dos frases no es un fallo de memoria: casi siempre significa que el motivo nunca existió.

🕸️

Tablas que nadie cruza

El esquema declara relaciones que ninguna consulta recorre jamás, porque se modeló un dominio relacional donde en realidad había documentos independientes. El coste no es el espacio: es que cada escritura mantiene una estructura que solo existe para satisfacer un diagrama.

🧯

Una capa de traducción propia

Entre el modelo que impone la librería y el que necesita la aplicación hay código tuyo que convierte de uno a otro en ambos sentidos. Esa capa es la factura literal del sobredimensionamiento, crece sola con cada funcionalidad y es donde acaban viviendo los errores más difíciles de diagnosticar.

// Sintoma medible: cuanto se paga antes de poder responder algo util.
const t0 = performance.now();
await inicializarAlmacen();
const listo = performance.now() - t0;

const t1 = performance.now();
await primeraConsultaReal();
const consulta = performance.now() - t1;

// Si "listo" es varias veces "consulta", estas pagando un motor
// que tu carga de trabajo no usa.
registrar({ listo, consulta, ratio: listo / consulta });
📝
El sobredimensionamiento tiene un coste que no aparece en ninguna métrica

Ninguna de las cuatro señales produce una alerta, un error ni una regresión de rendimiento medible, y por eso el exceso de complejidad sobrevive años en repositorios donde todo el mundo está descontento con la capa de datos y nadie sabe articular por qué. El coste real se paga en una moneda que no se instrumenta: el tiempo que tarda una persona nueva en poder tocar los datos sin miedo, el número de cambios que se posponen porque implican una migración, y la cantidad de decisiones de producto que se descartan en la conversación previa porque eso sería complicado con lo que tenemos. Cuando revises esta lista, cuenta también esas tres cosas, aunque sea a ojo, porque suelen ser mayores que todo lo demás junto.

La señal contraria, la de haberse quedado corto, es más fácil de reconocer porque duele: aparecen bucles que leen de una colección las claves encontradas en otra, funciones con nombres como filtrarEnMemoria, índices compuestos nuevos en cada iteración del producto, y consultas cuyo tiempo crece de forma visible con el volumen del usuario. Cuando esas cuatro cosas coinciden, el diagnóstico no admite discusión: falta un planificador.

Hay además una asimetría entre los dos diagnósticos que conviene tener presente al decidir cuál investigar primero. Quedarse corto se manifiesta como dolor agudo y localizado —una vista concreta que tarda, un usuario concreto que se queja— y por eso se atiende, aunque sea tarde. Pasarse se manifiesta como una fiebre baja repartida por todo el proyecto, y por eso no se atiende nunca. En la práctica esto significa que el sesgo del equipo, si no se corrige deliberadamente, es a acumular complejidad sin retirarla jamás: los saltos hacia arriba tienen quien los reclame y los de vuelta no tienen a nadie. Poner la revisión del perfil en el calendario, con fecha y responsable, es la única contramedida que funciona, porque convierte la retirada en una tarea prevista en lugar de en una confesión.

Conviene tomarse en serio la posibilidad de que las dos señales aparezcan a la vez, porque es más común de lo que parece y desconcierta a quien la encuentra. Una aplicación puede estar sobredimensionada en una parte de sus datos e infradimensionada en otra: un motor completo administrando cuatro preferencias que se leen al arrancar, y a la vez uniones a mano sobre la colección que de verdad importa. Cuando eso ocurre, el diagnóstico no es estamos en el tramo equivocado sino estamos tratando dos problemas distintos con la misma herramienta, y la solución no es moverse de tramo sino separar los dos conjuntos de datos y dejar que cada uno viva donde le corresponde. Un origen puede perfectamente tener un almacén de clave-valor para lo trivial y un motor serio para lo que lo merece.

Cómo se rectifica

Lo que hace tolerable equivocarse en este nivel no es acertar a la primera sino haber preservado la capacidad de rectificar, y esa capacidad tiene un solo requisito: que la aplicación no hable con la base de datos, sino con un vocabulario de dominio propio. Si el resto del código pide documentosRecientesDe(autor) en vez de componer rangos sobre índices o escribir SQL en el sitio donde se pinta, cambiar de tramo se convierte en reescribir una carpeta. Si no, se convierte en tocar cada componente.

// La frontera que abarata la mudanza: un vocabulario del negocio.
// datos/documentos.js  -- lo unico que sabe que existe una base.
export async function documentosRecientesDe(autorId, desde) { /* ... */ }
export async function guardarDocumento(doc) { /* ... */ }
export function observarDocumentosDe(autorId, alCambiar) { /* ... */ }

// El resto de la aplicacion no importa nunca la libreria de datos,
// no compone rangos, no escribe SQL y no conoce el nombre de ningun indice.
import { documentosRecientesDe } from "../datos/documentos.js";

La prueba de que la frontera existe de verdad es mecánica y se puede automatizar: busca en todo el repositorio los ficheros que importan la librería de datos. Si son más de los que caben en una carpeta, la frontera no existe todavía, por mucho que el diagrama de arquitectura diga lo contrario. Y si existe, ponle una regla de importación que la haga cumplir, porque una frontera que solo vive en la buena voluntad del equipo dura exactamente hasta la primera entrega con prisa.

Rectificar hacia arriba es lo previsto y suele salir bien: se añade el motor nuevo, se migran los datos, se reimplementan las mismas funciones de dominio y se retira el anterior. La única precaución que hay que tomar es no hacerlo de golpe: con la frontera puesta se puede convivir un tiempo con las dos implementaciones, dirigiendo unas funciones al motor nuevo y otras al viejo, y comparando resultados antes de retirar nada. Esa convivencia temporal es incómoda de mantener pero convierte una migración arriesgada en una serie de cambios pequeños y reversibles.

Rectificar hacia abajo, que es lo que exige el sobredimensionamiento, es psicológicamente más difícil y técnicamente más fácil, porque siempre se puede reproducir con menos lo que se hacía con más. La resistencia no es técnica: es que nadie quiere firmar que lo elegido el año pasado sobraba. Ayuda enmarcarlo como lo que es —una medición nueva sobre un producto que ha cambiado, no un juicio sobre quien decidió entonces con la información de entonces— y ayuda todavía más haber escrito en su momento el perfil con cifras, porque entonces la conversación deja de ser sobre gustos y pasa a ser sobre qué número se movió.

La complejidad correcta es la mínima que sostiene tus preguntas de hoy y una migración prevista

Queda una última idea que ordena todo lo anterior y que conviene enunciar sin adornos, porque es la que separa a quien elige bien de quien elige llamativo. La pregunta que casi todo el mundo se hace al llegar aquí es cuál de estas opciones aguantará el crecimiento futuro, y esa pregunta, formulada así, empuja sistemáticamente hacia arriba: como nadie sabe cuánto va a crecer nada, y como equivocarse por defecto parece más grave que equivocarse por exceso, la respuesta prudente parece ser siempre elegir el tramo siguiente al que necesitas. El razonamiento es seductor y es falso, por tres motivos que se refuerzan entre sí. El primero es que el coste del exceso no se paga una vez en la elección sino continuamente en cada iteración del producto: cada arranque más lento, cada esquema que hay que migrar, cada desarrollador que tarda una semana en entender la capa de datos, cada error que ocurre dentro de una máquina que nadie del equipo domina del todo. El segundo es que el crecimiento que se temía llega mucho menos veces de lo que se cree, y cuando llega casi nunca llega por donde se anticipó: el volumen se dispara en una colección que no era la vigilada, o lo que crece no es el volumen sino la complejidad de las preguntas, o el producto pivota y la mitad de los datos deja de existir. Optimizar para un futuro concreto imaginado es, casi siempre, optimizar para un futuro que no ocurre. Y el tercero, el decisivo, es que la migración entre tramos es barata si se ha preservado la frontera, y esa preservación cuesta muchísimo menos que el tramo de más: una carpeta con funciones de dominio, un vocabulario propio, ninguna consulta escrita en un componente. Con esa frontera puesta, la elección correcta deja de ser una apuesta sobre el futuro y pasa a ser una descripción del presente, que es algo que sí puedes conocer. La regla que se deduce es incómoda de aceptar y sencilla de aplicar: elige el tramo más bajo que sostenga las preguntas que tu aplicación hace hoy, mide el eje que podría moverte de tramo, ponle un umbral escrito, y dedica el esfuerzo que te has ahorrado a construir la frontera que hará barata la mudanza. Eso no es conservadurismo ni falta de ambición técnica; es la única estrategia que produce sistemas que siguen siendo modificables dentro de tres años, que es el plazo en el que se juzga de verdad una decisión de arquitectura.

⚔️ Sitúa tu proyecto en el árbol y prepara la mudanza
  1. Recorre el árbol con tu proyecto real y escribe en qué hoja cae, respondiendo la primera bifurcación con total honestidad.
  2. Comprueba las cuatro señales de exceso de complejidad y anota cuántas se cumplen hoy en tu código.
  3. Mide el tiempo de inicialización frente al de la primera consulta útil y decide si el ratio es defendible.
  4. Busca las funciones que filtran en memoria y los bucles que unen a mano: son el diagnóstico contrario y también hay que anotarlo.
  5. Extrae toda consulta que hoy viva dentro de un componente a una capa de funciones de dominio con nombres del negocio.
  6. Elige el eje que te movería de tramo, ponle un umbral numérico escrito y define qué harás exactamente el día que se cruce.