# Partitions, SORT BY and DEDUP KEY
> How PARTITION BY, SORT BY and DEDUP KEY of a time-series table change where rows go, their order, and what happens to duplicates; DROP PARTITION deletes old data fast.
- Category: Time-series
- Keywords: partition, partitions, PARTITION BY, DAY, MONTH, YEAR, NONE, SYMBOL(col), KEY(col), partition by symbol, symbol partitions, DROP PARTITION OLDER THAN, OLDER THAN, TRUNCATE, keyless table, batch dedup, KDB\_NO\_BATCH\_DEDUP, how loads find duplicates, SORT BY, SORT, sort order, row order, DEDUP KEY, DEDUP, KEY, dedup, deduplication, duplicates, duplicate rows, upsert, INSERT OR REPLACE, INSERT OR IGNORE, DROP PARTITION, delete old data, retention, purge, files, key range, TIMESERIES, time-series table, QuestDB, DROP PARTITION LIST, dedup, skip dedup, DEDUP TABLE, repair duplicates, dedup\_insert, WITH (dedup\_insert, plain INSERT upserts, DEDUP UPSERT, compression, WITH (compression, SET (compression, COMPRESS TABLE, packed, pack, compress, bytes a row, disk size, sealed partition, tail files, tail\_files, KDB\_TAIL\_FILES, --tail-files, append, appends, checkpoint, rewrite, delta files, KDB\_NO\_PARTITION\_DELTA, partition\_deltas, small update, small replace, trash growth
- See also: [CREATE TABLE](create-table.md), [ALTER, RENAME and DROP TABLE](alter-table.md), [INSERT](insert.md), [UPDATE and DELETE](update-delete.md), [Time and time zones (storage)](time-and-time-zones.md#storage), [EXPLAIN (explain)](explain.md#explain), [Performance tips](performance-tips.md)
A time-series table (`CREATE TABLE ... TIMESERIES(time)`) has three extra options. This page shows what they **do**. The full `CREATE TABLE` and `ALTER TABLE` grammar is in [create-table](create-table.md) and [alter-table](alter-table.md).
| Option | What it decides | Default |
|---|---|---|
| [PARTITION BY](partitions-and-dedup.md#partition-by) | which **file** a row goes into: one file per day, month or year, and with [SYMBOL(col)](partitions-and-dedup.md#partition-by-symbol) one per symbol too | `DAY` |
| [SORT BY](partitions-and-dedup.md#sort-by) | the **order** of rows inside each file | the first `SYMBOL` column, then the time column |
| [DEDUP KEY](partitions-and-dedup.md#dedup-key) | when two rows are **the same row** (a duplicate) | none: every row is kept |
`PARTITION BY` and `SORT BY` cannot be changed after the table is made. Choose them when you create the table. The `DEDUP KEY` can be set or taken off later with [ALTER TABLE ... DEDUP ENABLE](alter-table.md#dedup-enable), which rewrites the rows.
**Example: The sample market tables and their partitions** (sample data: sample)
```sql
SHOW TABLES;
```
Result:
| name (STRING) | kind (STRING) | partition (STRING) | columns (INT) | rows (LONG) |
|---|---|---|---|---|
| brokers | relational | NONE | 2 | 5 |
| live_data | timeseries | DAY | 12 | 12 |
| ochl_day | timeseries | YEAR | 7 | 57 |
| ochl_minutes | timeseries | MONTH | 7 | 956 |
| securities | relational | NONE | 3 | 5 |
| trade_log | timeseries | DAY | 8 | 32 |
(6 rows)
## PARTITION BY
Syntax:
```sql
CREATE TABLE name (...) TIMESERIES(time_column)
PARTITION BY DAY | MONTH | YEAR | NONE
```
A **partition** is one part of the table on disk. KDB puts each row into the partition of its time: with `PARTITION BY MONTH`, all rows of September 2026 go into the partition `2026-09`.
| PARTITION BY | Partition names | Good for |
|---|---|---|
| `DAY` (default) | `'2026-09-10'` | trades, live price snapshots (many rows per day) |
| `MONTH` | `'2026-09'` | one-minute candles |
| `YEAR` | `'2026'` | daily candles, yearly reports |
| `NONE` | (one part only) | small tables |
- **Notes:**
- Partitions follow the **UTC** date. `'2026-09-10T02:00:00+05:45'` (2 AM in Nepal) is still 9 September in UTC, so it goes into partition `2026-09-09`.
- A query with a time condition on the first table (`time IN '2026-09'`, `>=`, `<`, `BETWEEN`) reads **only** the partitions in that range. `EXPLAIN` shows it as `1 of 2 partitions`.
- A condition without the time column (`close > 500`) reads every partition.
- A partition is made when its first row arrives. Old partitions can be deleted fast with [DROP PARTITION](partitions-and-dedup.md#drop-partition).
- Too small partitions (`DAY` for 50 rows a day) make many small files; too big ones make time filters less useful. See the table above.
- Only time-series tables have partitions. `PARTITION BY` on a relational table is an error.
- A table that is nearly always read one symbol at a time can be split by symbol too: [PARTITION BY ..., SYMBOL(col)](partitions-and-dedup.md#partition-by-symbol).
**Example: A condition on time skips whole partitions** (sample data: sample)
```sql
EXPLAIN SELECT symbol, close FROM ochl_day WHERE time IN '2025-12';
EXPLAIN SELECT symbol, close FROM ochl_day WHERE close > 500;
```
Result:
`EXPLAIN SELECT symbol, close FROM ochl_day WHERE time IN '2025-12'`
| plan (STRING) |
|---|
| scan table ochl_day (1 of 2 partitions, about 57 rows) |
| build 3 of 7 stored column(s) per row: time, symbol, close |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
`EXPLAIN SELECT symbol, close FROM ochl_day WHERE close > 500`
| plan (STRING) |
|---|
| scan table ochl_day (2 of 2 partitions, about 57 rows) |
| build 3 of 7 stored column(s) per row: time, symbol, close |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
**Example: Month partitions: rows of three months make three partitions** (sample data: sample)
```sql
CREATE TABLE prices (time TIMESTAMP, symbol SYMBOL, close DOUBLE)
TIMESERIES(time) PARTITION BY MONTH;
INSERT INTO prices VALUES
('2026-07-31T11:00:00+05:45', 'CITY', 511.0),
('2026-08-30T11:00:00+05:45', 'CITY', 507.3),
('2026-09-10T11:00:00+05:45', 'CITY', 504.6);
EXPLAIN SELECT close FROM prices WHERE time IN '2026-09';
```
Result:
`CREATE TABLE prices (`
```text
table 'prices' created
```
`INSERT INTO prices VALUES ('2026-07-31T11:00:00+05:45', 'CITY', 511.0), ('2026-08-30T11:00:00+05:45'`
```text
3 row(s) inserted
```
`EXPLAIN SELECT close FROM prices WHERE time IN '2026-09'`
| plan (STRING) |
|---|
| scan table prices (1 of 3 partitions, about 3 rows) |
| build 2 of 3 stored column(s) per row: time, close |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
**Example: Early morning in Nepal is the day before in UTC: the error lists partition 2026-09-09** (sample data: sample; shows an error)
Setup:
```sql
CREATE TABLE ticks (time TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO ticks VALUES ('2026-09-10T02:00:00+05:45', 'CITY', 1), ('2026-09-10T11:00:00+05:45', 'CITY', 2);
```
```sql
ALTER TABLE ticks DROP PARTITION '2026-09-11';
```
Result:
Error: `table 'ticks' has no partition '2026-09-11'; partitions: 2026-09-09, 2026-09-10`
**Example: Partitions are only for time-series tables** (sample data: sample; shows an error)
```sql
CREATE TABLE app_notes (id INT PRIMARY KEY, text STRING) PARTITION BY DAY;
```
Result:
Error: `line 1, column 58: PARTITION BY is only for time-series tables; expected TIMESERIES() on table 'app_notes'`
## PARTITION BY time, SYMBOL(col)
Syntax:
```sql
CREATE TABLE name (...) TIMESERIES(time_column)
PARTITION BY YEAR | MONTH | NONE, SYMBOL(column)
SORT BY (column, ...) [DEDUP KEY (column, ..., time_column)]
PARTITION BY ..., KEY(column) -- the same, for a column that is not a market symbol
```
A second level of partitions: each time bucket is split again into **one file per value** of the column. A query for one symbol then opens only that symbol's files.
| Rule | Why |
|---|---|
| the time part is `YEAR`, `MONTH` or `NONE`; `DAY` is refused | one small file per symbol per day makes thousands of files a week |
| the column is a stored `SYMBOL` or `STRING` column | its value names the file |
| it is the **first** `SORT BY` column | then reading the files one after the other keeps the `SORT BY` order |
| a `DEDUP KEY` must include it | a duplicate is looked for in one partition only |
| a value is at most 64 bytes, with no 0 byte | the value is part of a file name |
| at most 4,096 values with `NONE`, 100,000 partitions with `YEAR` or `MONTH` | every value is a file of its own |
- **Returns:** `SHOW TABLES` shows the partition as `YEAR, SYMBOL(symbol)`. `KEY(col)` is shown as `SYMBOL(col)`.
- **Notes:**
- `WHERE symbol = 'CITY'` or `symbol IN (...)` opens only those files. `EXPLAIN` shows it as `1 of 4 partitions`.
- Good for data read **one value at a time**: one symbol's candles, one source's news. Not for a table read a whole day at a time across all symbols (a trade list): plain `PARTITION BY DAY` is right there.
- Reads get faster, writes do not: a load that holds 300 symbols writes 300 files.
- Not for a column with very many values (a user id). Keep such a column as the first `SORT BY` column instead: that already reads only its rows.
- `DROP PARTITION '2025'` drops every symbol of that year. Single symbols cannot be dropped by name: use `DELETE ... WHERE symbol = ...`. With `NONE` there is no time part to drop: use `DELETE` or [TRUNCATE](update-delete.md#truncate).
- Only for new tables: an existing table cannot be changed to it. Not with `CREATE TABLE ... AS`.
- An older KDB build cannot open a database with such a table (see [versions](versions.md)).
**Example: Daily candles in one file per year and symbol** (sample data: sample)
Setup:
```sql
CREATE TABLE candles (time TIMESTAMP ROUND 1d, symbol SYMBOL, close DOUBLE)
TIMESERIES(time) PARTITION BY YEAR, SYMBOL(symbol) SORT BY (symbol, time) DEDUP KEY (symbol, time);
INSERT INTO candles VALUES
('2025-09-10', 'CITY', 500.0), ('2026-09-10', 'CITY', 504.6),
('2026-09-10', 'COAST', 376.1), ('2026-09-10', 'VOLT', 191.2);
```
```sql
SHOW TABLES;
EXPLAIN SELECT time, close FROM candles WHERE symbol = 'COAST';
ALTER TABLE candles DROP PARTITION '2025';
SELECT time, symbol, close FROM candles ORDER BY symbol;
```
Result:
`SHOW TABLES`
| name (STRING) | kind (STRING) | partition (STRING) | columns (INT) | rows (LONG) |
|---|---|---|---|---|
| brokers | relational | NONE | 2 | 5 |
| candles | timeseries | YEAR, SYMBOL(symbol) | 3 | 4 |
| live_data | timeseries | DAY | 12 | 12 |
| ochl_day | timeseries | YEAR | 7 | 57 |
| ochl_minutes | timeseries | MONTH | 7 | 956 |
| securities | relational | NONE | 3 | 5 |
| trade_log | timeseries | DAY | 8 | 32 |
(7 rows)
`EXPLAIN SELECT time, close FROM candles WHERE symbol = 'COAST'`
| plan (STRING) |
|---|
| scan table candles (1 of 4 partitions, about 4 rows) |
| read only 1 key range(s) of each partition |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
`ALTER TABLE candles DROP PARTITION '2025'`
```text
1 partition(s) of 'candles' dropped
```
`SELECT time, symbol, close FROM candles ORDER BY symbol`
| time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) |
|---|---|---|
| 2026-09-10T00:00:00.000Z | CITY | 504.6 |
| 2026-09-10T00:00:00.000Z | COAST | 376.1 |
| 2026-09-10T00:00:00.000Z | VOLT | 191.2 |
(3 rows)
**Example: News split by source: KEY(col) and PARTITION BY NONE** (sample data: sample)
Setup:
```sql
CREATE TABLE news (time TIMESTAMP, source STRING, title STRING)
TIMESERIES(time) PARTITION BY NONE, KEY(source) SORT BY (source, time);
INSERT INTO news VALUES
('2026-09-10T11:00:00+05:45', 'wire one', 'City Bank profit rises 18%'),
('2026-09-11T09:30:00+05:45', 'market desk', 'Index closes higher');
```
```sql
EXPLAIN SELECT title FROM news WHERE source = 'market desk';
SHOW CREATE TABLE news;
```
Result:
`EXPLAIN SELECT title FROM news WHERE source = 'market desk'`
| plan (STRING) |
|---|
| scan table news (1 of 2 partitions, about 2 rows) |
| read only 1 key range(s) of each partition |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
`SHOW CREATE TABLE news`
| create_sql (STRING) |
|---|
| CREATE TABLE news ( time TIMESTAMP, source STRING, title STRING ) TIMESERIES(time) PARTITION BY NONE, SYMBOL(source) SORT BY (source, time) |
(1 row)
**Example: DAY is refused with SYMBOL(col)** (sample data: sample; shows an error)
```sql
CREATE TABLE ticks (time TIMESTAMP, symbol SYMBOL, price DOUBLE)
TIMESERIES(time) PARTITION BY DAY, SYMBOL(symbol);
```
Result:
Error: `PARTITION BY ..., SYMBOL(column) on table 'ticks': the time part must be YEAR, MONTH or NONE (as in PARTITION BY YEAR, SYMBOL(symbol))`
**Example: The column must be the first SORT BY column** (sample data: sample; shows an error)
```sql
CREATE TABLE candles (time TIMESTAMP, symbol SYMBOL, close DOUBLE)
TIMESERIES(time) PARTITION BY MONTH, SYMBOL(symbol) SORT BY (time);
```
Result:
Error: `PARTITION BY ..., SYMBOL(column) on table 'candles': 'symbol' must be the first SORT BY column (as in SORT BY (symbol, time))`
**Example: The DEDUP KEY must hold the column** (sample data: sample; shows an error)
```sql
CREATE TABLE candles (time TIMESTAMP, symbol SYMBOL, close DOUBLE)
TIMESERIES(time) PARTITION BY MONTH, SYMBOL(symbol) DEDUP KEY (time);
```
Result:
Error: `PARTITION BY ..., SYMBOL(column) on table 'candles': the DEDUP KEY must include 'symbol'`
## SORT BY
Syntax:
```sql
CREATE TABLE name (...) TIMESERIES(time_column)
[PARTITION BY ...]
SORT BY (column [, column ...])
```
Sets the **order of rows inside each partition**. Rows are kept sorted by these columns, whatever order you insert them in.
- **Returns:** a `SELECT` without `ORDER BY` gives rows partition by partition, and inside a partition in `SORT BY` order.
- **Notes:**
- The default is the first `SYMBOL` column, then the time column: `SORT BY (symbol, time)`. Without a `SYMBOL` column it is the time column.
- With `SORT BY (symbol, time)`, `WHERE symbol = 'CITY'` or `symbol IN (...)` reads only CITY's rows (with a time range: only that part of them). `EXPLAIN` shows `read only 1 key range(s)`. This is why the default is good for market data: most queries ask for one symbol.
- With `SORT BY (time)`, a filter on `symbol` alone must read every row of the partitions. A time range still reads only that part.
- Do not depend on the order without `ORDER BY`: the partitions, `SORT BY` and the query plan decide it. Write `ORDER BY` when the order matters.
- `SORT BY` only decides the order. Two rows with the same `SORT BY` values are **not** duplicates; that is the job of [DEDUP KEY](partitions-and-dedup.md#dedup-key).
**Example: Default SORT BY (symbol, time): rows come grouped by symbol** (sample data: sample)
Setup:
```sql
CREATE TABLE trades (time TIMESTAMP, symbol SYMBOL, broker INT, qty LONG) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO trades VALUES
('2026-09-10T11:02:00+05:45', 'COAST', 58, 10),
('2026-09-10T11:00:00+05:45', 'CITY', 45, 20),
('2026-09-10T11:01:00+05:45', 'COAST', 34, 30),
('2026-09-10T11:00:00+05:45', 'VOLT', 1, 40);
```
```sql
SELECT * FROM trades;
EXPLAIN SELECT * FROM trades WHERE symbol = 'COAST';
```
Result:
`SELECT * FROM trades`
| time (TIMESTAMP) | symbol (SYMBOL) | broker (INT) | qty (LONG) |
|---|---|---|---|
| 2026-09-10T05:15:00.000Z | CITY | 45 | 20 |
| 2026-09-10T05:16:00.000Z | COAST | 34 | 30 |
| 2026-09-10T05:17:00.000Z | COAST | 58 | 10 |
| 2026-09-10T05:15:00.000Z | VOLT | 1 | 40 |
(4 rows)
`EXPLAIN SELECT * FROM trades WHERE symbol = 'COAST'`
| plan (STRING) |
|---|
| scan table trades (1 of 1 partitions, about 4 rows) |
| read only 1 key range(s) of each partition |
| filter while scanning: 1 condition(s) |
| engine: column |
(4 rows)
**Example: SORT BY (time): rows come in time order, but a symbol filter reads everything** (sample data: sample)
Setup:
```sql
CREATE TABLE trades (time TIMESTAMP, symbol SYMBOL, broker INT, qty LONG)
TIMESERIES(time) PARTITION BY DAY SORT BY (time);
INSERT INTO trades VALUES
('2026-09-10T11:02:00+05:45', 'COAST', 58, 10),
('2026-09-10T11:00:00+05:45', 'CITY', 45, 20),
('2026-09-10T11:01:00+05:45', 'COAST', 34, 30),
('2026-09-10T11:00:00+05:45', 'VOLT', 1, 40);
```
```sql
SELECT * FROM trades;
EXPLAIN SELECT * FROM trades WHERE symbol = 'COAST';
```
Result:
`SELECT * FROM trades`
| time (TIMESTAMP) | symbol (SYMBOL) | broker (INT) | qty (LONG) |
|---|---|---|---|
| 2026-09-10T05:15:00.000Z | CITY | 45 | 20 |
| 2026-09-10T05:15:00.000Z | VOLT | 1 | 40 |
| 2026-09-10T05:16:00.000Z | COAST | 34 | 30 |
| 2026-09-10T05:17:00.000Z | COAST | 58 | 10 |
(4 rows)
`EXPLAIN SELECT * FROM trades WHERE symbol = 'COAST'`
| plan (STRING) |
|---|
| scan table trades (1 of 1 partitions, about 4 rows) |
| filter while scanning: 1 condition(s) |
| engine: column |
(3 rows)
**Example: SORT BY needs real columns** (sample data: sample; shows an error)
```sql
CREATE TABLE bad (time TIMESTAMP, symbol SYMBOL) TIMESERIES(time) SORT BY (sym);
```
Result:
Error: `line 1, column 76: SORT BY column 'sym' does not exist; expected one of the columns of 'bad'`
## DEDUP KEY
Syntax:
```sql
CREATE TABLE name (...) TIMESERIES(time_column)
[PARTITION BY ...] [SORT BY (...)]
DEDUP KEY (column [, column ...], time_column)
INSERT INTO name VALUES (...) -- a duplicate: error, nothing is saved (see dedup_insert)
INSERT OR REPLACE INTO name VALUES (...) -- a duplicate: the old row is replaced
INSERT OR IGNORE INTO name VALUES (...) -- a duplicate: the new row is skipped
```
Says which columns make a row **unique**. Two rows with the same values in all `DEDUP KEY` columns are the same row. `DEDUP KEY (symbol, time)` means "one row per symbol per time".
| Statement | Key is new | Key is already there |
|---|---|---|
| `INSERT` | row inserted | **error**, nothing from the statement is saved |
| `INSERT OR REPLACE` | row inserted | the whole old row is **replaced** by the new one |
| `INSERT OR IGNORE` | row inserted | the new row is **skipped** |
| `UPDATE` that would make two equal keys | - | error |
- **Returns:** messages like `1 row(s) inserted, 1 replaced` or `0 row(s) inserted, 1 skipped (already there)`.
- **NULL:** `NULL` values in key columns are compared like values: two rows with the same time and a `NULL` symbol are duplicates.
- **Notes:**
- The DEDUP KEY **must include the time column**. KDB finds duplicates inside one partition, and the time decides the partition. A stored column holding the time's **minute** may take its place: see [a key on the minute](partitions-and-dedup.md#minute-key).
- Without a DEDUP KEY, every INSERT adds rows, also exact copies. Load the same file twice and you have every row twice. Such a table loads fast: nothing is looked up (see [how loads find duplicates](partitions-and-dedup.md#load-dedup)).
- Duplicates **inside one statement** count too: with `INSERT OR REPLACE`, the last one wins.
- `INSERT OR REPLACE` replaces the **whole** row: columns you do not give become their DEFAULT or `NULL`. To change some columns only, use `UPDATE`.
- With `ROUND 1d` or `ROUND 1m` on the time column, two times in the same day or minute become equal, so they are duplicates ([create-table](create-table.md)).
- Time-series tables cannot have a `PRIMARY KEY`. Use `DEDUP KEY (..., time)`.
- Tip: give every time-series table that you import into a DEDUP KEY and load with `INSERT OR REPLACE`. Then you can run the same import again and again ([insert](insert.md)).
**Example: Live snapshots: a snapshot sent again replaces the old one** (sample data: sample)
Setup:
```sql
CREATE TABLE snapshots (time TIMESTAMP, symbol SYMBOL, ltp DOUBLE, volume LONG)
TIMESERIES(time) PARTITION BY DAY DEDUP KEY (symbol, time);
INSERT INTO snapshots VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 513.0, 65850);
```
```sql
INSERT OR REPLACE INTO snapshots VALUES
('2026-09-10T11:05:00+05:45', 'CITY', 513.2, 66010),
('2026-09-10T11:06:00+05:45', 'CITY', 513.5, 67000);
INSERT OR IGNORE INTO snapshots VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 999, 1);
SELECT * FROM snapshots;
```
Result:
`INSERT OR REPLACE INTO snapshots VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 513.2, 66010), ('2026-`
```text
1 row(s) inserted, 1 replaced
```
`INSERT OR IGNORE INTO snapshots VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 999, 1)`
```text
0 row(s) inserted, 1 skipped (already there)
```
`SELECT * FROM snapshots`
| time (TIMESTAMP) | symbol (SYMBOL) | ltp (DOUBLE) | volume (LONG) |
|---|---|---|---|
| 2026-09-10T05:20:00.000Z | CITY | 513.2 | 66010 |
| 2026-09-10T05:21:00.000Z | CITY | 513.5 | 67000 |
(2 rows)
**Example: A plain INSERT of a duplicate is refused** (sample data: sample; shows an error)
```sql
INSERT INTO ochl_day VALUES ('2026-09-10', 'CITY', 504.6, 507.0, 500.7, 504.9, 183040);
```
Result:
Error: `a row with symbol='CITY', time='2026-09-10T00:00:00.000Z' already exists in 'ochl_day'. Use INSERT OR REPLACE to overwrite it, or INSERT OR IGNORE to skip it`
**Example: Trade log: the same transaction twice in one import keeps one row (the last)** (sample data: sample)
```sql
INSERT OR REPLACE INTO trade_log (date, transaction_id, symbol, buyer, seller, quantity, rate) VALUES
('2026-09-11', 200001, 'CITY', 45, 1, 10, 515.0),
('2026-09-11', 200001, 'CITY', 45, 1, 10, 516.0);
SELECT date, transaction_id, symbol, quantity, rate, amount FROM trade_log WHERE date IN '2026-09-11';
```
Result:
`INSERT OR REPLACE INTO trade_log (date, transaction_id, symbol, buyer, seller, quantity, rate) VALUE`
```text
1 row(s) inserted, 1 replaced
```
`SELECT date, transaction_id, symbol, quantity, rate, amount FROM trade_log WHERE date IN '2026-09-11`
| date (TIMESTAMP) | transaction_id (LONG) | symbol (SYMBOL) | quantity (LONG) | rate (DOUBLE) | amount (DOUBLE) |
|---|---|---|---|---|---|
| 2026-09-11T00:00:00.000Z | 200001 | CITY | 10 | 516 | 5160 |
(1 row)
**Example: Without a DEDUP KEY every copy is kept** (sample data: sample)
Setup:
```sql
CREATE TABLE raw_ticks (time TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESERIES(time) PARTITION BY DAY;
```
```sql
INSERT INTO raw_ticks VALUES ('2026-09-10T11:00:00+05:45', 'CITY', 510);
INSERT INTO raw_ticks VALUES ('2026-09-10T11:00:00+05:45', 'CITY', 510);
SELECT count(*) AS rows_kept FROM raw_ticks;
```
Result:
`INSERT INTO raw_ticks VALUES ('2026-09-10T11:00:00+05:45', 'CITY', 510)`
```text
1 row(s) inserted
```
`INSERT INTO raw_ticks VALUES ('2026-09-10T11:00:00+05:45', 'CITY', 510)`
```text
1 row(s) inserted
```
`SELECT count(*) AS rows_kept FROM raw_ticks`
| rows_kept (LONG) |
|---|
| 2 |
(1 row)
**Example: A market index: one value per minute, DEDUP KEY on the time alone** (sample data: sample)
Setup:
```sql
CREATE TABLE market_index (time TIMESTAMP ROUND 1m, value DOUBLE)
TIMESERIES(time) PARTITION BY MONTH DEDUP KEY (time);
```
```sql
INSERT OR REPLACE INTO market_index VALUES
('2026-09-10T11:00:05+05:45', 2840.1),
('2026-09-10T11:00:40+05:45', 2841.5),
('2026-09-10T11:01:10+05:45', 2842.0);
SELECT * FROM market_index;
```
Result:
`INSERT OR REPLACE INTO market_index VALUES ('2026-09-10T11:00:05+05:45', 2840.1), ('2026-09-10T11:00`
```text
2 row(s) inserted, 1 replaced
```
`SELECT * FROM market_index`
| time (TIMESTAMP) | value (DOUBLE) |
|---|---|
| 2026-09-10T05:15:00.000Z | 2841.5 |
| 2026-09-10T05:16:00.000Z | 2842 |
(2 rows)
**Example: An UPDATE may not make a duplicate** (sample data: sample; shows an error)
```sql
UPDATE ochl_day SET time = '2026-09-09' WHERE symbol = 'CITY' AND time IN '2026-09-10';
```
Result:
Error: `the update would create a second row with symbol='CITY', time='2026-09-09T00:00:00.000Z' in 'ochl_day'`
**Example: The DEDUP KEY must include the time column** (sample data: sample; shows an error)
```sql
CREATE TABLE bad (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time) DEDUP KEY (symbol);
```
Result:
Error: `DEDUP KEY must include the time column 'time', e.g. DEDUP KEY (symbol, time), or a column GENERATED ALWAYS AS (date_trunc('minute', time)) STORED`
## Ragged seconds: a DEDUP KEY on the minute
Syntax:
```sql
minute TIMESTAMP GENERATED ALWAYS AS (date_trunc('minute', time)) STORED
... DEDUP KEY (symbol, minute)
```
A feed of minute candles often sends the same minute with different seconds: `11:01:07`, then `11:01:42`. With `DEDUP KEY (symbol, time)` those are two rows. Put the minute in a stored [generated column](create-table.md#generated) and make **that** the key: then both are the same row, and the later one replaces the earlier one.
| Column in the key | `11:01:07` then `11:01:42`, same symbol |
|---|---|
| `time` | two rows |
| `minute` (`date_trunc('minute', time)`) | one row: the later one (with `INSERT OR REPLACE`, `dedup_insert = 'upsert'`, `POST /write` or `/imp`) |
- **Notes:**
- The time column keeps the real time of the row (`11:01:42`). Only the key uses the minute.
- The column must be `date_trunc('',