Let's get started with a Microservice Architecture with Spring Cloud:
Introduction to IoTDB
Last updated: January 6, 2026
1. Introduction
In this tutorial, we’ll take a look at the Apache IoTDB database. We’ll see what it is, how to use it and what we can do with it.
2. What is IoTDB
IoTDB is a free, open-source database designed for storing time-series data. It’s designed with IoT devices in mind, but can be used for storing any series of metrics or recordings that are identified by their timestamp. It’s also designed to be SQL compatible, so integrating it into our applications is easier.
Since IoTDB is designed with IoT devices in mind, its storage structure reflects this. Our databases, timeseries and data fields are typically modelled as a tree structure that reflects the fact that we’re recording different pieces of data from different devices, potentially of a range of types.
For example, we might want to record the speed of turbines as they vary over time. We could record this into the timeseries root.baeldung.turbine.device1.speed. This breaks down as:
- root.baeldung – our database
- turbine.device1 – our device
- speed – the data we want to record
If we need to record the same data about multiple devices, we simply create multiple timeseries to record into,
3. Running IoTDB
IoTDB consists of three different services that can be used together:
- Config nodes
- Data nodes
- Optionally – AI nodes
We can run this in a standalone mode, where we have a single process that consists of a Config node and a Data node in one. Alternatively, we can run this in a high-availability cluster. In this case, we need to run 3 Config nodes and at least 3 Data nodes, though we can add more as necessary. This ensures that we can continue to operate if a node fails.
In addition, we have the ability to run AI nodes that provide some AI models we can use to work with our data in real-time.
The easiest way to get started with IoTDB is to use Docker. Apache provide some starter Docker Compose files for running in various setups. For this article, we’re going to use the docker-compose-standalone.yml file.
This file assumes we’ve created a Docker network bridge for the container to use:
$ docker network create --driver=bridge --subnet=172.18.0.0/16 --gateway=172.18.0.1 iotdb
ca767e800f66e4b067a6952420c784392661fd0aa21318e3ce3dc53fdad4ab00
We can then start our container:
$ docker compose up
[+] Running 1/1
✔ Container iotdb-service Created 0.0s
Attaching to iotdb-service
.....
iotdb-service | 2025-12-28 10:16:48,541 [main] INFO o.a.i.db.service.DataNode:261
- Congratulations, IoTDB DataNode is set up successfully. Now, enjoy yourself!
iotdb-service | 2025-12-28 10:16:48,542 [main] INFO o.a.i.db.service.DataNode:288
- DataNode started
iotdb-service | 2025-12-28 10:16:51,080 [pool-36-IoTDB-DataNodeInternalRPC-Processor-1]
INFO o.a.i.d.q.p.ClusterTopology:152 - [Topology] latest view from config-node: {1=[1]}
At this point, our database is running and ready to use.
4. Connecting to IoTDB
Now that our database is running, we need to be able to connect to it.
4.1. Command Line Interface
To administer our database, IoTDB includes a command-line interface for connecting to it. This is also installed inside the Docker containers, so we can easily run it using Docker:
$ docker exec -it iotdb-service start-cli.sh
---------------------
Starting IoTDB Cli
---------------------
_____ _________ ______ ______
|_ _| | _ _ ||_ _ `.|_ _ \
| | .--.|_/ | | \_| | | `. \ | |_) |
| | / .'`\ \ | | | | | | | __'.
_| |_| \__. | _| |_ _| |_.' /_| |__) |
|_____|'.__.' |_____| |______.'|_______/ version 2.0.5 (Build: 0917050)
Successfully login at 127.0.0.1:6667
IoTDB>
This gives us a command-line interface similar to the psql command from Postgres.
4.2. JDBC Driver
IoTDB also provides a number of different drivers for different programming languages. These include JDBC drivers that allow us to connect to the database from Java.
In order to use this, we first need to add the drivers to our project. If we’re using Maven, we can include this dependency in our pom.xml file:
<dependencies>
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-jdbc</artifactId>
<version>2.0.5</version>
</dependency>
</dependencies>
Once we’ve done this, we can use it the same as any other JDBC driver:
Class.forName("org.apache.iotdb.jdbc.IoTDBDriver");
try (Connection con = DriverManager
.getConnection("jdbc:iotdb://127.0.0.1:6667/", "root", "root")) {
// use conn here
}
Since these are standard JDBC drivers, we can use them with anything that’s compatible – e.g. Spring JdbcTemplate.
5. Managing Databases
Within IoTDB, all of our data is stored in databases. These work similarly to other database management systems, comprising a collection of time series, each containing the corresponding data.
We create a new database using the CREATE DATABASE command:
IoTDB> CREATE DATABASE root.baeldung;
Msg: The statement is executed successfully.
We can see the databases that exist using the SHOW DATABASES command:
IoTDB> SHOW DATABASES;
+-------------+-----------------------+---------------------+-------------------+---------------------+
| Database|SchemaReplicationFactor|DataReplicationFactor|TimePartitionOrigin|TimePartitionInterval|
+-------------+-----------------------+---------------------+-------------------+---------------------+
|root.baeldung| 1| 1| 0| 604800000|
+-------------+-----------------------+---------------------+-------------------+---------------------+
Total line number = 1
It costs 0.107s
We can also delete the entire database using the DELETE DATABASE command:
IoTDB> DELETE DATABASE root.baeldung;
Msg: The statement is executed successfully.
Note that this is irreversible, so we need to be careful to ensure we’re doing it correctly.
6. Managing Timeseries
Within our database, we then collect data together into a timeseries. These are roughly analogous to tables in a traditional SQL database.
A simple timeseries consists of a timestamp and a value, which can be of any suitable type. We then record data in our timeseries as we generate it.
We can create our timeseries using the CREATE TIMESERIES command:
IoTDB> CREATE TIMESERIES root.baeldung.turbine.device1.speed FLOAT;
Msg: The statement is executed successfully.
Alternatively, we can create an aligned timeseries. These are a special case where there are several different measurements that are all related to the same input, and are all being made at the same instant. As such, we want to be able to relate these measurements to each other more easily. We create these using the CREATE ALIGNED TIMESERIES command:
IoTDB> CREATE ALIGNED TIMESERIES root.baeldung.car.device2(lat FLOAT, lng FLOAT);
Msg: The statement is executed successfully.
We can then see all of the timeseries that we’ve created using the SHOW TIMESERIES command:
IoTDB> SHOW TIMESERIES;
+-----------------------------------+-----+-------------+--------+--------+-----------+----+----------+--------+------------------+--------+
| Timeseries|Alias| Database|DataType|Encoding|Compression|Tags|Attributes|Deadband|DeadbandParameters|ViewType|
+-----------------------------------+-----+-------------+--------+--------+-----------+----+----------+--------+------------------+--------+
|root.baeldung.turbine.device1.speed| null|root.baeldung| FLOAT| GORILLA| LZ4|null| null| null| null| BASE|
| root.baeldung.car.device2.lng| null|root.baeldung| FLOAT| GORILLA| LZ4|null| null| null| null| BASE|
| root.baeldung.car.device2.lat| null|root.baeldung| FLOAT| GORILLA| LZ4|null| null| null| null| BASE|
+-----------------------------------+-----+-------------+--------+--------+-----------+----+----------+--------+------------------+--------+
Total line number = 3
It costs 0.083s
Here we’ll see that the aligned timeseries actually shows up as the separate fields. This is how they’re stored, but because we created them as aligned time series, we interact with the data in them as a single entity.
We can also delete timeseries using the DELETE TIMESERIES command:
IoTDB> DELETE TIMESERIES root.baeldung.turbine.device1.speed;
Msg: The statement is executed successfully.
Note that if we wish to delete an aligned timeseries, we need to delete each field individually. We can’t delete the entire set in one go.
7. Managing Data
Now that we’ve got a database and some time series within it, we need some data within these. Similar to a traditional SQL database, we’re able to insert, query and delete records. However, we can’t update existing records in place.
7.1. Inserting Data
We insert data using the INSERT INTO command:
try (PreparedStatement stmt = con.prepareStatement("INSERT INTO root.baeldung.turbine.device1(timestamp, speed) VALUES (?, ?)")) {
stmt.setObject(1, Instant.now().toEpochMilli());
stmt.setObject(2, 10);
stmt.executeUpdate();
}
Our timestamp here is provided as milliseconds since the epoch.
Alternatively, we can omit the timestamp from our statement:
try (PreparedStatement stmt = con.prepareStatement("INSERT INTO root.baeldung.turbine.device1(speed) VALUES (?)")) {
stmt.setObject(1, 20);
stmt.executeUpdate();
}
If we do this, IoTDB will use the current time instead of the value that we provided.
If we’re working with an aligned timeseries, we can insert data into all of our fields in a single statement:
try (PreparedStatement stmt = con.prepareStatement("INSERT INTO root.baeldung.car.device2(lat, lng) VALUES (?, ?)")) {
stmt.setObject(1, 40.6892);
stmt.setObject(2, 74.0445);
stmt.executeUpdate();
}
Doing this helps ensure that the data always uses the same timestamp for all of the aligned fields. This is especially useful when IoTDB provides the current time for us.
7.2. Querying Data
Once we’ve added data to our timeseries, we need to be able to query it. We do this with the SELECT statement in the same way as with traditional SQL databases:
try (PreparedStatement stmt = con.prepareStatement("SELECT * FROM root.baeldung.turbine.device1")) {
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
long timestamp = rs.getLong(1);
float speed = rs.getFloat(2);
// Do something with the data
}
}
}
This works the same as with any SQL database, allowing us to specify the timeseries to query, the fields to return, and any conditions to apply to the returned data:
try (PreparedStatement stmt = con.prepareStatement("SELECT lat FROM root.baeldung.car.device2 WHERE timestamp = ?")) {
stmt.setObject(1, Instant.now().toEpochMilli());
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
long timestamp = rs.getLong(1);
float lat = rs.getFloat(2);
// Do something with the data
}
}
}
Note that we always get the timestamp for the records back, even if we didn’t request it. As such, restricting the returned fields is only useful when querying aligned timeseries that have multiple data fields to return.
8. Summary
In this article, we’ve taken a brief look at the IoTDB time-series database. There’s much more we can do with this system. Next time you need to work with data feeds from IoT devices, why not give it a try?
As usual, all of the examples from this article are available over on GitHub.















