SQL Single-Row Functions · Guide + 24 Interview Questions

SQL Single-Row Functions: Customize Output Row by Row

Single-row functions are how you shape values before they leave the database. Uppercase a name, trim stray spaces, round a price, convert a string to a date, replace NULL with something usable — they all run once per row and hand back one value, leaving the rest of the result untouched. This guide walks through the string, number, date, and NULL-handling functions you will use most, with the same customer and order tables throughout, and finishes with the twenty-four questions that come up most often in interviews.

🎯 Key Takeaways
24 Interview Questions Beginner to Advanced Real Examples Updated Oct 6, 2026
Kailas Warade ✓ Verified Author
Data Engineer · Oracle Certified · SQL & PL/SQL
Kailas works with SQL, PL/SQL, Python, and PySpark to build data pipelines. He writes the SQL interview guides at Sankalan Data Tech, focused on practical, real-world answers.
🏆 Oracle Certified 💼 Data Engineer 📝 SQL & PL/SQL Guides 📍 Pune, India
📤 Share:

On This Page

What Single-Row Functions Really Do

A single-row function is a small transformation you attach to a column or a value. It runs once for every row the query touches, and it hands back one result for each. If your query reads seven rows, a single-row function gets called seven times. If it reads a million, it gets called a million times. Nothing about other rows changes.

Contrast that with an aggregate function like SUM or COUNT. Those read every row and give you back one number. They are not per-row transformations — they are summarizers. Keeping the two families straight in your head matters, because the clauses they are allowed in are different. You can use a single-row function almost anywhere, including WHERE. Aggregates are stricter — they belong in SELECT and HAVING.

In day-to-day work, single-row functions are the reason a report looks right. They are how you capitalise a name, format a currency, strip stray whitespace from a data import, replace NULL with zero, or convert a text date into something the database can actually sort. The query logic stays the same; the output just reads better.

How They Fit Into a Query

A single-row function wraps the column or expression you want it to work on. The shape is always the same: the function name, then parentheses, then the value inside.

SELECT customer_name,
       UPPER(customer_name) AS name_upper,
       ROUND(credit_limit, 0) AS limit_rounded,
       NVL(credit_limit, 0) AS limit_with_default
FROM   customers;

The rules worth memorising

  • Functions wrap a column, a literal, or another function. UPPER('hello'), UPPER(customer_name), and UPPER(TRIM(customer_name)) are all valid.
  • Every function returns a value you can name with AS. Without an alias, the column header becomes something ugly like UPPER(CUSTOMER_NAME).
  • You can use single-row functions in SELECT, WHERE, GROUP BY, ORDER BY, and HAVING. They are not restricted the way aggregates are.
  • Wrapping a column in a function usually disables any index on that column. If WHERE UPPER(customer_name) = 'ALICE CORP' runs slowly, that is why — the database cannot use the plain column index.
  • NULL handling differs between databases. Oracle has NVL and NVL2. Everyone supports COALESCE, which is the portable choice.
A habit worth building: always alias the computed column. SELECT UPPER(customer_name) FROM customers; works, but the header comes back as something like UPPER(CUSTOMER_NAME). Downstream code that reads the result by column name will break. AS name_upper takes two seconds and saves you a debugging session later.

The Sample Data We Use in Every Example

Every example below runs against the same two tables. Keeping the data consistent means you can compare results side by side and see exactly what each function changes. If you are following along on your own database, set up the same rows and the numbers will line up.

-- customers table
-- customer_id | customer_name    | region | credit_limit | created_on
-- C001        | Alice Corp       | North  | 50000        | 2021-03-15
-- C002        | Bob Traders      | South  | 25000        | 2022-07-01
-- C003        | Charlie Ltd      | North  | 75000        | 2019-11-20
-- C004        | Diana Retail     | West   | 40000        | 2023-01-10
-- C005        | Ethan Supplies   | South  | 55000        | 2020-06-05
-- C006        | Fiona Mart       | West   | NULL         | 2024-02-14
-- C007        | George Wholesale | North  | 80000        | 2018-05-30

And the orders table used in the number and date examples:

-- orders table
-- order_id | customer_id | order_amount | order_status | order_date
-- 1001     | C001        | 12000.456    | Delivered    | 2024-01-15
-- 1002     | C002        | 4500.789     | Shipped      | 2024-02-03
-- 1003     | C001        | 7500.123     | Delivered    | 2024-02-18
-- 1004     | C003        | 22000.5      | Pending      | NULL
-- 1005     | C002        | 3200.999     | Delivered    | 2024-03-07
-- 1006     | C004        | 18000.25     | Shipped      | 2024-03-22
-- 1007     | C001        | 9000.0       | Delivered    | NULL

