| Lesson 4 | Using NVL with Oracle SQL |
| Objective | Use the NVL built-in function with Oracle SQL. |
Oracle SQL uses NULL to represent a value that is missing, unknown, or not applicable. A null value is not the same as zero, a blank
character, or a datatype default. Because nulls affect comparisons, calculations, joins, and report output, SQL developers must handle them deliberately.
Oracle's NVL built-in function provides a direct way to replace a null expression with a meaningful alternative.
This lesson explains how SQL's three-valued logic produces an UNKNOWN result, how NVL substitutes display and numeric values,
and why null handling is especially important when an outer join preserves rows that have no match in another table.
Most programming conditions are described as either true or false. SQL adds a third logical result: UNKNOWN. This three-valued logic is
necessary because a condition cannot always be evaluated when one of its operands is NULL.
Consider a row in which salary is null:
salary = 50000
Oracle cannot determine whether the missing salary equals 50,000, so the condition evaluates to UNKNOWN. It does not evaluate to
FALSE. This distinction matters because a WHERE clause returns only rows whose condition evaluates to TRUE. Rows that
produce either FALSE or UNKNOWN are filtered out.
Use the null predicates IS NULL and IS NOT NULL when testing for missing values:
SELECT employee_name,
salary
FROM employee
WHERE salary IS NULL;
Do not use = NULL or <> NULL. Those comparisons evaluate to UNKNOWN, so they do not identify null rows.
The result of a compound condition depends on the other operands. For example:
TRUE AND UNKNOWN evaluates to UNKNOWN.FALSE AND UNKNOWN evaluates to FALSE, because the complete condition cannot be true.TRUE OR UNKNOWN evaluates to TRUE.FALSE OR UNKNOWN evaluates to UNKNOWN.NOT UNKNOWN remains UNKNOWN.These rules influence filters, joins, constraints, conditional expressions, and data-quality checks. A null-handling function does not change SQL's logical model; it changes the expression supplied to that model.
The syntax of NVL is:
NVL(expression, replacement_value)
If expression is null, Oracle returns replacement_value. Otherwise, Oracle returns the original expression. The two arguments
must be the same datatype or convertible to compatible datatypes.
A null salary does not mean that an employee earns zero. It may mean that a salary has not yet been assigned. A report can preserve that meaning by returning descriptive text:
SELECT
ename,
NVL(
TO_CHAR(salary, 'FM999G999G990'),
'NOT YET ASSIGNED'
) AS salary
FROM employee;
Example output:
ENAME SALARY
----------- ----------------
BARRETT 85,000
BURLEEZE 101,000
DAHLSTROM NOT YET ASSIGNED
TO_CHAR converts the numeric salary to character data before NVL is evaluated. This gives both possible return values compatible
character datatypes. The phrase NOT YET ASSIGNED is a presentation value; the underlying salary remains null.
For arithmetic and aggregate calculations, replacing null with zero may be appropriate:
SELECT quantity,
unit_price,
NVL(discount_amount, 0) AS discount_amount,
quantity * unit_price - NVL(discount_amount, 0) AS net_amount
FROM order_item;
Without NVL, a null discount_amount would cause the entire subtraction expression to return null. With NVL, the
calculation treats a missing discount as zero.
Use zero only when it accurately represents the business meaning. A missing test score, account balance, or measurement may be unknown rather than zero. Substituting zero indiscriminately can produce misleading totals and averages.
Oracle may implicitly convert one NVL argument to the datatype of the other. Explicit conversion is safer because it makes the intended
datatype visible and avoids conversion errors that depend on session settings.
NVL(TO_CHAR(hire_date, 'YYYY-MM-DD'), 'DATE UNKNOWN')
This expression is clearer than relying on Oracle to convert a date automatically. The same principle applies to numeric formatting and localized date or number representations.
An outer join preserves rows from one side of a relationship even when no matching row exists on the other side. Oracle fills the missing side's columns with nulls. This behavior is essential for complete reports, but calculations based on those null-extended columns must be handled carefully.
The data dictionary views used in this lesson illustrate the problem:
DBA_DATA_FILES describes permanent database data files.DBA_FREE_SPACE describes free extents in tablespaces.A data file that contains no free extents can have no matching row in DBA_FREE_SPACE. An inner join would omit that file, making a completely full
file disappear from the report. A left outer join preserves the row from DBA_DATA_FILES and returns null values for the unmatched
DBA_FREE_SPACE columns.
The historical images use Oracle's legacy outer-join operator (+):
FROM
sys.dba_free_space a,
sys.dba_data_files b
WHERE
a.file_id (+) = b.file_id
AND
b.status = 'AVAILABLE'
The (+) marker identifies the optional side of the join. Oracle still recognizes this syntax, but ANSI join syntax is clearer and should be used
for new SQL:
FROM sys.dba_data_files b
LEFT OUTER JOIN sys.dba_free_space a
ON a.file_id = b.file_id
WHERE b.status = 'AVAILABLE'
In both forms, DBA_DATA_FILES is the preserved side. Every qualifying data file can therefore appear even when no free extent is found.
When no DBA_FREE_SPACE row matches a data file, expressions such as SUM(a.bytes) and MAX(a.bytes) return null. The report
can convert those results to zero:
NVL(SUM(a.bytes), 0)
NVL(MAX(a.bytes), 0)
This is a valid substitution because the absence of any free extent means that the report should show zero free bytes and a zero-byte largest free extent.
Aggregate row counting requires an additional distinction. With an outer join, COUNT(*) counts the preserved result row even when every column from
the optional table is null. To count actual matching free extents, count a non-null column from DBA_FREE_SPACE:
COUNT(a.bytes) AS free_extents
This returns zero for a data file with no matching free-space row.
The following legacy images remain useful because they show the progression from an outer join to null substitution. Their SQL has been transcribed into the captions so that the lesson remains searchable and accessible.
FROM
SYS.DBA_FREE_SPACE A,
SYS.DBA_DATA_FILES B
WHERE
A.FILE_ID (+) = B.FILE_ID
AND
B.STATUS = 'AVAILABLE'
GROUP BY
1. The legacy (+) operator preserves rows from DBA_DATA_FILES even when a data file has no matching entry in
DBA_FREE_SPACE.
FROM
SYS.DBA_FREE_SPACE A,
SYS.DBA_DATA_FILES B
WHERE
A.FILE_ID (+) = B.FILE_ID
AND
B.STATUS = 'AVAILABLE'
GROUP BY
2. When no free extent exists, columns from DBA_FREE_SPACE are null. For this report, those missing free-space values should be
represented as zero.
B.TABLESPACE_NAME,
SUBSTR(B.FILE_NAME, 1, 100),
B.FILE_ID,
NVL(COUNT(*), 0),
NVL(ROUND
((SUM(B.BYTES) / COUNT(*)) / 1048576), 0),
NVL(ROUND
((SUM(A.BYTES)) / 1048576), 0),
NVL(ROUND
(100 -
(NVL(100 -
(SUM(NVL(A.BYTES, 0)) / (SUM(NVL(B.BYTES, 0))
/ COUNT(*))) * 100, 0))), 0),
NVL(ROUND
(MAX(A.BYTES / 1048576)), 0),
NVL(ROUND
(100 -
3. The selected expressions use NVL to replace null numeric results with zero. The image shows only a partial excerpt of the
original SELECT list.
| DBA_DATA_FILES | DBA_FREE_SPACE |
|---|---|
|
|
4. Some rows represented by DBA_DATA_FILES have no corresponding row in DBA_FREE_SPACE.
5. The outer join preserves the data-file rows. Where no free-space row matches, the result contains null values for the
DBA_FREE_SPACE columns.
SELECT NVL(column_name, 0)
6. The expression returns zero when column_name is null; otherwise, it returns the column value.
The following version preserves the purpose of the legacy query while using ANSI join syntax and more direct calculations. It lists every available permanent
data file, including a file that has no rows in DBA_FREE_SPACE.
SELECT
b.tablespace_name,
SUBSTR(b.file_name, 1, 100) AS file_name,
b.file_id,
COUNT(a.bytes) AS free_extents,
ROUND(b.bytes / 1048576) AS allocated_mbytes,
ROUND(NVL(SUM(a.bytes), 0) / 1048576) AS free_mbytes,
ROUND(
NVL(SUM(a.bytes), 0) /
NULLIF(b.bytes, 0) * 100,
2
) AS pct_free,
ROUND(
NVL(MAX(a.bytes), 0) / 1048576
) AS largest_free_extent_mbytes,
ROUND(
NVL(MAX(a.bytes), 0) /
NULLIF(b.bytes, 0) * 100,
2
) AS pct_largest_free_extent,
SYSDATE AS stats_date,
ROUND(
(b.bytes - NVL(SUM(a.bytes), 0)) /
1048576
) AS used_mbytes,
ROUND(
(b.bytes - NVL(SUM(a.bytes), 0)) /
NULLIF(b.bytes, 0) * 100,
2
) AS pct_used,
NULL AS db_name,
NULL AS server_name
FROM sys.dba_data_files b
LEFT OUTER JOIN sys.dba_free_space a
ON a.file_id = b.file_id
WHERE b.status = 'AVAILABLE'
GROUP BY
b.tablespace_name,
b.file_name,
b.file_id,
b.bytes
ORDER BY
b.tablespace_name,
b.file_id;
The query incorporates several defensive SQL techniques:
NVL(SUM(a.bytes), 0) reports zero free bytes when no free extent exists.COUNT(a.bytes) counts actual free extents instead of the null-extended outer-join row.NULLIF(b.bytes, 0) prevents division by zero in percentage calculations.LEFT OUTER JOIN makes the preserved and optional sides of the relationship explicit.1048576 converts bytes to mebibytes, although database reports commonly retain an MBYTES alias.NVL is concise and familiar in Oracle SQL, but it is not the only null-handling construct. COALESCE returns the first non-null
expression from a list:
COALESCE(work_phone, mobile_phone, home_phone, 'NO PHONE')
Use COALESCE when several fallback expressions are required. A searched CASE expression is appropriate when the replacement depends
on more than a simple null check:
CASE
WHEN salary IS NULL THEN 'NOT ASSIGNED'
WHEN salary < 50000 THEN 'BELOW RANGE'
ELSE 'ASSIGNED'
END
For a direct two-value substitution in Oracle-specific SQL, NVL remains clear and effective.
Null-handling expressions are often inexpensive, but their placement can affect access paths. Applying a function to an indexed column in a predicate can prevent Oracle from using a conventional index entry unless an appropriate function-based index exists.
WHERE NVL(status_code, 'N') = 'A'
Before using this form in a high-volume query, review the execution plan and consider whether an equivalent predicate expresses the requirement more selectively. If the expression is used frequently and is important to query performance, a function-based index may be appropriate after workload testing.
Do not add NVL merely to make every output non-null. First determine what a null means, then choose a replacement that preserves the semantics of
the data. This approach prevents a visually convenient report from becoming analytically incorrect.
NULL represents missing, unknown, or inapplicable information; it does not automatically mean zero.UNKNOWN.IS NULL and IS NOT NULL to test for null values.NVL(expression, replacement_value) returns the replacement only when the first expression is null.DBA_DATA_FILES in a left outer join so that full data files do not disappear from a free-space report.COUNT(optional_table.column), rather than COUNT(*), when counting actual matches from the optional side of an outer join.After learning how NVL replaces null expressions, the next lesson examines another Oracle conditional translation function:
DECODE.