The database is pinned at 90% CPU, the app team swears nothing changed, and AWR is topped by cursor: pin S wait on X and a parse-heavy profile. Nobody wrote a slow query. What happened is quieter: somewhere a developer built
SQL by pasting the value straight into the string — "...WHERE id = " + orderId — and now every one of a
million requests a day is a brand-new statement that Oracle has never seen, must parse from scratch, and stores
forever in a shared pool that has no idea two of them are the same query.
That’s the hard-parse storm, and it’s one of the most common self-inflicted Oracle performance problems there is. The fix is a single habit — bind variables — and the difference is not subtle. This post is what a parse actually costs, why literals multiply it, a lab that measures the blast radius, the cases where literals are genuinely the right call, and the security bonus you get for free.
What Oracle does before it runs your query
Before a SQL statement returns a single row, Oracle has to parse it: check the syntax, resolve the tables and columns, check your privileges, and — the expensive part — have the optimizer work out a plan. To avoid doing that over and over, Oracle caches the finished, parsed statement (the cursor) in the library cache, part of the shared pool. The next time the exact same SQL text arrives, it finds the cursor and skips the work.
That gives two kinds of parse:
- A hard parse is the full job: no matching cursor exists, so Oracle builds one from scratch — optimize the plan, allocate shared-pool memory, latch the library cache to insert it. Expensive in CPU, and it serializes on library-cache mutexes that every other session parsing is also fighting for.
- A soft parse is the cheap path: a cursor for this exact text already exists, so Oracle reuses it. Still not free, but a fraction of the cost.
The trigger for reuse is the SQL text, matched almost byte-for-byte. WHERE id = 4711 and WHERE id = 4712
are two different texts, so they are two different cursors and two hard parses — even though they are obviously
“the same query.” That is the whole problem in one sentence.
Literals multiply; a bind collapses
A bind variable is a placeholder — WHERE id = :b — with the actual value passed alongside the statement
at execution time, not baked into its text. Now the text is identical no matter what value you look up, so Oracle
parses it once and every subsequent execution is a soft parse against the one shared cursor.
flowchart TD
A["App runs the same query 1,000 times"] --> B{"How is the value passed?"}
B -->|"literal: WHERE id = 4711"| C["1,000 distinct SQL texts"]
C --> D["1,000 hard parses<br/>1,000 cursors in V$SQL<br/>~38 MB of shared pool<br/>(none of it reusable)"]
B -->|"bind: WHERE id = :b"| E["1 SQL text"]
E --> F["1 hard parse<br/>1 cursor, 1,000 executions<br/>~39 KB of shared pool"] Prove it
Take one query — SELECT COUNT(*) FROM widgets WHERE id = <n> — and run it 1,000 times two ways: once with the
value concatenated into the text ('...WHERE id = ' || i), once with a bind ('...WHERE id = :b' passing i).
Then read the cost straight from V$SQL and V$MYSTAT — cursor count, hard-parse delta, and shared-pool memory,
all deterministic counts, no stopwatch:
| 1,000 runs of one query | cursors in V$SQL | hard parses | shared pool |
|---|---|---|---|
| with literals | 1,000 | 1,001 | ~38 MB |
with a bind (:b) | 1 | 1 | ~39 KB |
The bind cursor records 1,001 executions — every run reused the single cursor. Same answer both ways. The literal version did it with a thousand times the parsing and roughly a thousand times the memory, and every one of those 1,000 cursors is dead weight: no future query will ever match its text, so it sits in the shared pool until it ages out — having evicted cursors that would have been reused on the way in.
Don’t take my word for it — run it. The bind-variables lab builds a 1,000-row table on Oracle Database Free and runs both loops, flushing the shared pool and snapshotting
parse count (hard)around each. It asserts the literal loop leaves ~1,000 cursors and ~1,000 hard parses while the bind collapses to one cursor, one hard parse, and 1,000 executions. If literals don’t explode or the bind doesn’t collapse, the run fails. Proven on every CI push.
Why a parse storm hurts more than “some extra CPU”
The parsing itself is wasted CPU, but the second-order effects are what actually take a database down:
- Library-cache and shared-pool contention. Inserting a new cursor takes a mutex on the library cache.
Thousands of sessions hard-parsing at once queue on those mutexes — the
library cache: mutex Xandcursor: pin S wait on Xwaits that dominate a parse-bound AWR. Throughput stops scaling with CPU because everyone is waiting in line to parse. - Shared pool churn and
ORA-04031. A flood of one-off cursors evicts reusable ones and fragments the pool. In the worst case allocation fails outright withORA-04031: unable to allocate ... shared memory— an outage caused entirely by un-shareable SQL. - A useless SQL cache. With every statement distinct,
V$SQLfills with thousands of near-identical rows, so AWR’s “SQL ordered by executions” is meaningless and you can’t even find your hot statement to tune it. Bind it, and one line in AWR shows a million executions — suddenly it’s visible and tunable.
When literals are actually the right call
Bind variables are the default for OLTP, but “always bind” is a myth worth puncturing. Because a bind hides the value at parse time, the optimizer plans for a generic value (helped by bind peeking and adaptive cursor sharing, which look at the first value and can spawn extra plans for skewed binds). When the value itself should change the plan, a literal can be the better choice:
- Data-warehouse and reporting SQL that runs a handful of times, over skewed data with histograms. Here you want the optimizer to see the actual value and pick the right plan — the same way an index is used for a rare value and ignored for a common one. Parsing a few big queries costs nothing; getting the plan right is everything.
- Genuinely static values — a status flag that is always
'ACTIVE', a constant — where the literal is the logic and never varies.
The rule of thumb: bind when the value varies but the plan shouldn’t; use a literal when the value should shape
the plan. OLTP by id, tenant, or key: bind. A reporting query where a date range or a rare category changes
everything: literal is fine. If you inherited an app that literal-izes everything and can’t be changed,
CURSOR_SHARING = FORCE is the emergency lever — Oracle rewrites literals to system binds so they share a cursor
— but it’s a workaround with its own plan-stability costs, not a substitute for binding in the code.
The bonus: bind variables stop SQL injection
The same habit that saves your shared pool is the single most effective defense against SQL injection.
Concatenating input into SQL text is exactly what lets '; DROP TABLE ... become executable code. A bind variable
is passed as data, never parsed as part of the statement, so there is nothing to inject into. Parameterize every
query and you get performance and security from one change — which is why “bind your variables” is the rare
optimization that a security review will also thank you for. It pairs with the rest of the
hardening checklist.
What teams get wrong
- Building SQL with string concatenation.
"...WHERE id = " + idis the source of both the parse storm and the injection hole. Use bind placeholders (:id,?, named parameters) in every language and framework. - Assuming the ORM handles it — without checking. Most ORMs bind by default, but
IN-lists, dynamicORDER BY, and “clever” query builders often concatenate. CheckV$SQLfor thousands of near-identical texts;FORCE_MATCHING_SIGNATUREgroups statements that differ only in literals, so a few signatures with huge counts are your literal offenders. - Reaching for
CURSOR_SHARING = FORCEas a permanent fix. It masks the symptom instance-wide and can destabilize plans (it defeats the optimizer’s view of literal values, the opposite problem). Fix the code; useFORCEonly as a bridge. - Binding data-warehouse queries over skewed columns. The inverse mistake: a bind hides the value the optimizer needed to see, so a report that should full-scan does an index range scan for a common value (or vice versa). Match the tool to the workload.
- Not making the value’s data type match. Binding a string where the column is a number
(
WHERE id = :bwith:baVARCHAR2) forces an implicit conversion that can disable an index — a separate trap from parsing, but a common one once people start binding.
Frequently asked questions
What is a bind variable in Oracle?
A bind variable is a placeholder in a SQL statement — written as a colon-prefixed name such as :id (or a ? / named parameter in application code) — whose actual value is supplied separately at execution time rather than written into the SQL text. Because the value is passed alongside the statement instead of embedded in it, the SQL text stays identical from one execution to the next regardless of the value, so Oracle parses the statement once and reuses the resulting cursor for every subsequent execution. Bind variables are the standard way to write repeated OLTP statements, and they are also the primary defense against SQL injection because the bound value is treated as data and never parsed as part of the statement.
What is the difference between a hard parse and a soft parse?
A hard parse is the full, expensive processing of a SQL statement: Oracle finds no matching cursor in the library cache, so it checks syntax and permissions, has the optimizer compute an execution plan, and allocates shared-pool memory and library-cache latches/mutexes to store the new cursor. A soft parse happens when a cursor for the identical SQL text already exists in the library cache, so Oracle skips the optimization and memory allocation and reuses the cached cursor — far cheaper, though not completely free. The deciding factor is whether the incoming SQL text matches an existing cursor almost exactly; literals make every value a new text and therefore a hard parse, while a bind variable keeps the text constant so all executions after the first are soft parses.
Why do literal values in SQL hurt performance and the shared pool?
When you concatenate a value into the SQL text, each distinct value produces a distinct statement, so a query run with a thousand different values becomes a thousand separate statements — each hard-parsed and each stored as its own cursor in the library cache. That wastes CPU on repeated optimization, and because inserting a cursor requires library-cache mutexes, many sessions parsing at once serialize on those mutexes, producing waits like 'library cache: mutex X' and 'cursor: pin S wait on X' that stop throughput from scaling. It also floods the shared pool with un-shareable cursors that evict reusable ones and fragment memory, in the worst case causing ORA-04031 allocation failures. In the companion lab the literal version of one query leaves about 1,000 cursors, ~1,000 hard parses, and ~38 MB of shared pool, versus one cursor, one hard parse, and ~39 KB for the bound version.
Do bind variables prevent SQL injection?
Yes — using bind variables (parameterized queries) is the single most effective defense against SQL injection. SQL injection works by concatenating attacker-controlled input into the SQL text so that the input is parsed as part of the statement; a bind variable passes the value separately as data, so it is never treated as SQL and there is nothing to inject into. This is why the same coding habit that keeps your shared pool healthy also closes the most common and most serious application-security hole, and why parameterizing every query is recommended by both performance and security guidance. The exception to watch is dynamic SQL that must vary structure (table or column names, ORDER BY): those cannot be bound and require strict allow-list validation instead.
Are bind variables always the right choice?
No. Bind variables are the right default for OLTP statements that run frequently with varying values but should use the same plan, which is the large majority of application SQL. They are not always ideal for data-warehouse and reporting queries that run a small number of times over skewed data, because a bind hides the actual value from the optimizer at parse time; there you often want a literal so the optimizer can see the value and choose the best plan for it (helped for binds by bind peeking and adaptive cursor sharing, but a literal is simplest when the value should shape the plan). A truly constant value can also just be a literal. The guiding principle is to bind when the value varies but the plan should not, and to use a literal when the value should influence the plan or never changes.
What is CURSOR_SHARING = FORCE and should I use it?
CURSOR_SHARING is an Oracle parameter that controls whether the database automatically replaces literals in incoming SQL with system-generated bind variables so that otherwise-identical statements share a cursor. With the default EXACT, statements must match textually; with FORCE, Oracle rewrites literals to binds so a flood of literal statements collapses onto shared cursors. It can be a valuable emergency mitigation for a third-party or legacy application that hard-codes literals and cannot be changed, relieving parse and shared-pool pressure instantly. It is not a substitute for binding in the code, however: replacing literals with binds instance-wide removes the literal values the optimizer may have needed and can destabilize execution plans, so the durable fix is to parameterize the application and reserve FORCE for cases you cannot edit.
How do I find statements that should be using bind variables?
Look in V$SQL (or the AWR equivalent) for large numbers of near-identical statements that differ only in literal values. The most direct way is to group by FORCE_MATCHING_SIGNATURE, a hash Oracle computes over each statement with its literals normalized away: many rows sharing one signature, each with a low execution count, are the same logical query being hard-parsed repeatedly, and the signature with the highest count of distinct SQL_IDs is your worst literal offender. High values in the instance's hard-parse and 'parse count (hard)' statistics, and parse-related mutex waits at the top of an AWR report, corroborate it. Once identified, fix the offending application code to use bind variables, and consider CURSOR_SHARING = FORCE only as an interim measure for code you cannot change.
Bind variables are the same performance discipline as the rest of the series — understand what the engine is actually doing and stop making it repeat work. Reading the execution plan shows you the plan the optimizer chose; good statistics and histograms are what it needs to choose well; SQL plan baselines keep a good plan from regressing; partition pruning and the right index cut how much data each execution touches — and bind variables cut how many times the engine has to parse in the first place. Bind when the value varies but the plan shouldn’t, keep literals for the reporting queries where the value should shape the plan, and prove the difference the way that ends the argument — with the bind-variables lab, where one query run a thousand times is a thousand cursors or exactly one.
Have a question or some feedback?
I write here in a personal capacity and enjoy comparing notes with other Oracle folks. Say hello.
Get in touch