Four things in this data set are deliberate. They are the ones that make single-row functions actually useful to demonstrate.

  • Customer names come in mixed case. UPPER and LOWER have something real to change.
  • Order amounts carry decimals. ROUND, TRUNC, CEIL, and FLOOR all have a job.
  • Fiona Mart has a NULL credit limit. NVL, NVL2, and COALESCE have something to fix.
  • Two orders have NULL dates. TO_CHAR and EXTRACT need to be tested against missing values.

String Functions

Strings arrive in every shape you can imagine. Users type them in inconsistent case, data imports add invisible whitespace, legacy systems format phone numbers without rules. String functions are the toolkit for turning all of that into something usable.

UPPER and LOWER — normalising case

SELECT customer_name,
       UPPER(customer_name) AS name_upper,
       LOWER(customer_name) AS name_lower
FROM   customers;

Every name gets a second and third column showing its uppercase and lowercase forms. The most common real-world use is normalising data before a comparison. If you store names in mixed case but want to match against user input reliably, comparing UPPER(stored) = UPPER(input) solves the problem without depending on the database's case-sensitivity setting.

TRIM — removing stray whitespace

SELECT TRIM(customer_name) AS trimmed_name
FROM   customers;

TRIM removes leading and trailing spaces. Modern databases mostly do this automatically on insert, but data that arrived through a CSV import or an API that did not clean up its payload will still have them. The classic symptom is a query that returns nothing because ' Alice Corp' is not equal to 'Alice Corp'.

TRIM can also take a character to strip. TRIM('*' FROM '*hello*') returns hello. That variant is useful when data uses a specific placeholder around real values.

CONCAT and the || operator — joining strings

SELECT CONCAT(customer_name, ' (', region, ')') AS display_name
FROM   customers;

That returns names like Alice Corp (North). The || operator does the same thing in Oracle and PostgreSQL: customer_name || ' (' || region || ')'. CONCAT reads slightly more clearly, the || syntax is shorter. Both end up at the same place.

LENGTH — counting characters

SELECT customer_name,
       LENGTH(customer_name) AS name_length
FROM   customers;

Returns the number of characters per name, including spaces. Use it to validate input before an insert — if a column has a maximum length of twenty characters, filter out anything longer first — or to spot data anomalies. A name with a length of three is probably a placeholder.

SUBSTR — pulling out part of a string

SELECT customer_id,
       SUBSTR(customer_id, 2, 3) AS id_numeric_part
FROM   customers;

Extracts characters from position two for a length of three. On C001 that returns 001. SUBSTR is how you pull a code out of a longer identifier, grab an area code from a phone number, or split a formatted field that packed two pieces of information into one column.

REPLACE — swapping one piece of text for another

SELECT REPLACE(customer_name, 'Corp', 'Corporation') AS formal_name
FROM   customers;

Every occurrence of the substring gets replaced. Alice Corp becomes Alice Corporation. Where REPLACE shines is bulk cleanup — renaming a whole category of values across a table in one statement, without writing per-row update logic.

LPAD and RPAD — padding to a fixed width

SELECT LPAD(customer_id, 6, '0') AS padded_id,
       RPAD(customer_name, 20, '.') AS dot_filled
FROM   customers;

LPAD pads on the left, RPAD on the right. C001 becomes 000C001 with LPAD. Names get filled to twenty characters with periods on the right. Both are common in fixed-width file exports where every column has to occupy exactly the same number of characters, and in reports where you want values to line up under each other.

Number Functions

Numbers rarely come out of a database in exactly the shape a report wants. You get three decimal places when you need zero, or an integer when the calculation produced a fraction. Number functions handle all of that.

ROUND — rounding to a fixed precision

SELECT order_id,
       order_amount,
       ROUND(order_amount, 2) AS rounded_amount
FROM   orders;

Rounds to two decimal places. Order 1001 becomes 12000.46, order 1005 becomes 3201.00. The second argument is the number of decimal places. Pass zero to round to whole numbers, or a negative number to round to the nearest ten, hundred, or thousand.

TRUNC — cutting off decimals without rounding

SELECT order_id,
       order_amount,
       TRUNC(order_amount, 2) AS truncated_amount
FROM   orders;

TRUNC and ROUND differ in one crucial way: TRUNC does not round. Order 1005's 3200.999 becomes 3200.99 with TRUNC, versus 3201.00 with ROUND. That matters in financial reporting where rounding up or down changes the total. TRUNC is also used on dates — TRUNC(order_date) strips the time portion and leaves you with the date at midnight.

CEIL — rounding up to the next whole number

SELECT CEIL(4.3)  AS ceil_example,   -- returns 5
       CEIL(-4.3) AS ceil_negative;   -- returns -4

