Every backend developer has done this at some point.
A request fails.
Something is not inserting into the database.
And suddenly the codebase turns into this:
dd($request->all());
dd($user);
dd($campaign);
Log::info($query);
You add logs in controllers.
Then services.
Then repositories.
Then queue workers.
Then event listeners.
And after two hours, you are still confused.
Because the real problem is not where you are looking.
The Day I Realized Controller Debugging Wasn’t Enough
A few months ago, a developer on a team I worked with faced a strange issue.
A simple form submission was supposed to:
- create a user
- assign a campaign
- generate a lead
- push a welcome email into queue
- write an activity log
Pretty normal workflow.
But randomly, some users were being created without campaign data.
Sometimes the queue job worked.
Sometimes it didn’t.
Sometimes the transaction rolled back silently.
The first reaction?
Start adding logs everywhere.
Inside controllers:
Log::info('Controller hit');
Inside services:
Log::info('Campaign assigned');
Inside queue workers:
Log::info('Email queued');
Inside transactions:
DB::beginTransaction();
The code became a forest of debugging statements.
Still no clear answer.
The Real Problem
Here’s what most developers forget:
No matter how beautiful your architecture is…
No matter whether you use:
- Laravel
- Raw PHP
- Node.js
- Python
- Java
At the end of the day, every query goes to one place:
MySQL.
Always.
Your application is just the messenger.
The database sees everything.
That realization changes how you debug systems.
Instead of Guessing, Watch the Database Directly
The moment we enabled MySQL General Query Log, everything became obvious.
We literally watched the queries execute live.
One terminal command showed the full story.
SET GLOBAL general_log = 'ON';
Then:
tail -f /var/lib/mysql/hostname.log
And suddenly we could see:
INSERT INTO users ...
UPDATE campaigns ...
ROLLBACK;
That last line explained everything.
The transaction was rolling back because another process locked a table for a few seconds.
No controller log would have shown the full picture that clearly.
This Is Why Framework-Level Debugging Often Fails
Modern applications are no longer simple.
A single request may involve:
- queues
- events
- transactions
- scheduled jobs
- caching
- background workers
- third-party APIs
By the time you debug from inside the controller, the actual issue may already be happening somewhere else entirely.
This is why developers sometimes spend hours debugging “application logic” when the real problem is:
- a deadlock
- a lock wait
- a slow query
- missing index
- rollback
- transaction conflict
And MySQL already knows all of this.
The Most Useful MySQL Debugging Tools
Honestly, there are four MySQL features every backend developer should know.
Not just DBAs.
Actual application developers.
Because these tools solve real-world production headaches.
1. General Query Log
See everything.
This is the closest thing to “watching the database think.”
Enable it:
SET GLOBAL general_log = 'ON';
Now every query gets logged.
You can literally submit a form and watch the database flow in real time.
This becomes incredibly useful when:
- one request touches multiple tables
- transactions fail
- duplicate queries happen
- hidden queries execute behind ORM magic
Especially in large Laravel applications where Eloquent sometimes hides too much.
2. Slow Query Log
Find hidden performance problems.
This one saves applications.
Seriously.
Sometimes developers optimize controllers endlessly while the real problem is one terrible query.
Enable it:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
Now MySQL logs anything slower than one second.
This helps identify:
- missing indexes
- N+1 problems
- huge scans
- inefficient joins
I once saw a dashboard drop from 12 seconds to under 800ms just because a slow query log exposed a missing index.
The application code itself was completely fine.
3. InnoDB Status
Understand deadlocks & locks.
This command feels like magic the first time you use it.
SHOW ENGINE INNODB STATUS;
If your application ever experiences:
- hanging requests
- frozen queues
- stuck inserts
- transaction conflicts
This command can immediately reveal:
LATEST DETECTED DEADLOCK
or:
LOCK WAIT
And suddenly the mystery disappears.
4. General Log Table Audit
This is where debugging becomes much more practical.
Most developers enable General Query Log and only think about the log file.
But MySQL can also write general logs into a table:
mysql.general_log
That means you can inspect logs using normal SQL.
You can filter by:
- latest queries
- specific table name
- specific command type
- specific MySQL thread ID
- insert/update/delete queries
- commit or rollback queries
- transaction flow
This is much better than randomly placing Log::info() everywhere.
Prerequisite: Make Sure General Log Writes to Table
Before querying mysql.general_log, check where MySQL is writing logs.
SHOW VARIABLES LIKE 'log_output';
SHOW VARIABLES LIKE 'general_log';
If log_output is only FILE, logs will go to a file.
If you want to query logs from a table, enable table output:
SET GLOBAL log_output = 'TABLE';
SET GLOBAL general_log = 'ON';
If you want both file and table logging:
SET GLOBAL log_output = 'TABLE,FILE';
SET GLOBAL general_log = 'ON';
Now MySQL will write General Query Log entries into:
mysql.general_log
Important note:
You need enough MySQL permission to run SET GLOBAL and read from mysql.general_log.
This is usually something you do in a local, staging, or temporary production debugging session with proper access.
View Latest Queries from General Log Table
Once General Query Log is enabled with table output, you can inspect the latest queries like this:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
ORDER BY event_time DESC
LIMIT 100;
This gives you the real database activity.
Not what you think Laravel executed.
Not what your controller expected.
Not what your service class assumed.
What MySQL actually received.
Filter Only Actual SQL Queries
The general log contains different command types.
For example:
ConnectQueryPrepareExecuteQuit
Most of the time, you only care about actual SQL statements.
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
ORDER BY event_time DESC
LIMIT 100;
This removes connection noise and focuses on real queries.
Find Queries for a Specific Table
Suppose your issue is related to the users table.
You can filter only queries that touched users:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE argument LIKE '%users%'
ORDER BY event_time DESC
LIMIT 100;
For campaign-related debugging:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE argument LIKE '%campaigns%'
ORDER BY event_time DESC
LIMIT 100;
This is extremely useful when one request touches multiple tables.
For example, one form submission may touch:
userscampaignsleadsjobsactivity_logs
Instead of checking five different PHP files, you can ask MySQL directly:
“What happened to this table?”
Check Only INSERT Queries
If your form submission is supposed to create a user, check whether the INSERT query actually reached MySQL.
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
AND argument LIKE 'INSERT%'
ORDER BY event_time DESC
LIMIT 100;
Specific table:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
AND argument LIKE 'INSERT%'
AND argument LIKE '%users%'
ORDER BY event_time DESC
LIMIT 100;
Now you can answer important questions:
Did the insert query happen?
Did it happen once?
Did it happen multiple times?
Did it happen before rollback?
Did another process insert something unexpectedly?
That is the kind of answer controller logs often fail to provide.
Check UPDATE and DELETE Queries
For update issues:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
AND argument LIKE 'UPDATE%'
ORDER BY event_time DESC
LIMIT 100;
For delete issues:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
AND argument LIKE 'DELETE%'
ORDER BY event_time DESC
LIMIT 100;
For a specific table update:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE command_type = 'Query'
AND argument LIKE 'UPDATE%'
AND argument LIKE '%campaigns%'
ORDER BY event_time DESC
LIMIT 100;
This helps you catch accidental updates or deletes triggered by:
- model observers
- queue jobs
- event listeners
- cron jobs
- scheduled commands
- background workers
Check COMMIT and ROLLBACK
This is one of the most important parts.
Sometimes the query runs successfully.
But the transaction rolls back later.
To check transaction result:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE LOWER(argument) IN ('commit', 'rollback')
ORDER BY event_time DESC
LIMIT 100;
If you see:
ROLLBACK
after your insert or update queries, then your problem is not the insert query itself.
Your problem is somewhere inside the transaction.
Maybe an exception happened.
Maybe a lock wait happened.
Maybe another process created a conflict.
Maybe a queue or event triggered something unexpected.
This is exactly why debugging only from the controller is not enough.
Trace One Request Using Thread ID
This is where things become really powerful.
Every MySQL connection has a thread_id.
If you find one suspicious query, copy its thread_id.
Then filter the full flow for that connection:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE thread_id = 12345
ORDER BY event_time ASC;
Now you can see the query sequence in order.
Example output may look like this:
START TRANSACTION;
INSERT INTO users ...
UPDATE campaigns ...
INSERT INTO leads ...
ROLLBACK;
That tells the real story.
Not guesswork.
Not random logs.
The actual database flow.
Trace Transaction Start, Commit, and Rollback
If you only want transaction-related statements:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE LOWER(argument) LIKE 'start transaction%'
OR LOWER(argument) = 'begin'
OR LOWER(argument) = 'commit'
OR LOWER(argument) = 'rollback'
ORDER BY event_time DESC
LIMIT 100;
For a specific thread:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE thread_id = 12345
AND (
LOWER(argument) LIKE 'start transaction%'
OR LOWER(argument) = 'begin'
OR LOWER(argument) = 'commit'
OR LOWER(argument) = 'rollback'
)
ORDER BY event_time ASC;
This is very useful when a form submission works sometimes and fails randomly.
Because now you can see:
- when the transaction started
- which queries happened inside it
- whether it committed
- whether it rolled back
Can We Filter POST Requests from MySQL?
Not directly.
MySQL does not know whether the original HTTP request was:
- GET
- POST
- PUT
- PATCH
- DELETE
MySQL only sees SQL.
So from the database side, you usually filter write operations:
INSERT
UPDATE
DELETE
COMMIT
ROLLBACK
For example, a POST request that creates a user usually results in:
INSERT INTO users ...
A PATCH request may result in:
UPDATE users ...
A delete action may result in:
DELETE FROM users ...
So instead of searching for “POST” inside MySQL, search for the SQL operation caused by that POST request.
Application-Level Request Tracking Example
If you want to connect application request logs with MySQL logs, add a request ID at application level.
Laravel example:
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
$requestId = request()->header('X-Request-Id') ?? (string) Str::uuid();
Log::withContext([
'request_id' => $requestId,
'method' => request()->method(),
'path' => request()->path(),
]);
Log::info('Request started');
Now your Laravel logs can tell you:
request_id: 7e71e1d8-3d9b-45e5-8a80-ef98c0d9c101
method: POST
path: /users
Then you can add a temporary MySQL session marker:
DB::statement("SET @debug_request_id = ?", [$requestId]);
This statement will also appear inside the General Query Log.
Now search for that marker:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE argument LIKE '%debug_request_id%'
ORDER BY event_time DESC
LIMIT 20;
Once you find the thread_id, inspect all queries from that same thread:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE thread_id = 12345
ORDER BY event_time ASC;
This gives you a bridge between:
- Laravel request
- controller/service logs
- actual MySQL queries
- transaction commit/rollback result
That is much better than guessing.
Example: Debugging a Failed User Creation
Imagine this flow:
- user submits form
- Laravel receives POST request
- application starts transaction
- user is inserted
- campaign is assigned
- lead is generated
- something fails
- transaction rolls back
From application logs, you may only see:
User creation failed
But from MySQL general log, you can check:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE argument LIKE '%users%'
OR argument LIKE '%campaigns%'
OR argument LIKE '%leads%'
OR LOWER(argument) IN ('commit', 'rollback')
ORDER BY event_time DESC
LIMIT 100;
Then you may discover:
INSERT INTO users ...
UPDATE campaigns ...
INSERT INTO leads ...
ROLLBACK;
Now the problem is clear.
The user insert happened.
The campaign update happened.
The lead insert happened.
But the transaction rolled back.
So the debugging direction changes immediately.
You stop asking:
“Why did user insert fail?”
And start asking:
“What caused the rollback?”
That is a huge difference.
Clean General Log Table
Do not let mysql.general_log grow forever.
After debugging, clean it.
First, disable General Query Log:
SET GLOBAL general_log = 'OFF';
Then truncate the table:
TRUNCATE TABLE mysql.general_log;
If you still need debugging, enable it again:
SET GLOBAL general_log = 'ON';
Full cleanup flow:
SET GLOBAL general_log = 'OFF';
TRUNCATE TABLE mysql.general_log;
SET GLOBAL general_log = 'ON';
Important note:
Do not use this as permanent production logging.
This is for temporary debugging.
Disable General Query Log After Debugging
This is the part developers often forget.
After investigation, turn it off:
SET GLOBAL general_log = 'OFF';
You can verify:
SHOW VARIABLES LIKE 'general_log';
If the value is OFF, logging is disabled.
One Important Thing Most Developers Ignore
Do not leave general_log permanently enabled in production.
That is a bad idea.
Because:
- logs become massive
- storage grows quickly
- performance can suffer
- sensitive query data may appear in logs
- debugging tables can become noisy very fast
The smarter workflow is temporary debugging.
Temporary Debugging Workflow
Enable table logging:
SET GLOBAL log_output = 'TABLE';
SET GLOBAL general_log = 'ON';
Reproduce the issue.
Check recent queries:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
ORDER BY event_time DESC
LIMIT 100;
Filter by table:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE argument LIKE '%users%'
ORDER BY event_time DESC
LIMIT 100;
Filter by transaction result:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE LOWER(argument) IN ('commit', 'rollback')
ORDER BY event_time DESC
LIMIT 100;
Trace one thread:
SELECT
event_time,
user_host,
thread_id,
command_type,
argument
FROM mysql.general_log
WHERE thread_id = 12345
ORDER BY event_time ASC;
Then disable logging:
SET GLOBAL general_log = 'OFF';
And clean the table:
TRUNCATE TABLE mysql.general_log;
That is the workflow.
Enable.
Reproduce.
Inspect.
Filter.
Understand.
Disable.
Clean.
That is how you stop guessing and start debugging from the place that sees everything:

Top comments (0)