Como seguramente ya sabréis la mayoría de vosotros, desde aproximadamente finales de 2022 InterSystems IRIS incluyo la funcionalidad de almacenamiento columnar a su base de datos, pues bien, en el artículo de hoy vamos a ponerla a prueba en comparación con el almacenamiento en filas habitual.
Almacenamiento columnar
¿Cuál es la principal característica de este tipo de almacenamiento? Pues bien, si consultamos la documentación oficial veremos esta fantástica tabla que nos explica las principales características de ambos tipos de almacenamiento (por filas o por columnas):
Como véis el almacenamiento columnar está pensado sobretodo para tareas de tipo analítico en las que se lanzan consultas contra campos específicos de nuestra tabla, mientras que el almacenamiento por filas es más óptimo cuando se requiere realizar gran número de operaciones de inserción, actualización y eliminación, así como obtener registros íntegros.
Si seguís leyendo la documentación veréis lo sencillo que es configurar nuestra tabla para poder utilizar el almacenamiento columnar:
CREATE TABLE table (column type, column2 type2, column3 type3) WITH STORAGETYPE = COLUMNAR
SQLSQL
Mediante este comando estaríamos definiendo todas las columnas de nuestra tabla con almacenamiento columnar, pero podríamos decantarnos por un modelo mixto en el que nuestra tabla tenga un almacenamiento en filas pero que determinadas columnas hagan uso del almacenamiento columnar.
Este escenario mixto podría ser interesante en casos en los que sean habitual operaciones de agregación como sumas, medias, etc. Para este caso podríamos definir que columna es la que hará uso de dicho almacenamiento:
CREATE TABLE table (column type, column2 type2, column3 type3 WITH STORAGETYPE = COLUMNAR)
SQLSQL
En el ejemplo anterior definiríamos una tabla con almacenamiento por filas y una columna (column3) con almacenamiento columnar.
Comparativa
Para realizar la comparativa entre el tiempo empleado por almacenamiento columnar y el de por filas en diferentes consultas hemos creado un pequeño ejercicio haciendo uso de Jupyter Notebook que va a insertar una serie de registros que generaremos en dos tablas, la primera con almacenamiento con filas (Test.PurchaseOrderRow) y la segunda con almacenamiento columnar en dos de sus columnas (Test.PurchaseOrderColumnar)
Test.PurchaseOrderRow
CREATE TABLE Test.PurchaseOrderRow (
Reference INTEGER,
Customer VARCHAR(225),
PaymentDate DATE,
Vat NUMERIC(10,2),
Amount NUMERIC(10,2),
Status VARCHAR(10))
SQLSQL
Test.PurchaseOrderColumnar
CREATE TABLE Test.PurchaseOrderColumnar (
Reference INTEGER,
Customer VARCHAR(225),
PaymentDate DATE,
Vat NUMERIC(10,2),
Amount NUMERIC(10,2) WITH STORAGETYPE = COLUMNAR,
Status VARCHAR(10) WITH STORAGETYPE = COLUMNAR)
SQLSQL
Si os descargais el proyecto de Open Exchange y lo desplegáis en vuestro Docker local podréis acceder a la instancia de Jupyter Notebook y revisar el archivo PerformanceTests.ipynb el cual será el encargado de generar los datos aleatorios que vamos a almacenar en diferentes fases en nuestras tablas y finalmente nos mostrará una gráfica con el rendimiendo de las operaciones de consulta.
Echemos un vistazo rápido a la configuración de nuestro proyecto:
docker-compose.yml
version: '3.7'
services:
# iris
iris:
init: true
container_name: iris
build:
context: .
dockerfile: iris/Dockerfile
ports:
- 52774:52773
- 51774:1972
volumes:
- ./shared:/shared
environment:
- ISC_DATA_DIRECTORY=/shared/durable
command: --check-caps false --ISCAgent false
# jupyter notebook
jupyter:
build:
context: .
dockerfile: jupyter/Dockerfile
container_name: jupyter
ports:
- "8888:8888"
environment:
- JUPYTER_ENABLE_LAB=yes
- JUPYTER_ALLOW_INSECURE_WRITES=true
volumes:
- ./jupyter:/home/jovyan
- ./data:/app/data
command: "start-notebook.sh --NotebookApp.token='' --NotebookApp.password=''"
YAMLYAML
Desplegamos en nuestro docker los contenedores de IRIS y Jupyter, configurando al inicio IRIS con el namespace "TEST" y las dos tablas requeridas para la prueba.
Para no aburriros con código podéis consultar vosotros mismos el archivo PerformanceTests.ipynb desde el que nos conectaremos a IRIS, generaremos los registros a insertar y los almacenaremos en IRIS
Ejecución de las pruebas
Los resultados han sido los siguientes (en segundos):
Inserciones:
Las inserciones realizadas son de tipo bulk:
INSERT INTO Test.PurchaseOrderColumnar (Reference, Customer, PaymentDate, Vat, Amount, Status) VALUES (?, ?, ?, ?, ?, ?)
SQLSQL
Y el tiempo para cada lote de inserciones es el siguiente:
Total de inserciones |
Almacenamiento por filas | Almacenamiento mixto |
1000 |
0.031733 |
0.041677 |
5000 |
0.159338 |
0.185252 |
20000 |
0.565775 |
0.642662 |
50000 |
1.486459 |
1.747124 |
100000 |
2.735016 |
3.265492 |
200000 |
5.395032 |
6.382278 |
Consultas:
La consulta lanzada incluye una función de agregación y una condición, ambas sobre las columnas con almacenamiento columnar:
SELECT AVG(Amount) FROM Test.PurchaseOrderColumnar WHERE Status = 'SENT'
SQLSQL
Total de filas |
Almacenamiento por filas | Almacenamiento mixto |
1000 |
0.002039 |
0.001178 |
5000 |
0.00328 |
0.000647 |
20000 |
0.005493 |
0.001555 |
50000 |
0.016616 |
0.000987 |
100000 |
0.036112 |
0.001605 |
200000 |
0.070909 |
0.002738 |
Conclusiones
Como veis en los resultados obtenidos, el funcionamiento es exactamente a lo que se indica en la documentación. La inclusión de columnas con almacenamiento columnar ha penalizado levemente el rendimiento durante la inserción (un 18% más lento aproximadamente para nuestro ejemplo) mientras que las consultas sobre esas mismas columnas han mejorado espectacularmente el tiempo de respuesta (258 veces más rápida).
Sin duda es algo a tener en cuenta en el momento de planificar el desarrollo de cualquier aplicación.
La verdad es que es impresionante. Dos órdenes de magnitud y sólo hablamos de 200000 filas... la mejora de rendimiento será incluso más evidente con mayor volumen de datos!
¡Así es! Sólo tienes que prolongar la gráfica para imaginar a donde puede llegar.