Our company works in the water sector. Our pump room is equipped with a variety of sensors—monitoring liquid levels, pressure, valve openings, and so on. Previously, we stored all this data in PostgreSQL and MongoDB. Recently, we've been looking for a more suitable solution and explored time-series databases, specifically trying out InfluxDB.
What Is a Time Series Database
A Time Series Database (TSDB), as the name suggests, is a database designed specifically for timestamped data.
Consider these scenarios: servers reporting CPU usage every second, IoT devices reporting temperature every minute, or stock prices fluctuating every millisecond. These data points share a common characteristic:
- Write-heavy, read-light: massive write volumes with occasional queries
- Time-based queries: typically structured as "last 5 minutes" or "past 1 hour"
- Immutable data: once written, records are rarely updated or modified
Traditional relational databases can certainly store this data, but Time Series Databases offer significant optimizations for write performance and time-range queries, resulting in a noticeable performance gap.
There are many Time Series Databases available on the market, such as InfluxDB, TimescaleDB, and TDengine. I am using InfluxDB in this project because it has an active community, comprehensive documentation, and an officially maintained .NET client, making it a hassle-free choice.
Some Concepts in InfluxDB
Before using InfluxDB, there are a few concepts you need to understand first, otherwise reading the code will be confusing.
Bucket
A Bucket is a container for data, similar to a "database" in a relational database. When you create a Bucket, data is written into it. During creation, you can also configure data retention policies, such as keeping only the most recent 30 days of data, with expired data automatically deleted, which is quite convenient.
Measurement
A Measurement is equivalent to a "table" in a relational database. For example, if I want to store system metrics, I create a Measurement called system_metrics; to store disk information, I create another one called disk_metrics.
Tag
This is a key point. A Tag is a key-value pair with an index, used to identify the dimensional information of the data.
For example, suppose I have three servers all reporting CPU usage. How do I distinguish which machine the data belongs to? By using Tags. For instance, add a host Tag with values server-01, server-02, and server-03.
Key characteristics of Tags:
- Indexed, so filtering by Tag during queries is very fast
- Stores strings, meaning Tag values can only be of string type
- Suitable for low-cardinality classification information, such as hostnames, regions, or device types
What does low cardinality mean? It means the field has a limited number of possible values. For instance, if there are only a few dozen hostnames, it’s a good fit for a Tag. However, if you stuff something like user IDs—where there could be millions of distinct values—into a Tag, the index will break down and performance will degrade significantly.
Field
A Field is the actual stored metric value—the data you truly care about. For example, a CPU usage of 45.2% or 8192 MB of memory used; these numerical values are Fields.
The biggest difference between a Field and a Tag is: Fields are not indexed. Therefore, you should not use Fields as filter conditions in queries (though it is possible, it will be very slow).
A simple way to remember: Tags represent "who," while Fields represent "how much."
Timestamp
Every data point must have a timestamp; it is the soul of a time-series database. InfluxDB supports second, millisecond, microsecond, and nanosecond precision—choose the level that fits your needs.
Understanding It as a Whole
A data point written to InfluxDB looks like this:
system_metrics,host=MY-SERVER cpu_usage_percent=45.2,memory_used_mb=8192 1720310400
│ │ │ │
Measurement Tag Field(s) Timestamp
A side-by-side comparison table might be more intuitive:
| InfluxDB Concept | Relational Database Analogy | Description |
|---|---|---|
| Bucket | Database | Data container |
| Measurement | Table | Data table |
| Tag | Indexed Column | Classification dimension, indexed |
| Field | Regular Column | Metric value, not indexed |
| Timestamp | Primary Key (Time) | Timestamp for each data record |
Organization
InfluxDB also has the concept of an Organization (Org), which serves as the isolation unit for a tenant or organization. You can think of it as the outermost namespace, with Buckets nested underneath an Org. For personal use, you can simply pick any name you like.
.NET Operations for InfluxDB
Now that we've covered the concepts, let's get down to the practical stuff.
Install NuGet Package
dotnet add package InfluxDB.Client
Configure Connection Information
First, configure the connection information in appsettings.json:
{
"InfluxDb": {
"Url": "https://your-influxdb-server",
"Token": "your-api-token",
"Org": "your-org",
"Bucket": "your-bucket"
}
}
Then create a configuration class to receive it:
public class InfluxDbConfig
{
public string Url { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
public string Org { get; set; } = string.Empty;
public string Bucket { get; set; } = string.Empty;
}
Create a Client
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json")
.Build();
var influxConfig = new InfluxDbConfig();
config.GetSection("InfluxDb").Bind(influxConfig);
using var client = new InfluxDBClient(influxConfig.Url, influxConfig.Token);
Writing Data
To write data, use PointData to build data points. The chained calls make it quite comfortable to write:
var writeApi = client.GetWriteApiAsync();
var point = PointData.Measurement("system_metrics")
.Tag("host", Environment.MachineName) // Tag:哪台机器
.Field("cpu_usage_percent", 45.2) // Field:CPU 使用率
.Field("cpu_frequency_mhz", 3200) // Field:CPU 频率
.Field("memory_total_mb", 16384.0) // Field:总内存
.Field("memory_used_mb", 8192.0) // Field:已用内存
.Field("memory_usage_percent", 50.0) // Field:内存使用率
.Timestamp(DateTime.UtcNow, WritePrecision.S); // 时间戳,精确到秒
await writeApi.WritePointAsync(point, influxConfig.Bucket, influxConfig.Org);
Keep the following points in mind:
- Use
Tag()for categorical information andField()for metric values - It is recommended to use UTC time for timestamps, then convert to local time during queries
WritePrecision.Sindicates precision to the second, which is sufficient for monitoring scenarios
If you have data across multiple dimensions, such as disk information that needs to be distinguished by drive letter, simply add more Tags:
var diskPoint = PointData.Measurement("disk_metrics")
.Tag("host", Environment.MachineName)
.Tag("drive", "C:") // 盘符
.Tag("label", "System") // 卷标
.Field("total_gb", 500.0)
.Field("free_gb", 200.0)
.Field("usage_percent", 60.0)
.Timestamp(DateTime.UtcNow, WritePrecision.S);
Query Data
Queries use InfluxDB's own query language, Flux. The syntax is completely different from SQL, so it may feel a bit unfamiliar at first, but it becomes second nature after a few uses.
var flux = """
from(bucket: "your-bucket")
|> range(start: -10m)
|> filter(fn: (r) => r._measurement == "system_metrics")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|> sort(columns: ["_time"], desc: true)
|> limit(n: 20)
""";
var queryApi = client.GetQueryApi();
var tables = await queryApi.QueryAsync(flux, influxConfig.Org);
foreach (var table in tables)
{
foreach (var record in table.Records)
{
var time = record.GetTimeInDateTime()?.ToLocalTime();
var cpuUsage = record.Values.GetValueOrDefault("cpu_usage_percent");
var memUsage = record.Values.GetValueOrDefault("memory_usage_percent");
Console.WriteLine($"{time:yyyy-MM-dd HH:mm:ss} CPU: {cpuUsage}% 内存: {memUsage}%");
}
}
Breaking down a Flux query:
| Statement | Purpose |
|---|---|
from(bucket: "...") |
Specifies which Bucket to query |
range(start: -10m) |
Queries the last 10 minutes |
filter(fn: (r) => ...) |
Filter condition; here it filters by Measurement |
pivot(...) |
Transposes Fields from rows to columns, similar to SQL's PIVOT |
sort(...) |
Sorts by time |
limit(n: 20) |
Retrieves only 20 records |
The pivot step is quite critical. InfluxDB returns each Field on a separate row by default. After using pivot to convert them into columns, a single record contains all Fields, making processing much more convenient.
Finally
That's it. Once you understand the difference between Tag and Field, you're basically ready to get started.
Clickhouse也不错!不过InfluxDB工业领域确实用得多!