Skip to main content
Unkey Analytics stores verification events across multiple time-series tables for efficient querying. This reference documents all available tables and their columns.
Use aggregated tables (per_hour, per_day, per_month) for queries spanning long time periods to improve performance.

Raw Events Table

The key_verifications_v1 table contains individual verification events as they occur.

Columns

ColumnTypeDescription
request_idStringUnique identifier for each verification request
timeInt64Unix timestamp in milliseconds when verification occurred
workspace_idStringWorkspace identifier (automatically filtered to your workspace)
key_space_idStringYour KeySpace identifier (e.g., ks_1234) - use this to filter by API. Find this in your API settings.
external_idStringYour user’s identifier (e.g., user_abc) - use this to filter by user
key_idStringIndividual API key identifier
outcomeStringVerification result (see Outcome Values)
regionStringUnkey region that handled the verification
tagsArray(String)Custom tags added during verification
spent_creditsInt64Number of credits spent on this verification (0 if no credits were spent)

Outcome Values

The outcome column contains one of these values:
OutcomeDescription
VALIDKey is valid and verification succeeded
RATE_LIMITEDVerification exceeded rate limit
INVALIDKey not found or malformed
EXPIREDKey has expired
DISABLEDKey is disabled
INSUFFICIENT_PERMISSIONSKey lacks required permissions
FORBIDDENOperation not allowed for this key
USAGE_EXCEEDEDKey has exceeded usage limit

Aggregated Tables

Pre-aggregated tables provide better query performance for long time ranges. Each aggregated table includes outcome counts.

Per Minute Table

key_verifications_per_minute_v1 - Aggregated by minute
ColumnTypeDescription
timeDateTimeTimestamp (DateTime for minute/hour, Date for day/month)
workspace_idStringWorkspace identifier
key_space_idStringAPI identifier
external_idStringYour user identifier
key_idStringAPI key identifier
outcomeStringVerification outcome (VALID, RATE_LIMITED, INVALID, etc.)
tagsArrayTags associated with verifications
countUInt64Total verification count for this aggregation
spent_creditsUInt64Total credits spent

Per Hour Table

key_verifications_per_hour_v1 - Aggregated by hour. Same columns as per-minute table.

Per Day Table

key_verifications_per_day_v1 - Aggregated by day. Same columns as per-minute table.

Per Month Table

key_verifications_per_month_v1 - Aggregated by month. Same columns as per-minute table.

Filtering by API and User

You can use your familiar identifiers directly in queries:
  • key_space_id - Your API identifier (e.g., ks_1234). Find this in your API settings.
  • external_id - Your user identifiers (e.g., user_abc123) from your application
All standard comparison operators are supported: =, !=, <, >, <=, >=, IN, NOT IN

Filter by API

SELECT COUNT(*) FROM key_verifications_v1
WHERE key_space_id = 'ks_1234'

Filter by User

SELECT COUNT(*) FROM key_verifications_v1
WHERE external_id = 'user_abc123'

Multiple Values

SELECT COUNT(*) FROM key_verifications_v1
WHERE key_space_id IN ('ks_1234', 'ks_5678')
  AND external_id IN ('user_abc', 'user_xyz')

Working with Tags

Tags are stored as Array(String) and require array functions to query.

Check if tag exists

SELECT COUNT(*) FROM key_verifications_v1
WHERE has(tags, 'path=/api/users')

Check if any tag exists

SELECT COUNT(*) FROM key_verifications_v1
WHERE hasAny(tags, ['environment=prod', 'environment=staging'])

Check if all tags exist

SELECT COUNT(*) FROM key_verifications_v1
WHERE hasAll(tags, ['environment=production', 'team=backend'])

Extract and group by tags

SELECT
  arrayJoin(tags) as tag,
  COUNT(*) as count
FROM key_verifications_v1
WHERE time >= now() - INTERVAL 24 HOUR
GROUP BY tag
ORDER BY count DESC

Filter tags with pattern

-- Get all tags starting with "path="
SELECT
  arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as path,
  COUNT(*) as requests
FROM key_verifications_v1
WHERE time >= now() - INTERVAL 24 HOUR
GROUP BY path

Time Functions

Timestamps are stored differently depending on the table:
  • Raw table (key_verifications_v1): time is Int64 (Unix milliseconds)
  • Aggregated tables: time is DateTime

Current Time

SELECT now() as current_datetime
SELECT toUnixTimestamp(now()) * 1000 as current_millis

