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.

starts_with

Prev Next

Applies to VAST Cluster 5.5.

Functions › Search and split › Searching. Returns true if a string begins with a given prefix. prefix and the ^@ operator are the same test, and the Query Engine rewrites LIKE 'ab%' to it.

Syntax

starts_with(s, prefix)
prefix(s, prefix)
s ^@ prefix
Argument Type Description
s VARCHAR The string to test.
prefix VARCHAR The text s must begin with.

Returns: BOOLEAN.

Aliases: prefix, ^@.

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_string_starts_with. 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 files. Rows 2 and 3 hold the same word, café, with the accent written two ways: as one character (U+00E9) and as e followed by a combining accent (U+0301). 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_string_starts_with"'

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}.files"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, path VARCHAR, note VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, '/logs/app/2026-10-13.log', 'Disk full'), "
    "(2, '/logs/db/2026-10-12.log', 'caf\u00e9 menu'), "
    "(3, '/tmp/load_50%.csv', 'cafe\u0301 menu'), "
    "(4, NULL, NULL)")
[]
[]
[(4,)]

What it does

starts_with(s, prefix) is true when the first characters of s are exactly prefix. The comparison is code point by code point:

  • Case matters. starts_with('Abc', 'a') is false. To ignore case, test lower(s) (lower) or use ILIKE 'ab%'.
  • Wildcards are plain characters. %, _ and * match only themselves: starts_with('a%c', 'a%') is true.
  • Accents must be written the same way. A precomposed é (U+00E9) and e followed by U+0301 look the same but are different code points, so starts_with('é', 'e\u0301') is false, while starts_with('e\u0301x', 'e') is true. Normalise the column with nfc_normalize when its text may be written either way.
  • An empty prefix matches every string, including the empty string.

NULLs and edge cases

Input Result
starts_with(NULL, 'He'), starts_with('abc', NULL) NULL
starts_with('abc', ''), starts_with('', '') true
starts_with('', 'a') false
starts_with('abc', 'abcd') (prefix longer than the string) false
starts_with('Abc', 'a') false
starts_with(' abc', 'a') (leading space) false
starts_with('a%c', 'a%'), starts_with('a_c', 'a_') true
starts_with('e\u0301x', 'e') (e and a combining accent) true
starts_with('é', 'e\u0301') (precomposed é, decomposed prefix) false
starts_with('🇫🇷x', '🇫') (half of a flag emoji) true
A number, date, boolean, BLOB or list on either side Error: No function matches the given name and argument types

The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:

run("SELECT starts_with(NULL, 'He'), starts_with('abc', NULL), starts_with('abc', ''), starts_with('', ''), "
    "starts_with('', 'a'), starts_with('abc', 'abcd')")
run("SELECT starts_with('Abc', 'a'), starts_with(' abc', 'a'), starts_with('a%c', 'a%'), starts_with('a_c', 'a_')")
run("SELECT starts_with('e\u0301x', 'e'), starts_with('\u00e9', 'e\u0301'), starts_with('\U0001F1EB\U0001F1F7x', '\U0001F1EB')")
run("SELECT starts_with(123, 'He')")
[(None, None, True, True, False, False)]
[(False, False, True, True)]
[(True, False, True)]
InternalError: Binder Error: No function matches the given name and argument types 'starts_with(INTEGER_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.

Examples

Test each path with all three spellings. The row with no path gives NULL:

run(f"SELECT id, starts_with(path, '/logs/'), prefix(path, '/tmp'), path ^@ '/logs/db' FROM {TABLE} ORDER BY id LIMIT 10")
[(1, True, False, False), (2, True, False, True), (3, False, True, False), (4, None, None, None)]

Keep the rows under /logs/:

run(f"SELECT id, path FROM {TABLE} WHERE starts_with(path, '/logs/') ORDER BY id LIMIT 10")
[(1, '/logs/app/2026-10-13.log'), (2, '/logs/db/2026-10-12.log')]

Rows 2 and 3 both start with café on screen, but only row 2 matches a precomposed prefix until the column is normalised:

run(f"SELECT id, starts_with(note, 'caf\u00e9'), starts_with(nfc_normalize(note), 'caf\u00e9') FROM {TABLE} "
    "ORDER BY id LIMIT 10")
[(1, False, False), (2, True, True), (3, False, True), (4, None, None)]

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 starts_with() is always computed by the Query Engine:

  • In the SELECT list, the scan reads the column and the Query Engine computes starts_with() for each row it returns.
  • In WHERE, the scan sends the column for every row, and a FILTER step in the Query Engine keeps the rows that match. No rows are skipped in storage.

LIKE 'ab%' and regexp_matches(s, '^ab') are rewritten to prefix(s, 'ab'), so they run the same way. The plan of the WHERE example shows the FILTER step above the scan:

plan(f"SELECT id FROM {TABLE} WHERE starts_with(path, '/logs/')")
plan(f"SELECT id FROM {TABLE} WHERE path LIKE '/logs/%'")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│starts_with(path, '/logs/')│
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            path           │
│             id            │
│                           │
└───────────────────────────┘
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│   prefix(path, '/logs/')  │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            path           │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Use a range instead of a prefix test. path >= '/logs/' AND path < '/logs0' selects the strings that start with /logs/, and it's pushed down to VAST DataBase, so rows that don't match never leave storage. The upper bound is the prefix with its last character replaced by the next one (/ is followed by 0). The plan has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE path >= '/logs/' AND path < '/logs0'")
run(f"SELECT id FROM {TABLE} WHERE path >= '/logs/' AND path < '/logs0' ORDER BY id LIMIT 10")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
[(1,), (2,)]
  • Combine it with a pushed-down condition. In id > 1 AND starts_with(path, '/logs/'), id > 1 is still evaluated in VAST DataBase, so starts_with() runs only on the rows that pass it.
  • Sort the table on the searched column. A range on a sorted column lets the scan skip whole blocks; see indexing and data skipping.

Limitations and errors

  • Strings only. A number, a date, a boolean, a BLOB or a list on either side fails with No function matches the given name and argument types, for example 'starts_with(INTEGER_LITERAL, STRING_LITERAL)'. Cast the value to VARCHAR first.
  • No startswith. The name without the underscore fails with Scalar Function with name startswith does not exist!. Use starts_with.
  • Exact code points. See What it does for case and accents.
run("SELECT starts_with(CAST(123 AS VARCHAR), '12')")
run("SELECT startswith('abc', 'a')")
[(True,)]
InternalError: Catalog Error: Scalar Function with name startswith does not exist!

Similar functions

Function Tests Where it runs in WHERE
starts_with, prefix, ^@ the beginning of the string FILTER step
LIKE 'He%' the same; rewritten to prefix() FILTER step
regexp_matches (s, '^He') the same; rewritten to prefix() FILTER step
s >= 'He' AND s < 'Hf' the same strings, as a range pushed down
ends_with the end of the string FILTER step
contains anywhere in the string FILTER step
strpos where the text first occurs (a position, not a boolean) FILTER step
run("SELECT starts_with('Hello World', 'He'), prefix('Hello World', 'He'), 'Hello World' ^@ 'He', "
    "'Hello World' LIKE 'He%', regexp_matches('Hello World', '^He'), "
    "'Hello World' >= 'He' AND 'Hello World' < 'Hf'")
run("SELECT ends_with('Hello World', 'ld'), contains('Hello World', 'o W'), strpos('Hello World', 'o')")
[(True, True, True, True, True, True)]
[(True, True, 5)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also