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.

atan

Prev Next

Applies to VAST Cluster 5.5.

Functions › Numeric and math functions › Inverse trigonometry. Returns the arc tangent of a number: the angle, in radians from -pi/2 to pi/2, whose tangent is that number. It takes any number, including infinity. To get the angle of a point, with the right quadrant, use atan2.

Syntax

atan(x)
Argument Type Description
x Any number (DOUBLE, REAL, an integer or a DECIMAL) The tangent: any value, including inf and -inf.

Returns: DOUBLE, from -pi/2 to pi/2.

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_atan. 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 six road segments with their gradient (rise over run). Row 3 has no value:

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_atan"'

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}.segments"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, gradient DOUBLE)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, 0.05), (2, -0.12), (3, NULL), (4, 0.0), (5, 1.0), (6, 2.5)")
[]
[]
[(6,)]

What it does

atan(x) is the inverse of tan on the range -pi/2 to pi/2: it returns the angle a, in radians, for which tan(a) = x. It takes any number. As x grows, the result approaches pi/2, and atan of infinity is pi/2 (1.5707963267948966). To get degrees, wrap it in degrees.

Call Result
atan(0) 0.0
atan(1) 0.7853981633974483 (pi/4)
atan(-1) -0.7853981633974483
atan(1e308) 1.5707963267948966 (pi/2)
atan(-1e308) -1.5707963267948966

The result is a DOUBLE for every numeric argument type: an integer, a REAL, a DECIMAL or a DOUBLE.

run("SELECT atan(0), atan(1), atan(-1), atan(1e308), atan(-1e308)")
run("SELECT typeof(atan(1::INTEGER)), typeof(atan(0.5::FLOAT)), typeof(atan(0.5))")
[(0.0, 0.7853981633974483, -0.7853981633974483, 1.5707963267948966, -1.5707963267948966)]
[('DOUBLE', 'DOUBLE', 'DOUBLE')]

NULLs and edge cases

Input Result
NULL NULL
'inf'::DOUBLE 1.5707963267948966 (pi/2)
'-inf'::DOUBLE -1.5707963267948966
'nan'::DOUBLE nan
-0.0::DOUBLE -0.0: the sign of zero is kept
The smallest positive DOUBLE (5e-324) 5e-324
'12' (a string literal holding a number) Converted to DOUBLE: 1.4876550949064553
'abc' (a string that isn't a number) Error: Could not convert string 'abc' to DOUBLE
A BLOB, date, boolean, list or interval Error: No function matches the given name and argument types, for example 'atan(DATE)'

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

run("SELECT atan(NULL), atan('inf'::DOUBLE), atan('-inf'::DOUBLE), atan('nan'::DOUBLE)")
run("SELECT atan(-0.0::DOUBLE), atan(5e-324::DOUBLE), atan('12')")
run("SELECT atan('abc')")
run("SELECT atan(DATE '2026-10-16')")
[(None, 1.5707963267948966, -1.5707963267948966, nan)]
[(-0.0, 5e-324, 1.4876550949064553)]
OSError: Could not convert string 'abc' to DOUBLE
InternalError: Binder Error: No function matches the given name and argument types 'atan(DATE)'. You might need to add explicit type casts.

Examples

The angle of each segment, in radians and in degrees. A gradient of 1 (one metre up for each metre along) is 45 degrees:

run(f"SELECT id, gradient, atan(gradient), degrees(atan(gradient)) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 0.05, 0.049958395721942765, 2.862405226111748), (2, -0.12, -0.11942892601833845, -6.84277341263094), (3, None, None, None), (4, 0.0, 0.0, 0.0), (5, 1.0, 0.7853981633974483, 45.0), (6, 2.5, 1.1902899496825317, 68.19859051364818)]

Keep only the segments steeper than 30 degrees:

run(f"SELECT id, gradient FROM {TABLE} WHERE degrees(atan(gradient)) > 30 ORDER BY id LIMIT 10")
[(5, 1.0), (6, 2.5)]

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

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

Make it faster

  • Compare the raw column with the tangent of the angle. atan() grows as its argument grows, so degrees(atan(gradient)) > 30 is the same condition as gradient > tan(radians(30)). A function on constants is computed once, before the scan, so the second form is a comparison of the column with a constant, which is pushed down to VAST DataBase. Its plan has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE gradient > tan(radians(30))")
run(f"SELECT id, gradient FROM {TABLE} WHERE gradient > tan(radians(30)) ORDER BY id LIMIT 10")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
[(5, 1.0), (6, 2.5)]
  • Store the angle if you filter on it often. Compute it when you write the rows and keep it in its own column: a condition on that column is pushed down, a condition on atan() isn't.

Limitations and errors

  • One argument: no quadrant. atan(y / x) can't tell the point (1, 1) from (-1, -1), because both ratios are 1. Use atan2(y, x), which takes the two coordinates separately:
run("SELECT atan(-1 / -1), atan2(-1, -1), atan(1 / -1), atan2(1, -1)")
[(0.7853981633974483, -2.356194490192345, -0.7853981633974483, 2.356194490192345)]
  • Numbers only. A BLOB, a date, a boolean, a list or an interval fails with No function matches the given name and argument types; the error lists the one form atan has, atan(DOUBLE) -> DOUBLE. A string literal is converted to DOUBLE if it holds a number, and otherwise fails with Could not convert string 'abc' to DOUBLE.
  • Two arguments aren't accepted. atan(1, 2) fails with No function matches the given name and argument types 'atan(INTEGER_LITERAL, INTEGER_LITERAL)'; the two-argument form is atan2.
  • Radians only. There's no degrees variant; use degrees(atan(x)).

Similar functions

Function Returns Result for 0.5
atan(x) the angle whose tangent is x, from -pi/2 to pi/2 0.4636476090008061
atan2 the angle of the point (x, y), from -pi to pi atan2(0.5, 1) and atan2(1, 2): 0.4636476090008061
asin the angle whose sine is x 0.5235987755982989
acos the angle whose cosine is x 1.0471975511965979
tan the tangent of an angle: the inverse of atan tan(atan(0.5)) gives back about 0.5
atanh the inverse hyperbolic tangent, for -1 to 1
pi pi; pi() = 4 * atan(1) is true
run("SELECT atan(0.5), atan2(0.5, 1), atan2(1, 2), asin(0.5), acos(0.5), tan(atan(0.5)), atanh(0.5)")
run("SELECT pi() = 4 * atan(1)")
[(0.4636476090008061, 0.4636476090008061, 0.4636476090008061, 0.5235987755982989, 1.0471975511965979, 0.49999999999999994, 0.5493061443340548)]
[(True,)]

Clean up:

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

See also