CEIL always goes up. It is the function you reach for when a partial unit still requires a full one — packing items into boxes when a box holds ten, computing how many full days a project needs when the last day is partial. Anything where rounding down would understate the answer.

FLOOR — rounding down to the whole number

SELECT FLOOR(4.7)  AS floor_example,  -- returns 4
       FLOOR(-4.7) AS floor_negative; -- returns -5

FLOOR always goes down. It is the conservative estimate. When you want to be sure you are not overcounting, FLOOR gives you the lower bound. Notice that FLOOR(-4.7) returns -5, not -4 — the function is strictly "round toward negative infinity", which is what you would expect once you think about it.

MOD — the remainder after division

SELECT order_id,
       MOD(order_id, 2) AS is_even_flag
FROM   orders;

MOD returns what is left over after dividing one number by another. MOD(10, 3) returns 1 because 10 divided by 3 leaves 1. The classic use is checking divisibility — MOD(n, 2) = 0 tells you a number is even. It also shows up in scheduling and round-robin logic where you cycle through a fixed set of options.

SIGN — positive, negative, or zero

SELECT order_amount,
       SIGN(order_amount - 10000) AS comparison_flag
FROM   orders;

SIGN returns 1 if the number is positive, -1 if it is negative, and 0 if it is zero. On the comparison order_amount - 10000, it returns 1 for orders above 10,000, -1 for orders below, and 0 for orders exactly at 10,000. It is a compact way to classify values without a full CASE expression.

Date Functions

Dates are where SQL interviews get interesting, because every database treats them slightly differently and everyone has been burned by a date format mismatch at least once.

TO_CHAR — formatting a date as text

SELECT order_id,
       TO_CHAR(order_date, 'YYYY-MM-DD')     AS date_only,
       TO_CHAR(order_date, 'DD Mon YYYY')    AS friendly_date,
       TO_CHAR(order_date, 'Month YYYY')     AS month_year
FROM   orders;

TO_CHAR takes a date and a format mask and returns a formatted string. The first column gives you 2024-01-15, the second gives 15 Jan 2024, the third gives January 2024. Use it whenever a raw date would not read cleanly in the output — which, in reports, is almost always. It is also how you group by month or year in a report without changing the underlying data.

TO_DATE — turning a string into a real date

SELECT TO_DATE('2024-11-26', 'YYYY-MM-DD') AS converted_date
FROM   dual;

The reverse of TO_CHAR. You give TO_DATE a string and a format mask, and it returns a proper date value the database can sort, filter, and calculate with. Without conversion, a date stored as text is just a string — comparisons are lexicographic, not chronological, and '2024-09-01' would come before '2024-10-01' even though September comes after October alphabetically. Conversion fixes that.

EXTRACT — pulling one part out of a date

SELECT order_id,
       EXTRACT(YEAR  FROM order_date) AS order_year,
       EXTRACT(MONTH FROM order_date) AS order_month,
       EXTRACT(DAY   FROM order_date) AS order_day
FROM   orders;

Each call returns one number — the year, the month, or the day. Filtering by a specific month becomes WHERE EXTRACT(MONTH FROM order_date) = 3. Grouping by year becomes GROUP BY EXTRACT(YEAR FROM order_date). It is the cleanest way to break dates into pieces without string manipulation.

NULL-Handling Functions

NULL is where most single-row function usage actually lives. NULLs disrupt arithmetic, distort averages, and turn comparisons into "unknown". These three functions exist to give NULL a usable substitute.

NVL — replace NULL with a default

SELECT customer_name,
       credit_limit,
       NVL(credit_limit, 0) AS limit_with_default
FROM   customers;

Fiona Mart has a NULL credit limit. With NVL, she shows zero instead. This matters because arithmetic involving NULL is NULL — credit_limit * 1.1 is NULL for Fiona, and any downstream calculation that depends on her value silently disappears. NVL keeps numbers moving through the pipeline.

NVL2 — different values for NULL vs non-NULL

SELECT customer_name,
       NVL2(credit_limit, 'Has limit', 'No limit') AS limit_status
FROM   customers;

NVL2 takes three arguments: the value to check, what to return if it is not NULL, and what to return if it is NULL. The result here is a label like "Has limit" or "No limit" for every customer. It is the clean version of a CASE expression when the only thing you are branching on is a NULL check.

COALESCE — first non-NULL wins

SELECT customer_name,
       COALESCE(credit_limit, order_amount, 0) AS effective_value
FROM   customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;

COALESCE walks through its arguments and returns the first one that is not NULL. If credit_limit has a value, that is returned. If not, it tries order_amount. If that is also NULL, it falls back to 0. NVL can only check one value; COALESCE can check as many as you give it.

