MySQL error 1412 occurs when a transaction tries to access a table whose structure has been modified by another session. This happens due to transaction isolation levels and metadata locking conflicts during concurrent DDL operations.
MySQL ERROR 1412 (ER_TABLE_DEF_CHANGED) is triggered when your transaction attempts to read from or write to a table that another session has altered after your transaction started. When a DDL operation (ALTER TABLE, TRUNCATE, etc.) modifies the table structure, MySQL increments a version counter for that table's metadata. If your transaction's snapshot was taken before the DDL, and the current statement detects a version mismatch, MySQL cancels the statement and returns error 1412. This is a safety mechanism to prevent inconsistent reads. MySQL prioritizes consistency by forcing the transaction to retry with the new table definition rather than allowing potentially corrupted reads.
The most straightforward solution is to catch error 1412 and retry the entire transaction. Most ORMs and database drivers support this pattern:
import mysql.connector
from mysql.connector import Error
def execute_with_retry(connection, query, max_retries=3):
for attempt in range(max_retries):
try:
cursor = connection.cursor()
cursor.execute(query)
connection.commit()
return cursor.fetchall()
except Error as e:
if e.errno == 1412 and attempt < max_retries - 1:
# Table definition changed, retry
connection.rollback()
continue
else:
raise
raise Exception(f"Failed after {max_retries} attempts")
# Usage
try:
result = execute_with_retry(connection, "SELECT * FROM my_table")
except Error as e:
print(f"Transaction failed: {e}")For Node.js/JavaScript:
async function executeWithRetry(pool, query, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const result = await connection.query(query);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
if (error.code === 'ER_TABLE_DEF_CHANGED' && attempt < maxRetries - 1) {
// Table definition changed, retry
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt)));
continue;
}
throw error;
} finally {
await connection.release();
}
}
}Prevent error 1412 by avoiding concurrent DDL and DML operations. Schedule migrations when no active transactions exist:
# Schedule maintenance for off-peak hours
# In your cron configuration:
0 2 * * * /usr/local/bin/run-database-migrations.sh
# Before running migrations, wait for active transactions to complete
# Check current connections:
SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE COMMAND != 'Sleep' AND ID != CONNECTION_ID();
# Run migration only when count is low or zero
mysql -u root -p < migrations.sqlFor backup tools, ensure backups don't run during scheduled maintenance:
# In mysqldump:
mysqldump --single-transaction --all-databases > backup.sql
# Or with mydumper (which handles 1412 better):
mydumper -u root -p password -o /backup/Error 1412 only occurs with REPEATABLE-READ isolation level. Switching to READ COMMITTED allows each read to establish its own fresh snapshot:
-- For current session only
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Verify the change
SELECT @@session.transaction_isolation;
-- For a single transaction
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM my_table;
COMMIT;
-- To set globally (requires SUPER privilege)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;Important: Only use this if your application can tolerate non-repeatable reads. READ COMMITTED may see different data on subsequent reads within the same transaction.
For application-level configuration:
# Django
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS': {
'isolation_level': 'read committed',
}
}
}
# SQLAlchemy
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:pass@localhost/db',
connect_args={'isolation_level': 'read committed'})For backup tools, use metadata locks to prevent any DDL from running during the backup:
-- Acquire metadata locks on tables before backup
FLUSH TABLES table_name FOR EXPORT;
-- Now run your backup safely
-- mysqldump or file copy operations
-- Release locks
UNLOCK TABLES;This approach prevents schema changes but is more restrictive than using transaction isolation alone.
Before performing DDL operations, check for active long-running transactions:
-- See all active transactions and their isolation levels
SELECT
p.ID,
p.USER,
p.HOST,
p.DB,
p.COMMAND,
p.TIME,
p.STATE,
p.INFO,
t.TRX_STATE,
t.TRX_ISOLATION_LEVEL,
t.TRX_STARTED
FROM INFORMATION_SCHEMA.PROCESSLIST p
LEFT JOIN INFORMATION_SCHEMA.INNODB_TRX t ON p.ID = t.TRX_MYSQL_THREAD_ID
WHERE p.COMMAND != 'Sleep'
ORDER BY t.TRX_STARTED;
-- Kill long-running transactions that block DDL (if safe)
KILL 12345; -- Replace with actual process IDMonitor for transactions waiting on metadata locks:
-- MySQL 8.0+: See blocked operations
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;How Table Version Matching Works: Each table maintains a version counter in its metadata. When a DDL operation commits, this counter is incremented. When a statement completes parsing and is ready for execution, MySQL compares the current table version to the version that was active when the statement started parsing. If versions don't match, error 1412 is thrown.
Why REPEATABLE-READ Triggers This: REPEATABLE-READ isolation level establishes a consistent snapshot on the first SELECT in the transaction. Subsequent operations must maintain that snapshot view. If the table structure changes, maintaining that view becomes impossible, hence the error.
READ COMMITTED Behavior: READ COMMITTED establishes a fresh snapshot for each statement. This means each query sees the current table definition, avoiding the conflict entirely.
Metadata Lock Waits: DDL operations require an exclusive metadata lock. If a long-running transaction holds a shared metadata lock, the DDL will wait (up to the lock_wait_timeout, default 31536000 seconds). While waiting, the DDL blocks subsequent transactions, potentially causing cascading failures.
Online DDL Workarounds: MySQL 8.0+ includes "Online DDL" operations that minimize locking. Some ALTER TABLE operations (like adding columns with ALGORITHM=INSTANT) avoid the copy phase entirely and don't trigger 1412.
Backup Tool Considerations: Tools like mysqldump use REPEATABLE-READ isolation by default (via START TRANSACTION WITH CONSISTENT SNAPSHOT). If production writes alter tables during backup, backup may fail with 1412. Tools like mydumper handle this better by using metadata locks (FLUSH TABLES ... FOR EXPORT) instead.
Documentation Inconsistency: There's a known documentation bug (MySQL bug #116132) where the manual incorrectly states that error 1412 only occurs for "operations that make a temporary copy of the original table." In reality, it can occur for any DDL that changes the table definition, including instant operations that make no copy.
ERROR 1064: You have an error in your SQL syntax
How to fix "ERROR 1064: You have an error in your SQL syntax" in MySQL
ERROR 1054: Unknown column in field list
Unknown column in field list
ER_WINDOW_RANGE_FRAME_NUMERIC_TYPE (3589): RANGE frame requires numeric ORDER BY expression
RANGE frame requires numeric ORDER BY expression in MySQL window functions
CR_ALREADY_CONNECTED (2058): Handle already connected
How to fix "CR_ALREADY_CONNECTED (2058): Handle already connected" in MySQL
ER_WINDOW_DUPLICATE_NAME (3591): Duplicate window name
How to fix ER_WINDOW_DUPLICATE_NAME (3591) in MySQL window functions