| <!-- | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, | |
| software distributed under the License is distributed on an | |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| KIND, either express or implied. See the License for the | |
| specific language governing permissions and limitations | |
| under the License. | |
| --> | |
| # DuckDB TsFile Extension | |
| This guide is intended for data engineers, platform developers, and analytics developers who need to analyze or generate Apache TsFile data in DuckDB. The TsFile extension is available as a DuckDB Community Extension. It makes TsFile both a SQL data source and a `COPY` output format, providing a complete read-query-write workflow. | |
| **Remember two things:** use `read_tsfile(path, table_name)` to read data, and use `COPY ... TO ... (FORMAT tsfile)` to write data. The current version supports local, Table-model TsFiles. Before writing, sort the data by the TAG columns and then by the time column. | |
| ## Quick Start | |
| ### Start DuckDB and Load the Extension | |
| For regular use, install and load the DuckDB Community Extension: | |
| ```sql | |
| INSTALL tsfile FROM community; | |
| ``` | |
| ```sql | |
| LOAD tsfile; | |
| ``` | |
| Install the Community Extension the first time you use it. After that, you only need to run `LOAD tsfile;` whenever you start DuckDB. | |
| ### Read Your First TsFile | |
| ```sql | |
| SELECT * | |
| FROM read_tsfile('/data/measurements.tsfile', 'sensors'); | |
| ``` | |
| The first argument to `read_tsfile` is a local file path, and the second is the name of the table stored in the TsFile. Each call reads one Table-model table from one file. | |
| ## Reading and Querying | |
| ### Select Columns | |
| Select the columns you need just as you would from a regular DuckDB table. Projection is pushed down to the TsFile scan whenever possible, reducing the amount of data read: | |
| ```sql | |
| SELECT time, device_id, temperature | |
| FROM read_tsfile('/data/measurements.tsfile', 'sensors') | |
| LIMIT 10; | |
| ``` | |
| ### Filter by Time | |
| The global TsFile time axis is exposed in DuckDB as a `BIGINT`. The time unit is determined by the file or protocol; milliseconds are common. The following operators can be pushed down to the scanner: `=`, `<`, `<=`, `>`, `>=`, and `BETWEEN`. | |
| ```sql | |
| SELECT time, device_id, temperature | |
| FROM read_tsfile('/data/measurements.tsfile', 'sensors') | |
| WHERE time BETWEEN 1700000000000 AND 1700003600000; | |
| ``` | |
| ### Filter by TAG | |
| String TAG columns support equality and range comparisons, null checks, and combinations using `AND` and `OR`. Supported operations include `=`, `!=`, `<`, `<=`, `>`, `>=`, inclusive `BETWEEN`, `IS NULL`, and `IS NOT NULL`. | |
| ```sql | |
| SELECT time, device_id, temperature | |
| FROM read_tsfile('/data/measurements.tsfile', 'sensors') | |
| WHERE (device_id = 'device-01' OR device_id = 'device-02') | |
| AND time BETWEEN 1700000000000 AND 1700003600000; | |
| ``` | |
| FIELD filters and TAG expressions that cannot be pushed down are still evaluated by DuckDB after the scan. A `NOT (...)` expression in any form is not pushed down. | |
| ### Check Filter Pushdown | |
| Use `EXPLAIN` to inspect the query plan. If pushdown succeeds, the `READ_TSFILE` node shows `Time Range` and `TAG Filter`: | |
| ```sql | |
| EXPLAIN | |
| SELECT time, device_id, temperature | |
| FROM read_tsfile('/data/measurements.tsfile', 'sensors') | |
| WHERE device_id = 'device-01' | |
| AND time >= 1700000000000; | |
| ``` | |
| ## Writing TsFile | |
| ### Write with COPY | |
| Writing uses DuckDB's standard `COPY` interface. Clean, cast, and sort the data in a subquery before writing it to TsFile: | |
| ```sql | |
| COPY ( | |
| SELECT time, device_id, temperature, humidity | |
| FROM measurements | |
| ORDER BY device_id, time | |
| ) | |
| TO '/data/measurements.tsfile' | |
| ( | |
| FORMAT tsfile, | |
| TABLE_NAME sensors, | |
| TIME_COLUMN time, | |
| TAG_COLUMNS (device_id) | |
| ); | |
| ``` | |
| `TIME_COLUMN` identifies the time axis. Columns listed in `TAG_COLUMNS` are written as TAG columns, and all other columns are written as FIELD columns. Identifier syntax is recommended for `TABLE_NAME` and `TIME_COLUMN`; use double quotes when an identifier contains special characters. | |
| ### Write Options | |
| |Option|Required|Description| | |
| |---|---|---| | |
| |`FORMAT tsfile`|Yes|Selects the TsFile `COPY` writer.| | |
| |`TABLE_NAME`|No|The local table name in the output file. The default is `default_table`.| | |
| |`TIME_COLUMN`|No|The input column used as the time axis. The default is `time`, and its type must be `BIGINT`.| | |
| |`TAG_COLUMNS`|No|A list of TAG column names. If omitted, all non-time columns are written as FIELD columns.| | |
| |`OVERWRITE true`|No|Use this option to replace an existing target. Keep the default temporary-file handling enabled.| | |
| ### TAG\_COLUMNS Constraints | |
| `TAG_COLUMNS` accepts a list of column names, not a list of values: | |
| ```sql | |
| TAG_COLUMNS (device_id, region) | |
| ``` | |
| - Each column must exist in the input query. | |
| - Each column must have the `VARCHAR` type. | |
| - A TAG column cannot also be the `TIME_COLUMN`. | |
| - Column-name matching is case-insensitive. | |
| If you do not need TAG columns, you can write only the time and FIELD columns: | |
| ```sql | |
| COPY ( | |
| SELECT time, temperature, humidity | |
| FROM measurements | |
| ORDER BY time | |
| ) | |
| TO '/data/field-only.tsfile' | |
| ( | |
| FORMAT tsfile, | |
| TABLE_NAME sensors, | |
| TIME_COLUMN time | |
| ); | |
| ``` | |
| ### Type Mapping and NULL Values | |
| |DuckDB Type|TsFile Type|Notes| | |
| |---|---|---| | |
| |BOOLEAN|BOOLEAN|FIELD NULL values are preserved.| | |
| |INTEGER|INT32|FIELD NULL values are preserved.| | |
| |BIGINT|INT64|FIELD NULL values are preserved; this type is also used for the time axis.| | |
| |FLOAT|FLOAT|FIELD NULL values are preserved.| | |
| |DOUBLE|DOUBLE|FIELD NULL values are preserved.| | |
| |VARCHAR|STRING|FIELD NULL values are preserved.| | |
| |BLOB|BLOB|FIELD NULL values are preserved.| | |
| |TIMESTAMP\_NS|TIMESTAMP|Read as DuckDB `TIMESTAMP_NS`.| | |
| ## End-to-End Example | |
| The following workflow reads a time range for device `a` from an existing TsFile, renames columns, writes a new TsFile, and then reads it back for verification: | |
| ```sql | |
| LOAD tsfile; | |
| COPY ( | |
| SELECT time, | |
| s0 AS device_id, | |
| s2 AS value, | |
| CAST(s8 AS VARCHAR) AS day | |
| FROM read_tsfile('test/data/simple_table_t1.tsfile', 'test') | |
| WHERE s0 = 'a' | |
| AND time BETWEEN 1760106022000 AND 1760106024000 | |
| ORDER BY device_id, time | |
| ) | |
| TO '/tmp/tsfile_subset.tsfile' | |
| ( | |
| FORMAT tsfile, | |
| TABLE_NAME subset, | |
| TIME_COLUMN time, | |
| TAG_COLUMNS (device_id) | |
| ); | |
| SELECT time, device_id, value, day | |
| FROM read_tsfile('/tmp/tsfile_subset.tsfile', 'subset') | |
| ORDER BY device_id, time; | |
| ``` | |
| The query returns three rows with timestamps `1760106022000`, `1760106023000`, and `1760106024000`. | |
| ## Current Limitations and Troubleshooting | |
| ### Pre-Write Checklist | |
| 1. Confirm that `TIME_COLUMN` exists and has the `BIGINT` type. | |
| 2. Confirm that the TIME column contains no NULL values. | |
| 3. Confirm that the input is sorted by all TAG columns and then by the time column. | |
| 4. If the target file already exists, keep temporary-file handling enabled and set `OVERWRITE true`. | |
| ### Known Limitations | |
| - The current version supports one Table-model table in one local TsFile. | |
| - FIELD filters are not pushed down. A `NOT (...)` TAG expression in any form is not pushed down. | |
| - Writing DATE FIELD columns is temporarily disabled to avoid conversions that depend on the local time zone. DATE columns in existing TsFiles can still be read. To write such a column, first cast it to `VARCHAR`. | |
| - When writing directly with `USE_TMP_FILE false`, the target path must not already exist. | |
| ## Quick Reference | |
| |Task|Entry Point| | |
| |---|---| | |
| |Read TsFile|`read_tsfile('/path/file.tsfile', 'table')`| | |
| |Inspect columns and data|`SELECT ... FROM read_tsfile(...)`| | |
| |Push down time and TAG conditions|Use supported comparisons and logical combinations in `WHERE`| | |
| |Inspect pushdown|`EXPLAIN SELECT ...`| | |
| |Write TsFile|`COPY (...) TO 'file.tsfile' (FORMAT tsfile, ...)`| | |
| |Replace an existing file|Default temporary-file handling + `OVERWRITE true`| |