| Lesson 5 | Using the DECODE function with Oracle SQL |
| Objective | Use the DECODE built-in function in Oracle SQL and PL/SQL SQL statements. |
Oracle's DECODE function performs compact, equality-based value translation. It compares one expression with a sequence of search values and
returns the result associated with the first match. This makes it useful for converting stored codes into readable labels, handling a small set of known
values, and labeling subtotal or grand-total rows generated by GROUPING and ROLLUP.
DECODE remains supported in Oracle Database 23ai. It is an Oracle-specific SQL function rather than an ANSI SQL construct. In PL/SQL, it is
normally used inside a SQL statement, such as a SELECT ... INTO, cursor query, view definition, or DML statement. For new SQL, CASE
is often preferred because it is more portable and supports a wider range of conditions, but DECODE remains important when maintaining established
Oracle applications.
The general syntax is:
DECODE(
expression,
search_value_1, result_1,
search_value_2, result_2,
...,
default_result
)
Oracle compares expression with each search value from left to right. When it finds a match, it returns the corresponding result. If no search
value matches, Oracle returns the optional default result. If the default is omitted, the function returns NULL.
The following query translates numeric region codes into names:
SELECT
emp_name,
DECODE(
region_code,
10, 'EAST',
20, 'WEST',
30, 'SOUTH',
40, 'NORTH',
'NONE'
) AS region_name
FROM employee;
Example output:
EMP_NAME REGION_NAME
---------- -----------
BARRETT WEST
BURLEEZE EAST
DAHLSTROM NONE
In this example, region_code is the expression being evaluated. Each number is a search value, each region name is its associated result, and
'NONE' is the default result. The default is returned when the region code does not match 10, 20, 30, or 40.
DECODE works best when the mapping is small and stable. When a code list contains many values, changes frequently, or must be shared by several
applications, a lookup table is usually easier to maintain and enforce with referential integrity.
A simple CASE expression can perform the same translation:
SELECT
emp_name,
CASE region_code
WHEN 10 THEN 'EAST'
WHEN 20 THEN 'WEST'
WHEN 30 THEN 'SOUTH'
WHEN 40 THEN 'NORTH'
ELSE 'NONE'
END AS region_name
FROM employee;
The two forms are similar for equality comparisons. CASE, however, also supports searched conditions such as ranges, inequalities, compound
predicates, and tests involving multiple columns:
CASE
WHEN salary IS NULL THEN 'NOT ASSIGNED'
WHEN salary < 50000 THEN 'BELOW RANGE'
WHEN salary > 150000 THEN 'ABOVE RANGE'
ELSE 'WITHIN RANGE'
END
Use DECODE when a compact Oracle-specific equality mapping is easy to read. Use CASE when the logic is more complex, portability
matters, or future maintainers are likely to understand the standard syntax more quickly.
The expression and search values must be comparable. Oracle can perform implicit conversions, but mixing character and numeric datatypes can make a query dependent on session settings or cause conversion errors. Consistent datatypes are safer:
DECODE(region_code, 10, 'EAST', 20, 'WEST', 'NONE')
The return values must also be compatible. In the preceding expression, every result is character data. When a numeric or date result must be displayed as
text, convert it explicitly with TO_CHAR.
DECODE has an Oracle-specific null comparison rule: it treats two null values as equal. This differs from an ordinary SQL comparison, where
NULL = NULL evaluates to UNKNOWN.
SELECT
emp_name,
DECODE(
commission_pct,
NULL, 'NO COMMISSION',
'COMMISSION ASSIGNED'
) AS commission_status
FROM employee;
The equivalent CASE expression is often more explicit:
CASE
WHEN commission_pct IS NULL THEN 'NO COMMISSION'
ELSE 'COMMISSION ASSIGNED'
END
This special null behavior is useful when reading legacy code, but it should not be confused with the normal three-valued logic used by SQL predicates.
DECODE becomes especially useful in summary reports when it labels rows generated by ROLLUP. A ROLLUP extends a
GROUP BY operation by adding progressively higher summary levels.
GROUP BY ROLLUP(region_name, job_title)
This grouping list can produce:
Oracle represents a rolled-up grouping column with a null placeholder. The GROUPING function distinguishes that generated null from a null
stored in the source data:
GROUPING(region_name)
The function returns 0 when the column participates in the current grouping level and 1 when Oracle generated the null placeholder.
DECODE can translate that result into a readable heading:
DECODE(
GROUPING(region_name),
1, 'All Regions',
region_name
)
The following query expresses the intended report consistently and uses an ANSI join. It assumes that employee.region_id references
region.region_id.
SELECT
DECODE(
GROUPING(r.region_name),
1, 'All Regions',
r.region_name
) AS region_name,
DECODE(
GROUPING(e.job_title),
1, 'All Jobs',
e.job_title
) AS job_title,
COUNT(*) AS employee_count,
ROUND(AVG(e.sal) * 12, 2) AS avg_annual_salary
FROM employee e
JOIN region r
ON r.region_id = e.region_id
GROUP BY ROLLUP(
r.region_name,
e.job_title
)
ORDER BY
GROUPING(r.region_name),
r.region_name,
GROUPING(e.job_title),
e.job_title;
Rows labeled All Jobs are regional subtotals. The row labeled All Regions and All Jobs is the grand total.
AVG is recalculated from the rows that contribute to each grouping level; the grand-total average is not merely an average of the displayed
regional averages.
The original image remains useful because it visually connects DECODE, GROUPING, ROLLUP, COUNT, and
AVG. Its SQL and displayed output are transcribed below.
SQL displayed in the legacy image:
SELECT
DECODE(GROUPING(region_name), 1, 'All Regions', rname)
AS rname,
DECODE(GROUPING(job_title), 1, 'All Jobs', job_title)
AS job_title,
COUNT(*) "#",
AVG(sal) * 12 "Avgg Sal"
FROM
Employee e,
Region r
WHERE
r.region_name = e.region_name
GROUP BY ROLLUP (rname, job_title);
Data displayed in the legacy image:
| RNAME | JOB | # | Avgg Sal |
|-------------|----------|---:|---------:|
| EAST | Editor | 43 | 93,100 |
| WEST | Editor | 12 | 63,100 |
| NORTH | Manager | 3 | 123,000 |
| WEST | Manager | 2 | 231,000 |
| All Regions | All Jobs | 14 | 73,100 |
The image conveys the intended concept, but the legacy SQL is internally inconsistent because it alternates between region_name and
rname, uses a comma join, and displays only selected rows from what would normally be a larger ROLLUP result. The modern query above
should be used as the implementation reference.
ROLLUP generates hierarchical grouping levels from right to left. For:
ROLLUP(region_name, job_title)
Oracle creates these grouping sets:
(region_name, job_title)
(region_name)
()
The empty grouping set represents the grand total.
GROUPING(column) identifies whether a null was generated for a subtotal or grand-total row. It does not itself perform the summarization;
ROLLUP creates the grouping levels.
DECODE translates the numeric result of GROUPING into labels such as All Jobs and All Regions.
COUNT(*) reports the number of rows contributing to each group. If sal stores a monthly salary, AVG(sal) * 12
annualizes the average. A descriptive alias improves the report:
ROUND(AVG(sal) * 12, 2) AS avg_annual_salary
TO_CHAR: Converts datetime or numeric values into formatted character data.UPPER: Converts character data to uppercase.LOWER: Converts character data to lowercase.SUBSTR: Extracts part of a character value.DECODE: Translates one value into another through ordered equality comparisons.COUNT, SUM, AVG, MIN, and MAX: Calculate values across groups of rows.Applying a function to an indexed column can prevent Oracle from using a conventional index on the unmodified column for that predicate. It does not, however, guarantee a full table scan. The optimizer chooses an access path according to statistics, selectivity, available indexes, transformations, partitioning, and estimated cost.
Consider a query that counts STATSPACK snapshots created in March:
SELECT COUNT(*)
FROM perfstat.stats$snapshot
WHERE TO_CHAR(snap_time, 'MM') = '03';
A conventional index on snap_time may not directly support the transformed expression. A matching function-based index can make that expression
indexable:
CREATE INDEX stats_snapshot_month_fbi
ON perfstat.stats$snapshot (
TO_CHAR(snap_time, 'MM')
);
The indexed expression and query expression must match closely enough for Oracle to recognize the access path.
For one month in a known year, a half-open date range is usually clearer and can use a conventional index:
SELECT COUNT(*)
FROM perfstat.stats$snapshot
WHERE snap_time >= DATE '2026-03-01'
AND snap_time < DATE '2026-04-01';
This form avoids converting the indexed date column and handles time components correctly.
TO_CHAR(date_column, 'MM')
TO_CHAR(date_column, 'MON')
TO_CHAR(date_column, 'FMMonth')
'MM' returns a two-digit month number from 01 through 12.'MON' returns a language-dependent abbreviated month name.'FMMonth' returns a full month name without blank padding.Month names depend on NLS_DATE_LANGUAGE. For filtering, a date range is generally safer than comparing localized character output.
| Technique | Best use |
|---|---|
DECODE | A small set of equality comparisons in Oracle-specific SQL. |
CASE | Portable SQL, ranges, compound conditions, or logic involving multiple columns. |
| Lookup table | Large or changing code lists that require constraints, descriptions, governance, or reuse. |
Because DECODE is a SQL function, PL/SQL code normally invokes it inside a SQL statement:
DECLARE
v_region_name VARCHAR2(20);
BEGIN
SELECT DECODE(
region_code,
10, 'EAST',
20, 'WEST',
30, 'SOUTH',
40, 'NORTH',
'NONE'
)
INTO v_region_name
FROM employee
WHERE employee_id = 100;
DBMS_OUTPUT.PUT_LINE(v_region_name);
END;
/
For procedural branching outside a SQL statement, use PL/SQL IF or CASE statements instead.
DECODE is supported in Oracle Database 23ai.NULL.DECODE treats two nulls as equal for its comparison semantics.CASE is usually preferred for new standards-oriented SQL and complex conditions.ROLLUP creates detail groups, subtotals, and a grand total.GROUPING distinguishes rollup-generated null placeholders from stored null values.DECODE(GROUPING(...), 1, label, column) assigns readable labels to summary rows.The next lesson examines additional Oracle-supplied SQL extensions and how they simplify reporting and data transformation.
Practice writing an Oracle SQL statement that uses a built-in function: