Cajitas que piensan

Designing Data-Intensive Applications : Chapter 3.2

OLTP - Online Transaction Processing

  • Se buscan poca cantidad de registros por key
  • Se utiliza un indice
  • Inserciones en base a la entrada del usuario
  • Aplicaciones interactivas

OLAP - Online Analytic Processing

  • Se escanean grandes cantidades de registros
  • Se usan muy pocas de las columnas de cada registro
  • Calculos agregados (suma, promedio, cantidad) en vez de retornar los registros completos.

comparative table

Si bien SQL es lo suficientemente flexible como para soportar ambos mundos, termino siendo el dominante en el mundo OLTP mientras que para OLAP se comenzó a utilizar otra base de datos separada de la transaccional, conocida como Data Warehouse.

Data Warehouse

Centralizar las información de los N sistemas de procesamiento transaccional (ventas, facturación, clientes, inventario, etc) en un solo lugar. Esos sistemas utilizan OLTP para funcionar, brindando alta disponibilidad y procesamiento con baja latencia puesto que son críticos para la operación.

Una query adhoc sobre estos sistemas puede perjudicar la performance de las transacciones que se están procesando en el sistema (pensemos una query que no tiene indices y genera sobre carga sobre el CPU de la base sobre la que corre la aplicación).

El data warehouse es una base de datos separada que los analistas pueden utilizar para sus análisis exploratorios sin afectar el sistema principal. Normalmente contiene una copia solo para lectura de la información que contienen los sistemas OLTP.

La data es extraída de las bases de dato transaccionales (periódicamente o con un flujo constante de actualizaciones), se transforma a un schema menos transaccional y que sea mas amigable con el análisis, se limpian los datos y se cargan en el data warehouse. A este proceso se le conoce como Extract-Transform-Load (ETL)

ETL

Toda esta infraestructura es solo necesaria en problemas donde el volumen de datos lo requiere, para los casos con baja cantidad de datos el sistema OLTP puede brindar ambas funcionalidades sin problemas.

Ventajas

Al ser un sistema separado, se optimizan los patrones de acceso a la data que se utilizan para análisis, esto es porque los algoritmos de indexación que vimos en el capitulo anterior son muy buenos en sistemas OLTP pero no para responder consultas analíticas.

Estrellas y copos de nieve

En análisis se usan pocos modelos de datos, la mayoría utilizan Star Scheme (o modelo dimensional).

Se compone de una tabla principal de “hechos” y cada fila es la representación de un evento ocurrido en un momento. De esta tabla se desprenden punteros al resto de las tablas del esquema.

star scheme

Normalmente la tabla de hechos tiende a ser muy grande, se guardan muchos eventos y se agregan columnas por cada nueva dimension. Cada fila de la tabla de hechos es un evento y las dimensiones son el quien, como cuando, donde, cuanto, y por que del evento. Las tablas tienden a ser mucho mas anchas que lo que se estila en OLTP, incluso de varios cientos de columnas.

Si usamos una dimension para la fecha en vez de un atributo, podremos buscar por ejemplo las ventas de los feriados o vacaciones.

Para evitar que las tablas queden con muchos datos, se puede continuar la desnormalizacion de las dimensiones en subdimensiones, llevando la estrella a parecer un copo de nieve. Si bien estos esquemas son mas normalizados no se utilizan mucho porque complejizan las queries al momento de trabajar con los análisis.

Storage orientado a columnas

Si tenemos millones de millones de filas y petabytes de datos en la tabla de hechos, guardarlos y consultarlos eficientemente es un problema.

Las tablas tienen cientos de columnas pero una consulta normalmente no usa mas de 4 o 5 en un contexto de análisis, todas las otras columnas son ignoradas, por ejemplo:

SELECT
	dim_date.weekday, 
	dim_product.category,
	SUM(fact_sales.quantity) AS quantity_sold
FROM
	 fact_sales
JOIN 
	dim_date ON fact_sales.date_key = dim_date.date_key
JOIN 
	dim_product ON fact_sales.product_sk = dim_product.product_sk
WHERE
	dim_date.year = 2013 AND
	dim_product.category IN ('Fresh fruit', 'Candy')
GROUP BY
	1,2;

En una base OLTP, los datos son guardados en formato fila, todos los valores de una fila de una tabla están almacenados uno al lado del otro. Esto es así en bases de datos documentales, todo el documento se guarda como una secuencia de bytes contigua.

Para la query anterior, necesitaríamos un indice por date_key y product_sk, pero en un motor de base de datos de fila, necesitaríamos cargar toda la info de las filas (cientos de atributos) del disco a la memoria, pasearlos y luego filtrar las columnas innecesarias.

La idea en Column Oriented Storage es no guardar toda la info de una fila junta, sino guardar toda la información de una columna junta. Si guardo cada columna en un archivo separado, una query solo necesita leer y pasear los archivos que son necesarios en la query.

column storage

