Tutorial sobre cómo usar el particionamiento de tablas
Demostración de particionamiento de tablas
Esta demostración te guía a través de la nueva funcionalidad de particionamiento de tablas en IRIS SQL, explicando qué hace y cómo funciona. Usaremos solo unas pocas decenas de filas para mostrar el concepto, aunque la funcionalidad está pensada para conjuntos de datos mucho más grandes.
Si queréis una introducción más breve y a alto nivel sobre Table Partitioning, podéis consultar el módulo de formación online Managing Tables with Partitioning in InterSystems IRIS
ℹ️ Table Partitioning está incluido en IRIS 2026.1 as an como una funcionalidad experimental.
¿Qué es el particionamiento de tablas?
El particionamiento de tablas ayuda a los usuarios a gestionar grandes tablas de forma eficiente, permitiendo dividir los datos entre varias bases de datos según un esquema lógico. Esto permite, por ejemplo, mover los datos más antiguos a una base de datos en un nivel de almacenamiento más económico, mientras se mantiene la información más reciente y de acceso frecuente en almacenamiento premium. La estructura de datos de las tablas particionadas también aporta varias ventajas operativas y de rendimiento cuando las tablas son muy grandes (> 1.000 millones de filas).
Para más información sobre qué es y cómo funciona el particionamiento de tablas, podéis consultar la sección de preguntas frecuentes al final de esta página.
Primeros pasos
El particionamiento de tablas está disponible como una funcionalidad experimental en cualquier kit o contenedor de IRIS o IRIS for Health 2016.1, y las claves de licencia para usarlo están disponibles a través del Programa de Acceso Temprano (EAP) de InterSystems. Si ya tenéis una clave de licencia que incluye IRIS Advanced Server, podéis usarla en su lugar.
Una vez instalada la instancia, podéis utilizar vuestra interfaz SQL favorita (DBeaver, el Shell o el SMP) para ejecutar los comandos descritos en este tutorial.
Creando el namespace de prueba
Me gusta empezar creando un namespace y una base de datos de prueba que podamos usar para la demostración. Yo he llamado al mío TESTTP1. Si no os apetece complicaros, podéis usar directamente el namespace USER integrado; funcionará igual de bien para esta demo.
Inicio > Menú > Configurar namespaces > Crear nuevo namespace
Creando bases de datos
El objetivo principal del particionamiento de tablas es permitir a los administradores dividir los datos de las tablas entre varias bases de datos para simplificar las operaciones, así que primero tendremos que crear varias bases de datos. Para ello, utilizaremos una nueva variante del comandoCREATE DATABASE existente —que normalmente crearía un namespace completo— para crear únicamente la base de datos local usando CREATE DATABASE FILE:
CREATE DATABASE FILE "data-2024";
CREATE DATABASE FILE "data-2023";
CREATE DATABASE FILE "archive" ON DIRECTORY '{install dir}/mgr/cheap/archive/'; -- change to match your setup
Esto creará tres bases de datos adicionales con la configuración por defecto, pero todavía no hay ningún namespace mapeado a ellas.
Creando una tabla particionada
En el particionamiento de tablas, distinguimos entre cómo se particionan los datos y dónde se almacenan esas particiones. La primera parte define la estructura de datos subyacente (subíndices globales, ver la sección “¿Cómo funciona?” más abajo) y forma parte de la definición de la tabla —es decir, del código—. La segunda es más un aspecto en tiempo de ejecución, específico de la instancia, que puede variar según dónde se despliegue la aplicación.
La parte del “cómo” se define mediante una clave de partición, que establece un campo de la tabla y un esquema para derivar una “partición” real a partir del valor de ese campo en cada fila. Los esquemas de partición disponibles son:
-
Particionamiento por rango - por ejemplo, dividir los datos de la tabla en función de un campo
date_created, donde cada partición corresponde a un mes (el intervalo de la clave de partición por rango) -
Particionamiento por lista - por ejemplo, dividir los datos de la tabla en función de un campo
region, donde cada partición corresponde exactamente a una región -
Particionamiento por hash - por ejemplo, dividir los datos de la tabla en función de un campo
customer_id, distribuyendo los datos entre un número fijo de particiones basado en un hash de la columnacustomer_id.También se admiten claves de partición compuestas, en cuyo caso podéis elegir un esquema de partición para cada campo de forma independiente.
Como primer ejemplo, crearemos una tabla sencilla de registro de transacciones que está particionada por fecha:
CREATE TABLE demo.log (
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
log_level VARCHAR(10) DEFAULT 'INFO',
message VARCHAR(1000)
) PARTITION BY RANGE (ts) INTERVAL 1 MONTH;
CREATE BITMAP INDEX levelBIdx ON demo.log(log_level);
¡Eso es todo! Ahora hemos creado nuestra primera tabla particionada, y cualquier dato que escribamos en la tabla se estructurará automáticamente en una partición para el mes representado por ts.
Vamos a añadir algunas filas para ver qué significa esto:
INSERT INTO demo.log (message) VALUES ('this is today''s first message');
INSERT INTO demo.log (message) VALUES ('this is today''s second message');
INSERT INTO demo.log (log_level, message) VALUES ('ERROR', 'this is an error message, sadly');
INSERT INTO demo.log (ts, message) VALUES (DATEADD('month', 6, CURRENT_TIMESTAMP), 'a message from the future!');
INSERT INTO demo.log (ts, log_level, message) VALUES (DATEADD('month', 6, CURRENT_TIMESTAMP), 'FATAL', 'it''s the end of the world as we know it');
INSERT INTO demo.log (ts, log_level, message) VALUES ('2024-08-12', 'INFO', 'Enjoy the Perseid meteor shower!');
Ahora podemos consultar el catálogo para ver nuestras particiones, ya sea utilizando la nueva vista en la sección de detalles de tablas del SMP, o consultando el catálogo directamente:
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS;
Esta consulta debería devolver 3 filas —una para el mes actual, otra para los dos registros futuros (según la llamada a la función DATEADD()), y otra para el registro pasado de agosto de 2024— con la ubicación donde se almacenan las particiones. Como veréis, todas están ubicadas en la base de datos TESTTP1, que es la base de datos por defecto del namespace TESTTP1. Esto ocurre porque hasta ahora solo hemos especificado cómo deben particionarse los datos de la tabla, pero no dónde deben mapearse. El dónde se define mediante una herramienta llamada Extent Mapper.
El Extent Mapper
ℹ️Antes de poder ejecutar correctamente los comandos MOVE PARTITION que aparecen a continuación, tendremos que ir a Journal Settings y asegurarnos de que la opción “Journal freeze on error” está activada.
Inicio > Administración del sistema > Configuración > Configuración del sistema > Configuración de journal
El Extent Mapper ayuda a mapear particiones a bases de datos distintas de la predeterminada para un namespace determinado. Incluye un conjunto de comandos SQL sencillos. Con el siguiente comando, mapearemos todos los datos de las particiones correspondientes a 2023 a la base de datos data-2023, y los datos de particiones anteriores a esa a la base de datos archive:
ALTER TABLE demo.log MOVE PARTITION BETWEEN '2023-01-01' AND '2023-12-31' TO "data-2023";
ALTER TABLE demo.log MOVE PARTITION BETWEEN '2000-01-01' AND '2022-12-31' TO "archive";
En el comando anterior, se utiliza la palabra clave BETWEEN para especificar un rango de fechas, ya que la clave de partición de nuestra tabla utiliza particionamiento por rango. Los valores que hemos indicado se usan para identificar la primera y la última partición que se va a mover. Consultad la documentación para más información sobre la sintaxis específica de otros esquemas de particionamiento.
Al trabajar con la información del catálogo que hemos usado antes, también podéis especificar directamente los IDs de partición, usando valores individuales o un rango (cuando se utiliza particionamiento por rango):
ALTER TABLE demo.log MOVE PARTITION ID '202411010000' TO "data-2024";
ALTER TABLE demo.log MOVE PARTITION ID BETWEEN '202401010000' AND '202412010000' TO "data-2024";
Si volvéis a consultar la tabla del catálogo que usamos antes, veréis un cambio importante. La entrada con PARTITION_ID = 202408010000 ahora muestra LOCATION = DATA-2024
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS;
Esto demuestra la parte de “movimiento” de MOVE PARTITION. El comando primero mapea las particiones a las bases de datos deseadas y después mueve físicamente los datos relevantes desde la base de datos de origen a la base de datos de destino.
Sin embargo, también notaréis que la tabla del catálogo no muestra información para la base de datos DATA-2023 ni para la base de datos ARCHIVE. Esto se debe a que una partición solo existe cuando contiene datos reales. Existe una tabla de catálogo separada para mostrar los mapeos actuales:
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITION_MAPPINGS;
Ahora vamos a añadir algunos datos para estos periodos y verificar que los datos se han almacenado en la base de datos correcta:
INSERT INTO demo.log (ts, log_level, message) VALUES ('2014-02-27', 'INFO', 'this happened over a decade ago!');
INSERT INTO demo.log (ts, log_level, message) VALUES ('2023-01-01', 'INFO', 'Happy 2023!!');
INSERT INTO demo.log (ts, log_level, message) VALUES ('2024-12-25', 'INFO', 'Merry Christmas!!');
INSERT INTO demo.log (ts, log_level, message) VALUES ('2020-04-12', 'INFO', 'Happy Easter!!');
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS;
Ahora deberíais ver cómo los registros (particiones) han acabado en la base de datos correcta. Como no hemos especificado un mapeo para los datos de 2025 y 2026, esos registros siguen yendo a la base de datos por defecto del namespace, aunque nada nos impide definir un mapeo para los registros actuales o futuros desde el principio.
Otras operaciones a nivel de partición
Además de mover particiones usando el Extent Mapper, también estamos introduciendo comandos para eliminar particiones completas, incluidas sus asignaciones a bases de datos:
ALTER TABLE demo.log DROP PARTITION ID '201402010000';
-- the 2014 entry "this happened over a decade ago!" is now gone
ALTER TABLE demo.log DROP PARTITION BETWEEN '2000-01-01' AND '2020-12-31';
-- the 2020 entry "Happy Easter!!" is now gone
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS;
-- the two ARCHIVE entries (PARTITION_IDs = 201402010000 and 202004010000) are now gone
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITION_MAPPINGS;
-- but the mapping for the ARCHIVE is still there
ℹ️ Este comando DROP PARTITION está pensado únicamente para administradores, ya que omite el journaling, el bloqueo, los triggers y no aplica acciones referenciales.
Consultando tablas particionadas
Podéis consultar las tablas particionadas igual que cualquier otra tabla. Cuando un predicado de filtrado en la cláusula WHERE de una consulta hace referencia a un campo de la clave de partición, IRIS SQL comprobará si puede utilizarlo para limitar el número de particiones que deben revisarse para construir el conjunto de resultados. Esto se denomina partition pruning y normalmente se traduce en una condición de rango adicional o en un nivel de paralelización en el plan de ejecución de la consulta.
Veamos cómo funciona con un ejemplo. Utilizad el botón “Show Plan” en el SMP o el comando EXPLAIN en vuestro shell o herramienta JDBC para comprobar el plan de ejecución de la siguiente consulta:
SELECT * FROM demo.log WHERE ts BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59';
Deberíais ver algo como esto, indicando que “This plan utilizes partition pruning”:

