MariaDB SQL DDL
Definition statements that change the schema : CREATE, ALTER, DROP, RENAME, plus sequences, generated columns, invisible columns, and partitioning. Read this before any schema migration on a production-size table.
Quick Reference
- ALWAYS specify
ALGORITHM=andLOCK=on everyALTER TABLEso the server fails fast instead of silently falling back to COPY. - ALWAYS prefer
ALGORITHM=INSTANTfor ADD COLUMN (10.3+) and DROP COLUMN (10.4+) when the table allows it. Metadata-only change, no rewrite. - ALWAYS use
ALGORITHM=INPLACE, LOCK=NONEfor adding non-INSTANT-able indexes on big tables. - NEVER default to
ALGORITHM=COPYon production-size tables : rebuilds the whole table, blocks DML in earlier versions. From 11.2+ COPY supportsLOCK=NONEbut still rewrites everything. - NEVER add an
INSTANTcolumn to aROW_FORMAT=COMPRESSEDtable or to a table with a FULLTEXT index : both reject INSTANT. - ALWAYS choose
VIRTUALfor generated columns unless you need an FK pointing at the column, replication of the computed value, or skip the recompute cost on heavy reads. PERSISTENT/STORED is synonyms but PERSISTENT is the canonical MariaDB keyword. - ALWAYS use atomic
RENAME TABLE old TO retired, new TO old;for blue-green deployments. Multi-table RENAME is atomic within a single statement (10.6+ atomic DDL). - NEVER use
ALTER VIEWand expect rebuild : views requireCREATE OR REPLACE VIEW. - Sequences are MariaDB-only (10.3+), Oracle/PostgreSQL-compatible syntax. MySQL has no sequences.
- Partitioned tables CANNOT have foreign keys and CANNOT be referenced by foreign keys : hard architectural boundary.
- Atomic DDL is rollback-safe on server crash (10.6+) for single-table DDL. Multi-table DROP is crash-safe but not fully atomic.
Decision Trees
Picking an ALTER algorithm
Is the change metadata-only (ADD COLUMN at any position 10.4+, DROP COLUMN 10.4+, RENAME COLUMN/INDEX 10.5.3+, modify ENUM by appending, set DEFAULT) ?
YES : ALGORITHM=INSTANT, LOCK=NONE
NO : continue
Is the change an ADD INDEX, DROP INDEX, or column modification that does not rewrite clustered-index keys ?
YES : ALGORITHM=INPLACE, LOCK=NONE
NO : continue
Does the change require rebuilding the clustered index (change PK, change column type, change ROW_FORMAT) ?
YES on small table : ALGORITHM=COPY, LOCK=SHARED, run during maintenance window
YES on big table : use pt-online-schema-change or gh-ost ; never inline COPY on prod
Generated column : VIRTUAL or PERSISTENT
Do you need an FK pointing at the generated column ?
YES : PERSISTENT (FK only allowed on stored)
NO : continue
Do you replicate row-based and need the computed value applied identically on the replica ?
YES : PERSISTENT
NO : continue
Is the read path heavy and the expression expensive ?
YES : PERSISTENT (trade disk for CPU)
NO : VIRTUAL (default ; no disk cost)
Partitioning : type selection
Filter is a date / numeric range with predictable boundaries ?
YES : RANGE or RANGE COLUMNS (multi-column variant)
Filter is a finite set of discrete values (region codes, tenant ids) ?
YES : LIST or LIST COLUMNS
You want even distribution by hashing a key, no semantic boundary ?
YES : HASH (LINEAR HASH for less-expensive partition addition)
You want hash-partitioning by primary key columns ?
YES : KEY (or LINEAR KEY)
You need automatic time-period partitions tied to system-versioning ?
YES : SYSTEM_TIME (MariaDB-only ; requires WITH SYSTEM VERSIONING)
Patterns
CREATE TABLE (modern default)
-- 10.6+ : production-grade table template
CREATE TABLE invoice (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status ENUM('draft','sent','paid','void') NOT NULL DEFAULT 'draft',
metadata JSON NULL CHECK (JSON_VALID(metadata)),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
KEY ix_customer_status (customer_id, status),
CONSTRAINT fk_invoice_customer FOREIGN KEY (customer_id) REFERENCES customer(id) ON DELETE RESTRICT
) ENGINE=InnoDB
ROW_FORMAT=DYNAMIC
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_uca1400_ai_ci;
Always set CHARSET explicitly per table : 10.6 and 10.11 LTS stock builds still default to latin1, which silently breaks emoji and accented strings.
Fast schema change : INSTANT ADD COLUMN
-- 10.4+ : metadata-only column add, no rewrite, no DML block
ALTER TABLE invoice
ADD COLUMN paid_at TIMESTAMP(6) NULL AFTER created_at,
ALGORITHM=INSTANT,
LOCK=NONE;
The server errors out (HY000 1845) if INSTANT is impossible (compressed row format, FULLTEXT index present). No silent COPY fallback.
Fast index add : INPLACE
-- 10.6+ : online index build, DML allowed during build
ALTER TABLE invoice
ADD KEY ix_status_created (status, created_at),
ALGORITHM=INPLACE,
LOCK=NONE;
ADD INDEX is INPLACE-only ; it cannot be INSTANT because the index data structure must be populated.
Multi-operation ALTER in one statement
-- 10.4+ : combine compatible operations to reduce one-touch overhead
ALTER TABLE invoice
ADD COLUMN tax_amount DECIMAL(12,2) NOT NULL DEFAULT 0 AFTER amount,
ADD COLUMN currency CHAR(3) NOT NULL DEFAULT 'EUR' AFTER tax_amount,
ALGORITHM=INSTANT,
LOCK=NONE;
All columns added in one metadata change. If any single sub-change rejects INSTANT, the whole statement errors out and nothing changes.
Atomic table swap for blue-green deploys
-- 10.6+ : single-statement atomic swap, no client sees the wrong shape
RENAME TABLE
invoice TO invoice_v1,
invoice_v2 TO invoice;
-- After verification :
DROP TABLE invoice_v1;
Within a single RENAME TABLE statement, all renames either succeed atomically or roll back together. Atomic for InnoDB, MyRocks, MyISAM, Aria.
Sequence as MySQL-incompatible ID generator
-- 10.3+ MariaDB-only
CREATE SEQUENCE invoice_seq
START WITH 1000
INCREMENT BY 1
MINVALUE 1000
MAXVALUE 9999999999
CACHE 1000
NOCYCLE;
INSERT INTO invoice (id, customer_id, amount) VALUES
(NEXT VALUE FOR invoice_seq, 42, 99.95);
-- Inspect the last value generated in this session
SELECT PREVIOUS VALUE FOR invoice_seq;
NEXT VALUE FOR seq is SQL-standard ; NEXTVAL(seq) is the legacy function form, both work. CACHE 1000 pre-allocates 1000 ids per request : restart loses unused cached ids, which is a gap-creator. CACHE 1 maximises consistency at the cost of per-row overhead.
Generated column with functional index (JSON path)
-- 10.2+ : VIRTUAL column over JSON path, indexed
ALTER TABLE event
ADD COLUMN customer_id BIGINT UNSIGNED
AS (JSON_VALUE(payload, '$.customer.id')) VIRTUAL,
ADD KEY ix_event_customer (customer_id),
ALGORITHM=COPY;
VIRTUAL re-evaluates on read but the index is materialised. ADD INDEX on a VIRTUAL column requires ALGORITHM=COPY (KB-documented limitation).
Invisible column for soft-deprecation
-- 10.3+ : hide a legacy column from SELECT * without breaking explicit queries
ALTER TABLE customer
MODIFY legacy_phone VARCHAR(20) INVISIBLE,
ALGORITHM=INSTANT,
LOCK=NONE;
SELECT * skips the column. SELECT legacy_phone still works. Marker for "remove in next major release" without immediate ALTER + app-deploy coordination.
Range partition by date
-- 10.6+ : RANGE COLUMNS supports multi-column and date types
CREATE TABLE event_log (
id BIGINT UNSIGNED AUTO_INCREMENT,
occurred_at DATETIME(6) NOT NULL,
payload JSON NULL CHECK (JSON_VALID(payload)),
PRIMARY KEY (id, occurred_at)
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS (occurred_at) (
PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
PARTITION p2025 VALUES LESS THAN ('2026-01-01'),
PARTITION p2026 VALUES LESS THAN ('2027-01-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
The PK must include the partitioning column (occurred_at) : a hard MariaDB rule. Foreign keys are forbidden on partitioned tables.
Drop table safely
-- IF EXISTS suppresses the no-such-table error ; useful in idempotent migrations
DROP TABLE IF EXISTS staging_import_2024;
-- Multi-table drop : crash-safe but not fully atomic in 10.6+
DROP TABLE IF EXISTS tmp_a, tmp_b, tmp_c;
If any table is referenced by a FK from another table, the DROP errors out (Cannot delete or update a parent row : a foreign key constraint fails). Drop child tables first or SET FOREIGN_KEY_CHECKS=0 during controlled maintenance only.
Atomic DDL (10.6+)
Single-statement DDL is rollback-safe across server crash : CREATE TABLE (except CREATE OR REPLACE), ALTER TABLE, single-table DROP TABLE, RENAME TABLE, CREATE VIEW, CREATE SEQUENCE, CREATE TRIGGER, DROP TRIGGER. The ddl_recovery.log tracks in-progress operations ; on restart MariaDB completes or reverses each entry up to 3 retries.
NOT fully atomic (crash-safe but partial-completion possible) : multi-table DROP TABLE, CREATE OR REPLACE TABLE, DROP DATABASE.
Storage-engine support : InnoDB, MyRocks, MyISAM, Aria. NOT supported : S3 engine, partitioning engine (some operations).
Cross-References
mariadb-syntax-sql-dmlfor INSERT/UPDATE/DELETE patterns including RETURNING and ON DUPLICATE KEY UPDATE.mariadb-syntax-indexingfor index-type details, leftmost-prefix, descending indexes, ignored indexes.mariadb-syntax-jsonfor JSON validation, JSON_VALUE/JSON_QUERY, functional indexes.mariadb-impl-schema-designfor naming conventions, PK choice (BIGINT vs UUID), charset strategy.mariadb-impl-migrationfor online schema-change tools (pt-online-schema-change, gh-ost).mariadb-core-storage-enginesfor ROW_FORMAT trade-offs and engine selection.
Reference Files
references/methods.md: complete CREATE/ALTER/DROP/RENAME grammar, sequence functions, partitioning expressions.references/examples.md: 12+ working DDL examples covering every category listed in this skill.references/anti-patterns.md: 9 anti-patterns from real production incidents with root-cause and fix.
Sources
Verified against : mariadb.com/kb/en/alter-table/, mariadb.com/kb/en/instant-add-column-for-innodb/, mariadb.com/kb/en/create-sequence/, mariadb.com/kb/en/generated-columns/, mariadb.com/kb/en/invisible-columns/, mariadb.com/kb/en/partitioning-types-overview/, mariadb.com/kb/en/partition-pruning-and-selection/, mariadb.com/kb/en/rename-table/, mariadb.com/kb/en/atomic-ddl/.