Case Study 32.2: Static vs Dynamic SQL — A Performance Decision
Background
Meridian National Bank's audit department has requested a new query program (ADTQUERY) that allows auditors to search the MERIDIAN.TRANSACTIONS table based on various combinations of criteria:
- Account number (exact match)
- Transaction date range
- Transaction amount range
- Transaction type (debit, credit, fee, interest)
- Branch ID
- Any combination of the above
The TRANSACTIONS table contains 450 million rows spanning 7 years of history. It is partitioned by transaction date (monthly partitions) with indexes on ACCOUNT_NUM, TXN_DATE, and a composite index on (BRANCH_ID, TXN_DATE).
The auditors currently use a static SQL program with 15 pre-coded queries covering the most common search patterns. They are frustrated because they cannot combine criteria flexibly. The development team proposes replacing this with a dynamic SQL program.
The Static SQL Approach (Current)
The current program has 15 cursor declarations:
* Query 1: By account number
EXEC SQL
DECLARE CSR-Q01 CURSOR FOR
SELECT TXN_ID, ACCOUNT_NUM, TXN_DATE, TXN_TYPE,
TXN_AMOUNT, TXN_DESC, BRANCH_ID
FROM MERIDIAN.TRANSACTIONS
WHERE ACCOUNT_NUM = :HV-ACCT-NUM
ORDER BY TXN_DATE DESC
END-EXEC.
* Query 2: By date range
EXEC SQL
DECLARE CSR-Q02 CURSOR FOR
SELECT TXN_ID, ACCOUNT_NUM, TXN_DATE, TXN_TYPE,
TXN_AMOUNT, TXN_DESC, BRANCH_ID
FROM MERIDIAN.TRANSACTIONS
WHERE TXN_DATE BETWEEN :HV-START-DATE
AND :HV-END-DATE
ORDER BY TXN_DATE DESC
END-EXEC.
* Query 3: By account and date range
EXEC SQL
DECLARE CSR-Q03 CURSOR FOR
SELECT TXN_ID, ACCOUNT_NUM, TXN_DATE, TXN_TYPE,
TXN_AMOUNT, TXN_DESC, BRANCH_ID
FROM MERIDIAN.TRANSACTIONS
WHERE ACCOUNT_NUM = :HV-ACCT-NUM
AND TXN_DATE BETWEEN :HV-START-DATE
AND :HV-END-DATE
ORDER BY TXN_DATE DESC
END-EXEC.
* ... 12 more cursor declarations for other combinations
The program uses an EVALUATE statement to select which cursor to open based on which input fields the auditor provided.
Advantages of this approach: - Every query has an optimized static access path - No runtime SQL parsing overhead - Authorization is managed at bind time - Predictable performance — each query's plan is known and tested
Disadvantages: - 15 cursor declarations and 15 OPEN/FETCH/CLOSE code paths make maintenance difficult - Cannot support new combinations without code changes - Combinatorial explosion: 5 filter criteria with optional presence means 2^5 = 32 possible combinations. The current 15 queries do not even cover all of them.
The Dynamic SQL Approach (Proposed)
The proposed replacement builds the WHERE clause dynamically:
BUILD-QUERY.
MOVE 0 TO WS-PARM-COUNT.
STRING 'SELECT TXN_ID, ACCOUNT_NUM, TXN_DATE, '
'TXN_TYPE, TXN_AMOUNT, TXN_DESC, BRANCH_ID '
'FROM MERIDIAN.TRANSACTIONS WHERE 1=1 '
DELIMITED BY SIZE INTO WS-SQL-TEXT.
IF HV-ACCT-NUM NOT = SPACES
STRING WS-SQL-TEXT DELIMITED BY ' '
' AND ACCOUNT_NUM = ? '
DELIMITED BY SIZE
INTO WS-SQL-TEXT
ADD 1 TO WS-PARM-COUNT
END-IF.
IF HV-START-DATE NOT = SPACES
STRING WS-SQL-TEXT DELIMITED BY ' '
' AND TXN_DATE >= ? '
DELIMITED BY SIZE
INTO WS-SQL-TEXT
ADD 1 TO WS-PARM-COUNT
END-IF.
IF HV-END-DATE NOT = SPACES
STRING WS-SQL-TEXT DELIMITED BY ' '
' AND TXN_DATE <= ? '
DELIMITED BY SIZE
INTO WS-SQL-TEXT
ADD 1 TO WS-PARM-COUNT
END-IF.
IF HV-TXN-TYPE NOT = SPACES
STRING WS-SQL-TEXT DELIMITED BY ' '
' AND TXN_TYPE = ? '
DELIMITED BY SIZE
INTO WS-SQL-TEXT
ADD 1 TO WS-PARM-COUNT
END-IF.
IF HV-BRANCH-ID NOT = 0
STRING WS-SQL-TEXT DELIMITED BY ' '
' AND BRANCH_ID = ? '
DELIMITED BY SIZE
INTO WS-SQL-TEXT
ADD 1 TO WS-PARM-COUNT
END-IF.
STRING WS-SQL-TEXT DELIMITED BY ' '
' ORDER BY TXN_DATE DESC '
DELIMITED BY SIZE
INTO WS-SQL-TEXT.
Then PREPARE and execute with a dynamic cursor:
EXEC SQL
PREPARE DYNSTMT FROM :WS-SQL-TEXT
END-EXEC.
EXEC SQL
DECLARE CSR-DYNAMIC CURSOR FOR DYNSTMT
END-EXEC.
* OPEN with appropriate USING clause based on WS-PARM-COUNT
* (requires SQLDA for variable parameter count)
Advantages: - Supports all 32 possible combinations with one code path - Easy to add new filter criteria - Cleaner, more maintainable code
Disadvantages: - Runtime PREPARE cost on every execution - Access path chosen at runtime — may not be optimal - Requires EXECUTE authority on the underlying tables (or DYNAMICRULES(BIND)) - More complex parameter handling with SQLDA
Performance Analysis
The team ran benchmarks with representative queries:
| Query Pattern | Static SQL (ms) | Dynamic SQL (ms) | Overhead |
|---|---|---|---|
| By account (indexed) | 3.2 | 4.8 | +50% |
| By date range (partition) | 1,200 | 1,245 | +3.8% |
| By account + date range | 8.5 | 11.2 | +32% |
| By branch + date range | 890 | 920 | +3.4% |
| Complex 4-criteria | 45 | 52 | +15.6% |
Key observations: - For short-running indexed lookups, the PREPARE overhead is proportionally significant (50% overhead on a 3ms query) - For longer-running queries that scan many rows, the PREPARE overhead is negligible (3-4% on queries over 1 second) - The optimizer chose the same access path for dynamic SQL in all tested cases (statistics were up-to-date)
The Hybrid Solution
After analysis, the team implemented a hybrid approach:
-
PREPARE once, EXECUTE many. The program keeps a cache of previously prepared statements. If the same combination of criteria is requested again, it reuses the prepared statement without re-PREPARing.
-
Use parameter markers (?) exclusively. Never concatenate literal values into the SQL text. This maximizes DB2's dynamic statement cache hit rate.
-
BIND with DYNAMICRULES(BIND). This allows the dynamic SQL to use the package owner's authorization, matching the security model of the static approach.
-
Keep the top 3 most common queries as static SQL. Analysis of audit query logs showed that 78% of queries used just three patterns: by account, by date range, and by account + date range. These remain static for maximum performance.
-
Route remaining queries to dynamic SQL. The less common combinations (22% of queries) go through the dynamic path.
EVALUATE TRUE
WHEN WS-QUERY-PATTERN = 'ACCT-ONLY'
PERFORM STATIC-QUERY-ACCT
WHEN WS-QUERY-PATTERN = 'DATE-RANGE'
PERFORM STATIC-QUERY-DATE
WHEN WS-QUERY-PATTERN = 'ACCT-DATE'
PERFORM STATIC-QUERY-ACCT-DATE
WHEN OTHER
PERFORM DYNAMIC-QUERY-BUILD
END-EVALUATE.
Results
The hybrid approach delivered: - 78% of queries use static SQL with optimal performance - 22% of queries use dynamic SQL with acceptable overhead - 100% of combinations are supported (up from 15 out of 32) - Maintenance effort reduced by 60% (from 15 code paths to 4) - Auditor satisfaction significantly improved — they can now combine any criteria
Discussion Questions
- At what query duration does the PREPARE overhead become negligible? Is there a rule of thumb?
- How does the DB2 dynamic statement cache affect the performance comparison? What if the cache is cold vs warm?
- What are the security implications of DYNAMICRULES(BIND) vs DYNAMICRULES(RUN)?
- Could you achieve similar flexibility with static SQL using "catch-all" WHERE clauses like
WHERE (ACCOUNT_NUM = :hv OR :hv = ' ')? What are the optimizer implications? - How would you monitor the dynamic SQL cache hit rate to validate the design decision?