Crear un motor Datalog interactivo con Rust
(github.com/frankmcsherry)- A partir de las limitaciones de usabilidad y rendimiento de las herramientas Datalog que quedaron en evidencia en un taller de programación lógica, comenzó el experimento de datatoad, un shell Datalog interactivo basado en Rust
- datatoad apunta a una arquitectura que permita agregar reglas en ejecución y seguir derivando nuevos hechos, y reduce el costo de procesar duplicados en el conjunto de hechos con almacenamiento
columnary capas LSM - La evaluación de reglas convierte el cuerpo de Datalog en un problema de joins y, según si es
stableo no, separa la evaluación completa de la evaluación incremental para evitar joins stable-stable ya calculados - En experimentos con el dataset Graspan, solo con reescritura manual de reglas e introducción de relaciones intermedias, el análisis de aliasing se redujo de 736.34 segundos y 50.13 GB a 119.34 segundos y 5.32 GB
- El trabajo posterior continúa con optimización de planes de join basada en e-graph, layered trie, representación de bytes de ancho fijo, spill a disco, evaluación distribuida, joins en streaming y demand transform
El problema que datatoad busca resolver
- En un taller de programación lógica durante el fin de semana de Memorial Day, se hicieron evidentes las incomodidades de las herramientas Datalog para análisis de programas, y eso motivó intentar una implementación de Datalog simple, útil y rápida
- El objetivo de la implementación no es un ejecutor de ejemplos estáticos, sino un shell Datalog interactivo
- Puede cargar grandes volúmenes de hechos
- Puede agregar nuevas reglas durante la ejecución
- Sigue reflejando en el estado existente los resultados de las reglas agregadas
- El código puede seguirse en el repositorio de datatoad
- El ya existente datafrog ofrece los algoritmos centrales de un motor Datalog, pero requiere que el usuario conecte las piezas manualmente, así que datatoad reorganiza las mismas ideas en una forma más fácil de usar
- En el ejemplo de nullability del grafo de flujo de datos
httpd, datatoad tardó 8.3 segundos con datosVec<String>y consultas no compiladas, unas 4 veces más lento que el ejemplo de datafrog con datos(u32, u32), que ronda los 2 segundos - En el problema de reachability produjo la misma cantidad de tuplas de salida que la implementación de datafrog, pero la verificación general de exactitud todavía no está cerrada
Modelo Datalog y estructura del shell
- Datalog es un lenguaje en el que se escriben reglas lógicas simples y se derivan todos los hechos alcanzables a partir de esas reglas
- Las reglas se componen de head y body
- Ejemplo:
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c). triyedgeson relaciones, ya,b,cson variables- Las variables que aparecen en el head también deben estar en el body
- Ejemplo:
- Los hechos se tratan como reglas con body vacío
- Ejemplo:
edge(1, 2) :- . - Se pueden usar varios heads para escribir varios hechos de una vez
- Ejemplo:
- Debido a la monotonicidad de Datalog, aunque se agreguen reglas o hechos, el conjunto de hechos verdaderos no se reduce, y el mismo conjunto de reglas de entrada llega al mismo resultado sin importar el orden de las reglas
- La representación en Rust se centra en tres estructuras:
Rule,AtomyTermRule { head: Vec<Atom>, body: Vec<Atom> }Atom { name: String, terms: Vec<Term> }Term::Var(String)oTerm::Lit(String)
- Para almacenar literales se usa
Vec<u8>en lugar deString- Las propiedades necesarias son igualdad de literales y algún ordenamiento arbitrario
- Queda abierto al usuario si esos bytes representan
String,(u32, u32)u otro significado
- El estado del intérprete guarda juntas las reglas y los hechos
rules: Vec<Rule>facts: facts::Facts
- Cuando el shell parsea una línea de entrada como Datalog, llama a
State::extendyState::update, y con el comando.listimprime el nombre de cada relación y la cantidad de hechos
Parsing y almacenamiento de hechos
- El parser está en
parse.rsy usa una forma tomada de la gramática de Soufflé - Las variables comienzan con
? - Los tokens se limitan a
.,,,(,),:-,?, y el resto del texto se trata como nombre de atom o term - El tokenizer elimina espacios y reemplaza
:-por←para escanearlo como un solo símbolo - El parsing de reglas lee los atoms del head hasta el turnstile y los atoms del body hasta el punto final
- Un atom se compone de nombre, paréntesis izquierdo, lista de terms y paréntesis derecho
- Si un term tiene
?, es variable; si no, es literal
- Las reglas inválidas devuelven
None, y por ahora no se informa en detalle qué parte está mal - Para agregar reglas negativas haría falta un token
Exclamation, pero todavía no se maneja
Ciclo de vida del conjunto de hechos
- Un almacenamiento simple con
Vec<Vec<String>>acumula asignaciones anidadas y no favorece la gestión de memoria - datatoad usa
columnarpara transformar tipos de Rust en unos pocos bloques de asignación lineal con layout plano- Los bytes de las cadenas, los límites de las cadenas y los límites de los hechos se guardan en arreglos separados
FactContainerenvuelve una lista de hechos ordenada y sin duplicados, e implica mediante un wrapper type el invariante de orden y deduplicación- Como el contenedor columnar es en la práctica append-only y no se adapta bien a cambios intermedios, para agregar nuevos hechos se usa una estructura tipo log-structured merge-tree (LSM)
FactLSM { layers: Vec<FactContainer> }- El tamaño de las capas se administra para que crezca geométricamente
- Se fusionan capas cuyos tamaños estén dentro de un factor de 2 para mantener el estado ordenado y deduplicado
FactBuildermantiene en conjunto un áreaactiveno ordenada y potencialmente duplicada, y unaslayersordenadas y deduplicadas- Los hechos de cada relación pasan por tres etapas
to_add: hechos recién llegados cuya novedad todavía no fue verificadarecent: hechos distintos que aún deben procesarsestable: hechos distintos que ya fueron procesados por completo
FactSet::advancemueverecentastabley elimina deto_addlos hechos que ya están enstablepara crear un nuevorecent
La evaluación de reglas como problema de joins
- El body de una regla Datalog puede verse como un equi-join de base de datos relacional
- El ejemplo de la regla de triángulo es el siguiente
tri(?a, ?b, ?c) :- edge(?a, ?b), edge(?b, ?c), edge(?a, ?c).
- Enumerar directamente todas las asignaciones de variables sería demasiado, aunque sea finito, así que se ordenan y fusionan usando como base las columnas clave de las variables compartidas
- La implementación reduce el body de derecha a izquierda
- Une las dos últimas relaciones para crear una relación intermedia y luego vuelve a unirla con la relación de la izquierda
- Si solo hay un atom en el body, solo transforma la forma al head
JoinPlancontiene la siguiente informaciónbodys, que reordena y filtra los atoms del body para adaptarlos al joinjoins, que guarda la aridad de clave de cada join intermedio y la proyección de salidaheads, que representa las coordenadas o literales que se insertarán en el atom del head- La aridad del join final que genera el head
- El plan actual es un right-linear join plan simple
- Al crear
JoinPlan, se usan las posiciones de aparición más a la izquierda y más a la derecha de cada variable para decidir hasta cuándo conservar cada valor de variable, y se dividen las columnas del body atom en columnas dead, key y value - La función central es
implement_plan(rule, plan, pos, stable, facts)- Cuando se agrega una regla nueva, empieza sobre todos los hechos con
stable = true - En la aplicación repetida de reglas existentes, calcula solo nuevas derivaciones con
stable = false
- Cuando se agrega una regla nueva, empieza sobre todos los hechos con
Join incremental y merge join
- El join es bilineal, así que se descompone así
(A + a) ⋈ (B + b) = A ⋈ B + A ⋈ b + a ⋈ B + a ⋈ b
A ⋈ B, que ya se construyó entre conjuntosstable, no necesita recalcularse- Si solo se necesitan nuevas derivaciones, basta con hacer tres joins
A ⋈ ba ⋈ Ba ⋈ b
join_withincluye o excluye el join stable-stable según la banderastable- El
joinreal es un merge join que recorre secuencialmente dos entradas ordenadas- Si las claves coinciden, llama a
actionpara todas las combinaciones de esa clave - Si las claves difieren, usa
galloppara saltar rápido hasta el siguiente punto de coincidencia posible
- Si las claves coinciden, llama a
gallopes una idea tomada de EmptyHeaded: avanza exponencialmente mientras se cumple una condición monótona y luego acota la búsqueda de forma binaria
Experimento de análisis de nullability
- Los datos del experimento provienen del proyecto Graspan y también siguen disponibles en Google Drive
- La entrada del análisis de flujo de datos tiene dos relaciones:
eynn(?a, ?b): el valor?apuede escribirse en la posición?be(?a, ?b): el valor de una posición?apuede moverse a otra posición?b
- La regla de reachability es la siguiente
n(?a, ?c) :- n(?a, ?b), e(?b, ?c) .
- En la entrada
httpd, el.listinicial muestra lo siguientee: 9,905,624n: 138,331
- Ejecutar la regla directamente toma unos 15 segundos, y
nllega a 9,393,283 elementos - Una de las razones de la lentitud es que la relación temporal
.temp-0-0-in, que reordenanpara ajustarlo a la clave del join, crece hasta 9,393,283 elementos - Si el usuario reescribe la regla, el rendimiento mejora
m(?loc, ?val) :- n(?val, ?loc) .m(?loc, ?val) :- m(?mid, ?val), e(?mid, ?loc) .
- Con la misma entrada
httpd, tras la reescritura la segunda regla se ejecuta en unos 8.43 segundos
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| datatoad | 8.43s | 24.33s | 55.01s |
| datafrog | 1.30s | 4.06s | 8.03s |
- La cifra de Graspan para
lnx_kernellleva*: el artículo reportó el tiempo total junto con otros datos, y por conflictos de identificadores de entrada es difícil considerarlo exactamente la misma ejecución - Si se quiere comparar con una herramienta que use gente de práctica, quizá Soufflé sea un objetivo más adecuado
Análisis de aliasing y optimización manual
- El segundo análisis es el análisis de aliasing de Zheng y Rugina que Graspan tomó
- Hay dos relaciones de entrada
A(?val, ?loc):?loc <- ?valD(?val, ?loc):?locse usa en la forma*?val
- El objetivo es obtener alias de memoria y alias de valor
- alias de memoria: dos expresiones lvalue pueden apuntar a la misma ubicación de memoria
- alias de valor: dos expresiones pueden evaluarse al mismo valor de puntero
- En la notación del artículo aparecen
^T,^?y^*^T: transposición de relación^?: término opcional^*: repetición de cero o más veces
- En Datalog,
^?se expresa dividiéndolo en dos reglas, y^*se maneja agregando explícitamente una relación identidad - La ejecución inicial de las reglas tarda mucho
- Tras ingresar las dos últimas reglas de inicialización de identidad, tarda 686.57 s y 736.34 s
- El proceso usa 50.13 GB
V: 361,947,256M: 92,806,768F: 2,669,647
- Al usar explícitamente las relaciones transpuestas
-V,-M,-ay-d, se elimina la relación temporal-in- Total: 815.92 s, alrededor de 13.6 min
- Memoria: 31.96 GB
- El enfoque de Zheng y Rugina es demand-driven y en la práctica solo se necesita
M, así queVse integra enM- Ya no se construyen los 361,947,256 elementos de
-V - La memoria baja a 18.96 GB
- Ya no se construyen los 361,947,256 elementos de
- Se le da el nombre
Fda un fragmento de join repetido para reutilizarlo, y después se calcula directamenteFden lugar deF, reduciendo también el problema de la identidad - La forma final, con una relación intermedia
MFd, produce estos resultados- Tiempo de ejecución: 119.34 s
- Memoria: 5.32 GB
-M: 92,806,768Fd: 1,858,986MFd: 73,474,947
- Solo con reescritura manual, la memoria y el tiempo de ejecución mejoran casi 10 veces frente al intento inicial
- Crear resultados intermedios con nombre permite construir en cierta medida el join plan bushy-tree deseado de forma manual, pero si se le pone nombre a una relación que en realidad no hace falta, como
V, se paga el costo de generarla
Consultas demand-driven y magic sets
- Una consulta demand-driven explora solo la parte necesaria para un hecho objetivo específico
- Como solución aproximada, pueden usarse los magic sets
- Es una transformación que incorpora el literal objetivo en la consulta
- Podría pensarse en empezar solo desde los
dde interés, en lugar de desde todos losd, pero si se aplica de forma simple puede dar resultados incorrectos
- Los magic sets no son la respuesta óptima, y planea leer más artículos relacionados para encontrar enfoques que puedan ser más eficientes
- Los enlaces relacionados son los siguientes
Optimización de join plans e IR paralela de datos
- El atractivo de Datalog no está tanto en la Horn clause en sí, sino en que expone de forma pura el problema central de la computación paralela de datos: el data rendezvous
- La regla
h(x, y, z) :- b1(x, y), b2(y, z) .puede verse como el problema de reunir en un mismo lugar losxyzrelacionados para caday - La operación básica de la computación paralela de datos consiste en agrupar registros por clave y entregarlos a la lógica del usuario; el join expresa dentro de eso un enrutamiento selectivo
- Un IR simple usa los siguientes opcodes
Var(String): colección con nombreMap(Action): filtro, permutación, projectionKey(usize): marca como clave las primeras columnasMul(usize): combina varias colecciones con la misma longitud de clave
- Después,
MapyKeyse integran enAction.key_arity Actioncontiene filtros de literales, filtros de igualdad de variables, projection y aridad de clave- El punto de partida más simple es hacer cross join de todos los átomos del body y luego aplicar filtros y projection para cada head, pero aunque produce la respuesta correcta, el rendimiento es muy malo
Optimización basada en e-graph
- Para la optimización se usan e-graph y equality saturation.
- Como material de referencia se usan la página de egg, un artículo anterior y
egg. - El term graph se representa como un mapa de
ENode<T>conId, compartiendo nodos idénticos para representar el programa de forma más compacta. - Se aplicaron tres reglas de e-graph.
MulPermute: hace equivalentes las permutaciones de entrada deMul(k).MulPartition: divideMul(k)de varias maneras y las hace equivalentes.MapPushdown: empujaMapdebajo deMul(2)para formar una especie de join con claves.
- La regla de ejemplo es la siguiente.
head(?a, ?b) :- a(?x, ?a), b(?y, ?x), b(?y, ?z), a(?z, ?b) .
- Después de equality saturation, en la etapa de extraction se asignan costos.
- El costo de
Mapes la cantidad de columnas de salida. - El costo de
Mules la suma de la cantidad de columnas clave y las columnas no clave de las entradas. - El costo de
Vares 0. - En caso de empate, se minimiza primero la cantidad de
Mapy luego la deMul.
- El costo de
- En el ejemplo, el plan seleccionado se encontró en wave two con un máximo de dos columnas no correlacionadas.
- map sobre las entradas
ayb - un join
- una proyección intermedia
- un join consigo mismo
- proyección final
- map sobre las entradas
- Esta búsqueda del plan toma unos 40 ms incluso en release build, y la mayor parte del tiempo se usa en equivalence saturation.
Actualización de la ejecución del plan optimizado
- En la actualización del 2025-06-29 se implementó la ejecución del plan optimizado.
- El plan sale como
Vec<ENode<Op>>, pero en la ejecución real no se ejecuta cada nodo de manera independiente. - La forma de ejecución prevista es la siguiente.
- Para cada
Var, aplicar variosMapdependientes en un solo escaneo de la colección externa. - Para cada
Mul, aplicar variosMapdependientes en un solo escaneo del join.
- Para cada
Op::Map(action)no es tanto una operación que se ejecute directamente, sino un trabajo que se encola en la operación de la que depende.- Para esto se introdujo
TempAction.- filtro de literales
- filtro de igualdad de variables
- proyección que puede contener referencias a columnas o literales de cadena
- La etapa de preparación del plan de ejecución separa body y head, y reúne las acciones
Mappor nodo de entrada. - En la ejecución de
Var, el manejo de nombres se divide en tres casos.- la generación de head usa el nombre de la relación head
- la transformación identidad reutiliza el nombre de entrada existente
- la transformación no trivial se guarda en un nombre temporal
.temp-*
- La ejecución de
Mul(2)verifica la key arity y los nombres de las dos entradas, y llama ajoin_withpara acumular los resultados en varios builders. - En el ejemplo complejo de aliasing, la ejecución del plan optimizado tardó 114.28 segundos, unos 5 segundos menos que los 119 y tantos anteriores, pero la razón no está clara.
- La optimización multi-rule todavía no está implementada, y puede que haya que replantear por completo el enfoque del plan.
Optimización de la representación de facts
- Se redujo de 50 GB a 5 GB, pero aun así parece usar unas 10 veces más memoria de la necesaria.
- La largest layer de la relación grande
-Mcontiene 57,289,225 facts y usa alrededor de 2,098,253,766 bytes.- límites de facts: 458,313,800 bytes
- límites de terms: 916,627,600 bytes
- datos reales en bytes: 723,312,366 bytes
- La primera optimización aprovecha que la arity es constante.
- Si todos los facts tienen 2 columnas, los límites de facts pueden representarse solo con stride y length.
- Esto elimina en la práctica unos 458 MB de datos de límites.
- La segunda optimización consiste en hacer que la longitud de los terms sea uniforme.
- Si los números se convierten en cadenas de ancho fijo de 7 dígitos, los límites de terms también pueden representarse con stride y length.
- A cambio, la cantidad real de bytes puede aumentar.
- La tercera optimización consiste en representar los números en binario en lugar de texto.
- Un número de 7 dígitos cabe en los 4 bytes de un
u32. - También cabe en 3 bytes, así que queda en
57,289,225 × 2 × 3 = 343,735,350bytes. - Baja de unos 2 GB a unos 350 MB, una reducción de aproximadamente 6.10 veces.
- Un número de 7 dígitos cabe en los 4 bytes de un
- Si se comprime la repetición del primer term, se reduce aún más.
- Hay 57,289,225 facts, pero solo 1,147,612 distinct first terms.
- Si se guarda como
(Term, [Term])en lugar de(Term, Term), la largest layer baja a unos 184,491,407 bytes. - Es una reducción de aproximadamente 11.37 veces frente a los 2 GB iniciales.
- En la actualización del 2025-07-02 se aplicó la primera optimización.
- El largest batch pasa a ser 343,735,382 bytes, es decir, el valor teórico de 343,735,350 bytes más 32 bytes.
- El tiempo de ejecución bajó de unos 115 segundos a unos 95 segundos, una mejora de alrededor del 20%.
Representación layered trie
- En la actualización del 2025-07-20, layered trie ya pudo funcionar.
- Se compara la representación row-oriented con la representación layered trie orientada a columnas.
toad-rowtoad-col
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| toad-row | 3.88s | 11.30s | 25.67s |
| toad-col | 3.47s | 11.94s | 23.09s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| toad-row | 28.21s | 28.25s | 7.62s |
| toad-col | 19.39s | 21.96s | 9.48s |
| datafrog | UNK | UNK | UNK |
- layered trie es un método que suprime la repetición de las columnas iniciales en una representación row ordenada.
- Cada columna es una lista de listas de valores.
- Cada lista es un conjunto ordenado de valores distinct que corresponde al prefix de la columna anterior.
- La cantidad de listas de una columna es igual al total de ítems de la columna anterior.
- Puede verse como un árbol, pero la implementación real se mantiene por columnas.
- La ventaja es que permite realizar búsqueda, join, diferencia y merge por unidades de prefix.
- Cuando casi no hay valores distinct y conviene más ver la fila completa de una vez, el enfoque row-oriented puede ser más ventajoso.
- Como abstracción común se introdujo el trait
FactContainer.formlenapplyjoinexceptmerge
applyrastrea los rangos de cada layer con un stack explícito en lugar de recursión, construye filas y se las pasa a action.alignes un helper común que alinea los prefix de dos layered trie para compararlos.Ordering::Less: rango presente solo en selfOrdering::Greater: rango presente solo en otherOrdering::Equal: el prefix de longitud arity está presente en ambos lados
join,exceptymergeestán implementados sobrealign.joindespliega las extensiones restantes sobre el prefix común y hace un cross join.exceptinjerta enTrieBuilderlos rangos que existen solo en self.mergeinjerta una vez cada uno de los rangos solo-self, solo-other e iguales, según corresponda.
Optimización de ancho fijo y rendimiento
- Cuando se puede subir a
[u8; 4]de ancho fijo, el rendimiento de comparación mejora mucho. - layered trie puede aplicar optimización de ancho fijo por columna, así que a largo plazo podría tener ventaja sobre row.
- En
exceptymergefue fácil aplicarupgradeydowngrade, pero aplicarlo ajoinfue más difícil por problemas de tipos en Rust.
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| toad-row | 3.88s | 11.30s | 25.67s |
| ^-- +opt | 3.11s | 9.49s | 19.83s |
| toad-col | 3.47s | 11.94s | 23.09s |
| ^-- +opt | 2.55s | 9.13s | 15.95s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| toad-row | 28.21s | 28.25s | 7.62s |
| ^-- +opt | 23.31s | 23.08s | 6.73s |
| toad-col | 19.39s | 21.96s | 9.48s |
| ^-- +opt | 14.26s | 16.45s | 8.33s |
| datafrog | UNK | UNK | UNK |
- Actualmente, cerca de 2/3 del tiempo se consume dentro de
join - Parece haber margen para al menos otra mejora de ~2x en la optimización de joins
- Un intento de simplemente reordenar el inner loop no produjo una mejora medible
Especialización al nivel de código compilado
- Si se detectan términos del mismo largo y hechos con la misma aridad, se puede ver
Vec<u8>como algo tipoVec<[[u8; B]; T]> - Esta forma permite que Rust conozca mejor la forma de los datos, reduciendo el costo de verificar bounds y longitudes, y haciendo las comparaciones especialmente baratas
- Las comparaciones se usan en varios puntos de datatoad
- ordenamiento y eliminación de duplicados de batches de hechos
- fusión de batches
- fusión de claves de join
- filtrado de hechos nuevos comparándolos con hechos existentes
- El rendimiento base es el siguiente
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 684s | 8640s | 42840s* |
| datatoad | 7.44s | 17.26s | 42.25s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| graspan | 8.4h | 6.0h* | 1.7h* |
| datatoad | 101.24s | 96.36s | 20.20s |
| datafrog | UNK | UNK | UNK |
- La optimización de ordenamiento se probó haciendo
unsafetransmute deVec<u8>aVec<[u8; 8]>para aplicar sort y dedup
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 7.44s | 17.26s | 42.25s |
| dt-sort | 4.99s | 13.55s | 32.15s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 101.24s | 96.36s | 20.20s |
| dt-sort | 52.99s | 53.19s | 11.20s |
| datafrog | UNK | UNK | UNK |
- La optimización de fusión se implementó con un enfoque simple: concatenar las dos entradas y luego aplicar sort y dedup
| dataflow | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 7.44s | 17.26s | 42.25s |
| dt-sort | 4.99s | 13.55s | 32.15s |
| dt-both | 3.71s | 11.23s | 23.58s |
| datafrog | 1.30s | 4.06s | 8.03s |
| aliasing | httpd | psql | lnx_kernel |
|---|---|---|---|
| dt-orig | 101.24s | 96.36s | 20.20s |
| dt-sort | 52.99s | 53.19s | 11.20s |
| dt-both | 31.32s | 30.08s | 8.56s |
| datafrog | UNK | UNK | UNK |
- Aún no se alcanza el rendimiento compilado de datafrog
- El costo restante de las comparaciones también está en join y antijoin, y el plan es volver a revisar la misma oportunidad de optimización al pasar a una estructura basada en trie
- Se está buscando una forma de eliminar
unsafe, y se agrega la conclusión de que el código unsafe debería evitarse
Trabajo que aún queda
-
Spill a disco
- El almacenamiento
columnarestá compuesto por pocas asignaciones grandes - Al crearlo, se puede escribir a archivo en lugar de memoria y volver a usarlo mediante memory map
- El almacenamiento
-
Evaluación distribuida
- Join, eliminación de duplicados y verificación de distinctness se basan en igualdad de claves
- Las claves y sus datos correspondientes pueden distribuirse entre varios workers
- Se puede escalar incluso a múltiples procesos con
timely_communication
-
Evaluación de reglas en streaming
- El join actual usa joins binarios y salida materialized
- Si existen los índices necesarios, se puede construir un plan que no materialice el estado interno
- Aquí también se abordará worst-case optimal join
-
Especialización de custom representation
- Existe la idea de detectar transitive closure y especializarla para strongly connected component decomposition
- Para equivalence relation se puede usar una estructura union-find
bddbddby factorized databases también tratan temas relacionados
-
Búsqueda de hechos relacionados
- Hay que entender y aplicar demand transform
- Se considera una transformación necesaria para la exploración interactiva de Datalog
1 comentarios
Opiniones en Hacker News
Me parece curioso ver que este artículo llegó al primer lugar.
Ahora mismo estoy creando un juego de estrategia en tiempo real con Differential Datalog y Rust, y hago que DDL se encargue de la lógica del juego.
En gran parte es más bien una excusa para conocer ideas nuevas y meterme en una interminable cadena de yak shaving.
https://github.com/vmware-archive/differential-datalog
Como referencia, el equipo de ddlog ahora fundó Feldera, y también vale la pena considerar usar DBSP directamente desde Rust.
https://github.com/feldera/feldera
https://github.com/salsa-rs/salsa
DDLog ya no se mantiene activamente, así que resulta todavía más interesante.
“Yo, el infame villano, fui invitado medio convencido de que iba a recibir el castigo que hacía tiempo merecía.” — la mejor primera frase que he leído este año en un blog técnico.
Las intervenciones del narrador también fueron excelentes, y es raro encontrar un texto tan profundo técnicamente que además se lea de forma tan entretenida.
El recorrido para optimizar las consultas de alias se sintió como una novela policial, y uno como lector también termina sufriendo con el uso de 50 GB de memoria y celebrando cuando baja a 5 GB.
Tanto el código como el texto son excelentes.
Avancé un poco en el port de mangle datalog a Rust.
Está en https://github.com/google/mangle/tree/main/rust y vive en el mismo repositorio que la implementación en Go.
No es de alta prioridad y además hay algo de síndrome del segundo sistema, así que el progreso es lento.
Mangle Rust apunta a manejar datos de tamaño arbitrario leyendo y escribiendo hechos desde disco mediante memory mapping, mientras que la implementación en Go funciona en memoria.
Este artículo me gusta porque parsea Datalog y también trata los árboles LSM, y es mucho más fácil de seguir que el material relacionado con datafrog.
En Rust hay muchas implementaciones de Datalog que usan macros procedurales, como ascent y crepe, pero tienen la desventaja de que es difícil recibir consultas en tiempo de ejecución.
Para usos de análisis estático donde las consultas y el programa son fijos, el enfoque con macros procedurales quizá sea mejor.
Aunque el actual renacimiento de Datalog parece estar perdiendo fuerza, da gusto ver que el núcleo de entusiastas sigue resistiendo.
La reciente conferencia Datalog 2.0 fue bastante más pequeña que antes, y la segunda conferencia HYTRADBOI también tuvo menos presencia de Datalog.
En la primera HYTRADBOI, una cuarta parte de las propuestas estaban relacionadas con Datalog.
También es alentador ver que otras personas compartan proyectos recientes de Datalog.
Ahora estoy preparando una migración de software a gran escala y creando un pipeline de calidad de datos para una base de datos SQL legacy.
Si se estructuran bien las consultas, quedan muy legibles, así que creo que Datalog es mucho más útil que SQL para identificar y localizar problemas de calidad de datos.
Datalog 2.0 es un workshop satélite de LPNMR, una conferencia europea relativamente poco conocida, y dio la casualidad de que esa conferencia se realizó en Dallas.
Cuando asistí en persona, el evento también se sintió algo vacío; incluso presenté un paper en el workshop, pero no vi a mucha gente del área.
Como excepción, sí se notaba la presencia de gente de Europa que presentó el solver Nemo.
Creo que la baja asistencia de este año muestra mejor que era un workshop satélite de una conferencia que ya no era tan famosa, y que el evento principal además era ICLP, más que una falta de interés por las implementaciones de Datalog.
Claro que no intento refutar la afirmación más amplia de que queda poca novedad en implementar motores Datalog puros.
El espacio de investigación ya fue mucho más allá y se movió hacia problemas más exóticos, como streaming (HydroFlow), elección (Dusa) y cosas más cercanas al chase general (el motor de chase de Egglog).
En general casi nadie discrepa en que el Datalog vanilla es aburrido, pero la saturación hacia adelante monótona y las cláusulas de Horn son una línea base rica y con un panorama de ingeniería de rendimiento bien entendido, sobre la cual es bueno construir teorías más interesantes como semiring o Z-set.
Si te parecieron interesantes la máquina de estados y la parte de parsing, también recomiendo la vieja charla de Rob Pike Lexical Scanning in Go.
https://www.youtube.com/watch?v=HxaD_trXwRE
Está en Go, pero la mayor parte se puede aplicar fácilmente a otros lenguajes.
Me gusta que lenguajes modernos como Rust, Zig y Go tengan soporte nativo para Unicode/rune/grafemas.
Comparados con Java, .NET, C++ o lenguajes de scripting, muchos problemas simplemente desaparecen.
En general me gusta el trabajo del autor sobre Datalog, pero preferiría que en los materiales introductorios no enseñara joins binarios.
Fuera de los casos ideales, los interiores se ensucian rápidamente, y los métodos de joins generales me resultaron mucho más fáciles de generalizar mentalmente.
https://en.wikipedia.org/wiki/Worst-case_optimal_join_algorithm
https://github.com/frankmcsherry/blog/blob/master/posts/2025-05-29.md
Hace mucho, durante mis estudios, tuve un breve contacto con Prolog, y tengo una idea general de para qué se usa y en qué es útil, pero no lo entendí en profundidad.
Desde entonces sigo oyendo que Datalog es increíble, pero no termino de entender qué mejora Datalog respecto de Prolog.
Acabo de hojear la página de Datalog en Wikipedia y parece que Prolog tiene un rendimiento relativamente malo, mientras que Datalog reduce expresividad y funcionalidades a cambio de mejorar mucho el rendimiento, lo que permite conjuntos de datos más grandes y procesamiento más paralelizado.
Parece incluir también la pérdida de completitud de Turing, pero me pregunto si eso es lo central o si estoy totalmente errado.
cutno solo evita cálculos desperdiciados, sino que también puede afectar el resultado.En cambio, Datalog en general se parece más a una base de datos relacional con otra sintaxis.
Incluso un espacio de búsqueda trivial pero enorme en Prolog puede consumir tanta memoria en Datalog que ni siquiera sea posible expresarlo.
Datalog es como un auto de uso diario con CVT, mientras que Prolog se parece más a un auto de F1.
Más que una mejora, es como una versión parcialmente recortada de Prolog para que la gente no se vuele el pie, y también es mucho más fácil de implementar e incrustar en otras aplicaciones.
Si estás acostumbrado a Prolog, Datalog por lo general se sentirá frustrante.
No hay
call/3, no hay expansión de términos/objetivos, y Datalog básicamente está diseñado tomando el mínimo común de funcionalidades de Prolog para usarlo en búsquedas interactivas en bases de datos.Es fácil escribir código Datalog rápido, pero su techo también es mucho más bajo.
Prolog también puede escribirse de forma que permita concurrencia, pero es una tarea de nivel intermedio que requiere entender la implementación.
Guarded Horn Clauses y lenguajes derivados se desarrollaron para formalizar esa parte, pero los avances japoneses posteriores a Prolog son muy difíciles de entender.
El rendimiento de Prolog depende mucho del programador, de la implementación usada y del contexto en que se use.
Como Lisp, Prolog también puede usarse para generar código máquina nativo desde un DSL en tiempo de compilación.
Si entiendes cómo funciona la implementación subyacente y escribes código alineado con eso, puede ser suficientemente rápido.
Pero para eso hay que escribir código Prolog durante años con una misma implementación.
También hay mucha investigación sobre optimización de compiladores Prolog, además de casos de implementaciones propietarias.
http://logicprogramming.stanford.edu/readings/ullman.pdf
https://www.ueda.info.waseda.ac.jp/AITEC_ICOT_ARCHIVES/ICOT/Museum/IFS/abst/078.html
https://www.sciencedirect.com/science/article/pii/S0743106696889813
https://link.springer.com/content/pdf/10.1007/3-540-18024-9_26.pdf
https://sicstus.sics.se/
Si quieres usar Datalog y Rust, cozodb está escrito en Rust y también ofrece una sintaxis de consultas Datalog.
Lo estuve revisando alrededor de noviembre de 2024 y encontré algunas mejoras fáciles en el backend de almacenamiento SQLite.
https://github.com/cozodb/cozo/issues/285
También lo usé para análisis estático de programas, y por dentro usa árboles ordenados y técnicas de tipos.
La documentación es suficientemente buena como para compararla con la explicación paso a paso del blog, y el trabajo de optimización de consultas es especialmente interesante.
Dicho eso, si no trabajas en memoria desde Rust, el costo de serialización de datos es alto, y el proyecto, siendo generosos, está en un estado silencioso.
Antes, los fans de Clojure decían que Datalog era mejor que SQL, y que era una pena que todas las bases de datos relacionales usaran SQL.
Nunca llegué a investigar lo suficiente por qué pensaban eso.
http://canonical.org/~kragen/binary-relations empezó como una consulta simple no recursiva, pero la traducción a SQL ya es criminal, y una solución SQL correctamente separada merecería la pena de muerte.
Hace poco se agregó recursión a ANSI SQL, así que ya no es completamente imposible, pero tiene tres grandes desventajas.
Primero, accidentalmente hicieron que SQL fuera Turing-completo; en cambio, las consultas Datalog tienen terminación garantizada.
Segundo, sigue siendo terriblemente tosco de usar.
Tercero, por la primera razón, muchas veces no se implementa por completo, así que es difícil confiar en él.
Si quieres probar Datalog en línea en un entorno de notebook amigable, recomiendo Percival.
https://percival.ink/
En las implementaciones de Datalog en general no existe un estándar equivalente a “ANSI SQL”, pero una vez que aprendes las ideas centrales, los demás Datalog no son tan difíciles.
También inicié un fork de Percival que compila Datalog a SQLite, así que si quieres ver cómo ambos expresan lo mismo, puedes revisarlo.
https://percival.jake.tl/
Las agregaciones y los joins más avanzados todavía están incompletos, pero las formas básicas funcionan bien.
Logica es un compilador Datalog→SQL mucho más serio y completo, creado por un investigador de Google, que compila a BigTable, DuckDB y algunos dialectos SQL.
https://logica.dev/
Donde Datalog se vuelve más de un orden de magnitud más fácil es al manejar consultas/reglas recursivas.
También es posible en SQL, pero se siente más o menos como beber Play-Doh con un popote.
Materialize.com de Frank tiene una forma SQL “WITH MUTUALLY RECURSIVE” mucho mejor que el antiguo enfoque recursivo de ANSI SQL, y Notion la está evaluando para consultas de carga de páginas y sincronización de datos.
https://materialize.com/blog/recursion-in-materialize/
Feldera también tiene una forma parecida para vistas recursivas.
https://www.feldera.com/blog/recursive-sql-queries-in-feldera
Me gusta que Feldera permita crear cada “regla” o subvista como una sentencia separada, en lugar de meterlo todo en una única sentencia enorme.
La principal desventaja que vi al probarlo es que el dialecto SQL de Feldera tiene bastantes restricciones heredadas de Apache Calcite, mientras que el dialecto SQL de Materialize se esfuerza mucho por mantener compatibilidad con PostgreSQL.
Qué bueno, un nuevo artículo de McSharry.
La última vez que revisé, parecía que VMWare se había alejado de differential datalog.
https://www.feldera.com/
Parece que pasaron de differential Datalog a differential SQL, probablemente porque se dieron cuenta de que vender Datalog es realmente difícil.