Optimizing a Complex SQL Query Without Losing Your Mind (or Your Server)

Fix SQL fix performance issues without rewriting application logic

I inherited a monstrous SQL query in a project with a lot of complex legacy code. It tried to pull in everything but the kitchen sink—fifty LEFT JOINs, mountains of GROUP_CONCATs, and enough DISTINCT clauses to give your DBA nightmares. My goal was to fix performance issues without rewriting application logic, so I could get the app up and running quickly. Saving things like AJAX refactors for later, I focused on performance first. Here’s how I turned a timeout-inducing monster into a snappy 285 ms query—without breaking the system.


TL;DR – How I Saved a Query from Collapse:

  • Used EXPLAIN to identify performance issues
  • Added missing indexes
  • Broke the query into manageable stages
  • Extracted expensive aggregations into separate queries
  • Reassembled everything in PHP using lightweight subqueries

Step 1: Admit the Problem

The original query was intended to populate a form for editing a product. It joined over 30 tables and aggregated data from related categories, content, vendors, pricing, files, showcase metadata, and more. The query worked fine in dev—but as data grew, so did the latency. Eventually, it just stopped responding.

Step 2: Use EXPLAIN Like a Detective Tool

I started with EXPLAIN and looked at each join. Sure enough, some joins were scanning thousands of rows. Even worse, many-to-many relationships were cross-multiplying inside GROUP_CONCATs. This is where the spiral begins.

Step 3: Add Indexes First

Before rewriting anything, I added missing indexes. This helped reduce table scans dramatically. For example, adding an index on SHOWCASE_PRODUCT_PATTERNS.product_id helped shrink a 700ms subquery to under 100ms.

SHOW INDEX FROM SHOWCASE_PRODUCT_PATTERNS;
CREATE INDEX idx_product_id ON SHOWCASE_PRODUCT_PATTERNS (product_id);

Step 4: Break It Into Chunks

I didn’t want to rewrite everything at once, so I reintroduced joins two at a time. This allowed me to test performance incrementally and keep mental clarity. If adding two joins slowed things, I knew right away where the pain was.

Step 5: Replace Complex Joins with Subqueries

GROUP_CONCAT(DISTINCT ... ORDER BY ...) was the main culprit. These caused temp tables to spill to disk. I replaced them with subqueries, or removed them from the main query and ran them separately afterward.

SELECT GROUP_CONCAT(DISTINCT pattern_id SEPARATOR ' / ') AS showcase_pattern_id
FROM SHOWCASE_PRODUCT_PATTERNS
WHERE product_id = 2823;

This ran in 109 ms by itself but took 57 seconds when buried in the full query. Lesson: sometimes you just have to split it out.

Step 6: Reassemble the Results in PHP

Once the main query was optimized, I pulled the core query and ran 3-4 separate lightweight queries to fill in the extras.

$main = $this->db->query($main_sql, [$id])->row_array();
$main['portfolio_urls'] = $this->db
    ->query(\"SELECT GROUP_CONCAT(DISTINCT url SEPARATOR '**') AS portfolio_urls FROM V_PRODUCT_PORTFOLIO_PICTURE WHERE product_id = ?\", [$id])
    ->row_array()['portfolio_urls'];

The app got everything it needed—no timeouts, no need to rewrite the frontend.


Key Takeaways:

  • Indexes first. Never skip this step.
  • Use EXPLAIN to confirm assumptions. It’s your lens into the database engine.
  • Avoid GROUP_CONCAT with DISTINCT and ORDER BY inside big joins. Break them out.
  • Test in stages. Add joins incrementally and measure.
  • Split your query and recombine in code if needed. Modern PHP is fast—don’t fear a few extra queries if it keeps your DB sane.

Let me know if this article helped—or if you’re fighting a monster query of your own. I’d love to hear about it!

Leave a Reply

Your email address will not be published. Required fields are marked *