Home Database ORA-01722 Invalid Number — Guard TO_NUMBER Calls
Intermediate 5 min · September 23, 2026

ORA-01722 Invalid Number — Guard TO_NUMBER Calls

Fix Oracle ORA-01722 by finding dirty rows with REGEXP_LIKE, then guarding TO_NUMBER with CASE.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 12 min
  • An Oracle database (11g+; 12.2+ for inline guards)
  • The failing SQL plus read access to its tables
  • Comfort running REGEXP_LIKE and DUMP queries
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • ORA-01722 means Oracle tried to turn a string into a number and the string wasn't numeric — a 'TBD', a comma, or a trailing space hiding in a varchar column
  • Find every offender in one query: SELECT id, code FROM items WHERE NOT REGEXP_LIKE(TRIM(code), '^[0-9]+(\.[0-9]+)?$') lists exactly the rows that break conversion
  • Guard with CASE WHEN REGEXP_LIKE THEN TO_NUMBER ELSE NULL, or TO_NUMBER(code DEFAULT NULL ON CONVERSION ERROR) on 12.2+ — never bare TO_NUMBER on dirty columns
  • Beware implicit conversion: comparing a varchar column to a number converts every row first, so one dirty row fails the whole query even with a careful WHERE clause
✦ Definition~90s read
What is ORA-01722 Invalid Number Fix?

ORA-01722 is raised when a character string that isn't a valid number is converted to NUMBER — explicitly by TO_NUMBER/CAST, or implicitly when Oracle must reconcile mixed types. Implicit conversion follows fixed rules: comparing a VARCHAR2 column to a numeric literal converts the strings (not the number), so WHERE code = 123 rewrites internally to TO_NUMBER(code) = 123 for every row.

Imagine a phone book where most entries are numbers but someone wrote 'call mom' in the phone-number column.

One 'N/A' among a million numeric strings fails the entire statement, which is why the error feels so disproportionate to its cause.