There is also a portability angle. COALESCE is part of the SQL standard and works in every major database. NVL and NVL2 are Oracle-specific. If you are writing SQL that might run on more than one engine, reach for COALESCE first.

Nesting Functions for Chained Transformations

Single-row functions can wrap each other. The output of one becomes the input to the next, which lets you chain transformations in a single expression. The rule is simple — the innermost function runs first.

SELECT UPPER(TRIM(customer_name)) AS clean_name
FROM   customers;

TRIM runs first, stripping spaces. UPPER runs on the trimmed result, capitalising every letter. The order matters — TRIM(UPPER(customer_name)) also works and produces the same output here, but if the inner function produced or removed a specific character, the two orders could differ.

SELECT customer_name,
       TO_CHAR(TRUNC(created_on), 'DD-Mon-YYYY') AS signup_date
FROM   customers;

Three functions deep. TRUNC drops the time portion. TO_CHAR formats the truncated date as a friendly string. The result is a clean signup date with no hidden time component and a consistent display format.

SELECT COALESCE(ROUND(credit_limit, 2), 0) AS safe_limit
FROM   customers;

ROUND runs on the credit limit. If ROUND returns NULL — which happens when the input is NULL — COALESCE catches it and returns zero. This is the pattern you use whenever a downstream calculation cannot tolerate a NULL even after another function has processed the value.

Read nested functions from the inside out. When you are debugging a long expression like UPPER(TRIM(REPLACE(name, '-', ' '))), the innermost call — REPLACE — runs first, then TRIM, then UPPER. Writing the expression one function at a time and checking each intermediate result is faster than trying to reason about all three at once.

24 Interview Questions and Answers

These are the questions that come up most often across interviews and training sessions. Every answer stands on its own — read any of them in any order. If you are preparing for an interview, read straight through once, then use the search box and the quiz at the bottom to test yourself.

🔍

No matching questions found

Try a keyword like ROUND, NVL, or TO_DATE.

A single-row function runs once for every row the query touches and returns one value for each. If a query reads seven rows, a single-row function gets called seven times. UPPER, LOWER, TRIM, ROUND, and TO_CHAR are all in this family.

SELECT customer_name,
       UPPER(customer_name) AS name_upper
FROM   customers;

That query returns seven rows — one per customer — with an uppercased version of the name alongside the original. The row count is unchanged.

A multi-row function, also called an aggregate, works differently. It reads every row and returns a single summary value. COUNT(*), SUM(order_amount), and AVG(credit_limit) are all aggregates. They collapse many rows into one.

The distinction matters when you write a query, because the two families live in different parts of the SQL statement. Single-row functions can appear almost anywhere, including WHERE. Aggregates are restricted to SELECT and HAVING — try to put one in a WHERE clause and the database stops you with an error.

UPPER takes a string and returns the same string with every letter capitalised. UPPER('hello') returns HELLO. Numbers, spaces, and punctuation are untouched.

SELECT customer_name,
       UPPER(customer_name) AS name_upper
FROM   customers;

Alice Corp becomes ALICE CORP. Bob Traders becomes BOB TRADERS. The rest of the row is unchanged.

In production, UPPER is most often used to normalise data before comparing it. Suppose you store customer names in whatever case the user typed, and you want to search for "alice corp" reliably. Instead of hoping the casing matches, write WHERE UPPER(customer_name) = 'ALICE CORP'. Now the comparison works regardless of how the data was entered.

One caveat worth remembering: wrapping a column in UPPER disables any index on that column. If the table is large and the query is slow, that is usually why. The fix is a function-based index, but that is a conversation for a performance-tuning discussion.

LOWER is the mirror of UPPER. It takes a string and returns the same string with every letter lowercased. LOWER('HELLO') returns hello.

SELECT customer_name,
       LOWER(customer_name) AS name_lower
FROM   customers;

The typical uses are similar to UPPER — normalising data for comparison and giving reports a consistent look. Lowercase is common for email addresses, usernames, and URLs, where case differences should be treated as identical. If your login system stores emails in mixed case and compares them literally, someone who types their email with different capitalisation will not be able to log in. Wrapping both sides in LOWER fixes that class of bug entirely.

TRIM removes spaces from the beginning and the end of a string. TRIM(' Alice Corp ') returns Alice Corp. Spaces inside the string are left alone — only the leading and trailing ones go.

SELECT TRIM(customer_name) AS trimmed_name
FROM   customers;

The reason this matters more than it seems: stray whitespace is the most common source of "the data is there but the query cannot find it" bugs. When a record comes in through a CSV import, a copy-paste, or an API that did not clean its payload, whitespace almost always gets attached. A query that expects 'Alice Corp' will not match a stored value of ' Alice Corp', and there is nothing visually different between the two in a spreadsheet.

