Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

md5_number

Prev Next

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 SELECT list, 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 a FILTER step 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 VARCHAR column, or 64 bits of it from md5_number_upper or md5_number_lower in a UBIGINT column. 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 lists md5_number(VARCHAR) -> HUGEINT and md5_number(BLOB) -> HUGEINT. Cast a number or a UUID to VARCHAR first.
  • Not the hex digits of md5. The bytes are in reverse order and the number is signed: see What it does. To compare with an md5() value, compare md5() results.
  • The halves don't add up to it. md5_number_upper(x) * 2^64 + md5_number_lower(x) is not md5_number(x): see md5_number_upper for which bytes each half holds.
  • Python reads it as Decimal. The ADBC driver returns a HUGEINT as DECIMAL(38, 0); convert it with int() 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