Parquet es un buen ejemplo de almacenamiento columnar, no así HBase y Cassandra, que si bien tiene familias de columnas, se almacenan todas las columnas de una fila juntas con un identificador de fila y no se utilizan compresiones de columna.

Compresion de columnas

Además de solo leer las columnas necesarias, el esquema columnar es muy util a la hora de comprimir la data. Una de las técnicas más efectivas es el bitmap encoding:

bitmap

Esta técnica juega con que la cantidad de valores diferentes en una columna es muchísimo menor a la cantidad de filas (imaginemos la cantidad de hoteles diferentes sobre la cantidad de ventas). Se toman los N valores distintos que puede tomar la columna, y se crea un bitmap para cada uno.

Si N es bajo, entonces esos bitmaps se pueden guardar en un bit de cada fila, pero si N es grande, habrán muchos ceros en la mayor parte del bitmap (esparcidos), en ese caso se corre un run length encode para compactarlos.

Utilizar estos bitmaps hace muy eficiente el filtrado puesto que se hace un OR o AND a nivel de bit entre los bitmaps y se puede encontrar rápidamente los resultados.

Ancho de banda en memoria y procesamiento vectorizado

Al hacer una búsqueda sobre millones de filas, uno de los mayores cuellos de botella esta en cargar en memoria la data desde el disco. Pero hay otro cuello de botella menos “visible”, que es entre la memoria y la cache del CPU, además que resulta importante utilizar instrucciones más performantes (SIMD).

Además de bajar el volumen de datos que se carga desde el disco, las bases de datos columnares hacen un uso eficiente de los ciclos del CPU, cargando un bloque de datos de una columna comprimidos y que ocupan toda la cache L1 del CPU, recorriendo por completo el bloque. EL CPU puede recorrer esto mucho más rápido que otro con muchas llamada o condiciones por cada registro, al estar comprimidos entran mas filas por bloque, y los operadores AND y OR se diseñan para operar sobre bloques enteros (muchas operaciones en Machine Learning se realizan vectorizadas para que sean mas eficientes).

Ordenamiento de las columnas

Si bien no es necesario ordenar las filas en este esquema a la hora de guardarlas, si podemos elegir un orden para luego utilizarlo como mecanismo de indexación, aunque siempre es mas sencillo agregarlas en el archivo de cada columna en el orden en que fueron insertadas.

Se tiene que ordenar toda la fila a la vez, para luego insertar en cada una de las columnas. Si las consultas serán normalmente por rango de fecha, por ejemplo la ultima semana, tal vez la primera clave de ordenación deba ser la clave de la fecha. Luego el optimizador de consultas podría solo leer las filas de la ultima semana en vez de leer toda la columna.

Es probable que haya varios registros en la misma semana, podríamos incluir una segunda columna para que nos facilite agrupar o filtrar las compras de un cliente en la semana anterior.

Multiples criterios de ordenación

Si voy a tener la data replicada en varias maquinas, porque no la guardo con diferentes criterios de orden (alguno mejor para consultas por cliente y otro por mes) y según la consulta que llegue utilizar la data de un nodo u otro.

Similar al concepto visto en el capitulo anterior de de Multiples indices secundarios en un esquema de filas, pero en vez de tener toda la información de la fila en un solo lugar (heap o indice clusterizado) y el indice secundario ser solo punteros al dato, aquí tendremos la data repetida en diferentes nodos pero cada uno con un orden.

Escribiendo en un storage columnar

Para ganar compresión, velocidad de consulta en grandes volúmenes de datos y ordenación, sacrificamos algo y en este caso es que las escrituras son más difíciles.

No es posible utilizar update-in-place cómo hacían los Arboles B, puesto que las columnas están comprimidas. Si queremos insertar una fila en el medio de una tabla ordenada, tenemos que reescribir todo los archivos de cada columna, puesto que una fila esta identificada por su posición en la columna.

Por esto se utilizan las SSTables que vimos anteriormente, permitiendo acumular en memoria la estructura ordenada y haciendo bajadas a disco de datos mergeando la info en memoria con los archivos de cada columna.

Las consultas deben buscar la info en disco más las modificaciones recientes que están en memoria y combinarlas, pero el motor abstrae al usuario de esto.

Cubos de datos y Vistas materializadas

En vez de tener una vista de SQL clásica, donde lo que tenemos es una query guardada que reutilizamos, las vistas materializadas son resultados de la query ya guardados, para evitar tener que procesarlos de vuelta, por ejemplo es cachear las ventas diarias por vendedor. Una vez que se trae el resultado queda guardado en disco.

Como la vista pasa a ser una copia desnoralizada de la consulta que se hizo, cada vez que cambian los datos originales hay que regenerar la vista, esto sobrecarga las escrituras y es por lo que no se utilizan mucho en contextos de OLTP.

El cubo de datos termina siendo un caso particular de vista materializada, que agrupa por varias dimensiones.

Ambos terminan siendo un preprocesamiento de consultas que son muy utilizadas y que se cachean en disco para acelerar la respuesta.