TRIM can also take a character to strip. TRIM('*' FROM '*hello*') returns hello. That form is useful when data uses a specific placeholder or padding character around real values.

CONCAT joins strings into one. CONCAT('Alice', ' ', 'Corp') returns Alice Corp. The function accepts two or three arguments depending on the database. For more complex joins, chain multiple CONCAT calls or use the || operator.

SELECT CONCAT(customer_name, ' (', region, ')') AS display_name
FROM   customers;

That produces labels like Alice Corp (North) and Bob Traders (South) — a formatted string built from two columns plus literal text. The || operator does the same thing: customer_name || ' (' || region || ')'. Most developers prefer one or the other based on the database they use most. Both end up at the same place.

In real work, CONCAT shows up most often for building display names, formatting addresses, or creating readable log entries. Anywhere you need to blend a value from a column with fixed text.

LENGTH returns the number of characters in a string, including spaces. LENGTH('hello') returns 5. LENGTH(' hello ') returns 9.

SELECT customer_name,
       LENGTH(customer_name) AS name_length
FROM   customers;

Where it earns its keep is validation. If a column is defined as VARCHAR2(20) and someone is about to insert a value of length 45, catching it before the insert fails gives you a much better error message than the database truncating or rejecting the row. On the read side, LENGTH is a quick way to spot data anomalies — a customer name with length 1 is almost always a placeholder or a broken import.

Note that LENGTH counts characters, not bytes. For multi-byte character sets, a single character might occupy more than one byte, so LENGTH and LENGTHB can return different numbers on the same string.

SUBSTR takes a starting position and a length, and returns the characters in between. SUBSTR('hello world', 1, 5) returns hello — starting at position 1 (the "h"), for 5 characters.

SELECT customer_id,
       SUBSTR(customer_id, 2, 3) AS id_numeric_part
FROM   customers;

On C001, that returns 001 — the numeric part of the ID. On C007, it returns 007. This is the pattern behind extracting area codes from phone numbers, usernames from email addresses, or a country code from an international reference number.

Positions in SQL strings are 1-based, not 0-based. SUBSTR('hello', 1, 1) returns the first character, not the second. That trips up developers who are used to languages like Python and JavaScript, where indexing starts at zero.

ROUND takes a number and a decimal-place count, and returns the number rounded to that many decimals. ROUND(123.456, 2) returns 123.46.

SELECT order_id,
       order_amount,
       ROUND(order_amount, 2) AS rounded_amount
FROM   orders;

On our data, order 1005 has an amount of 3200.999. With ROUND to two decimals, it becomes 3201.00. Order 1007's 9000.0 stays 9000.0.

The use case is not obscure. Prices, percentages, tax calculations, and any other value a human is going to read eventually need a fixed precision. ROUND is the standard way to get there. Pass a second argument of 0 to round to whole numbers, or a negative number to round to the nearest ten, hundred, or thousand — that second form is common in dashboards where you want a chart axis that is easier to read.

CEIL rounds a number up to the next whole number. CEIL(4.3) returns 5. CEIL(4.9) also returns 5. CEIL(4.0) returns 4, because the number is already whole.

SELECT order_id,
       CEIL(order_amount / 1000) AS thousands_ceil
FROM   orders;

Order 1001 is 12000.456. Divided by 1000, that is 12.0004. CEIL of that is 13. If you are bucketing orders into thousand-unit buckets and you want the bucket to be big enough to hold the value, CEIL gives you the answer that does not understate.

The classic use is any "how many containers do I need" question. A box holds ten items, you have 47 items, and 47 divided by 10 is 4.7 — which means you need 5 boxes, not 4. CEIL gets you to the right number without any special-case logic.

FLOOR rounds a number down to the previous whole number. FLOOR(4.7) returns 4. FLOOR(4.1) also returns 4.

SELECT order_id,
       FLOOR(order_amount / 1000) AS thousands_floor
FROM   orders;

Order 1005 is 3200.999. Divided by 1000, that is 3.200999. FLOOR of that is 3. If you are counting how many complete thousand-unit buckets the order fills, FLOOR gives you the conservative answer.

FLOOR and CEIL are the two directional rounders. ROUND is the closest-neighbour rounder. TRUNC is the "just drop the decimals" version that does not care about direction. Knowing which one you want before you write the query saves a round of debugging later.

MOD returns what is left over after dividing one number by another. MOD(10, 3) returns 1, because 10 divided by 3 leaves a remainder of 1.

SELECT order_id,
       MOD(order_id, 2) AS even_check
FROM   orders;

