> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockdb.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Time tables

> Time Table: a series table bucketed by physical time, with built-in dedup and storage compaction.

# Time tables

Time Table is one of BlockDB's business abstraction tables. It describes **a state series that changes with real-world (wall-clock) time**, for example a token's USD price or per-minute TVL snapshots. It sits alongside Block State / Block Event tables (which are block-oriented) and answers the question "how does this object change over time?"

## Business scenarios

Time Table fits any metric that changes with physical time and is unrelated to block height:

* A token's USD price every minute.
* TVL snapshots per minute or per hour.
* A user's holdings or holdings value curve over a period.

## Table structure

A Time Table has a fixed structure with three columns:

| Column | Logical type | Description |
| - | - | - |
| `id` | Character logical type | Business primary key. After creation you can only change its type (choose from `STRING` / `HASH` / `ADDRESS` / `TOKENID` / `TXID` / `BLOCKID` / `CHAINID`) |
| `time_at` | `TIMESTAMP` | Time bucket, created automatically |
| `value` | `FLOAT` | State value, created automatically |

Underneath, two shadow tables are derived: the **main table** (holds each object's latest bucket) and **`_archive`** (holds historical buckets, identified by `id` + `time_at`).

## Time bucketing rules

Time Table records state at **time bucket** granularity rather than storing every raw write:

* **The smallest time bucket is fixed at 60 seconds.** The `time_at` you write is aligned by **rounding up with seconds zeroed** — `17:04:35` aligns to `17:05:00`; if the seconds are already 0 (for example `17:04:00`) it is already aligned and left unchanged.
* **Only one point is kept per object per 60 seconds.**
* **The further back in time, the coarser the granularity**: older data is compacted into larger time buckets (for example 1 minute within 3 days, and 1 hour / 1 day further back), so historical data becomes progressively sparser.

When reading, you get the **most recent recorded bucket** with `time_at ≤ your query timestamp`. The further back you go the sparser it is, but you always get the nearest valid state.

## Built-in dedup

You **do not** need to dedup on the business side, and you do not need to worry about repeated writes bloating storage:

* **Only one value is kept per object per time bucket**: writing repeatedly to the same `id + time_at` either overwrites the previous value or is skipped; no duplicate rows are produced.
* **The write API compacts in real time**: if the value change between two adjacent points is below a fixed threshold (currently **0.1%**), the row is dropped automatically and never lands in storage.
* So your business can hand everything to the Time Table as it comes, and BlockDB decides which points are worth recording.

## Offline compaction: automatic storage optimization

Beyond real-time dedup at write time, BlockDB also **compacts historical data offline automatically**:

* The compaction rules match real-time compaction, continuously removing redundant points.
* When the time granularity coarsens, the point with the largest `time_at` in the interval is kept.
* Local redundancy produced by high-concurrency writes or out-of-order bulk imports is cleaned up gradually by background tasks.

So as time passes, historical data is pruned automatically and storage stays under control — the further back, the more compact. **You never need to design your own downsampling logic for "how often to keep a point".**

## Read and write interfaces

| Interface | Description |
| - | - |
| `UpsertTimeRows(table_id, time_at, rows)` | Batch write into a time bucket. `time_at` is automatically rounded up to the whole-minute bucket; a `time_at` field inside a row is ignored in favor of the one passed to the interface |
| `GetValue(table_id, row_id, time_at)` | Read an object's state at a time bucket, returning the most recent recorded bucket with `time_at ≤ your query time` |

## Example: a token USD price table

Store a token's USD price every minute:

```python theme={null}
from blockdb import TimeTable, Table

ID = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"   # a token

tt = TimeTable("token_time_demo")

# Write: into a time bucket (17:04:35 is aligned to the 17:05:00 bucket)
tt.upsert_time_rows([{"id": ID, "value": 2543.12}], "2026-05-18 17:04:35")

# Read: the latest bucket's value (reads the main table)
latest = Table("token_time_demo").get_row(ID)

# Read: the value at a point in time (bucket-aligned, most recent bucket with time_at ≤ anchor)
row = tt.get_value(ID, "2026-05-18 17:04:40")
```

## Summary

* Time Table manages series data at **time bucket** granularity, minimum 60 seconds, with writes rounded up automatically.
* **Built-in dedup**: one point per bucket, and adjacent values changing by less than 0.1% are dropped automatically, so repeated writes are never a concern.
* **Offline compaction**: historical data is pruned automatically and storage keeps improving — the further back, the more compact.
* Use `UpsertTimeRows` / `GetValue` for time-oriented reads and writes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.