Javier Valencia Javier Valencia
MariaDB desde cero (II): storage engines, tipos y restricciones

MariaDB desde cero (II): storage engines, tipos y restricciones

Javier Valencia · · 5 min de lectura · 57 visitas · Desarrollo
bases-de-datos tutorial mariadb mysql sql mariadb-desde-cero

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 (o PERSISTENT en 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=InnoDB explícito en todas.
  • CHECK sobre estado y published_at: si el post está publicado, debe tener fecha.
  • CHECK sobre el JSON para que sea válido.
  • ON DELETE RESTRICT en la FK de autor (no se puede borrar un autor con posts).
  • ON DELETE CASCADE en la tabla de unión.
  • Índice secundario en post_tags(tag_id) para recorrer "tag → posts".

Por dónde seguir

Si llegaste tarde: I: instalación y primeros pasos.