Medium severityquery
MySQL Error:
1093
What does this error mean?
MySQL rejects any DELETE or UPDATE statement where the target table also appears in a subquery's FROM clause within the same statement. The engine cannot simultaneously read from and write to the same table in a single DML pass. In a data pipeline this surfaces when a dbt post-hook, an ADF Script activity, or a stored procedure tries to clean up rows by selecting candidate IDs from the same table it wants to modify. The statement fails immediately with ER_UPDATE_TABLE_USED and the pipeline step errors out — no rows are deleted or updated, and any downstream models that depend on the cleanup remain stale.
Common causes
- 1DELETE FROM t WHERE id IN (SELECT id FROM t WHERE condition) — the most common pattern, often copy-pasted from PostgreSQL where it is valid. MySQL sees both the outer DELETE target and the inner FROM t as the same table object and refuses to execute.
- 2UPDATE t SET status = 'archived' WHERE id IN (SELECT id FROM t WHERE created_at < NOW() - INTERVAL 90 DAY) — self-referencing UPDATE in a WHERE subquery, identical problem to the DELETE variant but less frequently spotted during code review.
- 3dbt post-hooks that implement a soft-delete or deduplication pattern: DELETE FROM {{ this }} WHERE id NOT IN (SELECT MAX(id) FROM {{ this }} GROUP BY natural_key). These hooks run after the model materializes and break because {{ this }} resolves to the same table on both sides.
- 4ADF Script activities that were migrated from SQL Server or PostgreSQL without adaptation. Both those engines handle self-referencing DML differently, so the SQL passes peer review but fails at runtime on the MySQL target.
- 5Stored procedures or event scheduler jobs that age out old records by selecting a batch of IDs from the same table: DELETE FROM events WHERE id IN (SELECT id FROM events WHERE ts < DATE_SUB(NOW(), INTERVAL 30 DAY) LIMIT 1000). The LIMIT inside the subquery adds extra complexity that MySQL also rejects.
- 6ORM-generated queries from Django, SQLAlchemy, or similar frameworks when a bulk_delete() call builds a subquery filter against the same model — the ORM does not automatically add the derived-table wrapper that MySQL requires.
- 7Airbyte or Fivetran normalization steps on MySQL destinations where the generated deduplication SQL was templated for a multi-engine target and does not include MySQL-specific workarounds.
How to fix it
- 1Step 1 — Wrap the subquery in a derived table to force materialization before the DELETE executes: DELETE FROM t WHERE id IN (SELECT id FROM (SELECT id FROM t WHERE condition) AS tmp); The aliased subquery (tmp) makes MySQL treat the inner result as a temporary result set, breaking the self-reference.
- 2Step 2 — Alternatively rewrite as a DELETE ... JOIN, which avoids the subquery entirely: DELETE t FROM t INNER JOIN (SELECT id FROM t WHERE condition) AS tmp ON t.id = tmp.id; This form is often faster on large tables because the optimizer can use an index on the join key.
- 3Step 3 — For UPDATE statements apply the same derived-table pattern: UPDATE t SET col = value WHERE id IN (SELECT id FROM (SELECT id FROM t WHERE condition) AS tmp);
- 4Step 4 — In dbt post-hooks replace the direct self-reference with a two-step approach: first insert the candidate IDs into a temporary table (CREATE TEMPORARY TABLE _tmp_ids AS SELECT id FROM {{ this }} WHERE condition;), then delete using that temp table (DELETE FROM {{ this }} WHERE id IN (SELECT id FROM _tmp_ids);), and finally drop it (DROP TEMPORARY TABLE _tmp_ids;). Wrap all three in a single post-hook list item using a semicolon-separated string or a macro.
- 5Step 5 — In ADF Script activities, paste the rewritten query with the derived-table alias directly into the Script property of the activity. Validate by running the query manually against the MySQL target in MySQL Workbench or via a Lookup activity with a test row count before wiring it into the pipeline.
- 6Step 6 — For ORM bulk deletes (Django / SQLAlchemy), bypass the ORM for this specific operation and execute the raw derived-table SQL via session.execute(text('...')) or connection.execute(). Document the workaround in a code comment so it is not reverted during a refactor.
- 7Step 7 — If this pattern recurs across multiple pipelines, create a helper stored procedure that accepts the table name and condition as parameters and encapsulates the derived-table workaround. Call the procedure from dbt hooks or ADF instead of repeating the raw SQL.
Example log output
ERROR 1093 (HY000): You can't specify target table 'events' for update in FROM clause
[dbt] Database Error in model post_hook (post-hook of model.my_project.events): (MySQLdb.OperationalError) (1093, "You can't specify target table 'events' for update in FROM clause")
[ADF] Activity 'CleanupExpiredRows' failed: MySQLException: Error Code: 1093. You can't specify target table 'staging_orders' for update in FROM clause