wandres.dev
AUTOMERGE · documentos e historia

El almacenamiento columnar de la versión 3 y por qué cambia todo

El formato columnar agrupa los metadatos por campo en lugar de por operación, y la versión 3 lo llevó del disco al tiempo de ejecución, con una reducción de consumo de más de un orden de magnitud.

⏱ 22 min

Durante años la objeción decisiva contra Automerge no fue teórica sino aritmética: consumía demasiada memoria. Un documento con historia larga podía necesitar cientos de megabytes al abrirse, y algunos sencillamente no llegaban a abrirse nunca. La objeción era legítima y el equipo no la disputó; lo que hizo fue tardar en resolverla, porque la solución exigía rehacer la representación interna entera. La versión 3, publicada a mediados de 2025, es esa reescritura. Su titular es una reducción de consumo de memoria de más de un orden de magnitud, con casos donde el factor es muchísimo mayor, y su detalle técnico es una idea que no es nueva en absoluto —el almacenamiento columnar, prestado de las bases de datos analíticas— aplicada en un sitio donde no se había aplicado. Esta lección explica qué es exactamente esa técnica, cuál fue el cambio concreto respecto de la serie 2.x, qué cifras reales se obtuvieron y qué las hizo posibles.

🎯 Al terminar esta lección sabrás
  • Explicar qué es el almacenamiento columnar y por qué comprime tanto mejor que el almacenamiento por filas.
  • Situar con precisión qué cambió en la versión 3 y qué se mantuvo intacto respecto de la serie anterior.
  • Citar las cifras reales de la mejora y atribuir cada una a su mecanismo.
  • Traducir la mejora a decisiones concretas sobre el dimensionamiento de tus documentos.

Qué es el almacenamiento columnar

La idea es vieja y viene del mundo analítico. Cuando tienes muchos registros con los mismos campos, puedes guardarlos de dos maneras. Por filas significa poner juntos todos los campos de un registro, después todos los del siguiente, y así. Por columnas significa poner juntos todos los valores del primer campo de todos los registros, después todos los valores del segundo campo, y así.

// Cuatro operaciones, dos disposiciones posibles.

// Por filas: cada operacion completa, una detras de otra.
// [actorA, 1, insertar, "h"] [actorA, 2, insertar, "o"]
// [actorA, 3, insertar, "l"] [actorB, 1, borrar,   null]

// Por columnas: cada campo agrupado con sus iguales.
// actores:   [actorA, actorA, actorA, actorB]
// secuencia: [1, 2, 3, 1]
// tipos:     [insertar, insertar, insertar, borrar]
// valores:   ["h", "o", "l", null]

La disposición por columnas no ahorra nada por sí sola: los mismos datos ocupan los mismos bytes. Lo que ahorra es lo que hace posible después, porque cada columna contiene valores del mismo tipo y con distribuciones muy sesgadas, y eso es exactamente lo que los algoritmos de compresión saben explotar. La columna de actores de un documento editado por tres personas es una repetición masiva de tres valores, y se codifica por longitud de repetición hasta quedar en casi nada. La columna de secuencia es una sucesión que crece de uno en uno, y se codifica por diferencias hasta quedar en un valor inicial y un patrón. La columna de tipos tiene una cardinalidad de cinco o seis valores posibles. La única columna que resiste es la de contenido real, que es precisamente la que lleva la información del usuario. Ese reparto tan desigual es lo que explica que el factor de mejora sea tan grande y, a la vez, tan variable de un documento a otro.

flowchart TD
A[operaciones del documento] --> B[disposicion por columnas]
B --> C[columna de actores]
B --> D[columna de secuencias]
B --> E[columna de tipos]
B --> F[columna de valores]
C --> G[codificacion por repeticion]
D --> H[codificacion por diferencias]
E --> G
F --> I[apenas se comprime porque es el dato real]
style G fill:#a6e3a1,color:#11111b
style H fill:#a6e3a1,color:#11111b
style I fill:#f9e2af,color:#11111b

Aquí está la clave de por qué esta técnica encaja tan bien con un CRDT y no es un truco genérico. El problema de un documento con historia completa es que los metadatos superan con mucho al contenido: por cada carácter que un usuario teclea hay un identificador de actor, un número de secuencia, un tipo de operación y una referencia posicional. Es decir, la parte que domina el tamaño es justamente la parte que se comprime hasta desaparecer, mientras que la parte incompresible es la minoritaria. La técnica ataca exactamente donde está el peso.

