Applies to VAST Cluster 5.5.
Functions › Binary, hash and UUID functions › Hashes. Returns the MD5 digest of a string or of a BLOB as a number: a signed 128-bit HUGEINT. It's the same digest as md5 gives as text, read as an integer.
Syntax
md5_number(x)
| Argument | Type | Description |
|---|---|---|
x |
VARCHAR or BLOB |
The value to hash. |
Returns: HUGEINT, a signed 128-bit integer. The ADBC driver returns it as a DECIMAL(38, 0) value, which Python prints as Decimal.
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_binary_hash_uuid_md5_number. 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 documents, each with a name and its first bytes. 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_binary_hash_uuid_md5_number"'
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}.documents"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, name VARCHAR, data BLOB)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 'hello', '\\x25\\x50\\x44\\x46'::BLOB), "
"(2, '', ''::BLOB), "
"(3, 'a', '\\x61'::BLOB), "
"(4, NULL, NULL)")
[]
[]
[(4,)]
What it does
md5_number computes the same 16-byte MD5 digest as md5, and reads the bytes as one signed 128-bit integer, least significant byte first (little-endian). The bytes come out in the reverse order of the text form:
| Call | Result |
|---|---|
md5('abc') |
'900150983cd24fb0d6963f7d28e17f72' |
hex(md5_number('abc')) |
'727FE1287D3F96D6B04FD23C98500190': the same 16 bytes, last first |
So lower(hex(md5_number(x))) is not md5(x). The number is signed, so it can be negative: md5_number('hello') is -145191729247072235933599665601768308387, and md5_number('abc') is positive.
As with md5, a VARCHAR is hashed as its UTF-8 bytes, so md5_number('hello') and md5_number('hello'::BLOB) are equal.
NULLs and edge cases
| Input | Result |
|---|---|
NULL |
NULL |
'' (empty string) |
167830467844043968176572005485231480276, the digest of no bytes as a number |
md5_number('hello') = md5_number('hello'::BLOB) |
true |
md5_number('hello') < 0 |
true: the result can be negative |
lower(hex(md5_number('hello'))) = md5('hello') |
false: the byte order differs |
A number, date, boolean, UUID, BIT or HUGEINT |
Error: No function matches the given name and argument types |
The same cases, as the cluster returns them. They use the helpers from Set up the examples:
run("SELECT md5_number(NULL), md5_number(''), md5_number('hello') = md5_number('hello'::BLOB)")
run("SELECT md5_number('hello') < 0, lower(hex(md5_number('hello'))) = md5('hello')")
run("SELECT md5('abc'), hex(md5_number('abc'))")
run("SELECT md5_number(123)")
[(None, Decimal('167830467844043968176572005485231480276'), True)]
[(True, False)]
[('900150983cd24fb0d6963f7d28e17f72', '727FE1287D3F96D6B04FD23C98500190')]
InternalError: Binder Error: No function matches the given name and argument types 'md5_number(INTEGER_LITERAL)'. You might need to add explicit type casts.
Examples
The digest of each document's name and of its bytes, as numbers:
run(f"SELECT id, md5_number(name), md5_number(data) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, Decimal('-145191729247072235933599665601768308387'), Decimal('64091887192647840467345693085243909311')), (2, Decimal('167830467844043968176572005485231480276'), Decimal('167830467844043968176572005485231480276')), (3, Decimal('129134844867590561315626153795017621772'), Decimal('129134844867590561315626153795017621772')), (4, None, None)]
Keep the documents whose name has a negative digest. Of the three names, only 'hello' has one:
run(f"SELECT id, name FROM {TABLE} WHERE md5_number(name) < 0 ORDER BY id LIMIT 10")
[(1, 'hello')]
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 md5_number() of a column is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computes the digest for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine hashes each value 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 md5_number(name) < 0")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (md5_number(name) < 0) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ name │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Store a digest if you filter or look rows up by it. Compute it when you write the rows and keep it in its own column: the text of md5 in a
VARCHARcolumn, or 64 bits of it from md5_number_upper or md5_number_lower in aUBIGINTcolumn. A comparison of that column with a constant is evaluated in VAST DataBase.
Limitations and errors
- Strings and bytes only. Other types fail with
No function matches the given name and argument types, and the error listsmd5_number(VARCHAR) -> HUGEINTandmd5_number(BLOB) -> HUGEINT. Cast a number or aUUIDtoVARCHARfirst. - Not the hex digits of
md5. The bytes are in reverse order and the number is signed: see What it does. To compare with anmd5()value, comparemd5()results. - The halves don't add up to it.
md5_number_upper(x) * 2^64 + md5_number_lower(x)is notmd5_number(x): see md5_number_upper for which bytes each half holds. - Python reads it as
Decimal. The ADBC driver returns aHUGEINTasDECIMAL(38, 0); convert it withint()in Python if you need an integer.
run("SELECT md5_number(DATE '2026-10-16')")
run("SELECT md5_number_upper('abc')::HUGEINT * 18446744073709551616 + md5_number_lower('abc')::HUGEINT = md5_number('abc')")
InternalError: Binder Error: No function matches the given name and argument types 'md5_number(DATE)'. You might need to add explicit type casts.
[(False,)]
Similar functions
| Function | Result for 'abc' |
Type |
|---|---|---|
| md5 | '900150983cd24fb0d6963f7d28e17f72' |
VARCHAR |
md5_number |
152195979970564155685860391459828531600 |
HUGEINT |
| md5_number_upper | 12704604231530709392: the digest's first 8 bytes, little-endian |
UBIGINT |
| md5_number_lower | 8250560606382298838: the digest's last 8 bytes, little-endian |
UBIGINT |
| hash | a 64-bit hash, not a standard digest | UBIGINT |
run("SELECT md5('abc'), md5_number('abc'), md5_number_upper('abc'), md5_number_lower('abc')")
run("SELECT typeof(md5_number('a')), typeof(md5_number_upper('a')), typeof(hash('a'))")
[('900150983cd24fb0d6963f7d28e17f72', Decimal('152195979970564155685860391459828531600'), 12704604231530709392, 8250560606382298838)]
[('HUGEINT', 'UBIGINT', 'UBIGINT')]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Binary, hash and UUID functions: all the functions in this family.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.