DEV Community

Cover image for Stop Guessing in Controllers: Debug MySQL Directly
Tahsin Abrar
Tahsin Abrar

Posted on • Edited on

Stop Guessing in Controllers: Debug MySQL Directly

diagram

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Inside services:

Log::info('Campaign assigned');
Enter fullscreen mode Exit fullscreen mode

Inside queue workers:

Log::info('Email queued');
Enter fullscreen mode Exit fullscreen mode

Inside transactions:

DB::beginTransaction();
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Then:

tail -f /var/lib/mysql/hostname.log
Enter fullscreen mode Exit fullscreen mode

And suddenly we could see:

INSERT INTO users ...

UPDATE campaigns ...

ROLLBACK;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

If your application ever experiences:

  • hanging requests
  • frozen queues
  • stuck inserts
  • transaction conflicts

This command can immediately reveal:

LATEST DETECTED DEADLOCK
Enter fullscreen mode Exit fullscreen mode

or:

LOCK WAIT
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

If you want both file and table logging:

SET GLOBAL log_output = 'TABLE,FILE';
SET GLOBAL general_log = 'ON';
Enter fullscreen mode Exit fullscreen mode

Now MySQL will write General Query Log entries into:

mysql.general_log
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  • Connect
  • Query
  • Prepare
  • Execute
  • Quit

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This is extremely useful when one request touches multiple tables.

For example, one form submission may touch:

  • users
  • campaigns
  • leads
  • jobs
  • activity_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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

If you see:

ROLLBACK
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example, a POST request that creates a user usually results in:

INSERT INTO users ...
Enter fullscreen mode Exit fullscreen mode

A PATCH request may result in:

UPDATE users ...
Enter fullscreen mode Exit fullscreen mode

A delete action may result in:

DELETE FROM users ...
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Now your Laravel logs can tell you:

request_id: 7e71e1d8-3d9b-45e5-8a80-ef98c0d9c101
method: POST
path: /users
Enter fullscreen mode Exit fullscreen mode

Then you can add a temporary MySQL session marker:

DB::statement("SET @debug_request_id = ?", [$requestId]);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Then you may discover:

INSERT INTO users ...

UPDATE campaigns ...

INSERT INTO leads ...

ROLLBACK;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Then truncate the table:

TRUNCATE TABLE mysql.general_log;
Enter fullscreen mode Exit fullscreen mode

If you still need debugging, enable it again:

SET GLOBAL general_log = 'ON';
Enter fullscreen mode Exit fullscreen mode

Full cleanup flow:

SET GLOBAL general_log = 'OFF';

TRUNCATE TABLE mysql.general_log;

SET GLOBAL general_log = 'ON';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

You can verify:

SHOW VARIABLES LIKE 'general_log';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Then disable logging:

SET GLOBAL general_log = 'OFF';
Enter fullscreen mode Exit fullscreen mode

And clean the table:

TRUNCATE TABLE mysql.general_log;
Enter fullscreen mode Exit fullscreen mode

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)