Hay una segunda ventaja de la disposición por columnas que importa tanto como la compresión y que se menciona menos. Al recorrer una columna estás leyendo memoria contigua de un solo tipo, lo cual es el patrón de acceso que mejor aprovecha la caché del procesador y el que permite tratar muchos valores por instrucción. La disposición por filas obliga a saltar entre campos de tipos distintos y desperdicia cada línea de caché que trae. Por eso las mejoras de esta familia de técnicas suelen aparecer duplicadas en los informes: menos memoria y, además, más velocidad de recorrido, y no son dos logros independientes sino dos caras de la misma decisión.

Lo que cambió en la versión 3

Aquí conviene ser muy preciso, porque el titular se cuenta mal con frecuencia. La serie 2.x ya usaba un formato columnar comprimido para guardar y transmitir los metadatos. El formato en disco era compacto y no era el problema. El problema estaba en otro sitio: cuando el documento se cargaba para editarlo, la biblioteca lo expandía a una representación descomprimida en memoria, porque era la única sobre la que sabía operar. El coste en disco era razonable y el coste en memoria se disparaba.

Lo que hace la versión 3 es eliminar esa expansión. La biblioteca se rearquitecturó para operar directamente sobre la representación comprimida en tiempo de ejecución, sin materializarla. Dicho de otro modo: el cambio no está en cómo se guardan los datos, sino en que ya no hay dos formas de tenerlos.

// Serie 2.x: dos representaciones y una frontera cara.
//
//   disco  [columnar comprimido]  pequeno
//     |
//     v  materializar al abrir  <-- aqui estaba el problema
//   memoria [operaciones expandidas]  enorme
//
// Serie 3.x: una sola representacion.
//
//   disco  [columnar comprimido]  pequeno
//     |
//     v  leer
//   memoria [columnar comprimido]  pequeno
//
// El formato de archivo es el mismo en ambas series.

Que la frontera desaparezca tiene un efecto de segundo orden que conviene anticipar. En la serie anterior, el consumo de memoria dependía del tamaño de la historia completa aunque solo quisieras leer el documento; ahora depende de lo que realmente toques. Abrir un documento para consultarlo, que es con diferencia el caso más frecuente en cualquier aplicación real, dejó de pagar el precio del caso más caro. Es el mismo tipo de reordenación que veíamos en el nivel anterior con los algoritmos basados en registros de eventos, obtenida aquí por otra vía.

ℹ️
El formato de archivo no cambió, y esa es la mitad más impresionante del trabajo

Automerge 3 usa el mismo formato de archivo que Automerge 2 y su API es casi enteramente compatible hacia atrás. Un documento escrito con la serie anterior se abre sin conversión ni migración. La única ruptura real está en el manejo de texto: se eliminó la clase que representaba texto colaborativo, las cadenas nativas pasaron a ser el caso por defecto y la clase de texto no colaborativo se renombró a un nombre más descriptivo. Que una reescritura de la representación interna de esta profundidad no obligara a cambiar el formato es la validación práctica de lo que decía la primera lección: lo demostrado se quedó quieto mientras lo negociable se rehacía entero.

Las cifras y qué las hizo posibles

Las mediciones publicadas son deliberadamente concretas y conviene citarlas tal cual, sin redondearlas al alza. El caso de referencia es pegar el texto completo de Moby Dick dentro de un documento: en Automerge 2 eso consumía unos 700 MB de memoria, y en Automerge 3 consume unos 1,3 MB. El titular general que el equipo defiende es más conservador que ese caso concreto: una reducción de más de diez veces, a veces bastante más.

La segunda cifra es la que mejor traduce la mejora a experiencia de usuario, porque no habla de bytes sino de tiempo. Documentos con historias muy largas que no habían terminado de cargar después de diecisiete horas pasaron a abrirse en nueve segundos. Ese salto no es una aceleración del mismo trabajo: es la desaparición de un trabajo que ya no hay que hacer, porque no queda nada que descomprimir ni que materializar al abrir.

🧠

Memoria, más de diez veces menos

De unos 700 MB a unos 1,3 MB en el documento de referencia, por operar sobre la representación comprimida sin expandirla.

⏱️

Carga, de horas a segundos

Documentos que no abrían tras diecisiete horas pasan a abrir en nueve segundos, porque se elimina la fase de materialización.

🗄️