That returns 1 for odd order ids and 0 for even ones. Divisibility checks like this are the most common use — MOD(n, 2) = 0 means even, MOD(n, 5) = 0 means the number is a multiple of 5.

MOD also shows up in round-robin and cycle logic. If you want to route orders to one of three support queues based on the order id, MOD(order_id, 3) gives you 0, 1, or 2 — a clean three-way split. Same trick works for scheduling, hashing, and any place where you need to cycle through a fixed set of options.

NVL takes two arguments. If the first is not NULL, it returns the first. If the first is NULL, it returns the second. The name is Oracle shorthand for "null value."

SELECT customer_name,
       NVL(credit_limit, 0) AS limit_with_default
FROM   customers;

Fiona Mart has a NULL credit limit. With NVL, she comes back with a value of 0 instead. Everyone else still has their real credit limit.

Why this matters: NULL propagates. Any arithmetic involving NULL returns NULL. Any function called with NULL returns NULL. Any comparison with NULL evaluates to "unknown" rather than true or false. A single NULL in a column can silently wipe out a total, distort an average, or break a downstream calculation. Replacing NULL with a sensible default is often the difference between a report that works and one that quietly returns wrong numbers.

NVL is Oracle-specific. The portable equivalent is COALESCE. If you are writing SQL that might run on more than one database, COALESCE is the safer choice.

The functional difference is in how many values each one can check. NVL takes exactly two arguments — the value to test, and the fallback. COALESCE takes any number of arguments and returns the first one that is not NULL.

SELECT COALESCE(credit_limit, order_amount, 0) AS effective_value
FROM   customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;

That query tries credit_limit first. If it is NULL, it tries order_amount. If that is also NULL, it falls back to 0. NVL cannot do this — you would have to nest two NVL calls together to achieve the same result.

The other difference is portability. COALESCE is part of the SQL standard and works in every major database, including Oracle. NVL is Oracle-specific. If a query is going to move between systems, COALESCE is the function to reach for. If you are deep in an Oracle codebase and everyone else is already using NVL, that is fine too.

One practical note: COALESCE short-circuits. It stops evaluating as soon as it finds a non-NULL value, which means the remaining arguments are not computed. That matters when the arguments involve expensive function calls.

TO_CHAR converts a date into a string using a format mask. You tell it what shape you want the output to take, and it hands back the text.

SELECT order_id,
       TO_CHAR(order_date, 'YYYY-MM-DD')  AS date_only,
       TO_CHAR(order_date, 'DD Mon YYYY') AS friendly_date,
       TO_CHAR(order_date, 'Month YYYY')  AS month_year
FROM   orders;

Order 1001's date of 2024-01-15 renders as 2024-01-15 in the first column, 15 Jan 2024 in the second, and January 2024 in the third. Same date, three different presentations, chosen by the format mask.

The two places this matters most are reports and time-based grouping. A report that shows raw date values is technically correct but hard to scan. Wrapping the date in TO_CHAR gives the reader something they can process in a glance. And when you want to group orders by month, TO_CHAR(order_date, 'YYYY-MM') gives you a consistent string key that sorts and groups cleanly.

Note that TO_CHAR turns the date into a string. Once it is a string, it no longer behaves like a date for comparisons or arithmetic. Do your date calculations first, then convert for display.

TO_DATE is the reverse of TO_CHAR. It takes a string and a format mask, and returns a proper date value the database can work with.

SELECT TO_DATE('2024-11-26', 'YYYY-MM-DD') AS converted_date
FROM   dual;

The string '2024-11-26' becomes a real date. Once it is a date, it can be sorted correctly, compared against other dates, and used in date arithmetic like order_date + 30.

Without conversion, string dates are just strings. Comparisons are lexicographic rather than chronological, which produces wrong results in surprising ways. '2024-09-01' sorts after '2024-10-01' as a string because "9" is bigger than "1" at the third character. As a date, September correctly comes before October. Conversion is what makes the difference.

This function is essential when you are importing data. CSV files, Excel exports, and external APIs typically deliver dates as strings. Until you pass them through TO_DATE with the matching format mask, SQL has no way of knowing they are dates and no way of treating them as such.

TO_NUMBER takes a string that looks like a number and returns it as an actual numeric value. TO_NUMBER('1234.56') becomes the number 1234.56.

SELECT TO_NUMBER('1234.56') AS converted_number
FROM   dual;

Numbers stored as text cannot be added, subtracted, averaged, or compared numerically. The string '100' is less than the string '99' because "1" comes before "9" in character order — even though 100 is the larger number. Once converted with TO_NUMBER, the comparison works the way anyone would expect.

You see this most often with imported data. A CSV file that contains an amount column will load as text unless you convert it, and any query that tries to sum those amounts will either error out or produce nonsense. TO_NUMBER with the right format mask fixes the data at read time. On the write side, the database may do implicit conversion when you insert a numeric string into a numeric column, but relying on that is riskier than being explicit.

