When we talk about observability data, we inevitably deal with the holy trinity: Metrics, Logs, and Traces. Much like how a doctor needs body temperature, blood reports, and CT scans to assess a patient's health, these three data types are essential for gaining a comprehensive view of system behavior.
In this article, weโll walk through how to build an efficient and flexible "Medical Record System" for your infrastructure using GreptimeDB, a comprehensive database purpose-built for observability workloads.
๐งฉ Step 1: Understand the Data Before Modeling โ
Before diving into schema design, itโs crucial to understand two foundational concepts:
1. Column Cardinality: How 'Unique' is Your Data โ
Imagine you're managing a library:
- Low-cardinality columns are like book genresโSci-Fi, History, Art. These usually contain only hundreds or thousands of unique values. Examples include:
regioncluster_namehttp_method
- High-cardinality columns are more like ISBNs or user IDsโpotentially millions or even billions of unique values, such as:
trace_iduser_idrequest_id
In one e-commerce observability project, region had only 7 values (low cardinality), while user_id reached the billion-scale (high cardinality). This distinction may seem trivial, but it has massive implications for performance.
2. Column Roles: Tag, Field, and Timestamp โ
Every column in GreptimeDB plays a specific role:
- Tag: Used in the primary key; determines how data is deduplicated and sorted.
- Field: Stores actual measurements or non-key values.
- Timestamp: Marked with
TIME INDEXconstraint; required for time-series context.
Example:
CREATE TABLE http_logs (
access_time TIMESTAMP TIME INDEX,
application STRING,
http_status INT,
request_id STRING,
PRIMARY KEY(application)
);๐ Step 2: Designing the Primary Key โ Choosing the Right Identifier โ
Scenario 1: Log Collection Without Primary Key โ
For write-heavy logging workloads, using no primary key with append-only mode can maximize performance:
CREATE TABLE access_logs (
timestamp TIMESTAMP TIME INDEX,
service STRING,
status_code INT,
response_time DOUBLE,
trace_id STRING
) with ('append_mode'='true');This pattern is ideal for 'write once, never update' use cases and can improve write performance by over 30%.
Scenario 2: Preferences for Low-cardinality Columns โ
When updates or dimension-based queries are required, thoughtful key design is critical. Golden rules:
- โ
Choose only low-cardinality columns (avoid
trace_id,user_id). - โ Keep the cardinality of the key combination under 100k.
- โ Use โค 5 key columns.
- โ Prefer strings and integers as keys, avoid floats and timestamps.
Example:
CREATE TABLE service_metrics (
datacenter STRING,
service_name STRING,
cpu_util DOUBLE,
memory_util DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(datacenter, service_name),
TIME INDEX(ts)
);๐ Step 3: Accelerate Queries with the Right Indexes โ
GreptimeDB provides three types of indexes. Use them based on your query and data characteristics:
Inverted Index โ Precise and Categorized Retrieval โ
Best for low-cardinality filtering:
CREATE TABLE api_metrics (
endpoint
STRING,
service_name STRING INVERTED INDEX,
http_status INT INVERTED INDEX,
latency DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(service_name,
endpoint
),
TIME INDEX(ts)
);Supports =, <, >, IN, and BETWEEN with Inverted Index.
Skipping Index โ Efficient on High-Cardinality Data โ
Great for selective equality filters on massive datasets:
CREATE TABLE user_events (
user_id STRING SKIPPING INDEX,
event_type STRING,
device STRING,
ts TIMESTAMP,
TIME INDEX(ts)
) with ('append_mode'='true');The skipping index is a very efficient option: it requires less storage and has minimal impact on write performance. However, it only supports equality queries.
Fulltext Index โ Log Keyword Search โ
CREATE TABLE error_logs (
message STRING FULLTEXT INDEX WITH(analyzer = 'English'),
level
STRING INVERTED INDEX,
ts TIMESTAMP,
TIME INDEX(ts)
) with ('append_mode'='true');Allows expressive queries like:
SELECT * FROM error_logs WHERE matches(message, 'connection timeout');๐งฎ Step 4: Wide Table vs. Multi-Table Schema โ
Wide Table โ A Supermarket of Metrics โ
Our best practice: store related metrics in a wide table, especially when collected together:
CREATE TABLE node_metrics (
host STRING,
cpu_user DOUBLE,
cpu_system DOUBLE,
memory_used DOUBLE,
disk_read_bytes DOUBLE,
disk_write_bytes DOUBLE,
net_in_bytes DOUBLE,
net_out_bytes DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(host),
TIME INDEX(ts)
);Benefits:
- ๐ Better compression (30โ50% less storage);
- ๐ Simplified queries (no
JOINneeded).
When to Use Multiple Tables โ
Split metrics into multiple tables only when:
- Metrics have very different collection frequencies (e.g., per second vs. hourly).
- Tag schema varies widely.
- Access control requires separation.
๐ Step 5: Merge Modes โ Update or Deduplicate Data โ
GreptimeDB supports two deduplication modes:
last_row: Row-Level Deduplication โ
Default mode last_row โ entire rows are replaced by the most recent entry.
last_non_null: Field-Level Updates โ
Update only selected fields โ using last_non_null is a better choice.
CREATE TABLE device_telemetry (
device_id STRING,
temperature DOUBLE,
humidity DOUBLE,
battery DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(device_id),
TIME INDEX(ts)
) with ('merge_mode'='last_non_null');Update example:
INSERT INTO device_telemetry(device_id, temperature, ts)
VALUES ('device1', 25.5, now());๐ Step 6: Distributed Table Design โ Scale to Petabyte Level โ
When data exceeds TB scale, distributed partitioning is essential:
CREATE TABLE global_metrics (
region STRING,
datacenter STRING,
host STRING,
cpu DOUBLE,
memory DOUBLE,
ts TIMESTAMP,
PRIMARY KEY(region, datacenter, host),
TIME INDEX(ts)
) PARTITION ON COLUMNS (region) (
region = 'region1',
region = 'region2',
region = 'region3'
);Best practice: partition by columns with even distribution and query-aligned patternsโregion, business unit, or app name work well.
๐ ๏ธ Case Study Analysis โ
Case 1: Large-Scale API Monitoring โ
For a system handling billions of API requests daily, we use:
CREATE TABLE api_metrics (
region STRING,
service STRING,
endpoint
STRING,
status_code INT INVERTED INDEX,
p50_latency DOUBLE,
p95_latency DOUBLE,
p99_latency DOUBLE,
error_count INT,
success_count INT,
ts TIMESTAMP,
PRIMARY KEY(region, service,
endpoint
),
TIME INDEX(ts)
) PARTITION ON COLUMNS (region) (
region = 'us-east-1',
region = 'eu-central-1',
region = 'ap-southeast-2'
) with ('merge_mode'='last_non_null');Advantages of this design:
- Uses multi-level, low-cardinality primary keys, making it easy to filter by service or endpoint.
- Stores related metrics in the same row, simplifying queries and improving compression.
- Creates an index on status codes to support fast filtering of error requests.
- Partitions by region to reduce cross-region query latency.
Case 2: IoT Device Monitoring Platform โ
For a platform that monitors hundreds of thousands of IoT devices, we adopted the following schema:
CREATE TABLE device_metrics (
device_type STRING,
`location` STRING,
device_id STRING SKIPPING INDEX,
temperature DOUBLE,
humidity DOUBLE,
battery_level DOUBLE,
signal_strength DOUBLE,
alert_count INT,
ts TIMESTAMP,
PRIMARY KEY(device_type, `location`),
TIME INDEX(ts)
) PARTITION ON COLUMNS (device_type) (
device_type = 'sensor',
device_type = 'actuator',
device_type = 'controller'
);Benefits:
- Uses device_type and location as low-cardinality primary keys instead of the high-cardinality device_id.
- Adds a skipping index on device_id to allow fast lookup of individual devices.
- Stores related sensor metrics in the same row to improve query performance.
- Partitions by device type for easier management of retention policies across device categories.
๐ก Performance Tuning Tips โ
Based on our hands-on experience, here are eight practical tips to significantly improve performance:
- Start simple: Begin with basic tables without primary keys to establish a performance baseline;
- Avoid over-indexing: Indexes increase write overhead โ only index frequently used query conditions;
- Monitor table size: Consider partitioning when a single table exceeds 500GB;
- Time partitioning is built-in: GreptimeDB automatically organizes data by time โ no extra steps needed;
- Watch for hot partitions: Ensure write and query loads are evenly distributed across partitions;
- Set TTLs wisely: Use different data retention periods based on importance;
- Capacity planning: Reserve at least 50% of system resources for queries after meeting write throughput;
- Downsampling strategy: Use Flow tasks to downsample data (e.g., from per-second to per-minute granularity).
๐ Flow Documentation
Conclusion โ
Designing for observability isnโt just about storing dataโitโs about making it accessible, fast, and scalable. With the right schema and indexing strategies, GreptimeDB can serve as a powerful foundation for your metrics, logs, and trace workloads.
Author's note: This article is based on features available in GreptimeDB v0.13 and above. Some details may change as new versions are released.
You're welcome to check out the official Data Modeling Guide.
About Greptime โ
Greptime offers industry-leading time series database products and solutions to empower IoT and Observability scenarios, enabling enterprises to uncover valuable insights from their data with less time, complexity, and cost.
GreptimeDB is an open-source, high-performance time-series database offering unified storage and analysis for metrics, logs, and events. Try it out instantly with GreptimeCloud, a fully-managed DBaaS solutionโno deployment needed!
The Edge-Cloud Integrated Solution combines multimodal edge databases with cloud-based GreptimeDB to optimize IoT edge scenarios, cutting costs while boosting data performance.
Star us on GitHub or join GreptimeDB Community on Slack to get connected.