Dirty values come in flavors: words ('TBD', 'N/A', 'pending'), formatted numbers ('1,000', '$5', '12%'), whitespace (' 42', '42 ' — leading/trailing spaces actually convert fine, but embedded spaces and tabs don't), empty strings (which Oracle treats as NULL and usually pass through), and invisible characters (non-breaking spaces, zero-width joiners from copy-paste). DUMP() exposes the exact bytes when eyeballing fails — the classic 'looks like 42 but isn''t' row usually hides a CHR(160) or a tab.

Two guard styles exist. CASE WHEN REGEXP_LIKE(col, pattern) THEN TO_NUMBER(col) ELSE NULL END converts only values that match, leaving the rest NULL — portable across versions. TO_NUMBER(col DEFAULT NULL ON CONVERSION ERROR) (12.2+) declares the fallback inline and reads cleaner.

Both beat bare conversion; neither excuses dirty data. The durable fix pairs a guard for reads with a CHECK constraint plus ETL validation for writes, so the column converges to numeric and the guard becomes a formality.

Plain-English First

Imagine a phone book where most entries are numbers but someone wrote 'call mom' in the phone-number column. ORA-01722 is the moment you try to dial the whole page and hit that entry. The fix has two halves: find every non-number entry (REGEXP_LIKE lists them), then stop dialing blindly — check each entry before dialing (CASE guard) or skip the bad ones gracefully (DEFAULT NULL ON CONVERSION ERROR). Long-term, stop letting words into the number column with a CHECK constraint at the door.

ORA-01722: invalid number. It strikes reports that ran fine for months: same query, same table, sudden failure. Nothing changed in the SQL — something changed in the data. One row out of millions now holds 'TBD', '1,000', or a trailing space, and the TO_NUMBER call (or the silent implicit conversion) that always worked just met the row it can't digest.

Oracle converts strings to numbers eagerly: explicitly via TO_NUMBER, or implicitly whenever a varchar meets a number in a comparison. Either path throws 01722 on the first non-numeric value it touches — and the optimizer decides which rows it touches first, so even a careful WHERE guard in the same query level can fail.

This guide shows the full response: reproducing minimally, hunting dirty rows with REGEXP_LIKE, understanding implicit conversion, guarding with CASE and ON CONVERSION ERROR, respecting predicate-evaluation order, and locking the door with constraints and ETL contracts. The patterns transfer to every database with implicit conversion — learn them once here.

What 01722 Means: A String Refused to Become a Number

Read the code literally: invalid number means a conversion was attempted on a value that isn't one. The conversion is either explicit (TO_NUMBER, CAST AS NUMBER) or implicit (a varchar compared to a number, a number function fed a string). The error names no value and no row — just the failure — which is why newcomers stare at the query while veterans query the data. New failure in old SQL is always new data.

Reproduce the mechanics in one line: SELECT TO_NUMBER('12A') FROM DUAL throws 01722 instantly, proving the pathway without touching production tables. Then bisect the real statement: run its conversion expression over ROWNUM-bounded slices until the slice containing the offender fails. Bisection beats reading 4.2 million rows, and the failing slice's MIN/MAX ids bound the cleanup precisely.

Watch the whitespace subtlety: leading and trailing spaces convert fine (' 42' works), but embedded spaces, tabs, commas, currency symbols, and percent signs all throw. When a value 'looks numeric' but fails, DUMP() the bytes — CHR(9) tabs and CHR(160) non-breaking spaces from copy-paste are invisible in every client and obvious in a hex dump. When DUMP shows nothing odd, check the column type — a NUMBER column can never throw 01722.

repro_1722.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Mechanics in one line (proves the pathway)
SELECT TO_NUMBER('12A') FROM DUAL;

-- Bisect the real column in slices
SELECT TO_NUMBER(unit_price_varchar) FROM lines WHERE ROWNUM <= 1000;

-- Invisible characters exposed (tabs, nbsp, zero-width)
SELECT line_id, unit_price_varchar, DUMP(unit_price_varchar, 16)
FROM lines
WHERE line_id = 881207;
📊 Production Insight
A '42' that wasn't: DUMP revealed a trailing CHR(160) from a spreadsheet paste. Eyeballing cost an hour; the hex dump cost ten seconds.
🎯 Key Takeaway
Reproduce with DUAL, bisect with ROWNUM slices, and DUMP any value that looks numeric but fails.

Find the Dirty Rows With REGEXP_LIKE

The finder query is the heart of every 01722 response: list every non-NULL value that doesn't match the numeric pattern, with ids for cleanup. The pattern '^[0-9]+(\.[0-9]+)?$' covers plain integers and decimals; extend with optional sign ([+-]?) or exponent if your domain needs them, but keep the pattern strict — a permissive finder that admits '1,000' just moves the failure into TO_NUMBER. TRIM the value inside the check so padded-but-valid entries don't flood the offender list.

Size the result before acting: a handful of rows means surgical UPDATEs; thousands means an ETL or backfill bug needing a set-based fix plus a source correction. Include DUMP in the offender listing when the count is small — each row's bytes tell you whether you're dealing with words, formatting, or invisible characters, and each class gets a different cleanup.

Save the finder as the permanent data-quality monitor. The same query that sizes today's incident, run nightly with a page-on-rows threshold, converts every future dirty insert into a next-morning ticket instead of a month-end outage. Offender lists are assets: keep the query in the runbook, not just the ticket, with the alert threshold beside it.

find_dirty_1722.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- Every value that breaks conversion (ids included for cleanup)
SELECT line_id, unit_price_varchar
FROM lines
WHERE unit_price_varchar IS NOT NULL
  AND NOT REGEXP_LIKE(TRIM(unit_price_varchar), '^[0-9]+(\.[0-9]+)?$');

-- With bytes, for small offender lists (names the dirt class)
SELECT line_id, unit_price_varchar, DUMP(unit_price_varchar, 16)
FROM lines
WHERE unit_price_varchar IS NOT NULL
  AND NOT REGEXP_LIKE(TRIM(unit_price_varchar), '^[0-9]+(\.[0-9]+)?$')
  AND ROWNUM <= 20;
📊 Production Insight
One finder query listed a single 'TBD' among 4.2M rows — the entire month-end outage, named in eight seconds. The query now runs nightly.
🎯 Key Takeaway
Keep the REGEXP_LIKE finder strict, size the offender list before cleanup, and promote it to a nightly monitor.

Implicit Conversion: The Comparison That Converts

Explicit TO_NUMBER is the visible half; implicit conversion is the trap. When a VARCHAR2 column meets a numeric literal — WHERE code = 123, JOIN ... ON v.code = n.id, a bind variable bound as NUMBER against a varchar column — Oracle converts the strings, every row, before comparing. The plan hides it in filter predicates as TO_NUMBER("CODE")=123, and one dirty row anywhere in the scanned set throws 01722 even though your SQL mentions no conversion at all.

The rule of thumb: the string side always converts toward the number. That makes varchar-to-number comparisons landmines in otherwise clean code — especially joins across systems where one side typed the key as VARCHAR2 and the other as NUMBER. The failure then depends on data distribution, so it appears 'randomly' after backfills and integrations that introduce the first non-numeric key.

Fix the comparison, not the column, for reads: quote the literal (code = '123') so no conversion happens, or convert the single number side once (code = TO_CHAR(123)). For joins, align types at the boundary — a conversion function on the join key also kills index use, so mismatched-type joins cost performance long before they cost correctness. Reserve column-side conversion for guarded CASE expressions only.

implicit_convert.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- Trap: converts EVERY row's string first (one 'N/A' fails all)
-- SELECT * FROM items WHERE code = 123;

-- Fix: quote the literal — no conversion happens
SELECT * FROM items WHERE code = '123';

-- See the hidden conversion in your plan
EXPLAIN PLAN FOR SELECT * FROM items WHERE code = 123;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- filter shows TO_NUMBER("CODE")=123  <- the landmine

-- Bind hygiene: bind varchars as strings, never as numbers
📊 Production Insight
A cross-system join compared VARCHAR2 keys to NUMBER ids — clean for a year until the first 'PENDING' key arrived. Quoting nothing: the fix was aligning types at the boundary.
🎯 Key Takeaway
Varchar-to-number comparisons convert every row's string — quote literals, align join types, and read plans for hidden TO_NUMBER.

Guard It: CASE and DEFAULT ON CONVERSION ERROR

Two guard styles cover the two version eras. CASE WHEN REGEXP_LIKE(col, pattern) THEN TO_NUMBER(col) ELSE NULL END works everywhere: it converts only matching values and NULLs the rest, per row, in order. TO_NUMBER(col DEFAULT NULL ON CONVERSION ERROR) (12.2 and later) declares the fallback inline — shorter, clearer, and composable with other DEFAULT ... ON CONVERSION ERROR variants (0, or a sentinel). Pick the inline form on modern databases, CASE where you support 11g.

Guards restore service; they don't clean data. A guarded report completes while silently NULLing offenders — correct for month-end survival, dangerous as a permanent state, because NULLed margins understate revenue exactly as loudly as the error overstated the problem. Every guard ships with its offender-listing companion query and a cleanup ticket, or the NULLs become the next incident's mystery.

Scope guards to the conversion, not the query. Wrapping entire WHERE clauses in CASE nests logic that the next reader can't unwind; converting once in the SELECT list (or a factored subquery) keeps the guard visible and the predicates clean. One guarded expression per dirty column, named clearly, beats cleverness spread across the statement.

guard_1722.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Portable guard (all versions): convert only matches
SELECT CASE
  WHEN REGEXP_LIKE(TRIM(unit_price_varchar), '^[0-9]+(\.[0-9]+)?$')
  THEN TO_NUMBER(unit_price_varchar)
  ELSE NULL END AS unit_price
FROM lines;

-- Modern inline guard (12.2+): fallback declared at the call
SELECT TO_NUMBER(unit_price_varchar DEFAULT NULL ON CONVERSION ERROR) AS unit_price
FROM lines;

-- Always ship the offender list alongside the guard
SELECT line_id FROM lines
WHERE unit_price_varchar IS NOT NULL
  AND NOT REGEXP_LIKE(TRIM(unit_price_varchar), '^[0-9]+(\.[0-9]+)?$');
📊 Production Insight
An inline DEFAULT NULL guard restored the month-end rollup in one deploy — while the offender query fed the cleanup ticket that removed the need for the guard.
🎯 Key Takeaway
CASE/REGEXP everywhere, DEFAULT ON CONVERSION ERROR on 12.2+ — and every guard ships with its offender list.

The Optimizer Can Evaluate Your Guard Last

The subtlest 01722 trap is a correct-looking guard that the optimizer outruns. WHERE REGEXP_LIKE(x, pattern) AND TO_NUMBER(x) > 5 reads safely top-to-bottom, but SQL has no top-to-bottom: the optimizer may apply the conversion predicate to unfiltered rows first, throwing on dirt your guard would have excluded. The query is logically sound and physically doomed — it fails or passes depending on the plan, which is why it 'works in dev, fails in prod' on identical data volumes with different statistics.

The safe shapes put conversion where order is guaranteed. A CASE expression evaluates its WHEN before its THEN per row, so conversion inside THEN only touches guard-passing values. A factored split — inner query filters with REGEXP_LIKE (NO_MERGE or materialized CTE in behavior), outer query converts — separates filtering from conversion across query levels. Both survive plan changes; the flat AND-guard does not.

Treat any 01722 on a 'guarded' query as a plan-ordering bug, not a data surprise. Confirm by checking whether the guard and conversion share a query level; if they do, restructure into CASE or layered queries. And take the hint the incident offers: a column needing guards needs a constraint more.

guard_ordering.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Fragile: optimizer may convert before the guard filters
-- SELECT * FROM lines
-- WHERE REGEXP_LIKE(unit_price_varchar, '^[0-9]+$')
--   AND TO_NUMBER(unit_price_varchar) > 5;

-- Safe: CASE evaluates WHEN before THEN per row
SELECT * FROM lines
WHERE CASE
  WHEN REGEXP_LIKE(unit_price_varchar, '^[0-9]+(\.[0-9]+)?$')
  THEN TO_NUMBER(unit_price_varchar)
  ELSE NULL END > 5;

-- Safe: filter inside, convert outside (separate levels)
SELECT TO_NUMBER(unit_price_varchar) AS p FROM (
  SELECT unit_price_varchar FROM lines
  WHERE REGEXP_LIKE(unit_price_varchar, '^[0-9]+(\.[0-9]+)?$'));
⚠ Same-Level Guards Don't Protect Conversions
SQL has no top-to-bottom: the optimizer can apply TO_NUMBER before your REGEXP_LIKE guard in the same query level. Convert inside CASE or across layered queries — never beside the guard in one flat WHERE.
📊 Production Insight
A 'guarded' report failed only in prod — different statistics, different predicate order. Restructuring into CASE ended the plan-dependence permanently.
🎯 Key Takeaway
Guards share fate with predicate order — convert inside CASE or layered queries so plan changes can't re-break you.

Prevention: Types, Constraints, and ETL Contracts

The endgame is a column that can't hold words. Store numbers as NUMBER: migrate the varchar column (guard-convert, backfill, swap) so the type system enforces what conventions couldn't. Where strings must stay (codes with leading zeros, mixed identifiers), add CHECK (REGEXP_LIKE(...)) constraints that reject non-numeric writes at the door with an error naming the row — a Thursday insert failure beats a month-end outage every time.

Push validation into the ETL contract beside the constraint. The loader that wrote 'TBD' should have failed its own numeric assertion before committing; add the same REGEXP check to the pipeline's data-quality stage with a hard stop, not a warning. Warnings get snoozed through quarters; hard stops get fixed before lunch.

Run the nightly scan as the backstop: the finder query over every converted column, paging on any row. Constraints stop new dirt, ETL checks stop bad loads, and the scan catches the paths nobody owns (manual updates, legacy jobs, spreadsheet imports). Three layers, zero month-end surprises — each layer cheap, together airtight. Review the scan hits monthly: a rising trickle of violations means a writer you missed, and the trend names it before close does.

nightly_dq_1722.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# Nightly backstop: page on any non-numeric value in converted columns
set -euo pipefail
BAD=$(sqlplus -S 'dq/pass@//db-prod:1521/shop' << 'EOF'
SET HEADING OFF FEEDBACK OFF
SELECT COUNT(*) FROM lines
WHERE unit_price_varchar IS NOT NULL
  AND NOT REGEXP_LIKE(TRIM(unit_price_varchar), '^[0-9]+(\.[0-9]+)?$');
EOF
)
BAD=$(echo "$BAD" | tr -d ' ')
if [ "$BAD" != "0" ]; then echo "PAGE: $BAD non-numeric unit_price rows"; exit 1; fi
echo "OK: unit_price column is clean"
📊 Production Insight
The nightly REGEXP scan now pages on dirty inserts within hours — including a manual spreadsheet import that would have become next quarter's 01722.
🎯 Key Takeaway
NUMBER types plus CHECK constraints plus ETL assertions plus a nightly scan — four cheap layers, zero conversion surprises.
● Production incidentPOST-MORTEMseverity: high

One 'TBD' Row Delayed Month-End Close by 6 Hours

Symptom
At 1:00 AM the month-end revenue rollup failed with ORA-01722 after 40 minutes of runtime — $0 reported, close blocked, 30 accountants idle by 8 AM. The query had run cleanly for 14 months. Re-runs failed identically at the same elapsed time. Finance escalated at 6 AM when the close checklist stalled; every downstream report (tax, board pack, commissions) queued behind the one broken rollup.
Assumption
The team assumed a database patch from the weekend had changed conversion behavior, so they opened a severity ticket with the DBA group and waited on a parameter comparison. Two hours burned comparing NLS settings across environments while the actual cause sat in one row of staging data nobody queried.
Root cause
A Thursday ETL patch for backorders wrote the literal 'TBD' into lines.unit_price_varchar for 1 row out of 4.2 million — a column the report converts with TO_NUMBER for margin math. The value was valid business communication ('price to be determined') in a column with no CHECK constraint and no ETL numeric validation. The report's WHERE clause filtered on region, but the optimizer applied the conversion across unfiltered rows first, so the guard never protected the query.
Fix
At 7:00 AM they quarantined the row (UPDATE lines SET unit_price_varchar = NULL WHERE line_id = 881207 — restoring NULL semantics), wrapped the conversion in CASE WHEN REGEXP_LIKE, and the rollup completed by 9 AM. Follow-ups the same week: CHECK (REGEXP_LIKE(unit_price_varchar, '^[0-9]+(\.[0-9]+)?$') OR unit_price_varchar IS NULL) on the column, numeric validation in the ETL contract, and a nightly data-quality query paging on any non-numeric value.
Key lesson
  • Validate at the write path, not the report: a CHECK constraint plus ETL numeric validation would have rejected 'TBD' on Thursday instead of failing close on the 31st.
  • Never trust a same-level WHERE guard around conversion: the optimizer evaluates predicates in its own order, so guard inside CASE or clean the data.
  • Page on data quality nightly: a two-minute REGEXP_LIKE scan over converted columns turns month-end surprises into Thursday tickets.
Production debug guideSix steps from the failing report to the exact dirty rows.6 entries
Symptom · 01
Report fails with ORA-01722 after months of clean runs
Fix
Reproduce minimally: SELECT TO_NUMBER('12A') FROM DUAL; confirms the mechanics, then bisect the report — run its conversion expression alone: SELECT TO_NUMBER(unit_price_varchar) FROM lines WHERE ROWNUM <= 1000; Narrow the ROWNUM window and add ORDER BY to isolate the failing slice. New failure in old SQL always means new data, so hunt rows, not plans.
Symptom · 02
You need every value that breaks conversion
Fix
List offenders exactly: SELECT line_id, unit_price_varchar, DUMP(unit_price_varchar) FROM lines WHERE unit_price_varchar IS NOT NULL AND NOT REGEXP_LIKE(TRIM(BOTH CHR(9) || ' ' FROM unit_price_varchar), '^[0-9]+(\.[0-9]+)?$'); DUMP exposes invisible characters (CHR(160) non-breaking spaces, tabs) that eyeballing misses. One row or ten thousand — the list sizes the cleanup.
Symptom · 03
No TO_NUMBER in the SQL, yet 01722 fires
Fix
Hunt implicit conversion: any comparison between a varchar column and a number (WHERE code = 123, JOIN on mismatched types, bind variable bound as NUMBER against VARCHAR2) converts every row's string first. Find them by reading the predicate types, or check the plan for TO_NUMBER in filter predicates. Fix by quoting the literal (code = '123') or converting the number side once, not the column per row.
Symptom · 04
A WHERE REGEXP guard exists but the query still fails
Fix
That's predicate-ordering: the optimizer may apply TO_NUMBER before your guard in the same query level. Move the conversion inside CASE (SELECT CASE WHEN REGEXP_LIKE(x) THEN TO_NUMBER(x) END ...) which evaluates per row in order, or filter in an inner query and convert in the outer. Never rely on WHERE-clause ordering to protect a conversion.
Symptom · 05
You need the report running before the data is clean
Fix
Deploy the inline guard: SELECT TO_NUMBER(unit_price_varchar DEFAULT NULL ON CONVERSION ERROR) FROM lines; (12.2+) converts the convertible and NULLs the rest — the report completes while listing offenders separately. On older versions use the CASE WHEN REGEXP_LIKE form. Treat the guard as a bridge: it restores service, the constraint plus ETL fix removes the need for it.
Symptom · 06
Cleaned up — now keep the column numeric forever
Fix
Lock the door: ALTER TABLE lines ADD CONSTRAINT chk_unit_price_num CHECK (unit_price_varchar IS NULL OR REGEXP_LIKE(unit_price_varchar, '^[0-9]+(\.[0-9]+)?$')); Then add the same REGEXP check to the ETL contract and a nightly scan (same finder query) that pages on any violation. Future 'TBD's die at insert with a constraint error naming the row, not at close with a 40-minute 01722.
ORA-01722 Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Word/symbol in numeric-intent columnREGEXP_LIKE finder lists offendersQuarantine rows; guard with CASECHECK constraint + ETL validation
Implicit varchar-to-number comparisonPlan shows TO_NUMBER in filterQuote literal; align join typesType-match predicates in review
Formatted numbers (commas, currency)DUMP shows 0x2C/0x24 bytesStrip format or TO_NUMBER format modelStore raw numerics; format on display
Guard outrun by optimizerFails despite same-level REGEXP guardConvert inside CASE / layered queriesNever flat AND-guards around conversion
Invisible characters (tabs, nbsp)Looks numeric; DUMP disagreesClean bytes; TRIM in guardsSanitize paste/import paths
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
repro_1722.sqlSELECT TO_NUMBER('12A') FROM DUAL;What 01722 Means
find_dirty_1722.sqlSELECT line_id, unit_price_varcharFind the Dirty Rows With REGEXP_LIKE
implicit_convert.sqlSELECT * FROM items WHERE code = '123';Implicit Conversion
guard_1722.sqlSELECT CASEGuard It
guard_ordering.sqlSELECT * FROM linesThe Optimizer Can Evaluate Your Guard Last
nightly_dq_1722.shset -euo pipefailPrevention

Key takeaways

1
01722 means a string refused numeric conversion
new failure in old SQL means new data.
2
List offenders with strict REGEXP_LIKE plus DUMP for invisible characters.
3
Varchar-to-number comparisons convert every row
quote literals, align join types.
4
Same-level WHERE guards don't protect conversions
use CASE or layered queries.
5
Guard reads to restore service; constrain writes to remove the need.
6
NUMBER types, CHECK constraints, ETL assertions, nightly scans
four layers.

Common mistakes to avoid

5 patterns
×

Wrapping the conversion in a same-level WHERE guard

Symptom
Passes in dev, 01722s in prod — predicate order differs by statistics.
Fix
Convert inside CASE or across layered queries where evaluation order is guaranteed.
×

Comparing varchar columns to numeric literals

Symptom
No TO_NUMBER in sight, yet whole-table 01722 on one dirty row.
Fix
Quote the literal or convert the number side once; align join-key types.
×

Leaving the guard as the permanent fix

Symptom
Reports complete but silently NULL margins — understatement replaces the error.
Fix
Ship every guard with an offender query and a cleanup ticket; add the constraint.
×

Blaming the database patch for new 01722s

Symptom
Hours comparing NLS settings while one dirty row sits in staging data.
Fix
New failure in old SQL means new data — run the finder first, compare settings later.
×

Storing numbers as VARCHAR2 'for flexibility'

Symptom
Every reader re-implements validation; dirt accumulates from every writer.
Fix
Use NUMBER columns; keep strings only for genuinely mixed codes with CHECK guards.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What causes ORA-01722?
Q02SENIOR
How do you find the rows breaking a conversion?
Q03SENIOR
Why does WHERE code = 123 fail on a varchar column with no TO_NUMBER in ...
Q04SENIOR
A REGEXP guard in WHERE still 01722s. Why, and what's the safe rewrite?
Q05SENIOR
How do you stop 01722s permanently on a dirty numeric-intent column?
Q01 of 05JUNIOR

What causes ORA-01722?

ANSWER
Converting a non-numeric string to NUMBER — via TO_NUMBER/CAST or implicitly when a varchar meets a number in a comparison. One bad row fails the whole statement.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why did a query that ran for months suddenly throw 01722?
02
Do spaces in numbers cause 01722?
03
Is TO_NUMBER(x DEFAULT NULL ON CONVERSION ERROR) safe everywhere?
04
Can an index prevent 01722?
05
Why does the error show no value or row?
06
Should I just disable the report's conversion?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Oracle. Mark it forged?

5 min read · try the examples if you haven't

Previous
ORA-00904 Invalid Identifier Fix
2 / 5 · Oracle
Next
ORA-01400 Cannot Insert NULL Fix