TRUNC cuts off a number or a date at a specific point, without any rounding. TRUNC(123.456, 2) returns 123.45 — the digits after position 2 are simply discarded, not rounded.

SELECT order_amount,
       TRUNC(order_amount, 2) AS truncated
FROM   orders;

Order 1005's amount is 3200.999. TRUNC to two decimals gives 3200.99. ROUND to two decimals gives 3201.00. The difference looks small until you are working with money and the accumulated discrepancy across a million rows adds up to real numbers.

TRUNC is the function of choice in financial contexts where rounding direction must be controlled by policy rather than by the database. When a report says "values are truncated to two decimals," TRUNC is what makes that true.

On dates, TRUNC strips the time component. TRUNC(SYSDATE) returns today's date at midnight. That is how you compare dates when you do not care about the time, or when you want to group by day without an implicit timezone surprise.

REPLACE swaps every occurrence of one substring with another. It takes three arguments — the original string, the substring to find, and the substring to replace it with.

SELECT REPLACE(customer_name, 'Corp', 'Corporation') AS formal_name
FROM   customers;

Alice Corp becomes Alice Corporation. The replacement happens for every matching occurrence in the string, and it happens for every row in the result.

The most common real-world use is bulk cleanup. If a product name changed, or a term is outdated in customer records, or an imported file uses a different abbreviation than your standard, REPLACE fixes it in one pass. You do not need to write a row-by-row update loop. Just wrap the column and let the function handle it.

One caution — REPLACE is case-sensitive. REPLACE(name, 'corp', 'Corporation') will not match "Corp". If your data has inconsistent case, you may need to combine REPLACE with UPPER or LOWER on the input, or write the replacement in multiple passes.

LPAD adds characters to the left side of a string until it reaches a specified length. LPAD('123', 5, '0') returns 00123 — two zeros added to the left to bring the length from 3 up to 5.

SELECT customer_id,
       LPAD(customer_id, 8, '0') AS padded_id
FROM   customers;

C001 becomes 0000C001. Every id in the table now occupies exactly 8 characters.

The reason to do this is almost always formatting. Fixed-width file exports require every column to be exactly the same length. Reports that put values in columns look messy if the widths vary. Padding ids or codes with leading zeros is standard practice in banking, invoicing, and any system that generates reference numbers. LPAD gives you that consistency in one function call, without any client-side formatting code.

If the padding character is not specified, LPAD pads with spaces by default. That is useful for producing column-aligned plain-text output.

RPAD is the mirror of LPAD. It adds padding characters to the right side of a string until it reaches the target length. RPAD('Hello', 10, '*') returns Hello*****.

SELECT customer_name,
       RPAD(customer_name, 20, '.') AS dot_filled
FROM   customers;

Alice Corp becomes Alice Corp.......... — ten dots filled to reach twenty characters.

Both LPAD and RPAD exist because fixed-width output is a real requirement in a lot of systems. Legacy mainframe feeds, COBOL-style interfaces, and reporting tools that expect exact column widths all need this. On the read side, RPAD is also a quick way to make text-based output line up in a terminal or in a text file that someone is going to open in a text editor.

SIGN tells you the direction of a number. It returns 1 if the number is positive, -1 if it is negative, and 0 if the number is exactly zero.

SELECT order_amount - 10000 AS difference,
       SIGN(order_amount - 10000) AS direction
FROM   orders;

For every order, the query returns the difference from 10,000 and a flag: 1 if the order is above the threshold, -1 if it is below, 0 if it is exactly at 10,000.

The reason this is useful is that it collapses a three-way decision into a single number. In finance, a positive value is a credit and a negative value is a debit. In a dashboard, sign flags let you split data into categories without writing a full CASE expression. It is a small function, but it earns its place in any codebase that deals with signed quantities.

NVL2 is a three-argument function that handles both cases of a NULL check. You give it a value to test, a value to return if the result is not NULL, and a value to return if the result is NULL.

SELECT customer_name,
       NVL2(credit_limit, 'Has limit', 'No limit') AS limit_status
FROM   customers;

That returns Has limit for every customer with a credit limit and No limit for Fiona Mart, who has NULL. Two different outputs depending on which side of the NULL check the value lands on.

It is the compact alternative to a full CASE expression when the only thing you are branching on is whether a value exists. NVL — the simpler sibling — only lets you replace NULL with a single fallback value. NVL2 lets you produce a label, a message, or a different default for each case. If you are generating a report and want the output to clearly mark which rows are complete and which are missing data, NVL2 is the cleanest tool for the job.

