Analyze application events with UUIDv7
Get started with TimescaleDB using application event data and UUIDv7 identifiers, with no separate timestamp column
Get started with TimescaleDB using application event data that leverages UUIDv7 identifiers. This example demonstrates how to handle event logging and analytics using time-embedded UUIDs for partitioning, with no separate timestamp column needed.
What you learn
Section titled “What you learn”- Using UUIDv7 for time-ordered unique identifiers.
- Efficient time-range queries with
to_uuidv7_boundary(). - Session tracking and user analytics.
- Event funnel and conversion analysis.
Prerequisites for this tutorial
To follow these steps, you'll need:
- Docker installed
- The
psqlPostgreSQL client
Set up and query the data
Section titled “Set up and query the data”- Start TimescaleDB
Start TimescaleDB using one of these options.
The easiest way to get started:
ImportantThis script is intended for local development and testing only. Do not use it for production deployments. For production-ready installation options, see the TimescaleDB installation guide.
On Linux or Mac:
Terminal window curl -sL https://tsdb.co/start-local | shThis command:
- Downloads and starts TimescaleDB, if not already downloaded.
- Exposes PostgreSQL on port
6543, a non-standard port that avoids conflicts with other PostgreSQL instances on port 5432. - Automatically tunes settings for your environment using timescaledb-tune.
- Sets up a persistent data volume.
Run TimescaleDB directly with Docker. This option also works on Windows:
Terminal window docker run -d --name timescaledb \-p 6543:5432 \-e POSTGRES_PASSWORD=password \timescale/timescaledb-ha:pg18This example uses port
6543, mapped to container port 5432, to avoid conflicts if you have other PostgreSQL instances running on the standard port 5432.Wait about 1 to 2 minutes for TimescaleDB to download and initialize.
- Connect to TimescaleDB
Connect using
psql:Terminal window psql -h localhost -p 6543 -U postgres# When prompted, enter password: passwordYou should see the PostgreSQL prompt. Verify TimescaleDB is installed:
SELECT extname, extversion FROM pg_extension WHERE extname = 'timescaledb';Expected output:
extname | extversion-------------+------------timescaledb | 2.x.xTipsPrefer a GUI? If you would rather use a graphical tool instead of the command line, you can download pgAdmin and connect to TimescaleDB using the same connection details (host:
localhost, port:6543, user:postgres, password:password). - Create the schema
Create the optimized hypertable by running this SQL in your
psqlsession:-- Create the app_events table with UUIDv7 partitioning-- Note: No separate timestamp column needed - the timestamp is embedded in the UUIDv7!-- Note: No PRIMARY KEY to allow direct compress during COPYCREATE TABLE IF NOT EXISTS app_events (event_id UUID NOT NULL,user_id UUID NOT NULL,session_id UUID NOT NULL,event_type TEXT NOT NULL,event_name TEXT,device_type TEXT,country_code TEXT,category TEXT,page_path TEXT,referrer TEXT,viewport_width INTEGER,element_id TEXT,position_x INTEGER,position_y INTEGER,product_id TEXT,quantity INTEGER,revenue_cents INTEGER) WITH (tsdb.hypertable,tsdb.partition_column = 'event_id',tsdb.segmentby = 'user_id');-- Create index on event_id for lookups (not unique to allow direct compress)CREATE INDEX idx_app_events_event_id ON app_events(event_id);This creates an
app_eventstable with:- UUIDv7 partitioning on
event_id(time is embedded in the UUID). - Segmentation by
user_idfor optimal compression. - No separate timestamp column needed.
- UUIDv7 partitioning on
- Load the sample data
First, download and decompress the sample data:
Terminal window # Download the sample datawget https://assets.timescale.com/timescaledb-datasets/events_uuid.csv.gz# Decompress the CSV filegunzip events_uuid.csv.gz# This will create events_uuid.csv ready for loadingLoad the data using one of these approaches.
This approach writes data directly to the columnstore, bypassing the rowstore entirely. You get instant analytical performance.
From the command line:
Terminal window psql -h localhost -p 6543 -U postgres \-v ON_ERROR_STOP=1 \-c "SET timescaledb.enable_direct_compress_copy = on;COPY app_events FROM STDIN WITH (FORMAT csv, HEADER true);" \< events_uuid.csvThis command reads the CSV file from your local filesystem and pipes it to PostgreSQL, which loads it directly into the columnstore.
Verify the data loaded:
SELECT COUNT(*) FROM app_events;This approach loads data into the rowstore first. Data is converted to the columnstore by a background policy (12 to 24 hours) for faster querying.
From the command line:
Terminal window psql -h localhost -p 6543 -U postgres \-v ON_ERROR_STOP=1 \-c "COPY app_events FROM STDIN WITH (FORMAT csv, HEADER true);" \< events_uuid.csvVerify the data loaded:
SELECT COUNT(*) FROM app_events;If you loaded data using standard copy, a background process converts your rowstore data to the columnstore in 12 to 24 hours. To get the best query performance immediately, you can convert it manually:
DO $$DECLARE ch TEXT;BEGINFOR ch IN SELECT show_chunks('app_events') LOOPCALL convert_to_columnstore(ch);END LOOP;END $$; - Run sample queries
Now explore the data with some analytical queries. Run these in your
psqlsession.Query 1: efficient time range query (chunk pruning).
-- Uses the boundary function for chunk exclusion\timing onSELECT COUNT(*), event_typeFROM app_eventsWHERE event_id >= to_uuidv7_boundary(now() - interval '7 days')GROUP BY event_type;Why this is fast: the
to_uuidv7_boundary()function creates a UUID boundary value that TimescaleDB can use to exclude entire chunks without scanning them.Query 2: inefficient query (anti-pattern).
-- Scans ALL chunks, extracts timestamp from every rowSELECT COUNT(*), event_typeFROM app_eventsWHERE uuid_timestamp(event_id) >= now() - interval '7 days'GROUP BY event_type;Why this is slow: the
uuid_timestamp()function must be evaluated for every row, which prevents chunk exclusion.Query 3: SkipScan on a single column (distinct users).
-- Demonstrates SkipScan optimization: uses the compression index to skip repeated values\timing onSELECT DISTINCT ON (user_id)user_id,event_type,uuid_timestamp(event_id) as event_timeFROM app_eventsWHERE event_id >= to_uuidv7_boundary(now() - interval '30 days')ORDER BY user_id, event_id DESCLIMIT 50;Why this uses SkipScan: because
user_idis thesegmentbycolumn, TimescaleDB automatically creates a compression index on it. SkipScan can jump directly to the next uniqueuser_idvalue instead of scanning all rows. TheWHEREclause ensures chunk exclusion, so it only scans chunks with recent data, making it even faster.To verify SkipScan is used, check the query plan with
EXPLAIN. You should seeCustom Scan (SkipScan)on the compressed chunks instead of a sequential scan.Query 4: funnel analysis.
WITH funnel AS (SELECTuser_id,session_id,MAX(CASE WHEN event_type = 'page_view' THEN 1 ELSE 0 END) as viewed,MAX(CASE WHEN event_type = 'add_to_cart' THEN 1 ELSE 0 END) as added_to_cart,MAX(CASE WHEN event_type = 'purchase' THEN 1 ELSE 0 END) as purchasedFROM app_eventsWHERE event_id >= to_uuidv7_boundary(now() - interval '30 days')GROUP BY user_id, session_id)SELECTSUM(viewed) as sessions_with_view,SUM(added_to_cart) as sessions_with_cart,SUM(purchased) as sessions_with_purchase,ROUND(100.0 * SUM(added_to_cart) / NULLIF(SUM(viewed), 0), 2) as view_to_cart_pct,ROUND(100.0 * SUM(purchased) / NULLIF(SUM(added_to_cart), 0), 2) as cart_to_purchase_pctFROM funnel;Query 5: revenue by country (last 30 days).
SELECTcountry_code,COUNT(*) as purchase_count,SUM(revenue_cents) / 100.0 as total_revenue,ROUND(AVG(revenue_cents) / 100.0, 2) as avg_order_valueFROM app_eventsWHERE event_id >= to_uuidv7_boundary(now() - interval '30 days')AND event_type = 'purchase'GROUP BY country_codeORDER BY total_revenue DESCLIMIT 10;
What's happening behind the scenes?
Section titled “What's happening behind the scenes?”UUIDv7 partitioning
Section titled “UUIDv7 partitioning”When you create a table with tsdb.partition_column = 'event_id' where event_id is a UUIDv7:
- TimescaleDB automatically partitions your data by the time-embedded UUID.
- No separate timestamp column is needed, because the timestamp is embedded in the UUID itself.
- chunk exclusion works with
to_uuidv7_boundary()for efficient time-range queries. - Vectorized UUID compression provides 30% storage savings and 2x query performance.
Columnstore compression
Section titled “Columnstore compression”With tsdb.enable_columnstore=true:
- Data is stored in a hybrid row-columnar format.
- Analytical queries only scan the columns they need, for a large speedup.
- Typical compression ratios: 90% or more for time-series data.
- Compression happens transparently, with no changes to your queries.
Direct to columnstore
Section titled “Direct to columnstore”When you use SET timescaledb.enable_direct_compress_copy = on:
- Data loads directly into compressed columnstore format.
- Bypasses the rowstore entirely.
- Provides instant analytical performance, with no waiting for background compression.
Segmentation
Section titled “Segmentation”The tsdb.segmentby='user_id' setting:
- Groups data by user within each chunk.
- Improves compression ratios by keeping similar data together.
- Speeds up queries that filter by
user_id. - Is better for user-based analytics.
UUIDv7 functions
Section titled “UUIDv7 functions”TimescaleDB provides comprehensive UUIDv7 functionality across all supported PostgreSQL versions (including PostgreSQL 15, 16, and 17), while PostgreSQL only provides UUIDv7 support in PostgreSQL 18.
For complete documentation on all available UUIDv7 functions, see the UUIDv7 functions reference.
Continuous aggregates (Advanced)
Section titled “Continuous aggregates (Advanced)”For real-time dashboards, you can create continuous aggregates that automatically update:
-- Create a continuous aggregate for hourly event statisticsCREATE MATERIALIZED VIEW app_events_hourlyWITH (timescaledb.continuous) ASSELECT time_bucket('1 hour', uuid_timestamp(event_id)) AS hour, event_type, COUNT(*) as event_count, COUNT(DISTINCT user_id) as unique_users, SUM(revenue_cents) / 100.0 as total_revenueFROM app_eventsGROUP BY hour, event_type;
-- Add a refresh policy to keep it updatedSELECT add_continuous_aggregate_policy('app_events_hourly', start_offset => INTERVAL '2 hours', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour');Now you can query app_events_hourly for instant results on pre-aggregated data.
Troubleshooting
Section titled “Troubleshooting”Data didn't load
Section titled “Data didn't load”- Check the CSV file path is correct.
- Ensure the CSV header matches the schema columns.
- Try loading a few rows first to test:
LIMIT 10in your data file.
Direct to columnstore not working
Section titled “Direct to columnstore not working”- Verify TimescaleDB version 2.24 or later:
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';. - Ensure you ran
SET timescaledb.enable_direct_compress_copy = on;in the same session. - Check for error messages in the output.
Queries seem slow
Section titled “Queries seem slow”- Verify you're using
to_uuidv7_boundary()for time-range queries, notuuid_timestamp(). - Check if data is compressed:
SELECT * FROM timescaledb_information.chunks WHERE hypertable_name = 'app_events';. - Ensure chunk exclusion is working: use
EXPLAIN ANALYZEto see chunk pruning.
UUIDv7 functions not found
Section titled “UUIDv7 functions not found”- Verify your TimescaleDB version supports UUIDv7:
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';. - UUIDv7 support requires TimescaleDB 2.24 or later with the uuidv7 extension enabled.
Use cases
Section titled “Use cases”This application events example demonstrates patterns applicable to:
- SaaS analytics: user behavior tracking, feature usage, and conversion funnels.
- E-commerce: shopping cart analysis, purchase patterns, and product recommendations.
- Application monitoring: error tracking, performance metrics, and user sessions.
- Audit logging: security events, compliance tracking, and change history.
- Event-driven architectures: microservices event sourcing and message queues.
- A/B testing: experiment tracking, variant analysis, and statistical significance.
Clean up
Section titled “Clean up”When you're done experimenting, remove the container and data.
If you used the one-line install:
# Stop the containerdocker stop timescaledb-ha-pg18-quickstart
# Remove the containerdocker rm timescaledb-ha-pg18-quickstart
# Remove the persistent data volumedocker volume rm timescaledb_data
# (Optional) Remove the Docker imagedocker rmi timescale/timescaledb-ha:pg18If you used the manual Docker command:
# Stop the containerdocker stop timescaledb
# Remove the containerdocker rm timescaledb
# (Optional) Remove the Docker imagedocker rmi timescale/timescaledb-ha:pg18If you created a named volume with the manual Docker command, you can remove it with docker volume rm <volume_name>.