Hexane, el motor columnar

El almacenamiento columnar vive en un motor propio, reescrito a fondo, con mejoras de entre dos y nueve veces según el banco de pruebas.

🔢

Enteros de longitud variable

Las columnas numéricas se apoyan en códecs canónicos por construcción, con codificación en zigzag para los valores con signo.

Merece la pena desglosar qué hizo posible el resultado, porque son tres cosas distintas y solo la primera es la idea principal. La primera es operar sobre la representación comprimida, que es lo que produce el salto de orden de magnitud. La segunda es el motor columnar sobre el que se apoya todo, que es un componente independiente reescrito por completo en su versión uno, con una interfaz pública más limpia y ganancias de entre dos y nueve veces según qué se mida: las lecturas puntuales de enteros mejoran algo menos de cuatro veces, el guardado con codificación por diferencias mejora entre siete y nueve, y la iteración sobre columnas booleanas más de siete. La tercera es el trabajo fino de codificación de enteros de longitud variable, canónicos por construcción, que es donde se recortan los últimos bytes de cada columna numérica.

Hay un cuarto elemento que no vive en la biblioteca del núcleo sino en la capa de repositorio, y que es el que de verdad controla el tamaño de lo que guardas en el disco del cliente. Cada cambio se escribe primero como un fragmento incremental, y periódicamente el repositorio decide compactar: toma todos los fragmentos acumulados, los funde en una única instantánea y borra los originales que había cargado. La clave con la que se guarda esa instantánea incluye las cabezas del documento en el momento de compactar, lo que hace la operación segura entre procesos concurrentes sin necesidad de ninguna transacción. Sin compactación, guardar cada cambio por separado sería inviable; con ella, el coste en disco se mantiene cerca del mínimo que el formato permite.

Merece la pena entender por qué esa clave está construida así, porque es una solución elegante a un problema que en otros sistemas exige transacciones. Si el identificador de la instantánea son las cabezas del documento en el momento de compactar, entonces dos procesos que compacten a la vez con el mismo conjunto de cambios escribirán bajo la misma clave contenido equivalente, y sobrescribirse mutuamente es inofensivo. Y como cada proceso solo borra los fragmentos incrementales que él mismo había cargado, ninguno puede destruir trabajo que no haya incorporado. El resultado es un almacén seguro frente a concurrencia que se puede implementar sobre un directorio de archivos, un depósito de objetos remoto o una base de datos, sin pedirle a ninguno de los tres ninguna garantía transaccional.

💡
Vigila el crecimiento con las estadísticas del documento, no con el tamaño del archivo

El número de operaciones es mejor indicador de salud que los bytes, porque los bytes ya están comprimidos y esconden la tendencia. La biblioteca expone estadísticas internas con el recuento de cambios, de operaciones y de actores distintos. Registrar esas tres cifras en tu telemetría, junto con el tiempo de apertura, te da una señal temprana de que un documento se está haciendo demasiado grande mucho antes de que un usuario lo note. Si el recuento de actores crece sin techo, revisa cómo estás asignando identificadores de sesión.

La compresión dejó de ser un formato de archivo para convertirse en un modelo de ejecución, y ese es el patrón que hay que llevarse

