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.

sign

Prev Next

Applies to VAST Cluster 5.5.

Functions › Numeric and math functions › Sign and rounding. Returns -1 for a negative number, 0 for zero and 1 for a positive number, as a TINYINT.

Syntax

sign(x)
Argument Type Description
x any number: the signed and unsigned integer types, DECIMAL, FLOAT or DOUBLE The value to test.

Returns: TINYINT: -1, 0 or 1.

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_numeric_sign. 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 five price changes: a change (DOUBLE), a change in quantity (INTEGER) and an amount (DECIMAL). Row 4 holds a NaN change, and 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_numeric_sign"'

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}.changes"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, delta DOUBLE, qty INTEGER, amount DECIMAL(10, 2))")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, 2.5, 12, 2.45), "
    "(2, -1.25, -18, -1.35), "
    "(3, 0.0, 0, 0.00), "
    "(4, 'nan'::DOUBLE, 5, 1.00), "
    "(5, NULL, NULL, NULL)")
[]
[]
[(5,)]

What it does

sign(x) returns -1 when x is less than zero, 0 when it's zero, and 1 when it's greater than zero. The result is always a TINYINT, whatever the type of x: sign(-2.45) is the TINYINT -1.

On DOUBLE and FLOAT values:

  • sign('inf'::DOUBLE) is 1 and sign('-inf'::DOUBLE) is -1.
  • A negative zero is zero: sign(-0.0::DOUBLE) is 0. To tell -0.0 from 0.0, use signbit.
  • NaN gives 0. sign('nan'::DOUBLE) is 0, so sign(x) = 0 doesn't mean x is zero. Test for NaN with isnan.
  • The smallest values count: sign(5e-324::DOUBLE) is 1 and sign(-1e-300) is -1.

NULLs and edge cases

Input Result
x is NULL NULL
sign(-0.0::DOUBLE) 0
sign('nan'::DOUBLE) 0
sign('inf'::DOUBLE), sign('-inf'::DOUBLE) 1, -1
sign(5e-324::DOUBLE) 1
sign(-2.45) -1, a TINYINT
sign(0::UTINYINT) 0
sign(-9223372036854775808::BIGINT) Error: the cast comes before the minus, and 9223372036854775808 doesn't fit a BIGINT
A string, even '12' Error: Could not choose a best candidate function for the function call "sign(STRING_LITERAL)"
A date, boolean, BLOB, list or interval 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 sign(NULL), sign(-0.0::DOUBLE), sign('nan'::DOUBLE), sign('inf'::DOUBLE), sign('-inf'::DOUBLE)")
run("SELECT sign(5e-324::DOUBLE), sign(-2.45), typeof(sign(-2.45)), sign(0::UTINYINT)")
run("SELECT sign(-9223372036854775808::BIGINT)")
run("SELECT sign('12')")
run("SELECT sign(true)")
[(None, 0, 0, 1, -1)]
[(1, -1, 'TINYINT', 0)]
OSError: Type INT128 with value 9223372036854775808 can't be cast because the value is out of range for the destination type INT64
InternalError: Binder Error: Could not choose a best candidate function for the function call "sign(STRING_LITERAL)". In order to select one, please add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'sign(BOOLEAN)'. You might need to add explicit type casts.

Examples

The sign of each column. Row 4's NaN change gives 0, the same as row 3's zero:

run(f"SELECT id, sign(delta), sign(qty), sign(amount) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 1, 1, 1), (2, -1, -1, -1), (3, 0, 0, 0), (4, 0, 1, 1), (5, None, None, None)]

Label each change as up, down or flat, and tell NaN apart with isnan():

run(f"SELECT id, CASE sign(delta) WHEN 1 THEN 'up' WHEN -1 THEN 'down' ELSE 'flat' END, isnan(delta) "
    f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'up', False), (2, 'down', False), (3, 'flat', False), (4, 'flat', True), (5, 'flat', None)]

Keep the falling prices:

run(f"SELECT id, delta FROM {TABLE} WHERE sign(delta) = -1 ORDER BY id LIMIT 10")
[(2, -1.25)]

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

  • In the SELECT list, the scan reads the column and the Query Engine computes sign() for each row it returns.
  • In WHERE, the scan sends the column for every row, and a FILTER step in the Query Engine computes sign() 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 sign(delta) = -1")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│     (sign(delta) = -1)    │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│           delta           │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Compare the raw column with zero. sign(delta) = -1 is the same as delta < 0, and that form is pushed down to VAST DataBase, so rows that don't match never leave storage. Its plan has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE delta < 0")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
  • Use sign() in the SELECT list, where it runs only on the rows the scan returns.

Limitations and errors

  • NaN is reported as 0. Check isnan(x) when a DOUBLE or FLOAT column can hold NaN. See What it does.
  • A cast binds more tightly than the minus sign. sign(-9223372036854775808::BIGINT) casts 9223372036854775808 before the minus, and fails with Type INT128 with value 9223372036854775808 can't be cast because the value is out of range for the destination type INT64. Put the negative number in parentheses before the cast.
  • Strings aren't converted. sign('12') fails with Could not choose a best candidate function. Cast the string to a number first.
  • Numbers only. A date, a boolean, a BLOB, a list or an interval fails with No function matches the given name and argument types.
  • No signum or sgn. Both fail with Scalar Function with name ... does not exist!. Use sign.
run("SELECT signum(-2)")
run("SELECT sign(-2), sign(CAST('-12' AS INTEGER))")
InternalError: Catalog Error: Scalar Function with name signum does not exist!
[(-1, -1)]

Similar functions

Function Returns Result for -5 Result for -0.0::DOUBLE
sign(x) -1, 0 or 1, as a TINYINT -1 0
signbit whether the sign bit is set, as a BOOLEAN true true
abs the value without its sign 5 0.0
isnan whether a float is NaN false false
run("SELECT sign(-5), signbit(-5), abs(-5), sign(-0.0::DOUBLE), signbit(-0.0::DOUBLE), abs(-0.0::DOUBLE)")
run("SELECT isnan(-5), isnan(-0.0::DOUBLE), sign('nan'::DOUBLE), isnan('nan'::DOUBLE)")
[(-1, True, 5, 0, True, 0.0)]
[(False, False, 0, True)]

Clean up:

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

See also