SSchemaSync
See plans

SchemaSync/Guides

How to Analyse a SQL Query for Performance

Learn how to analyse a SQL query to find bottlenecks, check indexes, and optimize performance using local tools without sending data to the cloud.

October 22, 2026 · 5 min read

You analyze a SQL query by reading its execution plan to see how the database engine retrieves data, then correcting the schema or query structure based on those insights. The goal is to eliminate expensive operations like full table scans and excessive joins by ensuring the database can use indexes to locate rows directly.

Why Analyzing SQL Queries Matters

A query that runs in milliseconds on a small development dataset can take minutes on production data. This happens because the database engine changes its strategy as data volume grows. Without analysis, you are guessing which parts of the query are slow. The execution plan reveals the specific steps the engine takes, such as scanning entire tables or sorting large result sets in memory. By fixing the underlying structure identified in the plan, you often solve performance issues without rewriting the SQL logic itself.

Step 1: Inspect the Execution Plan

Most databases provide a command to show the execution plan without actually running the query. In PostgreSQL, you use EXPLAIN ANALYZE. This command executes the query and displays the actual time spent and rows processed at each step. Look for nodes labeled "Seq Scan" (sequential scan) on large tables. A sequential scan means the engine is reading every row in the table. This can happen when no suitable index exists, but it can also be chosen by the optimizer even when an index is available if it estimates that a full scan is cheaper than using the index, particularly when the query needs to retrieve a large percentage of the table's rows.

Consider this query joining three tables: orders, customers, and order_items. It retrieves recent orders for a specific customer region.

EXPLAIN ANALYZE
SELECT o.order_id, c.name, oi.product_id
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.order_id = oi.order_id
WHERE c.region = 'West'
  AND o.created_at > '2023-01-01';

If the plan shows Seq Scan on orders and Seq Scan on order_items, the database is reading every order and every item line, then filtering them in memory. This is often inefficient for selective queries. You generally want to see Index Scan or Bitmap Index Scan instead, though the optimizer may legitimately choose a sequential scan for small tables or broad filters.

Step 2: Identify Indexing Bottlenecks

The most common cause of slow joins is missing indexes on foreign key columns. PostgreSQL does not automatically index foreign key columns, so you must create them explicitly. MySQL with the InnoDB engine does create an index on foreign key columns if one is not already present, but you should still verify this behavior in your specific setup.

If orders.customer_id lacks an index, the join to customers cannot use a fast index lookup on the orders side. The optimizer may choose a hash join or merge join that scans each table once, which is better than repeated full scans but still expensive for large datasets. Similarly, order_items.order_id needs an index to quickly find all items belonging to a specific order.

You can check existing indexes by querying the system catalog or using a schema analysis tool. For instance, if you paste your raw SQL dump into SchemaSync, it detects indexing bottlenecks and suggests normalization strategies directly from the structure. This local analysis helps you identify potential indexing needs by examining the relationships defined in your dump.

Step 3: Check for Normalization Issues

Poor normalization can force the database to join more tables than necessary or duplicate data, slowing down writes and reads. However, over-normalization can also hurt performance by requiring too many joins. The goal is a balance where data is stored once but retrieved efficiently.

Look for tables that repeat values frequently. If the customers table stores the full region name in every row, and you filter by region, you might benefit from a separate regions table if you also filter by other region attributes. Conversely, if you frequently join orders and order_items to get total revenue, consider if a pre-calculated total_amount column in orders would reduce the need to aggregate order_items on every read. This is a form of denormalization for read performance.

Ensure your joins are on indexed columns. If you find yourself joining on non-indexed text columns (like name or description), consider adding a foreign key ID column to link the tables instead.

Step 4: Use Local Tools for Privacy

Analyzing production schemas often involves sensitive data. You do not need to upload your entire database to a cloud service to find indexing issues. Local tools can parse raw SQL dumps directly in your browser. This approach keeps your data private and allows you to analyze schema structures without connecting to a live database.

For example, you can export your schema as a .sql file and import it into a local analyzer. The tool analyzes the raw SQL dump to detect bottlenecks and suggest optimizations. Because it runs offline, you can analyze sensitive customer or financial schemas while reducing data-exposure concerns, though you should still manage local access and storage securely.

Common Mistakes When Optimizing

Do not add indexes blindly. Each index speeds up reads but slows down writes and consumes disk space. If you add an index on every column used in a WHERE clause, you may degrade insert performance significantly. Focus on columns used in joins and high-selectivity filters.

Avoid using SELECT *. It forces the database to retrieve all columns, which may prevent the use of covering indexes. A covering index includes all columns needed for the query, allowing the database to read data from the index alone without touching the main table. If you select only order_id and customer_id, a composite index on those two columns can satisfy the query instantly.

Finally, do not ignore the order of joins. While modern optimizers are smart, writing joins in a logical order that starts with the most selective table can help. If you filter by a specific customer, start with the customers table if it has a selective index on the filter column.

Applying the Fix

Returning to our example, the execution plan showed sequential scans on orders and order_items. The fix is to add indexes on the join columns.

CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_order_items_order_id ON order_items (order_id);

After adding these indexes, re-run EXPLAIN ANALYZE. You may see Index Scan nodes, but the planner might still choose sequential scans or hash joins based on cost estimates, statistics, and table size. If the query still filters by created_at and region, consider a composite index on orders (customer_id, created_at) to support both the join and the date filter. This allows the database to jump directly to the relevant rows for that customer within the date range, drastically reducing the number of rows processed.

Do it in SchemaSync

Everything in this guide works in the browser — open the tool and try it on your own input.

Open SchemaSync →

Questions people also ask

Does analyzing SQL queries require sending data to the cloud?

No, you can analyze queries locally using tools that parse raw SQL dumps directly in your browser or via command line. This approach keeps sensitive production data private while still allowing you to inspect execution plans and schema structures.

What is the difference between indexing and normalization?

Indexing is a physical storage optimization that speeds up data retrieval by creating lookup structures, while normalization is a logical design principle that reduces redundancy and ensures data integrity. You typically normalize the schema first to avoid anomalies, then add indexes to optimize the resulting query performance.

Can I analyze SQL queries from MySQL and SQLite dumps too?

Yes, most schema analysis tools support standard SQL dump formats from major databases like MySQL, PostgreSQL, and SQLite. You can import these dumps to visualize relationships and identify missing indexes without needing a live database connection.

How long does it take to analyze a large schema?

Parsing and analyzing a large schema typically takes seconds to minutes, depending on the number of tables and relationships. The bottleneck is usually the initial import of the SQL dump, while the actual structural analysis is nearly instantaneous for most tools.

More guides