Applies to VAST Cluster 5.5.
Functions › Text basics › Formatting. Builds a string from a C-style template, replacing each % conversion, such as %s or %05d, with the next value. printf has no aliases; sprintf isn't available.
Syntax
printf(template, value, ...)
| Argument | Type | Description |
|---|---|---|
template |
VARCHAR |
The text to build, with a % conversion for each value. |
value, ... |
any type its conversion accepts | The values for the conversions, in order. |
Returns: VARCHAR.
Aliases: none.
Set up the examples
The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_printf. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.
This block connects, defines the run() and plan() helpers, and creates a table of four orders. The last row has no values:
import os
import re
import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb
BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_printf"'
conn = dbapi.connect(
driver=adbc_driver_vastdb.get_driver_path(),
db_kwargs={
"vast.db.endpoint": os.environ["QE_ENDPOINT"],
"vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
"vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
},
autocommit=True,
)
def server_message(e):
"""The server's message in a driver error, without the request ID or the echoed statement."""
text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
found = (re.search(r'message: "(.*?)", request_id', text, re.S)
or re.search(r"description: (.*)\)", text, re.S))
message = found.group(1) if found else text.strip().splitlines()[-1]
message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
return message.replace(BUCKET, "<bucket>")
def run(sql):
"""Run one statement. Print the rows it returns, or the error."""
try:
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall() if cur.description else []
except (adbc_driver_manager.Error, OSError) as e:
print(f"{type(e).__name__}: {server_message(e)}")
else:
print(rows)
def plan(sql):
"""Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
with conn.cursor() as cur:
cur.execute("EXPLAIN " + sql)
steps = cur.fetchall()[0][1]
print("\n".join(line for line in steps.splitlines() if "Rows" not in line))
TABLE = f"{SCHEMA}.orders"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, customer VARCHAR, qty BIGINT, amount DECIMAL(10, 2), ordered DATE)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 'Ada Lovelace', 3, 1234.50, DATE '2026-10-01'), "
"(2, 'Grace Hopper', 12, 89.99, DATE '2026-10-02'), "
"(3, 'José Martí', 1, 15.00, DATE '2026-10-05'), "
"(4, NULL, NULL, NULL, NULL)")
[]
[]
[(4,)]
What it does
printf copies template and replaces each conversion with the next value, from left to right. A conversion is %, optional flags, an optional width, an optional precision after a ., and a letter that says how to write the value. %% is a literal %.
Conversions
| Conversion | Meaning | Call | Result |
|---|---|---|---|
%s |
a string, written as it is | printf('[%s]', 'Hello') |
'[Hello]' |
%-6s %5s |
a string left-aligned in 6 characters, or right-aligned in 5 | printf('%-6s\|%5s\|', 'ab', 'cd') |
'ab \| cd\|' |
%.3s |
at most 3 characters of a string | printf('%.3s', 'abcdef') |
'abc' |
%d, %05d |
an integer; %05d pads it with zeros to 5 digits |
printf('%05d', 42) |
'00042' |
%i |
the same as %d |
printf('%i', 1) |
'1' |
%+d |
an integer with its sign, also when positive | printf('%+d', 5) |
'+5' |
%,d |
an integer with thousands separators | printf('%,d', 1234567) |
'1,234,567' |
%.2f |
fixed-point with 2 decimals | printf('%.2f', 3.14159) |
'3.14' |
%5.1f |
1 decimal, right-aligned in 5 characters | printf('%5.1f\|%-8.3e', 3.14159, 2.5) |
' 3.1\|2.500e+00' |
%e %-8.3e |
scientific notation; with 3 decimals, left-aligned in 8 | printf('%e', 12345.678) |
'1.234568e+04' |
%g |
a decimal in the shorter of fixed-point and scientific notation | printf('%g', 0.5) |
'0.5' |
%x %X %o |
hexadecimal in lower or upper case, and octal | printf('%x %X %o', 255, 255, 8) |
'ff FF 10' |
%04X |
upper-case hexadecimal, padded with zeros to 4 digits | printf('%04X', 233) |
'00E9' |
%c |
the character with that code point | printf('%c', 65) |
'A' |
%% |
a literal % |
printf('%%') |
'%' |
%1$s |
the value at a position, counting from 1 | printf('%1$s', 'a') |
'a' |
A width is a minimum: %-8.3e on 2.5 gives 2.500e+00, nine characters, with no padding.
Which values each conversion accepts
%s accepts strings, dates, booleans, lists and BLOB values, but not numbers. Write integers with %d and decimals with %f or %e. The number conversions don't mix: %f on an integer such as 42 fails with Invalid type specifier "f" for formatting a value of type int, and %d on a decimal such as 1.5 with Invalid type specifier "d" for formatting a value of type float:
| Value | printf('[%s]', value) |
|---|---|
'Hello' |
'[Hello]' |
DATE '2026-10-16' |
'[2026-10-16]' |
true |
'[true]' |
['a', 'b'] |
'[[a, b]]': list elements aren't quoted |
'\xAA'::BLOB |
'[\xAA]': a byte that isn't a printable ASCII character is written as \x and two hexadecimal digits |
123 |
Error: Invalid type specifier "s" for formatting a value of type int |
1.5 |
Error: Invalid type specifier "s" for formatting a value of type float |
%d on a string fails the same way: Invalid type specifier "d" for formatting a value of type string.
NULLs and edge cases
| Input | Call | Result |
|---|---|---|
template is NULL |
printf(NULL) |
NULL |
A NULL value |
printf('%s', NULL) |
NULL: the whole result, not an empty conversion |
'' as the value |
printf('[%s]', '') |
'[]' |
A date for %s |
printf('[%s]', DATE '2026-10-16') |
'[2026-10-16]' |
A boolean for %s |
printf('[%s]', true) |
'[true]' |
A list for %s |
printf('[%s]', ['a', 'b']) |
'[[a, b]]' |
A BLOB for %s |
printf('[%s]', '\xAA'::BLOB) |
'[\xAA]' |
| Fewer values than conversions | printf('%s %s', 'a') |
Error: Argument index "1" out of range |
An integer for %s |
printf('[%s]', 123) |
Error: Invalid type specifier "s" for formatting a value of type int |
A decimal for %s |
printf('[%s]', 1.5) |
Error: Invalid type specifier "s" for formatting a value of type float |
A string for %d |
printf('%d', 'x') |
Error: Invalid type specifier "d" for formatting a value of type string |
A NULL among several values also makes the whole result NULL: printf('%s %s', 'a', NULL) returns NULL.
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT printf(NULL), printf('%s', NULL), printf('[%s]', ''), printf('[%s]', DATE '2026-10-16'), printf('[%s]', true), printf('[%s]', ['a', 'b']), printf('[%s]', '\\xAA'::BLOB)")
run("SELECT printf('%s %s', 'a')")
run("SELECT printf('[%s]', 123)")
run("SELECT printf('[%s]', 1.5)")
run("SELECT printf('%d', 'x')")
[(None, None, '[]', '[2026-10-16]', '[true]', '[[a, b]]', '[\\xAA]')]
OSError: Argument index "1" out of range
OSError: Invalid type specifier "s" for formatting a value of type int
OSError: Invalid type specifier "s" for formatting a value of type float
OSError: Invalid type specifier "d" for formatting a value of type string
The conversions from the table above:
run("SELECT printf('%-6s|%5s|', 'ab', 'cd'), printf('%.3s', 'abcdef'), printf('%05d', 42), printf('%,d', 1234567)")
run("SELECT printf('%.2f', 3.14159), printf('%5.1f|%-8.3e', 3.14159, 2.5), printf('%e', 12345.678)")
run("SELECT printf('%x %X %o', 255, 255, 8), printf('%c', 65), printf('%%')")
[('ab | cd|', 'abc', '00042', '1,234,567')]
[('3.14', ' 3.1|2.500e+00', '1.234568e+04')]
[('ff FF 10', 'A', '%')]
Examples
A sentence for each order: %s for the name and the date, %d for the quantity. The row with no values gives NULL:
run(f"SELECT id, printf('%s ordered %d items on %s', customer, qty, ordered) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace ordered 3 items on 2026-10-01'), (2, 'Grace Hopper ordered 12 items on 2026-10-02'), (3, 'José Martí ordered 1 items on 2026-10-05'), (4, None)]
Aligned columns for a fixed-width report: the name left-aligned in 14 characters, the quantity right-aligned in 4, and the amount with two decimals, right-aligned in 10:
run(f"SELECT id, printf('%-14s|%4d|%10.2f', customer, qty, amount) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace | 3| 1234.50'), (2, 'Grace Hopper | 12| 89.99'), (3, 'José Martí | 1| 15.00'), (4, None)]
Find an order by its display code, an id padded with zeros to five digits:
run(f"SELECT id, customer FROM {TABLE} WHERE printf('ORD-%05d', id) = 'ORD-00002' ORDER BY id LIMIT 10")
[(2, 'Grace Hopper')]
How it runs in VAST
The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase can evaluate a comparison of a column with a constant, but not a function of a column, so printf() is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the columns and the Query Engine computesprintf()for each row it returns. - In
WHERE, the scan sends the columns for every row, and aFILTERstep in the Query Engine computesprintf()and keeps the rows that match. No rows are skipped in storage.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE printf('ORD-%05d', id) = 'ORD-00002'")
┌───────────────────────────┐
│ FILTER │
│ ──────────────────── │
│ (printf('ORD-%05d', id) = │
│ 'ORD-00002') │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
Make it faster
- Filter on the raw value, and format only for display.
printf('ORD-%05d', id) = 'ORD-00002'keeps the same row asid = 2, but only the second is a comparison of a raw column with a constant, which is pushed down to VAST DataBase, so rows that don't match never leave storage. Its plan has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE id = 2")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Combine it with a pushed-down condition. In
id > 100 AND printf('%s', customer) = '...',id > 100is still evaluated in VAST DataBase, soprintf()runs only on the rows that pass it.
Limitations and errors
%sdoesn't accept numbers. An integer fails withInvalid type specifier "s" for formatting a value of type int, and a decimal with... of type float. ADOUBLE, such as2.5::DOUBLE, fails the same way. Use%dfor integers and%ffor decimals, cast the number toVARCHAR, or use format, whose{}accepts any type.- The value must suit the conversion. A string for
%dfails withInvalid type specifier "d" for formatting a value of type string.%ddoesn't accept decimals (... of type float) and%fdoesn't accept integers (... of type int). Use the conversion that matches the value's type, or format with a plain{}. - One value per conversion. Fewer values than conversions fails with
Argument index "1" out of range: values count from 0, so index 1 is the second value. - A
NULLvalue makes the whole resultNULL. Replace it first with coalesce. sprintfisn't available. It fails withScalar Function with name sprintf does not exist!. Useprintf.
run("SELECT printf('%d', 123), printf('%s', CAST(123 AS VARCHAR))")
run("SELECT sprintf('%s', 'a')")
[('123', '123')]
InternalError: Catalog Error: Scalar Function with name sprintf does not exist!
Similar functions
| Function | Template | Builds 00042\|3.14\|x |
A NULL value |
|---|---|---|---|
printf |
C-style % codes |
printf('%05d\|%.2f\|%s', 42, 3.14159, 'x') |
the result is NULL |
| format | {} placeholders; {} accepts any type |
format('{:05d}\|{:.2f}\|{}', 42, 3.14159, 'x') |
the result is NULL |
| concat | none: joins its arguments | skipped: concat('a', NULL, 'b') is 'ab' |
|
| || (concatenation) | none: joins two values | the result is NULL |
|
| concat_ws | none: joins with a separator | skipped: concat_ws('-', 'a', NULL, 'b') is 'a-b' |
run("SELECT printf('%05d|%.2f|%s', 42, 3.14159, 'x'), format('{:05d}|{:.2f}|{}', 42, 3.14159, 'x')")
run("SELECT printf('%s', NULL), format('{}', NULL), concat('a', NULL, 'b'), 'a' || NULL || 'b', concat_ws('-', 'a', NULL, 'b')")
[('00042|3.14|x', '00042|3.14|x')]
[(None, None, 'ab', None, 'a-b')]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Text basics: all the text functions.
- format: the same job with
{}placeholders. - Where filters run and pushdowns.
- EXPLAIN: how to read a plan.