MySQL / MariaDB Schema Migration¶
Data type mapping, AUTO_INCREMENT handling, character set conversion, collation mapping, foreign key management, index types, partitioning conversion, and storage engine migration for MySQL/MariaDB to Azure targets.
Scope
This guide covers schema conversion for all three Azure targets: Azure MySQL Flexible Server (same engine, minimal changes), Azure PostgreSQL Flexible Server (engine switch, significant conversion), and Azure SQL Database (engine switch, significant conversion). Each section identifies target-specific differences.
1. Storage engine conversion¶
1.1 InnoDB requirement¶
Azure Database for MySQL Flexible Server supports multiple storage engines, but InnoDB is required for zone-redundant high availability (replication requires transactional tables). Convert all non-InnoDB tables before migration.
-- Find all non-InnoDB tables
SELECT table_schema, table_name, engine
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
AND engine != 'InnoDB'
ORDER BY table_schema, table_name;
-- Convert MyISAM to InnoDB
ALTER TABLE mydb.my_table ENGINE = InnoDB;
-- Convert MariaDB Aria to InnoDB
ALTER TABLE mydb.my_table ENGINE = InnoDB;
-- Convert MEMORY to InnoDB (data will persist after restart)
-- Note: MEMORY tables lose data on restart anyway
ALTER TABLE mydb.my_table ENGINE = InnoDB;
1.2 Engine conversion considerations¶
| Source engine | Conversion notes |
|---|---|
| MyISAM | No foreign keys on MyISAM; add FK constraints after conversion if needed. Full-text indexes work on InnoDB (MySQL 5.6+). Table-level locking becomes row-level. |
| Aria (MariaDB) | Similar to MyISAM conversion. Check for crash-recovery behavior changes. |
| MEMORY / HEAP | Data lost on conversion; reload after ALTER TABLE. Consider InnoDB with buffer pool caching. |
| ARCHIVE | Insert-only engine; InnoDB allows all DML. Consider compression (ROW_FORMAT=COMPRESSED). |
| CSV | No indexes, no transactions; InnoDB adds both. Test applications expecting CSV file access. |
| BLACKHOLE | Used for replication filtering; remove or redesign replication topology. |
| FEDERATED | Remote table access; replace with Azure Data Factory or application-level data access. |
| ColumnStore (MariaDB) | Analytics engine; migrate to Microsoft Fabric for analytics workloads. |
2. Data type mapping¶
2.1 MySQL to Azure MySQL Flexible Server¶
Same engine, so most data types require no change. Key exceptions:
| MySQL type | Azure MySQL Flexible Server | Notes |
|---|---|---|
| All standard types | Supported as-is | No conversion needed |
GEOMETRY (MyISAM spatial) | GEOMETRY (InnoDB spatial) | Convert engine to InnoDB first |
YEAR | YEAR | Supported |
SET | SET | Supported |
ENUM | ENUM | Supported |
| Custom UDF return types | Not applicable | UDFs not supported; rewrite as stored functions |
2.2 MySQL to Azure PostgreSQL¶
| MySQL type | PostgreSQL type | Conversion notes |
|---|---|---|
TINYINT | SMALLINT | PostgreSQL has no TINYINT; SMALLINT is 2 bytes |
TINYINT(1) / BOOLEAN | BOOLEAN | Map to native boolean |
SMALLINT | SMALLINT | Direct mapping |
MEDIUMINT | INTEGER | PostgreSQL has no MEDIUMINT |
INT / INTEGER | INTEGER | Direct mapping |
BIGINT | BIGINT | Direct mapping |
INT UNSIGNED | INTEGER + CHECK (col >= 0) | No unsigned types in PostgreSQL |
BIGINT UNSIGNED | BIGINT + CHECK (col >= 0) or NUMERIC(20,0) | For values > 9.2 quintillion |
FLOAT | REAL | 4-byte floating point |
DOUBLE | DOUBLE PRECISION | 8-byte floating point |
DECIMAL(p,s) | NUMERIC(p,s) | Exact numeric |
CHAR(n) | CHARACTER(n) | Fixed-length string |
VARCHAR(n) | VARCHAR(n) | Variable-length string |
TINYTEXT | TEXT | PostgreSQL TEXT is unlimited |
TEXT | TEXT | Direct mapping |
MEDIUMTEXT | TEXT | PostgreSQL TEXT is unlimited |
LONGTEXT | TEXT | PostgreSQL TEXT is unlimited (max 1 GB) |
TINYBLOB | BYTEA | Binary data |
BLOB | BYTEA | Binary data (max 1 GB) |
MEDIUMBLOB | BYTEA | Binary data |
LONGBLOB | BYTEA | Consider Azure Blob Storage for > 100 MB |
DATE | DATE | Direct mapping |
TIME | TIME | Direct mapping |
DATETIME | TIMESTAMP | PostgreSQL TIMESTAMP has microsecond precision |
TIMESTAMP | TIMESTAMPTZ | Use TIMESTAMPTZ for timezone awareness |
YEAR | SMALLINT | No YEAR type in PostgreSQL |
ENUM('a','b','c') | CREATE TYPE enum_name AS ENUM ('a','b','c') or VARCHAR + CHECK | Custom type or constraint |
SET('a','b','c') | TEXT[] (array) or junction table | No SET type in PostgreSQL |
JSON | JSONB | JSONB is binary, indexable, superior |
BIT(n) | BIT(n) or BIT VARYING(n) | Direct mapping |
BINARY(n) | BYTEA | No fixed-length binary in PostgreSQL |
VARBINARY(n) | BYTEA | No variable-length binary type |
GEOMETRY | PostGIS GEOMETRY | Install PostGIS extension |
POINT | PostGIS POINT | Install PostGIS extension |
POLYGON | PostGIS POLYGON | Install PostGIS extension |
2.3 MySQL to Azure SQL Database¶
| MySQL type | Azure SQL type | Conversion notes |
|---|---|---|
TINYINT | TINYINT | Direct mapping (0-255, unsigned in SQL Server) |
TINYINT SIGNED | SMALLINT | SQL Server TINYINT is unsigned only |
SMALLINT | SMALLINT | Direct mapping |
MEDIUMINT | INT | No MEDIUMINT in SQL Server |
INT | INT | Direct mapping |
BIGINT | BIGINT | Direct mapping |
FLOAT | REAL or FLOAT | REAL is 4 bytes; FLOAT is 8 bytes |
DOUBLE | FLOAT | SQL Server FLOAT is 8 bytes |
DECIMAL(p,s) | DECIMAL(p,s) | Direct mapping |
CHAR(n) | CHAR(n) or NCHAR(n) | Use NCHAR for Unicode |
VARCHAR(n) | VARCHAR(n) or NVARCHAR(n) | Use NVARCHAR for Unicode; max 8000 (or MAX) |
TEXT | NVARCHAR(MAX) | TEXT is deprecated in SQL Server |
MEDIUMTEXT | NVARCHAR(MAX) | Up to 2 GB |
LONGTEXT | NVARCHAR(MAX) | Up to 2 GB |
BLOB | VARBINARY(MAX) | Up to 2 GB |
DATE | DATE | Direct mapping |
TIME | TIME | Direct mapping |
DATETIME | DATETIME2 | DATETIME2 has better precision and range |
TIMESTAMP | DATETIME2 + trigger | SQL Server TIMESTAMP/ROWVERSION is different |
YEAR | SMALLINT | No YEAR type |
ENUM | VARCHAR + CHECK | No ENUM type in SQL Server |
SET | Junction table | No SET type |
JSON | NVARCHAR(MAX) + JSON functions | No native JSON type; text-based |
BIT(n) | BIT (single bit) or BINARY | SQL Server BIT is 1 bit |
GEOMETRY | GEOMETRY / GEOGRAPHY | Native spatial types |
AUTO_INCREMENT | IDENTITY(1,1) | Different syntax |
3. AUTO_INCREMENT conversion¶
3.1 MySQL to Azure MySQL Flexible Server¶
No change needed. AUTO_INCREMENT works identically.
3.2 MySQL to Azure PostgreSQL¶
MySQL AUTO_INCREMENT converts to PostgreSQL sequences:
-- MySQL
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- PostgreSQL option 1: SERIAL (traditional)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
-- PostgreSQL option 2: GENERATED ALWAYS (SQL standard, preferred)
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100)
);
-- PostgreSQL option 3: GENERATED BY DEFAULT (allows manual inserts)
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(100)
);
After data migration, reset sequences to the maximum existing value:
-- Reset sequence for each table with SERIAL/IDENTITY
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT MAX(id) FROM users));
3.3 MySQL to Azure SQL Database¶
-- MySQL
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- Azure SQL
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100)
);
After bulk data import with SET IDENTITY_INSERT ON:
SET IDENTITY_INSERT users ON;
-- Insert data with explicit ID values
INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob');
SET IDENTITY_INSERT users OFF;
-- Reseed identity
DBCC CHECKIDENT('users', RESEED);
4. Character set and collation conversion¶
4.1 Inventory source character sets¶
-- Database level
SELECT schema_name, default_character_set_name, default_collation_name
FROM information_schema.schemata
WHERE schema_name NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys');
-- Table level
SELECT table_schema, table_name, table_collation
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
AND table_type = 'BASE TABLE'
ORDER BY table_schema, table_name;
-- Column level (only non-default)
SELECT table_schema, table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
AND character_set_name IS NOT NULL
ORDER BY table_schema, table_name, ordinal_position;
4.2 MySQL to Azure MySQL Flexible Server¶
Target utf8mb4 as the standard character set:
-- Convert database
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Convert all tables in a database (generate ALTER statements)
SELECT CONCAT('ALTER TABLE `', table_schema, '`.`', table_name,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;')
FROM information_schema.tables
WHERE table_schema = 'mydb'
AND table_type = 'BASE TABLE'
AND table_collation != 'utf8mb4_0900_ai_ci';
4.3 MySQL to Azure PostgreSQL¶
PostgreSQL uses UTF-8 natively. The database encoding is set at creation time and cannot be changed afterward.
-- Create PostgreSQL database with UTF-8 encoding
CREATE DATABASE mydb
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8';
pgloader handles character set conversion automatically during data migration.
4.4 Collation mapping table¶
| MySQL collation | PostgreSQL collation | Azure SQL collation | Notes |
|---|---|---|---|
utf8mb4_0900_ai_ci | en-US-x-icu (ICU) | Latin1_General_100_CI_AI | Accent-insensitive, case-insensitive |
utf8mb4_0900_as_cs | en-US-x-icu with options | Latin1_General_100_CS_AS | Accent-sensitive, case-sensitive |
utf8mb4_unicode_ci | en-US-x-icu | Latin1_General_CI_AI | Unicode comparison |
utf8mb4_general_ci | en-US-x-icu | SQL_Latin1_General_CP1_CI_AS | MySQL fast comparison |
utf8mb4_bin | C collation | Latin1_General_BIN2 | Binary comparison |
latin1_swedish_ci | sv-SE-x-icu | Finnish_Swedish_CI_AS | Swedish/Finnish sorting |
5. Foreign key handling¶
5.1 Export and validate foreign keys¶
-- List all foreign keys
SELECT
tc.table_schema, tc.table_name, tc.constraint_name,
kcu.column_name, kcu.referenced_table_schema,
kcu.referenced_table_name, kcu.referenced_column_name,
rc.update_rule, rc.delete_rule
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.referential_constraints rc
ON tc.constraint_name = rc.constraint_name
AND tc.table_schema = rc.constraint_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'mydb'
ORDER BY tc.table_name;
5.2 Foreign key migration strategy¶
For large data migrations, disable foreign keys during data load:
-- MySQL (source and Azure MySQL target)
SET FOREIGN_KEY_CHECKS = 0;
-- ... load data ...
SET FOREIGN_KEY_CHECKS = 1;
-- PostgreSQL target
-- Drop constraints, load data, re-add constraints
-- Or use: SET session_replication_role = 'replica'; (disables triggers and FK checks)
SET session_replication_role = 'replica';
-- ... load data ...
SET session_replication_role = 'origin';
-- Azure SQL target
ALTER TABLE child_table NOCHECK CONSTRAINT fk_name;
-- ... load data ...
ALTER TABLE child_table CHECK CONSTRAINT fk_name;
5.3 Cross-database foreign keys¶
MySQL allows foreign keys between tables in different databases on the same server. This is not supported on Azure MySQL Flexible Server (each Flexible Server hosts databases independently, and cross-database FK enforcement depends on the same server).
Solution: Either consolidate tables into a single database, or enforce referential integrity at the application level.
6. Index conversion¶
6.1 Index type mapping¶
| MySQL index type | Azure MySQL | PostgreSQL | Azure SQL | Notes |
|---|---|---|---|---|
PRIMARY KEY | Same | Same | Same | Direct |
UNIQUE | Same | Same | Same | Direct |
INDEX (B-tree) | Same | CREATE INDEX (B-tree default) | Same | Direct |
FULLTEXT | Same | GIN on tsvector column | CREATE FULLTEXT INDEX | Moderate conversion for PG/SQL |
SPATIAL | Same (InnoDB) | GiST (PostGIS) | CREATE SPATIAL INDEX | Moderate conversion |
Prefix index (col(10)) | Same | Expression index (LEFT(col, 10)) | Not supported | Moderate for PG |
| Invisible index | Same | Not available | Not available | MySQL-specific |
| Multi-valued index (JSON) | Same | GIN on JSONB | Computed column + index | Direct for MySQL; moderate for PG/SQL |
6.2 Full-text index conversion for PostgreSQL¶
-- MySQL
CREATE FULLTEXT INDEX ft_articles ON articles(title, body);
SELECT * FROM articles WHERE MATCH(title, body) AGAINST('search term');
-- PostgreSQL equivalent
-- Step 1: Add tsvector column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- Step 2: Populate tsvector
UPDATE articles SET search_vector =
to_tsvector('english', COALESCE(title, '') || ' ' || COALESCE(body, ''));
-- Step 3: Create GIN index
CREATE INDEX ft_articles ON articles USING GIN(search_vector);
-- Step 4: Create trigger to maintain tsvector on INSERT/UPDATE
CREATE FUNCTION articles_search_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_vector := to_tsvector('english',
COALESCE(NEW.title, '') || ' ' || COALESCE(NEW.body, ''));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvector_update BEFORE INSERT OR UPDATE
ON articles FOR EACH ROW EXECUTE FUNCTION articles_search_trigger();
-- Step 5: Query using tsquery
SELECT * FROM articles WHERE search_vector @@ to_tsquery('english', 'search & term');
7. Partitioning conversion¶
7.1 MySQL partitioning to PostgreSQL declarative partitioning¶
-- MySQL RANGE partitioning
CREATE TABLE orders (
id INT AUTO_INCREMENT,
order_date DATE,
amount DECIMAL(10,2),
PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- PostgreSQL declarative partitioning equivalent
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY,
order_date DATE NOT NULL,
amount NUMERIC(10,2),
PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2023 PARTITION OF orders
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE orders_future PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('9999-12-31');
7.2 MySQL partitioning to Azure SQL¶
Azure SQL Database supports table partitioning with partition functions and schemes:
-- Create partition function
CREATE PARTITION FUNCTION pf_order_date (DATE)
AS RANGE RIGHT FOR VALUES ('2024-01-01', '2025-01-01', '2026-01-01');
-- Create partition scheme
CREATE PARTITION SCHEME ps_order_date
AS PARTITION pf_order_date ALL TO ([PRIMARY]);
-- Create partitioned table
CREATE TABLE orders (
id INT IDENTITY(1,1),
order_date DATE NOT NULL,
amount DECIMAL(10,2),
CONSTRAINT pk_orders PRIMARY KEY (id, order_date)
) ON ps_order_date(order_date);
8. Views migration¶
8.1 Standard views¶
Most MySQL views migrate directly to all three targets with minimal syntax changes:
-- MySQL view
CREATE VIEW active_customers AS
SELECT c.id, c.name, c.email, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.created_at > DATE_SUB(NOW(), INTERVAL 1 YEAR)
WHERE c.status = 'active'
GROUP BY c.id, c.name, c.email;
-- PostgreSQL equivalent
CREATE VIEW active_customers AS
SELECT c.id, c.name, c.email, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.created_at > NOW() - INTERVAL '1 year'
WHERE c.status = 'active'
GROUP BY c.id, c.name, c.email;
-- Azure SQL equivalent
CREATE VIEW active_customers AS
SELECT c.id, c.name, c.email, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.created_at > DATEADD(YEAR, -1, GETDATE())
WHERE c.status = 'active'
GROUP BY c.id, c.name, c.email;
8.2 MySQL-specific view features¶
| Feature | Azure MySQL | PostgreSQL | Azure SQL |
|---|---|---|---|
ALGORITHM = MERGE/TEMPTABLE | Supported (optimizer hint) | No equivalent (optimizer decides) | No equivalent |
WITH CHECK OPTION | Supported | Supported | Supported |
DEFINER | Supported | No DEFINER concept; views use invoker privileges | No equivalent |
| Updatable views | Supported (with restrictions) | Supported (with INSTEAD OF triggers for complex cases) | Supported (with INSTEAD OF triggers) |
9. Schema migration checklist¶
- Convert all non-InnoDB tables to InnoDB
- Map all data types to target platform (use tables in section 2)
- Convert AUTO_INCREMENT to target equivalent (SERIAL, IDENTITY)
- Standardize character sets to utf8mb4
- Map collations to target platform equivalents
- Export and validate all foreign key relationships
- Convert indexes (especially full-text and spatial for PG/SQL targets)
- Convert partitioned tables to target partitioning syntax
- Migrate views with syntax adjustments
- Convert stored procedures and functions (for PG/SQL targets)
- Convert triggers (for PG/SQL targets)
- Convert events to target scheduler (pg_cron, SQL Agent)
- Validate schema in target with empty tables before data migration
- Test application queries against target schema
Next: Data Migration | Flexible Server Migration | PostgreSQL Migration
Maintainers: csa-inabox core team Last updated: 2026-04-30