NVL2 is Oracle-specific. In other databases, the equivalent is a CASE expression or a combination of COALESCE and NULLIF.

EXTRACT pulls a single part out of a date value — the year, the month, the day, the hour, the minute, or the second. Each call returns one number.

SELECT order_id,
       EXTRACT(YEAR  FROM order_date) AS order_year,
       EXTRACT(MONTH FROM order_date) AS order_month,
       EXTRACT(DAY   FROM order_date) AS order_day
FROM   orders;

Order 1001's date is 2024-01-15. The three calls return 2024, 1, and 15 respectively.

The most common uses are filtering and grouping. To keep only March orders, write WHERE EXTRACT(MONTH FROM order_date) = 3. To summarise by year, write GROUP BY EXTRACT(YEAR FROM order_date). Both read more clearly than equivalent string manipulation, and both work regardless of the date's display format — because EXTRACT operates on the underlying date value, not on a formatted string.

Some databases support additional parts — QUARTER, WEEK, DAYOFWEEK — which makes EXTRACT a Swiss-army-knife for date-based reporting. Check your specific database for the full list.

CONVERT changes a value from one data type to another. You pass in a target type and the value to convert, and it returns the value in the new type.

SELECT CONVERT(VARCHAR, 123) AS text_value,
       CONVERT(INT, '456')    AS number_value
FROM   dual;

The first converts the number 123 into the string '123'. The second converts the string '456' into the number 456.

You reach for CONVERT when a function expects one type but you have another. String functions like UPPER and SUBSTR require string input, so if you have a numeric column and want to slice it, convert first. Date arithmetic requires date input, so if you have a text date you need to convert before adding days to it.

CONVERT is the SQL Server name for this operation. Oracle uses TO_CHAR, TO_DATE, and TO_NUMBER for the specific conversions. PostgreSQL uses :: or CAST. They all do the same thing — change the type — just with different syntax. The portable form across every database is CAST(value AS type).

Mistakes I Keep Seeing in Interviews

These come up again and again. None of them are exotic, and every one of them is easy to avoid once you know it is coming.

Mistake one: forgetting to alias computed columns. SELECT UPPER(customer_name) FROM customers; returns a column with an awkward auto-generated header like UPPER(CUSTOMER_NAME). Downstream code that reads by column name breaks. Always write AS name_upper.
Mistake two: applying a function to a column inside WHERE and wondering why the query got slow. WHERE UPPER(customer_name) = 'ALICE CORP' disables the index on customer_name. The fix is a function-based index or a restructured query — but the first step is recognising the cause.
Mistake three: assuming NVL is available everywhere. It is Oracle-specific. If your query needs to run on PostgreSQL or SQL Server, use COALESCE instead. Same effect, portable across engines.
Mistake four: expecting TRUNC and ROUND to give the same result. On 3200.999, ROUND to two decimals gives 3201.00 and TRUNC gives 3200.99. In financial contexts, that one-cent difference matters.
Mistake five: comparing a text date to a real date. If order_date is stored as a string, comparisons are lexicographic and can silently return the wrong rows. Convert with TO_DATE before comparing, or better, store dates as dates.
Mistake six: nesting functions in the wrong order. TRIM(UPPER(name)) and UPPER(TRIM(name)) happen to produce the same result here, but for other combinations — like TRUNC(ROUND(n, 2), 1) vs ROUND(TRUNC(n, 2), 1) — the order changes the output. Read nested expressions from the inside out.

Follow-Up Questions Interviewers Ask

Getting the first answer right is only half of the interview. What usually decides the outcome is what happens next. Here is what to prepare for on the topics in this guide.

After "What is a single-row function?" — expect

  • "How does that differ from an aggregate function?"
  • "Can you use it in WHERE? In HAVING?"
  • "What happens to indexes when you wrap a column in a function?"
  • "Give me an example where the order of nested functions matters."

After "What does UPPER do?" — expect

  • "How would you use it for case-insensitive search?"
  • "Does it work on numbers and dates too?"
  • "What is the equivalent in PostgreSQL? MySQL?"
  • "What is the difference between UPPER and INITCAP?"

After "What is the difference between NVL and COALESCE?" — expect

  • "Which one is more portable?"
  • "Can you nest COALESCE calls?"
  • "How does NVL2 differ from both?"
  • "When would you use NULLIF instead?"

For every question above, prepare a second-level answer. The candidates who pass are not the ones who memorised the first answer — they are the ones who kept going for another thirty seconds and said something useful about why it matters.

Quick Quiz

Three questions to check whether the topics above have landed.

1. What does UPPER('Alice Corp') return?
2. What does ROUND(123.456, 2) return?
3. Which function replaces NULL with a default value and works across every major database?