Lo que ocurrió en la versión 3 tiene un nombre más general que la anécdota de Automerge, y reconocerlo te va a servir en sistemas que no tienen nada que ver con los CRDT. Durante décadas la compresión se entendió como una operación de frontera: los datos viven comprimidos en reposo, y en el momento de usarlos se descomprimen a una representación cómoda sobre la que el código sabe operar. Ese contrato es tan universal que casi nadie lo cuestiona, y tiene una consecuencia que suele pasar inadvertida hasta que duele: el consumo de memoria de un sistema no lo determina el tamaño de sus datos, sino el tamaño de la representación con la que su código sabe trabajar. Automerge 2 guardaba Moby Dick en un archivo perfectamente compacto y necesitaba 700 MB para editarlo, y ninguna mejora del formato de archivo habría cambiado eso ni un byte, porque el problema no estaba en el archivo. Lo que hace la versión 3 es mover la frontera hasta hacerla desaparecer: en lugar de descomprimir para operar, se enseña al código a operar sobre lo comprimido. Eso exige mucho más trabajo —hay que reescribir cada acceso, cada búsqueda, cada iteración para que entienda la codificación por repeticiones y por diferencias, y hay que hacerlo sin perder rendimiento en las operaciones puntuales— y a cambio elimina de golpe una clase entera de problemas: desaparece el pico de memoria al abrir, desaparece el tiempo de materialización, desaparece la asimetría entre lo que cabe en disco y lo que cabe en RAM. Es exactamente la misma jugada que hicieron los motores analíticos columnares al aprender a filtrar y agregar sin descodificar, y la misma que hacen los formatos de datos modernos al permitir consultas sobre el buffer tal cual llega de la red. El patrón, enunciado para que puedas aplicarlo: cuando un sistema consume mucha más memoria que el tamaño de sus datos, el problema casi nunca es la cantidad de datos sino la existencia de dos representaciones, y la pregunta correcta no es cómo comprimir mejor sino si el código puede aprender a trabajar directamente sobre la forma compacta. La segunda mitad de la observación es igual de importante y más incómoda: esa reescritura solo fue abordable porque la corrección estaba demostrada por fuera de la representación. Cuando la garantía de un sistema depende de cómo están dispuestos los bytes, nadie se atreve a reordenarlos; cuando depende de un argumento sobre las operaciones, la disposición se vuelve un detalle que se puede optimizar sin miedo durante diez años seguidos. La libertad de optimizar se compra antes, y se compra separando lo que se demuestra de lo que se implementa.

Qué significa esto para tu aplicación

La consecuencia práctica más importante es que la regla de dimensionamiento que circulaba durante la serie 2.x quedó obsoleta y sigue repitiéndose. Los umbrales que la comunidad manejaba —troceen los documentos, no pasen de tantos miles de operaciones, hagan instantáneas y trunquen la historia— nacieron de una limitación que ya no existe con la misma severidad. Si evaluaste Automerge hace dos o tres años y lo descartaste por consumo de memoria, la evaluación caducó y merece repetirse con números propios.

Eso no significa que el dimensionamiento haya dejado de importar, sino que el criterio se desplazó. El documento sigue siendo la unidad de sincronización y de carga: se carga entero, se sincroniza entero y se comparte entero, y ningún avance en compresión cambia eso. Lo que era un límite de memoria pasó a ser un límite de granularidad, y ese es un problema de modelado que la lección siguiente aborda de frente.

Queda una nota de versiones para no equivocarse al instalar. El núcleo de la versión 3 es lo que se obtiene por defecto al instalar las versiones actuales de la capa de repositorio y de la integración con interfaces reactivas; si tu proyecto arrastra una versión anterior del núcleo como dependencia transitiva, puedes estar ejecutando la representación antigua sin saberlo. Comprobar qué versión del núcleo está realmente cargada es el primer paso de cualquier medición.

📝
Los documentos patológicos son bienvenidos y hay una herramienta para enviarlos

Si te encuentras con un documento que se comporta mal y no lo puedes compartir porque contiene datos de un usuario, existe un anonimizador que elimina el contenido conservando la estructura exacta: el mismo número de operaciones, los mismos actores, la misma forma del grafo. El equipo pide activamente esos documentos para incorporarlos a sus bancos de pruebas. Merece la pena saberlo antes de pasar dos semanas construyendo un caso reproducible sintético que probablemente no reproduzca lo que te está pasando.

Cierra el nivel una advertencia sobre cómo leer cualquier cifra de este tipo, incluidas las de esta lección. Todas proceden de casos concretos con perfiles concretos, y el factor que obtengas dependerá de la proporción entre metadatos y contenido en tus propios documentos. Un documento con mucho texto y poca historia mejora menos, porque su peso ya estaba en la parte incompresible; uno con historia larga y contenido escaso mejora muchísimo más que el factor anunciado. Medir con tus datos no es una formalidad: es la única forma de saber en qué lado de esa horquilla caes.

⚔️ Mide el cambio de representación con tus propios documentos
  1. Construye un documento grande a partir de una traza de edición real y anota su recuento de operaciones.
  2. Mide la memoria residente y el tiempo de apertura con la serie 2.x, si puedes instalarla en un entorno aparte.
  3. Repite ambas mediciones con la versión 3 y calcula el factor obtenido en cada eje.
  4. Compara el tamaño en disco antes y después de forzar una compactación del repositorio.
  5. Registra el recuento de cambios, operaciones y actores en tu telemetría y define umbrales de alerta.
  6. Verifica qué versión del núcleo carga realmente tu aplicación y comprueba si coincide con la que creías.