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 and Field() for metric values
  • It is recommended to use UTC time for timestamps, then convert to local time during queries
  • WritePrecision.S indicates 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.