Databases 22 min read

SQL Query Optimization for Large-Scale MES Reporting Using Stored Procedures

An in-depth case study describes how to dramatically improve the performance of a massive MES reporting query—reducing execution from over half an hour to seconds—by analyzing inefficient SQL, adding proper indexes, avoiding full table scans, redesigning with stored procedures, temporary tables, and selective use of NOLOCK.

Architect
Architect
Architect
SQL Query Optimization for Large-Scale MES Reporting Using Stored Procedures

Data Background

The project is a Siemens China MES deployment that runs on a production line for more than three years, accumulating billions of rows across several tables (5 tables > 1 billion rows, >10 tables > 10 million rows, many tables > 1 million rows). The original SSRS report query took more than half an hour (or timed out) because it performed full scans on these massive tables.

Exploration and Pitfalls

Initial attempts focused on adding missing indexes, but query speed did not improve significantly. The next idea was to partition the large tables, but partitioning directly on the live production tables caused deadlocks because the production line continuously writes data. Creating a new empty table, building partitions on it, and copying data seemed viable, yet the business logic (tracing product and serial numbers) had no regular filter conditions (e.g., date or sequential IDs), making partitioning ineffective and even detrimental.

Analysis of the Original SQL

WHERE clauses contain many @var IN (…) OR (@var = '') patterns.

Frequent use of LIKE '%'+@var+'%' leading to scans.

Use of CASE … END expressions inside predicates.

Multiple joins to the same view/table, some of which may be unnecessary.

Complex nested views that hide the real table sizes.

The query essentially performs full scans on a >1 billion‑row table and three >10 million‑row tables, which explains the extreme latency.

Optimization Design

The core idea is to replace the monolithic query with a stored procedure that builds temporary result sets based on the provided filter parameters. This approach offers several benefits:

Eliminates repetitive = @var OR (@var = '') checks.

Removes dynamic SQL concatenation, improving readability and safety.

Uses temporary tables to replace full scans with targeted look‑ups.

Replaces IN / NOT IN with EXISTS / NOT EXISTS .

Removes unnecessary LIKE clauses when fuzzy search is not required.

Applies appropriate indexes for the specific query paths.

Allows dirty reads with WITH (NOLOCK) for non‑critical reporting queries.

The stored procedure creates a global temporary table #FinalLotName (and later #FinalCO_SN ) and populates it step‑by‑step based on which input parameters are supplied. Each block checks for a non‑empty parameter, splits the comma‑separated list into a temporary table via fn_String_To_Table , and joins the relevant base tables to collect the needed columns.

CREATE Procedure spName1
    @MESOrderID nvarchar(320),
    @LotName nvarchar(700),
    @DateCode nvarchar(500),
    @BatchID nvarchar(700),
    @comdef nvarchar(700),
    @SNCust nvarchar(1600),
    @OnPlant nvarchar(20)
AS
BEGIN
    SET NOCOUNT ON;
    -- Define temporary tables and populate them based on supplied parameters
    CREATE TABLE #FinalLotName (LotName NVARCHAR(50), SourceLotName NVARCHAR(50), SNCust NVARCHAR(128));
    IF @LotName <> ''
    BEGIN
        SELECT Val INTO #WorkLot FROM fn_String_To_Table(@LotName,',',1);
        SELECT LotPK,LotName INTO #WorkLotPK FROM MMLots WITH(NOLOCK) WHERE EXISTS (SELECT 1 FROM #WorkLot b WHERE b.Val = MMLots.LotID);
        -- Additional joins and inserts omitted for brevity
    END
    -- Similar blocks for @BatchID, @SNCust, @DateCode, @comdef, etc.
    -- Final join to produce the report result set
    SELECT a.matl_def_id, b.Descript, a.MESOrderID AS pom_order_id, a.SN AS LotName,
           a.SourceSN AS ComLot, a.ComMaterials, c.Descript AS ComMatDes,
           a.VendorID, a.DateCode, a.SNNote, a.OnPlantID, a.SNCust
    FROM #FinalCO_SN a
    JOIN MMDefinitions b WITH(NOLOCK) ON a.matl_def_id = b.DefID
    JOIN MMDefinitions c WITH(NOLOCK) ON a.ComMaterials = c.DefID
    WHERE NOT EXISTS (SELECT 1 FROM #FinalCO_SN x WHERE x.SN = a.SourceSN AND x.SourceSN = a.SN);
END
GO

After replacing the SSRS report with this stored procedure, the query runs in seconds in SQL Server Management Studio, and the front‑end B/S application reports results within 1–2 seconds.

Conclusion

Performance problems often stem from writing overly complex, unindexed SQL that forces full table scans. The key take‑aways are to avoid full scans, use proper indexes, rewrite logic with temporary tables and stored procedures, and consider WITH (NOLOCK) when dirty reads are acceptable. These practices turn a half‑hour query into a sub‑second operation.

Performance OptimizationSQLIndexingDatabaseStored ProcedureMES
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.