Time Ranges (Raw Table)

For the raw key_verifications_v1 table, compare time with millisecond timestamps:
-- Last hour
WHERE time >= toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000

-- Last 24 hours
WHERE time >= toUnixTimestamp(now() - INTERVAL 24 HOUR) * 1000

-- Last 7 days
WHERE time >= toUnixTimestamp(now() - INTERVAL 7 DAY) * 1000

-- Last 30 days
WHERE time >= toUnixTimestamp(now() - INTERVAL 30 DAY) * 1000

-- This month
WHERE time >= toUnixTimestamp(toStartOfMonth(now())) * 1000

-- Today
WHERE time >= toUnixTimestamp(toStartOfDay(now())) * 1000

Time Ranges (Aggregated Tables)

For aggregated tables, use DateTime comparisons directly:
-- Last 7 days
WHERE time >= now() - INTERVAL 7 DAY

-- This month
WHERE time >= toStartOfMonth(now())

-- Today
WHERE time >= toStartOfDay(now())

Time Rounding (Raw Table)

-- Round to start of hour
SELECT toStartOfHour(toDateTime(time / 1000)) as hour

-- Round to start of day
SELECT toStartOfDay(toDateTime(time / 1000)) as day

-- Round to start of month
SELECT toStartOfMonth(toDateTime(time / 1000)) as month

-- Convert to date
SELECT toDate(toDateTime(time / 1000)) as date

Specific Date Ranges

-- Between specific dates (Unix milliseconds)
WHERE time >= 1704067200000  -- Jan 1, 2024 00:00:00 UTC
  AND time < 1735689600000   -- Jan 1, 2025 00:00:00 UTC

Common ClickHouse Functions

Aggregate Functions

FunctionDescriptionExample
COUNT()Count rowsSELECT COUNT(*) FROM key_verifications_v1
SUM()Sum valuesSELECT SUM(valid_count) FROM key_verifications_per_day_v1
AVG()AverageSELECT AVG(spent_credits) FROM key_verifications_v1
MIN()Minimum valueSELECT MIN(time) FROM key_verifications_v1
MAX()Maximum valueSELECT MAX(time) FROM key_verifications_v1
countIf()Conditional countSELECT countIf(outcome = 'VALID')
uniq()Count distinctSELECT uniq(key_id) FROM key_verifications_v1

String Functions

FunctionDescriptionExample
lower()Convert to lowercaseWHERE lower(outcome) = 'valid'
upper()Convert to uppercaseWHERE upper(region) = 'US-EAST-1'
concat()Concatenate stringsSELECT concat(region, '-', outcome)
substring()Extract substringSELECT substring(key_id, 1, 8)
startsWith()Check prefixWHERE startsWith(key_id, 'key_')

Array Functions

FunctionDescriptionExample
has()Check elementWHERE has(tags, 'environment=production')
hasAny()Check any elementWHERE hasAny(tags, ['team=backend', 'team=api'])
hasAll()Check all elementsWHERE hasAll(tags, ['environment=prod', 'tier=1'])
arrayJoin()Expand arraySELECT arrayJoin(tags) as tag
arrayFilter()Filter arrayarrayFilter(x -> startsWith(x, 'path='), tags)
length()Array lengthWHERE length(tags) > 0

Math Functions

FunctionDescriptionExample
round()Round numberSELECT round(AVG(spent_credits), 2)
floor()Round downSELECT floor(spent_credits / 100) * 100 as credit_bucket
ceil()Round upSELECT ceil(spent_credits)
abs()Absolute valueSELECT abs(difference)

Conditional Functions

FunctionDescriptionExample
if()If-then-elseSELECT if(outcome = 'VALID', 1, 0)
CASEMulti-conditionCASE WHEN outcome = 'VALID' THEN 'success' ELSE 'failure' END

Performance Tips

  1. Always filter by time - Use time-based WHERE clauses to leverage indexes
  2. Use aggregated tables - Query hourly/daily/month tables for long ranges
  3. Limit result sets - Add LIMIT clauses to prevent large results
  4. Filter before grouping - Use WHERE instead of HAVING when possible
  5. Avoid SELECT * - Only select columns you need

Query Limits

ResourceLimitError Code
Execution time30 secondsquery_execution_timeout
Memory usage1 GBquery_memory_limit_exceeded
Rows to read10 millionquery_rows_limit_exceeded
Queries per hour1000query_quota_exceeded
See Query Restrictions for more details on query limits and restrictions.

Next Steps