One of EF Codd's rules for relational databases is the no-back-door rule: all info about tables should be accessible only by a query on tables. Since version 5, the MySQL implementation of information_schema (I_S) helps meet Codd's requirement. I_S supplies metadata in tables, so it's the first place to look for how to compare the structures of two databases.
Since MySQL 8.0 and MariaDB 10.2, Common Table Expressions simplify and optimise this query ...
drop procedure if exists comparedbs;
create procedure comparedbs(
vdb1 varchar(64), vdb2 varchar(64)
)
WITH
desc1 AS (
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING (table_schema,table_name)
WHERE t1.table_schema=vdb1
),
desc2 AS (
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING (table_schema,table_name)
WHERE t1.table_schema=vdb2
)
SELECT
a.table_schema as SchemaA,
a.table_name as TableA,
a.table_type as TypeA,
a.engine as EngineA,
a.column_name as ColA,
a.column_type as ColTypeA,
a.column_default as DefaultA,
a.is_nullable as NullA,
a.column_key as KeyA,
IfNull(b.table_schema,'') as SchemaB,
IfNull(b.table_name,'') as TableB,
IfNull(b.table_type,'') as TypeB,
IfNull(b.engine,'') as EngineB,
IfNull(b.column_name,'') as ColB,
If(b.column_type Is Null,'',If(a.column_type=b.column_type,lower(b.column_type),upper(b.column_type))) as ColTypeB,
If(b.column_default Is Null,'',If(a.column_default=b.column_default,lower(b.column_default),upper(b.column_default))) as DefaultB,
If(b.is_nullable Is Null,'',If(a.is_nullable=b.is_nullable,lower(b.is_nullable),upper(b.is_nullable))) as NullB,
If(b.column_key Is Null,'',If(a.column_key=b.column_key,lower(b.column_key),upper(b.column_key))) as KeyB
FROM desc1 a
LEFT JOIN desc2 b USING(table_schema,table_name,table_type,column_name)
UNION ALL
SELECT
b.table_schema as SchemaA,
b.table_name as TableA,
b.table_type as TypeA,
b.engine as EngineA,
b.column_name as ColA,
b.column_type as ColTypeA,
b.column_default as DefaultA,
b.is_nullable as NullA,
b.column_key as KeyA,
IfNull(a.table_schema,'') as SchemaB,
IfNull(a.table_name,'') as TableB,
IfNull(a.table_type,'') as TypeB,
IfNull(a.engine,'') as EngineB,
IfNull(a.column_name,'') as ColB,
If(a.column_type Is Null,'',If(a.column_type=b.column_type,lower(a.column_type),upper(a.column_type))) as ColTypeB,
If(a.column_default Is Null,'',If(a.column_default=b.column_default,lower(a.column_default),upper(a.column_default))) as DefaultB,
If(a.is_nullable Is Null,'',If(a.is_nullable=b.is_nullable,lower(a.is_nullable),upper(a.is_nullable))) as NullB,
If(a.column_key Is Null,'',If(a.column_key=b.column_key,lower(a.column_key),upper(a.column_key))) as KeyB
FROM desc1 a
RIGHT JOIN desc2 b USING(table_schema,table_name,table_type,column_name)
WHERE a.table_name IS NULL
ORDER BY TableA,ColA,SchemaA;
Before MySQL 8.0 and MariaDB 10.2
With versions earlier than MySQL 8.0 and MariaDB, there's more work. Elsewhere on this page there's a simple query template for comparing data in two structurally similar tables:
SELECT MIN(TableName) as TableName, id, col1, col2, col3, ...
FROM (
SELECT 'Table a' as TableName, a.id, a.col1, a.col2, a.col3, ...
FROM a
UNION ALL
SELECT 'Table b' as TableName, b.id, b.col1, b.col2, b.col3, ...
FROM b
) AS tmp
GROUP BY id, col1, col2, col3, ...
HAVING COUNT(*) = 1
ORDER BY ID;
To apply this logic to the comparison of two database structures:
write temp tables collecting desired I_S metadata on each database
map the compare-data query template to those two metadata tables
This logic is easiest to apply when it's parameterised in a stored procedure, in a system database. Before MySQL version 8, the sproc needs to create named temporary tables:
USE system;
DROP PROCEDURE IF EXISTS CompareDBs;
DELIMITER go
CREATE PROCEDURE CompareDBs(
vdb1 VARCHAR(64), vdb2 VARCHAR(64)
)
BEGIN
DROP TEMPORARY TABLE IF EXISTS desc1,desc2;
CREATE TEMPORARY TABLE desc1
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING (table_schema,table_name)
WHERE t1.table_schema=vdb1
ORDER BY t1.table_name,c1.column_name;
CREATE TEMPORARY TABLE desc2
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING (table_schema,table_name)
WHERE t1.table_schema=vdb2
ORDER BY t1.table_name,c1.column_name;
SELECT
TableName,column_name,MIN(SchemaName),table_type,engine,
ordinal_position,column_type,column_default,is_nullable,column_key
FROM (
SELECT
a.table_schema AS SchemaName,a.table_name AS TableName,
a.table_type,a.engine,a.column_name,a.ordinal_position,
a.column_type,a.column_default,a.is_nullable,a.column_key
FROM desc1 a
UNION ALL
SELECT
b.table_schema AS SchemaName,b.table_name AS TableName,
b.table_type,b.engine,b.column_name,b.ordinal_position,
b.column_type,b.column_default,b.is_nullable,b.column_key
FROM desc2 b
) AS tmp
GROUP BY TableName,table_type,engine,column_name,ordinal_position,
column_type,column_default,is_nullable,column_key
HAVING COUNT(*) = 1
ORDER BY TableName,column_name,SchemaName;
DROP TEMPORARY TABLE desc1, desc2;
END;
go
DELIMITER ;
CALL compareDBs('db1','db2');
MEMORY tables would it speed it up, but MySQL MEMORY tables do not support BLOB/TEXT columns.
Since MySQL version 8 and MariaDB 10.2, Common Table Expressions implement temporary tables as virtual tables in a WITH clause, uncluttering the code:
DROP PROCEDURE IF EXISTS CompareDBs;
DELIMITER go
CREATE PROCEDURE CompareDBs( vdb1 VARCHAR(64), vdb2 VARCHAR(64) )
BEGIN
WITH
desc1 AS (
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING (table_schema,table_name)
WHERE t1.table_schema=vdb1
ORDER BY t1.table_name,c1.column_name
),
desc2 AS (
SELECT
t1.table_schema,
t1.table_name,
t1.table_type,
t1.engine,
c1.column_name,
c1.ordinal_position,
c1.column_type,
c1.column_default,
c1.is_nullable,
c1.column_key
FROM information_schema.tables t1
JOIN information_schema.columns c1 USING(table_schema,table_name)
WHERE t1.table_schema=vdb2
ORDER BY t1.table_name,c1.column_name
)
SELECT
TableName,column_name,MIN(SchemaName),table_type,engine,
ordinal_position,column_type,column_default,is_nullable,column_key
FROM (
SELECT
a.table_schema AS SchemaName,a.table_name AS TableName,
a.table_type,a.engine,a.column_name,a.ordinal_position,
a.column_type,a.column_default,a.is_nullable,a.column_key
FROM desc1 a
UNION ALL
SELECT
b.table_schema AS SchemaName,b.table_name AS TableName,
b.table_type,b.engine,b.column_name,b.ordinal_position,
b.column_type,b.column_default,b.is_nullable,b.column_key
FROM desc2 b
) AS tmp
GROUP BY
TableName, table_type,engine, column_name, ordinal_position,
column_type, column_default, is_nullable, column_key
HAVING COUNT(*) = 1
ORDER BY TableName, column_name, SchemaName;
END;
go
DELIMITER ;
CALL compareDBs('db1','db2');