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

*Segunda entrega de la serie **[MariaDB desde cero a pro](/search?tag=mariadb-desde-cero)**. Tiempo de lectura estimado: 11 minutos.*

En la [entrega anterior](/post/mariadb-desde-cero-i-instalacion-y-primeros-pasos) 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.

```sql
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.

```sql
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.

```sql
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](/search?tag=clickhouse-desde-cero) 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

```sql
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

```sql
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

```sql
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

```sql
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:

```sql
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

```sql
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

```sql
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
```

Alternativa moderna con `SEQUENCE` (desde 10.3):

```sql
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

```sql
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:

```sql
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).

```sql
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:

```sql
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.

```sql
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

```sql
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

- **[III: consultas, CTEs y window functions](/post/mariadb-desde-cero-iii-consultas-ctes-y-window-functions)** — el SQL moderno.
- **[IV: índices, EXPLAIN y tuning](/post/mariadb-desde-cero-iv-indices-explain-y-tuning)** — rendimiento y diagnóstico.
- **[V: replicación, Galera y producción](/post/mariadb-desde-cero-v-replicacion-galera-y-produccion)** — cierre de la serie.

Si llegaste tarde: [I: instalación y primeros pasos](/post/mariadb-desde-cero-i-instalacion-y-primeros-pasos).
