MariaDB desde cero (II): storage engines, tipos y restricciones
Segunda entrega de la serie MariaDB desde cero a pro. Tiempo de lectura estimado: 11 minutos.
En la entrega anterior instalamos MariaDB, creamos una base de datos y vimos los primeros comandos. Ahora vamos a lo que distingue MariaDB del resto: el concepto de storage engine, el sistema de tipos y las restricciones disponibles.
Storage engines
A diferencia de PostgreSQL (un único motor) o ClickHouse (varios engines pero una familia unificada), en MariaDB cada tabla puede usar un motor distinto. Cada engine tiene pros y contras claros. Elegir bien es parte del diseño.
InnoDB
El engine por defecto desde hace mucho. Transaccional, ACID, soporta foreign keys, MVCC, row-level locking.
Para casi todas las tablas de una aplicación normal, la respuesta es InnoDB. No tienes que pensárselo.
CREATE TABLE ... ENGINE=InnoDB;
Es el único engine que soporta foreign keys respetadas. Los demás pueden declararlas pero no las hacen cumplir.
Aria
Sucesor de MyISAM. Crash-safe, no transaccional. Usado internamente para tablas de sistema y buen candidato para tablas temporales grandes que no necesitan transacciones ni foreign keys.
CREATE TABLE tmp_report ... ENGINE=Aria;
MyRocks
Basado en RocksDB de Facebook. Optimizado para escritura intensiva y alta compresión. Ocupa notablemente menos que InnoDB en datasets grandes.
CREATE TABLE events ... ENGINE=RocksDB;
Casos donde MyRocks brilla:
- Logs, eventos, time series con mucho INSERT y poco UPDATE.
- Datasets que no caben en RAM pero sí en SSD.
- Sistemas donde el tamaño en disco importa.
No lo uses como engine principal de una app típica. Es una herramienta especializada.
ColumnStore
Motor columnar al estilo ClickHouse, integrado en MariaDB. Pensado para analítica. Si tu carga es 80% OLTP y 20% reporting, ColumnStore puede evitar montar otro sistema.
Dicho esto: si la carga analítica es seria, sigue siendo mejor un ClickHouse dedicado al lado. ColumnStore encaja como solución intermedia.
Spider
Engine de sharding federado: tablas lógicas que viven en varios servidores MariaDB backend. Poderoso, pero complejo. Solo si realmente entiendes lo que estás montando.
MEMORY, CSV, ARCHIVE, BLACKHOLE
Engines de nicho:
- MEMORY: tabla en RAM. Rápida, se pierde al reiniciar.
- CSV: fichero CSV presentado como tabla. Útil para ETL.
- ARCHIVE: insert-only, muy comprimido.
- BLACKHOLE: descarta escrituras pero loggea en binlog. Truco clásico para construir replicación custom.
Qué engine elegir
- OLTP normal → InnoDB.
- Logs/métricas con mucho write → MyRocks.
- Analytics en la misma instancia → ColumnStore.
- Tablas temporales → Aria.
- Resto → InnoDB.
El sistema de tipos
Enteros
TINYINT -- 1 byte, -128..127 (o 0..255 con UNSIGNED)
SMALLINT -- 2 bytes
MEDIUMINT -- 3 bytes (peculiar de MySQL/MariaDB)
INT -- 4 bytes
BIGINT -- 8 bytes
El modificador UNSIGNED dobla el rango positivo. Para claves primarias autoincrementales suele ir bien BIGINT UNSIGNED.
Precisión: INT(11) no significa "entero de 11 dígitos". Significa "ancho de display si usas ZEROFILL". Es confuso y MariaDB 10.6+ lo marca como deprecado. Escribe INT a secas.
Decimales y floats
DECIMAL(P, S) -- precisión exacta. DECIMAL(12,4) para dinero
FLOAT -- 4 bytes
DOUBLE -- 8 bytes
Mismo mantra que en las otras bases: dinero = DECIMAL, siempre.
Strings
CHAR(n) -- fija, hasta 255
VARCHAR(n) -- variable, hasta 65.535 bytes por fila total
TINYTEXT -- hasta 255 bytes
TEXT -- 64 KB
MEDIUMTEXT -- 16 MB
LONGTEXT -- 4 GB
BINARY, VARBINARY, BLOBs -- equivalentes binarios
Para texto en español con emoji, usa VARCHAR(n) con CHARACTER SET utf8mb4. Recuerda: en utf8mb4, un carácter puede ocupar hasta 4 bytes, así que VARCHAR(255) podría consumir 1020 bytes.
Fechas y tiempos
DATE -- 3 bytes
TIME -- 3 bytes
DATETIME -- 5..8 bytes, sin zona
TIMESTAMP -- 4..7 bytes, convertido a UTC internamente
YEAR -- 1 byte
Diferencia importante entre DATETIME y TIMESTAMP:
TIMESTAMP: rango 1970..2038 (históricamente), se almacena como UTC y se convierte a la zona horaria de la sesión.DATETIME: rango 1000..9999, se almacena tal cual, sin conversión.
Para timestamps con soporte horario-aware, usa TIMESTAMP en MariaDB, con cuidado del horizonte 2038 si tu software perdura (en MariaDB moderno se amplía). Para fechas de eventos histórico-arbitrarios, DATETIME.
JSON
Desde MariaDB 10.2 existe JSON. Internamente se almacena como LONGTEXT con validación. Funciones:
CREATE TABLE events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(40) NOT NULL,
data JSON NOT NULL,
CHECK (JSON_VALID(data))
) ENGINE=InnoDB;
INSERT INTO events (type, data)
VALUES ('signup', JSON_OBJECT('email', 'a@b.com', 'plan', 'free'));
-- Acceso
SELECT JSON_VALUE(data, '$.email') AS email FROM events;
SELECT JSON_EXTRACT(data, '$.plan') FROM events;
-- Búsqueda
SELECT * FROM events WHERE JSON_VALUE(data, '$.plan') = 'free';
-- Columna virtual indexable
ALTER TABLE events
ADD COLUMN email VARCHAR(320) AS (JSON_VALUE(data, '$.email')) VIRTUAL,
ADD INDEX ix_events_email (email);
No es tan potente como JSONB de PostgreSQL (no hay índices GIN nativos), pero el patrón "columna virtual + índice" cubre los casos más habituales.
ENUM y SET
status ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft'
ENUM es rápido y ocupa poco. Contras: añadir valores requiere ALTER TABLE, lo que en tablas grandes puede ser costoso. Alternativa: VARCHAR con CHECK.
SET permite guardar combinaciones de valores permitidos en una sola columna. Poca gente lo usa; cuando aparece un caso, suele ser más limpio con una tabla relacional.
Restricciones
NOT NULL y DEFAULT
Igual que en cualquier SQL. Úsalas sin miedo: una columna no nula es una invariante que el resto del sistema puede asumir.
PRIMARY KEY y AUTO_INCREMENT
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
Alternativa moderna con SEQUENCE (desde 10.3):
CREATE SEQUENCE seq_posts_id START WITH 1 INCREMENT BY 1;
CREATE TABLE posts (
id BIGINT NOT NULL DEFAULT NEXTVAL(seq_posts_id),
...
);
La ventaja de las secuencias es que cruzan transacciones sin reservar rangos y son más parecidas a lo que hay en Oracle o PostgreSQL.
UNIQUE
UNIQUE KEY ux_authors_email (email)
UNIQUE (tenant_id, slug)
Al contrario que PostgreSQL, MariaDB no tiene índices únicos parciales. Truco habitual: columna virtual + índice:
ALTER TABLE users
ADD COLUMN email_active VARCHAR(320) AS (IF(deleted_at IS NULL, email, NULL)) VIRTUAL,
ADD UNIQUE KEY ux_users_email_active (email_active);
CHECK
Desde MariaDB 10.2 los CHECK se aplican de verdad (en versiones previas solo se parseaban y se ignoraban).
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
sku VARCHAR(30) NOT NULL CHECK (sku REGEXP '^[A-Z0-9-]{4,20}$')
) ENGINE=InnoDB;
Foreign keys
Solo InnoDB las respeta. Sintaxis:
CONSTRAINT fk_posts_author
FOREIGN KEY (author_id) REFERENCES authors(id)
ON DELETE RESTRICT ON UPDATE CASCADE
Opciones: RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT.
Peculiaridad histórica: MariaDB no hace deferred constraint checking. Las FK se validan en el mismo statement. Esto evita ciertos patrones de inserción por lotes que sí funcionan en PostgreSQL (con DEFERRABLE INITIALLY DEFERRED).
Generated columns
Columnas calculadas a partir de otras. Dos modos:
VIRTUAL: se calculan al leer. No ocupan disco.STORED(oPERSISTENTen MariaDB): se calculan al escribir y se guardan.
CREATE TABLE invoices (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subtotal DECIMAL(12,2) NOT NULL,
vat_rate DECIMAL(4,2) NOT NULL,
total DECIMAL(12,2) AS (subtotal * (1 + vat_rate)) VIRTUAL,
issued_at DATE NOT NULL,
year_month VARCHAR(7) AS (DATE_FORMAT(issued_at, '%Y-%m')) PERSISTENT,
INDEX ix_invoices_ym (year_month)
) ENGINE=InnoDB;
Puedes indexar columnas virtuales. Es el patrón estándar para acelerar consultas sobre expresiones (equivalente a los expression indexes de PostgreSQL, vía columna intermedia).
Claves compuestas y orden de columnas
En InnoDB, la PRIMARY KEY es el clustering key: los datos de la tabla se almacenan físicamente ordenados por ella. Esto tiene implicaciones enormes:
- Claves primarias anchas inflan todos los índices secundarios (que incluyen la PK).
- Claves primarias auto-incrementales secuenciales son baratas (inserts al final).
- Claves primarias aleatorias (UUID v4) fragmentan las páginas y degradan escritura.
Regla práctica en InnoDB:
- PK numérica auto-incremental corta (BIGINT o INT).
- Si necesitas UUIDs externos, considera UUID v7 (ordenados por tiempo) o guarda el UUID como columna secundaria.
Orden en claves compuestas: igualdad antes que rango, mismo principio que el resto de sistemas.
Un esquema completo
CREATE DATABASE IF NOT EXISTS blog
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE blog;
CREATE TABLE authors (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
email VARCHAR(320) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_authors_email (email),
CONSTRAINT ck_authors_email_format CHECK (email LIKE '%@%')
) ENGINE=InnoDB;
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
author_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(300) NOT NULL,
slug VARCHAR(300) NOT NULL,
body MEDIUMTEXT NOT NULL,
status ENUM('draft','published','archived') NOT NULL DEFAULT 'draft',
metadata JSON NULL,
published_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_posts_slug (slug),
KEY ix_posts_author_published (author_id, published_at),
CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE RESTRICT,
CONSTRAINT ck_posts_published CHECK (status != 'published' OR published_at IS NOT NULL),
CONSTRAINT ck_posts_metadata CHECK (metadata IS NULL OR JSON_VALID(metadata))
) ENGINE=InnoDB;
CREATE TABLE tags (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
UNIQUE KEY ux_tags_name (name)
) ENGINE=InnoDB;
CREATE TABLE post_tags (
post_id BIGINT UNSIGNED NOT NULL,
tag_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (post_id, tag_id),
KEY ix_post_tags_tag (tag_id),
CONSTRAINT fk_pt_post FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
CONSTRAINT fk_pt_tag FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB;
Detalles:
ENGINE=InnoDBexplícito en todas.CHECKsobre estado ypublished_at: si el post está publicado, debe tener fecha.CHECKsobre el JSON para que sea válido.ON DELETE RESTRICTen la FK de autor (no se puede borrar un autor con posts).ON DELETE CASCADEen la tabla de unión.- Índice secundario en
post_tags(tag_id)para recorrer "tag → posts".
Por dónde seguir
- III: consultas, CTEs y window functions — el SQL moderno.
- IV: índices, EXPLAIN y tuning — rendimiento y diagnóstico.
- V: replicación, Galera y producción — cierre de la serie.
Si llegaste tarde: I: instalación y primeros pasos.