A deadlock is not a bug in one query. It is a cycle in a graph — and once you see it that way, every fix makes sense, because each strategy attacks a different condition that the cycle requires.
One node per transaction, an arrow A→B when A waits on a lock B holds. A closed cycle = a deadlock, and nothing else qualifies.
Mutual exclusion, hold-and-wait, no preemption, circular wait. All four must coexist — kill any one and the cycle cannot form.
The engine periodically checks the graph for cycles, picks a victim by a cost heuristic, and aborts it. Postgres reports SQLSTATE 40P01 (deadlock_detected).
Take locks in one global order (cheapest at runtime, costs discipline), or roll back and retry with backoff + jitter so retries don't rebuild the same cycle.
The setup. Transaction A holds row X and asks for row Y. Transaction B holds row Y and asks for row X. Both are holding something while waiting for something else — hold-and-wait, live.
The graph closes. The wait-for graph gains two arrows: A→B and B→A. That is a cycle. With mutual exclusion, no preemption, and no third way out, all four Coffman conditions are now true at once.
The engine reacts. Detection waits a beat (Postgres: deadlock_timeout, default 1 second — that is the wait before checking for a cycle, so raising it slows reporting, it does not prevent anything), finds the cycle, and aborts the victim.
The app must retry. Roll back, release, and retry — but with backoff plus jitter. If both transactions retry at the same instant, they simply rebuild the identical cycle. The application is expected to survive a transaction it did nothing wrong in.
SHOW ENGINE INNODB STATUS — read it, confirm it names the two statements you suspect.deadlock_timeout (default 1s) actually control — and why does raising it make real deadlocks take longer to report instead of preventing them?