ERROR 1020 (ER_CHECKREAD) occurs when another transaction modifies a record between reads in a multi-user environment. This commonly affects MyISAM tables and can be resolved through retry logic, isolation level adjustments, or row-level locking.
MySQL Error 1020 โ SQLSTATE: HY000 (ER_CHECKREAD) signals a data consistency conflict in multi-user environments. When a transaction reads a record, MySQL stores its version. If another concurrent transaction modifies that same record before the first transaction completes, MySQL detects this inconsistency and raises ERROR 1020. This error is primarily associated with MyISAM tables, which lack MVCC (Multi-Version Concurrency Control) and full ACID guarantees. InnoDB tables use consistent snapshots and don't exhibit this race condition. The error serves as a safeguard against reading stale or inconsistent data during concurrent operations.
The simplest solution is to catch ERROR 1020 and retry the transaction. Since the conflict is transient, a retry often succeeds:
const MAX_RETRIES = 3;
const RETRY_DELAY = 100; // ms
async function executeWithRetry(query) {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return await db.execute(query);
} catch (err) {
if (err.errno === 1020 && attempt < MAX_RETRIES) {
await new Promise(r => setTimeout(r, RETRY_DELAY * attempt));
continue;
}
throw err;
}
}
}This approach works best when conflicts are occasional and retries resolve quickly.
InnoDB's MVCC eliminates ERROR 1020 entirely. Convert MyISAM tables to InnoDB:
ALTER TABLE your_table ENGINE = InnoDB;Verify the change:
SELECT TABLE_NAME, ENGINE FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'your_table';InnoDB provides:
- MVCC for consistent reads without blocking writers
- Row-level locking instead of table-level
- ACID transaction guarantees
- Better performance for concurrent workloads
This is the recommended long-term solution.
MySQL supports four isolation levels. The default REPEATABLE READ can cause conflicts; try READ COMMITTED:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM your_table WHERE id = 123;
UPDATE your_table SET column = value WHERE id = 123;
COMMIT;Or set it globally:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;Isolation levels (from strictest to loosest):
- SERIALIZABLE: Highest consistency, lowest concurrency
- REPEATABLE READ: Default, can cause ERROR 1020
- READ COMMITTED: Lower isolation, fewer conflicts
- READ UNCOMMITTED: Dirty reads possible, rare to use
Warning: Lower isolation levels may allow dirty or phantom reads, so understand your consistency requirements first.
Unindexed scans require reading every table row, increasing conflict likelihood:
-- Before: Without index
SELECT * FROM orders WHERE customer_id = 5;
-- After: With index
CREATE INDEX idx_customer_id ON orders(customer_id);Verify the index is being used:
EXPLAIN SELECT * FROM orders WHERE customer_id = 5;Look for "Using index" in the output. Indexed queries:
- Reduce scan time
- Minimize the window for concurrent modifications
- Decrease lock contention
- Improve overall performance
For critical operations, lock the row before reading to prevent concurrent modifications:
BEGIN;
SELECT * FROM your_table WHERE id = 123 FOR UPDATE;
-- Row is now locked, no other transaction can modify it
UPDATE your_table SET column = value WHERE id = 123;
COMMIT;This ensures:
- No other transaction can modify the locked row
- Consistent read and write operations
- No race conditions during the transaction
Trade-off: This reduces concurrency, so use only when necessary.
Long transactions increase the window for conflicts. Keep transactions short:
// Bad: Long transaction
db.beginTransaction();
const user = await db.users.findById(id);
// ... expensive operation (API call, file processing, etc.)
await db.users.update(id, userData);
db.commit();
// Good: Transaction only for database operations
const user = await db.users.findById(id);
// ... expensive operation (outside transaction)
db.beginTransaction();
await db.users.update(id, userData);
db.commit();Principles:
- Move non-database operations outside transactions
- Commit/rollback as soon as possible
- Minimize the time locks are held
- Reduce overlap with other concurrent transactions
ERROR 1020 is fundamentally a MyISAM issue. InnoDB's MVCC architecture prevents it entirely by maintaining multiple versions of each row. If you must use MyISAM (rare in modern applications), understand that concurrent writes risk ERROR 1020 even with single-connection scenarios (MySQL bug #26055 documents this edge case).
For distributed systems, application-level retry logic is often more practical than database-level changes. Implement exponential backoff to avoid retry storms during high contention.
Some frameworks (Sequelize, TypeORM) have built-in retry mechanisms for transient errorsโcheck your ORM's documentation. MariaDB, MySQL's open-source fork, similarly exhibits this behavior on MyISAM tables.
The HANDLER statement (used for sequential table scans) is particularly prone to ERROR 1020 if the table changes between HANDLER READ commands. This is documented in MySQL source code and MariaDB documentation.
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