Hemos traducido la condición de rango sobre nuestro campo ts de la consulta a una condición de rango en la estructura de subíndices del master map (más sobre esto en la sección Under the Hood más adelante). A primera vista puede no parecer gran cosa, ya que esto es habitual en condiciones sobre campos indexados. La diferencia es que esta vez no hemos definido ningún índice sobre ts. Simplemente estamos aprovechando la estructura particionada del master map para convertir este escaneo completo de la tabla en un escaneo mucho más reducido de particiones, excluyendo aquellas que seguro no contienen datos que cumplan la condición de ts.
Veamos otra consulta:
SELECT COUNT(*) FROM demo.log WHERE ts BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59' AND log_level IN ('FATAL', 'ERROR');
y su plan de ejecución:

Lo nuevo aquí es que se está utilizando el mismo truco de partition pruning que vimos en el ejemplo del master map, pero ahora aplicado al uso del índice sobre log_level. Esto es posible porque aplicamos la misma estructura particionada también a los índices, lo cual es necesario para poder mapear los datos de los índices junto con los datos de las filas, como se describió anteriormente.
ℹ️ El particionamiento de índices también está disponible para el índice de Approximate Nearest Neighbor utilizado en Vector Search. En este índice, se construye un grafo complejo basado en los datos vectoriales, y el rendimiento de búsqueda disminuye a medida que el grafo crece. Gracias a la naturaleza por “buckets” de las tablas particionadas, existe un tamaño máximo para este grafo y podemos buscar de forma eficiente en paralelo entre los distintos buckets.
Bajo el capó
Hasta ahora, solo hemos visto la parte visible de SQL. Si ya estáis familiarizados con cómo IRIS almacena los datos de las tablas en globals, vamos a ver qué ocurre “bajo el capó”. Si pensáis en la palabra “global” como un tipo de calentamiento global, podéis saltar directamente a la siguiente sección, ¿Cómo convertir datos existentes?.
Datos de la tabla
Por defecto, IRIS almacena los datos de las tablas en una estructura global simple con un entero como subíndice que representa el ID de la fila, y un $listbuild que contiene los datos de las columnas:
^demo.log( <row-ID> ) = $lb( <column-1>, <column-2>, ... )
ℹ️ Ten en cuenta que el nombre de este global en la práctica será algo distinto —hasheamos las partes de esquema y tabla para ciertas eficiencias de bajo nivel—, pero eso haría el ejemplo menos legible. Consulta la documentación sobre extent sets para más detalles.
Para organizar los datos en particiones y mapear esas particiones a bases de datos usando el Extent Mapper, necesitamos introducir un nivel adicional de subíndice que codifique los valores del campo de partición en un ID de partición que sea fácilmente mapeable. Por ejemplo, en nuestro caso de particionamiento por rango, simplemente estamos codificando la fecha en un formato entero sencillo.
La siguiente estructura lograría ese objetivo básico:
^demo.log( <partition-ID>, <row-ID> ) = $lb( <column-1>, <column-2>, ... )
Sin embargo, en este modelo, las particiones individuales aún pueden crecer de forma arbitrariamente grande y presentarían algunos de los mismos problemas que tenemos hoy con tablas grandes no particionadas —escalado de bloqueos, índices poco manejables, etc. Por estos motivos, estamos dividiendo cada partición en buckets que tienen un tamaño máximo predecible de aproximadamente 2 millones de filas (32*64.000 para ser exactos). Según nuestros benchmarks, este tamaño de bucket ofrece un buen equilibrio entre oportunidades de paralelización y sobrecarga, y encaja bien con nuestras estructuras de datos bitmap y columnar.
Además, vamos a cambiar el identificador global de la tabla en el último subíndice por un entero que solo es único dentro de la partición, asegurando el mayor rendimiento posible en cada partición. Esto significa que el ID de fila pasa a ser un ID compuesto, combinando el ID de partición con ese entero (lo llamaremos PRowID):
^demo.log( <partition-ID>, <bucket-ID>, <P-row-ID> ) = $lb( <column-1>, <column-2>, ... )
Si navegáis a los mapas e índices de la tabla y seleccionáis el global del master map, o simplemente consultáis el global directamente, veréis en qué se traduce esto para nuestra tabla:
^DvH1.Ccdz.1 = ""
^DvH1.Ccdz.1(202301010000) = 1
^DvH1.Ccdz.1(202301010000,1,1) = $lb(1154594035806846976,"INFO","Happy 2023!!")
^DvH1.Ccdz.1(202412010000) = 1
^DvH1.Ccdz.1(202412010000,1,1) = $lb(1154656589406846976,"INFO","Merry Christmas!!")
^DvH1.Ccdz.1(202501010000) = 3
^DvH1.Ccdz.1(202501010000,1,1) = $lb(1154658438312846976,"INFO","this is today's first message")
^DvH1.Ccdz.1(202501010000,1,2) = $lb(1154658438313846976,"INFO","this is today's second message")
^DvH1.Ccdz.1(202501010000,1,3) = $lb(1154658438314846976,"ERROR","this is an error message, sadly")
^DvH1.Ccdz.1(202507010000) = 2
^DvH1.Ccdz.1(202507010000,1,1) = $lb(1154674076715846976,"INFO","a message from the future!")
^DvH1.Ccdz.1(202507010000,1,2) = $lb(1154674076716846976,"FATAL","it's the end of the world as we know it")
Estamos trabajando solo con unas pocas filas, todas caben en un único bucket por partición. Sin embargo, con esta estructura, estamos preparados para gestionar de forma eficiente tablas de miles de millones, e incluso billones de filas.
Índices
Antes de que lanzáramos esta funcionalidad, algunos clientes habían mapeado manualmente rangos de IDs de fila (clásicos) a distintas bases de datos como forma de particionamiento (no les culpamos —¡más bien deberían culparnos a nosotros por no haberlo lanzado antes!). Esto puede ayudar a cubrir la necesidad más urgente de dividir los datos de una tabla, pero no ofrece mucho control sobre qué datos se mapean realmente (comparado con la lógica de particiones por mes frente a la aleatoriedad de los IDs de fila) y no resuelve el problema con los índices, ya que estos no tienen una estructura de subíndices predecible sobre la que basar los mapeos.
Veamos primero cómo se ve hoy un índice sencillo en nuestro campo log_level de la tabla de logs:
^demo.log.lvl( <log-level-value>, <row-ID> ) = "" // regular
^demo.log.lvb( <log-level-value>, <chunk> ) = $bit(...) // bitmap
En el nuevo modelo, esto pasa a ser:
^demo.log.lvl( <partition-ID>, <bucket-ID>, <log-level-value>, <P-row-ID> ) = "" // regular
^demo.log.lvb( <partition-ID>, <bucket-ID>, <log-level-value>, <chunk> ) = $bit(...) // bitmap
Y para el índice bitmap de nuestra tabla en la práctica:
^DvH1.Ccdz.3(202301010000,1," INFO",1) = /* $bit(2) - PRowIDs: 1 */
^DvH1.Ccdz.3(202412010000,1," INFO",1) = /* $bit(2) - PRowIDs: 1 */
^DvH1.Ccdz.3(202501010000,1," ERROR",1) = /* $bit(4) - PRowIDs: 3 */
^DvH1.Ccdz.3(202501010000,1," INFO",1) = /* $bit(2,3) - PRowIDs: 1,2 */
^DvH1.Ccdz.3(202507010000,1," FATAL",1) = /* $bit(3) - PRowIDs: 2 */
^DvH1.Ccdz.3(202507010000,1," INFO",1) = /* $bit(2) - PRowIDs: 1 */
Puede que hayáis notado que, para búsquedas de índices simples como las que se usan para garantizar la unicidad, la estructura anterior requiere recorrer todas las particiones y buckets. Efectivamente, este es el precio que pagamos por la escalabilidad que obtenemos, y lo mitigamos mediante el uso adecuado de la paralelización. Obviamente, cuando el índice o la restricción de unicidad también incluye los campos de la clave de partición, esa sobrecarga ya no se aplica.
Los mapeos del namespace
Antes hemos hablado de cómo MOVE PARTITION primero asigna las particiones a las bases de datos deseadas. Podéis ver estos mapeos en la página de Global Mappings. Deberíais ver los globals divididos según sus subíndices de partición, con cada parte asignada a la base de datos correspondiente.
Inicio > Menú > Configurar namespaces > Mapeos globales (para la fila TESTTP1)
La definición de la clase
Aunque estamos presentando esta nueva funcionalidad desde la perspectiva de SQL, la clave de partición y otros elementos relevantes de la configuración de particionamiento también pueden expresarse mediante UDL.
Para la tabla demo.log descrita en este tutorial, se crean los siguientes miembros en la definición de la clase (hemos eliminado algunos elementos no específicos para mayor claridad):
Class demo.log Extends %Persistent
{
/* ... */
/// Bucket Id Property, auto-generated for partitioned class
Property "%%BUCKETID" As %Library.BigInt [ Private, SqlComputeCode = {set {*}={x__prowid}\2048000+1}, SqlComputed, SqlFieldName = x__bucketid ];
/// Partition Id 1 property, auto-generated for partitioned class
Property "%%PARTITIONID1" As %Library.Integer [ Private, SqlComputeCode = {set {*}=$$partitionIdFromDateTime^%occPartition({ts},"%Library.PosixTime",1,"MONTH")}, SqlComputed, SqlFieldName = x__partitionid1 ];
/// Partition-RowId property, auto-generated for partitioned class
Property "%%PROWID" As %Library.BigInt [ Private, SqlComputeCode = {set {*}=$sequence(^DvH1.Ccdz.1({x__partitionid1}))}, SqlComputed, SqlFieldName = x__prowid ];
/// Partition IdKey, auto-generated for partitioned class
Index "%%PartitionIdKey1" On ("%%PARTITIONID1", "%%BUCKETID", "%%PROWID") [ IdKey, Unique ];
/// PartitionKey index, auto-generated by DDL CREATE TABLE statement
Index PartitionKey On logts(range(1 month) [ Abstract, SqlName = "%%PartitionKey" ];
/* ... */
}
ℹ️ Como podéis imaginar, estos miembros específicos de la clase no deben modificarse.
Cómo convertir datos existentes
Tenemos la capacidad de particionar una tabla que aún no está particionada. De la misma forma, también podemos desparticionar una tabla que ya está particionada. Empecemos recreando esencialmente nuestra tabla demo.log, pero esta vez sin la cláusula PARTITION BY, para que no esté particionada.
CREATE TABLE demo.log2NotPart (
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
log_level VARCHAR(10) DEFAULT 'INFO',
message VARCHAR(1000)
);
Y vamos a insertar los mismos datos en esta tabla.
INSERT INTO demo.log2NotPart (log_level, message) VALUES ('ERROR', 'this is an error message, sadly');
INSERT INTO demo.log2NotPart (ts, message) VALUES (DATEADD('month', 6, CURRENT_TIMESTAMP), 'a message from the future!');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES (DATEADD('month', 6, CURRENT_TIMESTAMP), 'FATAL', 'it''s the end of the world as we know it');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES ('2024-08-12', 'INFO', 'Enjoy the Perseid meteor shower!');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES ('2014-02-27', 'INFO', 'this happened over a decade ago!');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES ('2023-01-01', 'INFO', 'Happy 2023!!');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES ('2024-12-25', 'INFO', 'Merry Christmas!!');
INSERT INTO demo.log2NotPart (ts, log_level, message) VALUES ('2020-04-12', 'INFO', 'Happy Easter!!');
Si comprobamos nuestra útil tabla de catálogo, veremos que no hay ninguna referencia a demo.log2NotPart. Esto es lo esperado, ya que la tabla no está particionada.
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS;
Ahora vamos a usar nuestro comando CONVERT PARTITION para particionar nuestra nueva tabla
ALTER TABLE demo.log2NotPart CONVERT PARTITION BY RANGE (ts) INTERVAL 1 MONTH
Cuando volvamos a consultar nuestra tabla de catálogo, deberíamos ver ahora siete entradas PARTITION_ID para demo.log2NotPart.
También podemos desparticionar la tabla utilizando el comando CONVERT PARTITION OFF.
ALTER TABLE demo.log2NotPart CONVERT PARTITION OFF
Una vez más, cuando volvamos a consultar nuestra tabla de catálogo, deberíamos ver de nuevo solo las particiones de demo.log y ninguna de demo.log2NotPart.
Hay algo más de matiz en la conversión de particiones de lo que hemos mostrado aquí. Si estáis probando esta funcionalidad, consultad la pregunta “¿Puedo convertir mis tablas existentes para que utilicen particionamiento?” en la sección de preguntas frecuentes.
Archivado automático de particiones antiguas - demo
Antes en esta demo hemos mostrado cómo ALTER TABLE ... MOVE PARTITION puede utilizarse para mover particiones a una base de datos específica, como una base de datos de archivo. Podemos combinar esto con el IRIS Task Manager para archivar automáticamente particiones antiguas. Al final de este artículo encontraréis un bloque de código con una clase de ejemplo demo.task.Archive.cls, que muestra cómo podría implementarse.
Empecemos recreando de nuevo nuestra tabla demo.log, pero esta vez contendrá datos de varios años.
CREATE TABLE demo.logMultiYear (
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
log_level VARCHAR(10) DEFAULT 'INFO',
message VARCHAR(1000)
) PARTITION BY RANGE (ts) INTERVAL 1 MONTH;
Y vamos a crear de nuevo algunas bases de datos adicionales
CREATE DATABASE FILE "OlderThanTwoYears" ON DIRECTORY '{install dir}/mgr/cheap/archive/'; -- change to match your setup
CREATE DATABASE FILE "OneToTwoYears";
CREATE DATABASE FILE "SixToTwelveMonths";
Usando VS Code o $SYSTEM.OBJ.Load() desde el terminal de IRIS, importad la clase del task de archivado en nuestro namespace TESTTP1. Además de los métodos habituales del task, incluye un método de clase que puede rellenar vuestra tabla con datos de varios años. Desde el terminal de IRIS, ejecutad el siguiente comando. Esto generará entradas en la tabla desde hace 1000 días en el pasado hasta 100 días en el futuro, añadiendo una entrada de log por cada día.
set tSC = ##class(demo.task.Archive).InsertLogEntriesDateRange("demo.logMultiYear",-1000,100)
Consultad la tabla demo.logMultiYear y nuestra útil tabla de catálogo. Deberíais ver las entradas de log y comprobar que los datos están distribuidos entre decenas de particiones.
SELECT TOP 1000 * FROM demo.logMultiYear;
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS WHERE TABLE_NAME = 'logMultiYear';
Ahora vamos a configurar la tarea de autoarchivado:
- En el System Management Portal, id a Inicio > Operación del sistema > Task Manager > Nueva tarea
- Asignadle un nombre como ArchiveDemoLogPartitions
- En el desplegable “Namespace to run task in”, seleccionad TESTTP1
- Para el tipo de tarea, seleccionad ArchivePartitions
- Si habéis usado el mismo nombre de tabla y bases de datos que en esta demo, podéis dejar el resto de valores por defecto
- Haced clic en Next
- Configurad la tarea para que se ejecute “Monthly (by day)”, y que se ejecute cada 1 mes(s) el primer domingo
- Programadla para que se ejecute una vez a las 00:01:00
- Haced clic en Finish
La tarea de autoarchivado está ahora programada para ejecutarse una vez al mes, el primer domingo de cada mes a la 1:00 de la madrugada. Pero como no queremos esperar, vamos a ejecutarla ahora para ver qué ocurre.
-
Id a Inicio > Operación del sistema > Task Manager > Programación de tareas
-
Seleccionad la entrada ArchiveDemoLogPartitions
-
Haced clic en Run, luego en Perform Action Now y después en Close
-
La tarea puede tardar hasta dos minutos en comenzar a ejecutarse y entre 1 y 4 minutos adicionales en completarse.
-
Actualizad la página hasta que veáis que “Last Finished” aparece con una marca de tiempo
-
Una vez más, consultad la tabla demo.logMultiYear y nuestra tabla de catálogo. La tabla de catálogo debería mostrar que las particiones más antiguas ahora tienen un LOCATION correspondiente a las bases de datos antiguas apropiadas.
SELECT TOP 1000 * FROM demo.logMultiYear;
SELECT * FROM INFORMATION_SCHEMA.TABLE_PARTITIONS WHERE TABLE_NAME = 'logMultiYear';
¡Eso es todo! Acabáis de configurar una tarea de archivado automático que mueve particiones antiguas a bases de datos que pueden estar en almacenamiento más económico. Esto es, por supuesto, solo una demostración, y en un entorno de producción podéis tener requisitos específicos que difieran de nuestro escenario TESTTP1. Pero, como veréis en la clase demo.task.Archive, el trabajo principal lo realizan los comandos ALTER TABLE ... MOVE PARTITION, que simplemente construimos en función de los objetivos y restricciones de nuestro caso de uso.
Limpieza
Un simple comando DROP TABLE eliminará automáticamente cualquier mapeo de base de datos creado a través del Extent Mapper, así que lo único que tenemos que hacer para limpiar nuestra tabla es lo siguiente:
DROP TABLE IF EXISTS demo.log;
DROP TABLE IF EXISTS demo.log2NotPart;
DROP TABLE IF EXISTS demo.logMultiYear;
Preguntas frecuentes
¿Quién necesita el particionamiento de tablas?
La mayoría de aplicaciones o esquemas en los que los datos se acumulan con el tiempo (¡o ya empiezan siendo grandes!) se beneficiarán de adoptar el particionamiento de tablas. Es especialmente relevante para clientes que se preocupan por el coste de alojar toda su base de datos en almacenamiento premium, solo porque una parte de esos datos es “hot data” que necesita estar disponible con la latencia ultrabaja que ofrece ese nivel de almacenamiento.
¿Esto ofrece una estratificación automática completa del almacenamiento?
La funcionalidad de particionamiento de tablas descrita aquí empezará como una característica de plataforma. Ofrece las herramientas para que un administrador defina una política de ciclo de vida de la información que incluya mover datos entre niveles de almacenamiento y archivar particiones antiguas, pero no automatiza este proceso de forma mágica. Este tipo de automatización requiere más conocimiento del entorno, especialmente sobre los niveles de almacenamiento disponibles y la intención global. Nuestros servicios en la nube, donde controlamos ese entorno, están mejor posicionados para ofrecer ese tipo de capa de automatización sobre la funcionalidad a nivel de plataforma.
¿Cuándo estará disponible esta funcionalidad en InterSystems IRIS?
Está disponible como funcionalidad experimental in IRIS and IRIS for Health 2026.1. Actualmente está en la hoja de ruta para su lanzamiento en versión GA en IRIS 2027.1. Podéis utilizar el Programa de Acceso Temprano para obtener una clave de licencia para Table Partitioning, o usar una clave de licencia que incluya IRIS Advanced Server.
¿Cómo funciona?
A nivel técnico, el particionamiento de tablas se basa en subíndices adicionales que codifican el valor de la clave de partición, utilizados de forma consistente para los datos de filas, índices y otras estructuras asociadas. En el diseño del esquema, los usuarios pueden elegir una clave de partición, como un campo de fecha o de región, y definir cómo deben traducirse esos valores en particiones, por ejemplo, rangos mensuales de fechas o una partición por región.
Cuando se añaden filas a una tabla particionada, el SQL (o Object) Filer calcula los valores de subíndice adecuados para cada fila y garantiza que los datos se almacenen en la ubicación correcta. Utilizando una nueva herramienta llamada Extent Mapper (disponible mediante simples comandos DDL), los usuarios pueden mapear o mover particiones de una base de datos a otra mediante criterios lógicos, como mover todas las particiones de 2021 a una base de datos y las de 2022 a otra.
¿No podría hacer esto ya con mapeos a nivel de subíndice?
En cierta medida, sí. De hecho, el particionamiento de tablas se basa en mapeos a nivel de subíndice para dirigir los datos al lugar correcto, pero la gran diferencia es que introduce esos subíndices adicionales sobre los que se basan los mapeos y permite expresar el mapeo en términos lógicos, alineados con lo que habéis definido como esquema de partición.
Antes de esto, los usuarios solo podían crear mapeos manuales basados en el RowID, que no tiene un significado lógico más allá de una aproximación muy general de la antigüedad del registro, y no había forma de dividir de manera significativa los globals de índices.
¿Puedo convertir mis tablas existentes para que utilicen particionamiento?
Sí, estamos incluyendo la sintaxis ALTER TABLE .. CONVERT .. para permitir transformar una tabla existente en una tabla particionada y viceversa. Ten en cuenta que algunas conversiones implican un cambio en el RowID y pueden requerir actualizaciones en las tablas que tengan una restricción de clave foránea apuntando a la tabla que se está convirtiendo.
¿Qué otros beneficios debería esperar?
La estructura de datos más refinada ofrece dos beneficios adicionales, además de la posibilidad de organizar fácilmente los datos entre bases de datos:
- gracias al nivel adicional de subíndice, la ingesta a gran escala puede ser más rápida, ya que hay menos probabilidad de escalado de bloqueos y de contención general
- el optimizador de consultas puede construir planes de ejecución más inteligentes cuando detecta predicados que implican el campo de la clave de partición, por ejemplo, eliminando (omitiendo) particiones en las que sabe que no puede haber filas coincidentes.
¿Dónde está la trampa?
El nivel adicional de subíndice introduce sobrecarga en el contexto de claves únicas que no incluyen los campos de la clave de partición. Al insertar nuevas filas, comprobar que el valor de la nueva fila no existe ya requiere revisar todas las particiones. De forma similar, una búsqueda basada en un valor de clave única requerirá recorrer todas las particiones.
Tenemos previsto añadir soporte para índices globales que no estén particionados y que ayuden con campos realmente únicos, pero estos, por supuesto, tampoco pueden dividirse entre bases de datos.
¿Qué son los buckets?
Puede que no siempre tengáis un candidato perfecto para una clave de partición, pero aun así queráis beneficiaros de las ventajas operativas que aporta el nivel adicional de subíndice en la ingesta. O quizá simplemente queréis mover “parte” de los datos de una tabla grande a otra base de datos sin usar una restricción lógica específica.
En las tablas particionadas, no solo introducimos un nivel de subíndice que codifica la clave de partición, sino también un “bucket ID” opaco que ayuda a organizar los datos en porciones más pequeñas, independientemente de si vuestra clave de partición es muy granular, equilibrada o ninguna de las dos cosas.
Una tabla con partición por defecto es aquella que adopta este nivel adicional de subíndice, pero no define una clave de partición lógica encima de él, y por tanto ofrece algunas de las ventajas del particionamiento sin necesidad de tomar decisiones de diseño adicionales.
A largo plazo, nuestra intención es que el bucketing sea el comportamiento por defecto al crear una nueva tabla.
¿Cómo se relaciona esto con el sharding?
El particionamiento de tablas es totalmente ortogonal al sharding.
Aunque a primera vista pueden parecer similares, ya que ambos se enfocan en tablas muy grandes, los motivos para usar particionamiento son principalmente operativos, mientras que el sharding está enfocado puramente en el rendimiento. Además, en el particionamiento se elige una clave de partición para poder mapear particiones específicas a bases de datos según el valor de esa clave, mientras que en el caso del sharding la distribución de los datos entre shards es completamente opaca y la clave solo se usa para asegurar relaciones de cosharding entre tablas que se hacen JOIN con frecuencia.
Ten en cuenta que combinar particionamiento con sharding añade complejidad adicional, ya que las bases de datos a las que se mapean tendrían que estar disponibles en cada shard. A partir de IRIS 2026.1, no se soporta el uso simultáneo de particionamiento de tablas y sharding en la misma tabla. Actualmente hay desarrollo en curso para futuras versiones con el objetivo de integrar mejor el sharding con el particionamiento de tablas.
¿Cómo se relaciona esto con el almacenamiento columnar?
El particionamiento de tablas es totalmente ortogonal al almacenamiento columnar, y esperamos que los clientes que implementen un data warehouse y escenarios similares los utilicen a menudo conjuntamente. Sin embargo, a partir de IRIS 2026.1, no se admite el particionamiento de tablas para una tabla con almacenamiento columnar. Actualmente hay desarrollo en curso para futuras versiones con el objetivo de soportar ambos en la misma tabla.
¿Qué pasa con nuestros otros modelos de datos?
La funcionalidad actualmente se llama particionamiento de tablas, pero en el futuro podríamos ampliar este concepto a otros modelos de datos, cuando exista la necesidad de organizar lógicamente los datos entre varias bases de datos.
¿Cómo obtener ayuda?
Si tenéis preguntas sobre esta demo y tutorial, podéis comentar este artículo y mencionarme a mí, Ben Schlanger. Si encontráis errores, podéis reportarlos a través del WRC, pero tened en cuenta que actualmente se trata de una funcionalidad experimental. Es posible que InterSystems no pueda ofrecer una solución completa a los problemas como lo hace con las funcionalidades estándar de la plataforma.
Archivado automático de particiones antiguas: clase de demo
/// This task is scheduled to run on the first Sunday of each month, when system activity is otherwise low
/// For the given table, it moves partitions to different databases based on the date range of the partition
Class demo.task.Archive Extends %SYS.Task.Definition
{
Parameter TaskName = "ArchivePartitions";
/// Default archive partitions older than six months
Property PartitionedTable As %String [ InitialExpression = "demo.logMultiYear" ];
/// Default archive for partitions older than two months
Property ArchiveOlderDatabase As %String [ InitialExpression = "OlderThanTwoYears" ];
/// Default archive for partitions between 1-2 years
Property ArchiveTwoYearDatabase As %String [ InitialExpression = "OneToTwoYears" ];
/// Default archive for partitions between 6-12 months years
Property ArchiveOneYearDatabase As %String [ InitialExpression = "SixToTwelveMonths" ];
/// Do multiple types of archiving
/// Partitions for data older than two years get moved to one database
/// Partitions for data between 1-2 years old get moved to another database
/// Partitions for data between 6-12 months old get moved to yet another database
/// Partitions for data more recent than 6 months remain in the primary database
ClassMethod MovePartitionsToArchivesDynamicSQL(pPartitionedTable As %String, pOlderArchive As %String, pTwoYearArchive As %String, pOneYearArchive As %String) As %Status
{
// Define our cutoff dates
set cutoffTwoYear = $SYSTEM.SQL.Functions.DATEADD("year",-2,$ZDATE($HOROLOG,3))
set cutoffOneYear = $SYSTEM.SQL.Functions.DATEADD("year",-1,$ZDATE($HOROLOG,3))
set cutoffSixMonths = $SYSTEM.SQL.Functions.DATEADD("month",-6,$ZDATE($HOROLOG,3))
// Partitions for data older than two years get moved to OlderThanTwoYears
set sql = "ALTER TABLE "_pPartitionedTable_
" MOVE PARTITION BETWEEN '1900-01-01' AND '"_cutoffTwoYear_"' "_
" TO '"_pOlderArchive_"'"
set rset = $SYSTEM.SQL.Execute(sql)
if (rset.%SQLCODE '= 0) {write "%Execute failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message return rset}
// Partitions for data between 1-2 years old get moved to OneToTwoYears
set sql = "ALTER TABLE "_pPartitionedTable_
" MOVE PARTITION BETWEEN '"_cutoffTwoYear_"' AND '"_cutoffOneYear_"' "_
" TO '"_pTwoYearArchive_"'"
set rset = $SYSTEM.SQL.Execute(sql)
if (rset.%SQLCODE '= 0) {write "%Execute failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message return rset}
// Partitions for data between 6-12 months old get moved to SixToTwelveMonths
set sql = "ALTER TABLE "_pPartitionedTable_
" MOVE PARTITION BETWEEN '"_cutoffOneYear_"' AND '"_cutoffSixMonths_"' "_
" TO '"_pOneYearArchive_"'"
set rset = $SYSTEM.SQL.Execute(sql)
if (rset.%SQLCODE '= 0) {write "%Execute failed:", !, "SQLCODE ", rset.%SQLCODE, ": ", rset.%Message return rset}
return $$$OK
}
/// Default purge code
Method OnTask() As %Status
{
return ..MovePartitionsToArchivesDynamicSQL(..PartitionedTable, ..ArchiveOlderDatabase, ..ArchiveTwoYearDatabase, ..ArchiveOneYearDatabase)
}
/// Populate a demo.log* table with entries spanning many days, with one entry per day
/// pTableName: the table you're inserting log entries into
/// pStartDate: With today's date as zero, what date should we start at?
/// To start in the past, use a negative value
/// pEndDate: With today's date as zero, what date should we end at?
/// To end in the past, use a negative value
/// Example: Starting from 1000 days in the past and going until 100 days into the future, add one log entry per day
/// set tSC = ##class(Test.Ben.TBLP.Task.Archive).InsertLogEntriesDateRange("demo.logMultiYear",-1000,100)
ClassMethod InsertLogEntriesDateRange(pTableName As %String = "demo.logMultiYear", pStartDate As %Integer = 1, pEndDate As %Integer = 10) As %Status [ Language = objectscript ]
{
SET stmt = ##class(%SQL.Statement).%New()
set status = stmt.%Prepare("INSERT INTO "_pTableName_"(ts, log_level, message) VALUES (?, ?, ?)")
if $$$ISERR(status) {
do $system.OBJ.DisplayError(status)
return status
}
TSTART
FOR i=pStartDate:1:pEndDate {
SET ts = $ZDATETIME($HOROLOG+i,3)
set logTable = stmt.%Execute(ts, "INFO", "Synthetic log message #" _ i)
if logTable.%SQLCODE < 0 {
write logTable.%SQLCode
return logTable
}
// Commit every 10k rows
IF '(i#10000) {
TCOMMIT
TSTART
}
}
TCOMMIT
return $$$OK
}
}