TigerData logo
TigerData logo
  • Product

    Tiger Cloud

    Robust elastic cloud platform for startups and enterprises

    Agentic Postgres

    Postgres for Agents

    TimescaleDB

    Postgres for time-series, real-time analytics and events

  • Docs
  • Pricing

    Pricing

    Enterprise Tier

  • Developer Hub

    Changelog

    Benchmarks

    Blog

    Community

    Customer Stories

    Events

    Support

    Integrations

    Launch Hub

  • Company

    Contact us

    About

    Timescale

    Partners

    Security

    Careers

Log InTry for free
Home
AWS Time-Series Database: Understanding Your OptionsStationary Time-Series AnalysisThe Best Time-Series Databases ComparedTime-Series Analysis and Forecasting With Python Alternatives to TimescaleWhat Are Open-Source Time-Series Databases—Understanding Your OptionsWhy Consider Using PostgreSQL for Time-Series Data?Time-Series Analysis in RWhat Is Temporal Data?What Is a Time Series and How Is It Used?Is Your Data Time Series? Data Types Supported by PostgreSQL and TimescaleUnderstanding Database Workloads: Variable, Bursty, and Uniform PatternsHow to Work With Time Series in Python?Tools for Working With Time-Series Analysis in PythonGuide to Time-Series Analysis in PythonUnderstanding Autoregressive Time-Series ModelingCreating a Fast Time-Series Graph With Postgres Materialized Views
Understanding PostgreSQLOptimizing Your Database: A Deep Dive into PostgreSQL Data TypesUnderstanding FROM in PostgreSQL (With Examples)How to Address ‘Error: Could Not Resize Shared Memory Segment’ How to Install PostgreSQL on MacOSUnderstanding FILTER in PostgreSQL (With Examples)Understanding GROUP BY in PostgreSQL (With Examples)PostgreSQL Join Type TheoryA Guide to PostgreSQL ViewsStructured vs. Semi-Structured vs. Unstructured Data in PostgreSQLUnderstanding Foreign Keys in PostgreSQLUnderstanding PostgreSQL User-Defined FunctionsUnderstanding PostgreSQL's COALESCE FunctionUnderstanding SQL Aggregate FunctionsUsing PostgreSQL UPDATE With JOINHow to Install PostgreSQL on Linux5 Common Connection Errors in PostgreSQL and How to Solve ThemUnderstanding HAVING in PostgreSQL (With Examples)How to Fix No Partition of Relation Found for Row in Postgres DatabasesHow to Fix Transaction ID Wraparound ExhaustionUnderstanding LIMIT in PostgreSQL (With Examples)Understanding PostgreSQL FunctionsUnderstanding ORDER BY in PostgreSQL (With Examples)Understanding WINDOW in PostgreSQL (With Examples)Understanding PostgreSQL WITHIN GROUPPostgreSQL Mathematical Functions: Enhancing Coding EfficiencyUnderstanding DISTINCT in PostgreSQL (With Examples)Using PostgreSQL String Functions for Improved Data AnalysisData Processing With PostgreSQL Window FunctionsPostgreSQL Joins : A SummaryUnderstanding OFFSET in PostgreSQL (With Examples)Understanding PostgreSQL Date and Time FunctionsWhat Is Data Compression and How Does It Work?What Is Data Transformation, and Why Is It Important?Understanding the Postgres string_agg FunctionWhat Is a PostgreSQL Left Join? And a Right Join?Understanding PostgreSQL SELECTSelf-Hosted or Cloud Database? A Countryside Reflection on Infrastructure ChoicesUnderstanding ACID Compliance Understanding percentile_cont() and percentile_disc() in PostgreSQLUnderstanding PostgreSQL Conditional FunctionsUnderstanding PostgreSQL Array FunctionsWhat Characters Are Allowed in PostgreSQL Strings?Understanding WHERE in PostgreSQL (With Examples)What Is a PostgreSQL Full Outer Join?What Is a PostgreSQL Cross Join?What Is a PostgreSQL Inner Join?Data Partitioning: What It Is and Why It MattersStrategies for Improving Postgres JOIN PerformanceUnderstanding the Postgres extract() FunctionUnderstanding the rank() and dense_rank() Functions in PostgreSQL
Guide to PostgreSQL PerformanceHow to Reduce Bloat in Large PostgreSQL TablesDesigning Your Database Schema: Wide vs. Narrow Postgres TablesBest Practices for Time-Series Data Modeling: Single or Multiple Partitioned Table(s) a.k.a. Hypertables Best Practices for (Time-)Series Metadata Tables A Guide to Data Analysis on PostgreSQLA Guide to Scaling PostgreSQLGuide to PostgreSQL SecurityHandling Large Objects in PostgresHow to Query JSON Metadata in PostgreSQLHow to Query JSONB in PostgreSQLHow to Use PostgreSQL for Data TransformationOptimizing Array Queries With GIN Indexes in PostgreSQLPg_partman vs. Hypertables for Postgres PartitioningPostgreSQL Performance Tuning: Designing and Implementing Your Database SchemaPostgreSQL Performance Tuning: Key ParametersPostgreSQL Performance Tuning: Optimizing Database IndexesDetermining the Optimal Postgres Partition SizeNavigating Growing PostgreSQL Tables With Partitioning (and More)Top PostgreSQL Drivers for PythonWhen to Consider Postgres PartitioningGuide to PostgreSQL Database OperationsUnderstanding PostgreSQL TablespacesWhat Is Audit Logging and How to Enable It in PostgreSQLGuide to Postgres Data ManagementHow to Index JSONB Columns in PostgreSQLHow to Monitor and Optimize PostgreSQL Index PerformanceSQL/JSON Data Model and JSON in SQL: A PostgreSQL PerspectiveA Guide to pg_restore (and pg_restore Example)PostgreSQL Performance Tuning: How to Size Your DatabaseAn Intro to Data Modeling on PostgreSQLExplaining PostgreSQL EXPLAINWhat Is a PostgreSQL Temporary View?A PostgreSQL Database Replication GuideHow to Compute Standard Deviation With PostgreSQLHow PostgreSQL Data Aggregation WorksBuilding a Scalable DatabaseRecursive Query in SQL: What It Is, and How to Write OneGuide to PostgreSQL Database DesignHow to Use Psycopg2: The PostgreSQL Adapter for Python
Best Practices for Scaling PostgreSQLHow to Design Your PostgreSQL Database: Two Schema ExamplesHow to Handle High-Cardinality Data in PostgreSQLHow to Store Video in PostgreSQL Using BYTEABest Practices for PostgreSQL Database OperationsHow to Manage Your Data With Data Retention PoliciesBest Practices for PostgreSQL AggregationBest Practices for Postgres Database ReplicationHow to Use a Common Table Expression (CTE) in SQLBest Practices for Postgres Data ManagementBest Practices for Postgres PerformanceBest Practices for Postgres SecurityBest Practices for PostgreSQL Data AnalysisTesting Postgres Ingest: INSERT vs. Batch INSERT vs. COPYHow to Use PostgreSQL for Data Normalization
PostgreSQL Extensions: amcheckPostgreSQL Extensions: Unlocking Multidimensional Points With Cube PostgreSQL Extensions: hstorePostgreSQL Extensions: ltreePostgreSQL Extensions: Secure Your Time-Series Data With pgcryptoPostgreSQL Extensions: pg_prewarmPostgreSQL Extensions: pgRoutingPostgreSQL Extensions: pg_stat_statementsPostgreSQL Extensions: Install pg_trgm for Data MatchingPostgreSQL Extensions: Turning PostgreSQL Into a Vector Database With pgvectorPostgreSQL Extensions: Database Testing With pgTAPPostgreSQL Extensions: PL/pgSQLPostgreSQL Extensions: Using PostGIS and Timescale for Advanced Geospatial InsightsPostgreSQL Extensions: Intro to uuid-ossp
Columnar Databases vs. Row-Oriented Databases: Which to Choose?Data Analytics vs. Real-Time Analytics: How to Pick Your Database (and Why It Should Be PostgreSQL)How to Choose a Real-Time Analytics DatabaseUnderstanding OLTPOLAP Workloads on PostgreSQL: A GuideHow to Choose an OLAP DatabasePostgreSQL as a Real-Time Analytics DatabaseWhat Is the Best Database for Real-Time AnalyticsHow to Build an IoT Pipeline for Real-Time Analytics in PostgreSQL
When Should You Use Full-Text Search vs. Vector Search?HNSW vs. DiskANNA Brief History of AI: How Did We Get Here, and What's Next?A Beginner’s Guide to Vector EmbeddingsPostgreSQL as a Vector Database: A Pgvector TutorialUsing Pgvector With PythonHow to Choose a Vector DatabaseVector Databases Are the Wrong AbstractionUnderstanding DiskANNA Guide to Cosine SimilarityStreaming DiskANN: How We Made PostgreSQL as Fast as Pinecone for Vector DataImplementing Cosine Similarity in PythonVector Database Basics: HNSWVector Database Options for AWSVector Store vs. Vector Database: Understanding the ConnectionPgvector vs. Pinecone: Vector Database Performance and Cost ComparisonHow to Build LLM Applications With Pgvector Vector Store in LangChainHow to Implement RAG With Amazon Bedrock and LangChainRetrieval-Augmented Generation With Claude Sonnet 3.5 and PgvectorRAG Is More Than Just Vector SearchPostgreSQL Hybrid Search Using Pgvector and CohereImplementing Filtered Semantic Search Using Pgvector and JavaScriptRefining Vector Search Queries With Time Filters in Pgvector: A TutorialUnderstanding Semantic SearchWhat Is Vector Search? Vector Search vs Semantic SearchText-to-SQL: A Developer’s Zero-to-Hero GuideNearest Neighbor Indexes: What Are IVFFlat Indexes in Pgvector and How Do They WorkBuilding an AI Image Gallery With OpenAI CLIP, Claude Sonnet 3.5, and Pgvector
Understanding IoT (Internet of Things)A Beginner’s Guide to IIoT and Industry 4.0Storing IoT Data: 8 Reasons Why You Should Use PostgreSQLMoving Past Legacy Systems: Data Historian vs. Time-Series DatabaseWhy You Should Use PostgreSQL for Industrial IoT DataHow to Choose an IoT DatabaseHow to Simulate a Basic IoT Sensor Dataset on PostgreSQLFrom Ingest to Insights in Milliseconds: Everactive's Tech Transformation With TimescaleHow Ndustrial Is Providing Fast Real-Time Queries and Safely Storing Client Data With 97 % CompressionHow Hopthru Powers Real-Time Transit Analytics From a 1 TB Table Migrating a Low-Code IoT Platform Storing 20M Records/DayHow United Manufacturing Hub Is Introducing Open Source to ManufacturingBuilding IoT Pipelines for Faster Analytics With IoT CoreVisualizing IoT Data at Scale With Hopara and TimescaleDB
What Is ClickHouse and How Does It Compare to PostgreSQL and TimescaleDB for Time Series?Timescale vs. Amazon RDS PostgreSQL: Up to 350x Faster Queries, 44 % Faster Ingest, 95 % Storage Savings for Time-Series DataWhat We Learned From Benchmarking Amazon Aurora PostgreSQL ServerlessTimescaleDB vs. Amazon Timestream: 6,000x Higher Inserts, 5-175x Faster Queries, 150-220x CheaperHow to Store Time-Series Data in MongoDB and Why That’s a Bad IdeaPostgreSQL + TimescaleDB: 1,000x Faster Queries, 90 % Data Compression, and Much MoreEye or the Tiger: Benchmarking Cassandra vs. TimescaleDB for Time-Series Data
Alternatives to RDSWhy Is RDS so Expensive? Understanding RDS Pricing and CostsEstimating RDS CostsHow to Migrate From AWS RDS for PostgreSQL to TimescaleAmazon Aurora vs. RDS: Understanding the Difference
5 InfluxDB Alternatives for Your Time-Series Data8 Reasons to Choose Timescale as Your InfluxDB Alternative InfluxQL, Flux, and SQL: Which Query Language Is Best? (With Cheatsheet)What InfluxDB Got WrongTimescaleDB vs. InfluxDB: Purpose Built Differently for Time-Series Data
5 Ways to Monitor Your PostgreSQL DatabaseHow to Migrate Your Data to Timescale (3 Ways)Postgres TOAST vs. Timescale CompressionBuilding Python Apps With PostgreSQL: A Developer's GuideData Visualization in PostgreSQL With Apache SupersetMore Time-Series Data Analysis, Fewer Lines of Code: Meet HyperfunctionsIs Postgres Partitioning Really That Hard? An Introduction To HypertablesPostgreSQL Materialized Views and Where to Find ThemTimescale Tips: Testing Your Chunk Size
Postgres cheat sheet
HomeTime series basicsPostgres basicsPostgres guidesPostgres best practicesPostgres extensionsPostgres for real-time analytics
Sections

Performance

Best Practices for Postgres PerformanceTesting Postgres Ingest: INSERT vs. Batch INSERT vs. COPY

Database design and modeling

How to Design Your PostgreSQL Database: Two Schema ExamplesHow to Handle High-Cardinality Data in PostgreSQLHow to Use PostgreSQL for Data Normalization

Database operations

Best Practices for PostgreSQL Database Operations

Data analysis

Best Practices for PostgreSQL Data Analysis

Data aggregation

Best Practices for PostgreSQL Aggregation

Database replication

Best Practices for Postgres Database Replication

Query optimization

How to Use a Common Table Expression (CTE) in SQL

Scaling postgres

Best Practices for Scaling PostgreSQL

Data management

How to Manage Your Data With Data Retention PoliciesHow to Store Video in PostgreSQL Using BYTEABest Practices for Postgres Data Management

Database security

Best Practices for Postgres Security

Products

Time Series and Analytics AI and Vector Enterprise Plan Cloud Status Support Security Cloud Terms of Service

Learn

Documentation Blog Forum Tutorials Changelog Success Stories Time Series Database

Company

Contact Us Careers About Brand Community Code Of Conduct Events

Subscribe to the Tiger Data Newsletter

By submitting, you acknowledge Tiger Data's Privacy Policy

2025 (c) Timescale, Inc., d/b/a Tiger Data. All rights reserved.

Privacy preferences
LegalPrivacySitemap

Published at Mar 5, 2024

SQL Queries

How to Use a Common Table Expression (CTE) in SQL

A tiger developer coding on his computing (and probably using a common table expression).

Written by Dylan Paulus

Common Table Expressions (CTEs) are my go-to tool when writing complex SQL queries. They can be thought of as working like sub-queries or temporary tables but with their own superpowers.

In this article, we'll take a deep dive into what CTEs are and how they can simplify your queries, how recursive CTES can turn impossible PostgreSQL queries into possible, and how to use CTEs to create optimization fences to improve the performance of your queries.

Finally, at the end of the article, we'll discuss Timescale's hyperfunctions and how they improve and simplify our queries like CTEs do.

What Is a Common Table Expression?

Common Table Expressions are auxiliary queries that provide a temporary result set to a primary query. In other words, think of CTEs as a temporary table that only exists for the duration of a query. To get started, let's break down the structure of CTEs in SQL:

WITH [cte name] AS ( [auxiliary query] ) [primary query]

Breakdown:

  • WITH clause: Starts the common table expression and comes before the primary query. (You may also encounter people calling CTEs "With statements.")

  • CTE name: Defines the name of the CTE that can be referenced in the primary query—the name must be unique within the query.

  • AS: The As keyword starts the CTE definition.

  • Auxiliary query: This defines the CTE and "populates the temporary table."

  • Primary query: The primary query is our main SQL query—we can reference the CTE by name and use it like a regular table.

Common table expressions are not limited to just one auxiliary statement. We can separate auxiliary statements with a comma to provide any number of CTEs.

WITH [cte name] AS ( [query] -- auxiliary one statement ), [cte 2 name] AS ( [query] -- auxiliary two statement ), [cte 3 name] AS ( [query] -- auxiliary three statement ) [primary query] -- primary statement

Now that we know the structure of CTEs, let's dive into an example to solidify our mental model of CTEs and learn why they're helpful!

Using CTEs

For this example, we manage an employee management system. The database contains two tables: a departments table and a employees table. The database schema looks like this:

CREATE TABLE departments ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL ); CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, department_id INTEGER REFERENCES departments(id), salary DECIMAL(10, 2), hire_date DATE );

Our manager has given us a task to determine how much money it costs to operate each department. Using CTEs, we can break the task up into two steps.

First, we need to sum up the salaries for every employee per department. The SQL to do this would look like this:

SELECT department_id, SUM(salary) AS total_salary FROM employees GROUP BY department_id

Second, we need to list and order the departments by the highest-costing departments. The query would be:

SELECT d.name, [department budgets].cost FROM [department budgets] ORDER BY [department budgets].cost DESC;

Since we have yet to learn each department's cost, we need to write placeholders for now. We can combine the two queries created into a single query using CTEs. The final report would look like this:

WITH department_budgets AS ( SELECT department_id, SUM(salary) AS cost FROM employees GROUP BY department_id ) SELECT d.name, db.cost FROM department_budgets db JOIN departments d ON d.id = db.department_id ORDER BY db.cost DESC;

With the queries combined, we can replace the placeholders with the name of the CTE.

Of course, the SQL statement could be written without using CTEs. That query would be messier. Also, the GROUP BY clause is a frequent source of frustration and confusion for many of us—especially within complex queries. CTEs allow us to organize our thoughts and tackle problems step-by-step. The end result is an easy-to-read query. Not only can we use CTEs to query data, but they are powerful tools for modifying data.

Modifying Data With CTEs

We can use INSERT, UPDATE, and DELETE statements within a common table expression's auxiliary or primary query. Paired with RETURNING clauses, which returns the data being modified, CTEs provide a great way to replicate or log changes to another table. For example, let's say we have a table to track employees who have left the company.

CREATE TABLE deactivated_employees ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, department_id INTEGER REFERENCES departments(id), deactivation_date DATE );

When an employee leaves the company, we want to delete that employee from the employees table and add them to the deactivated_employees table. This can be done by deleting the row within the CTE and RETURNING the information we need from that row. Then, have the primary query INSERT the deleted data into deactivated_employees by referencing the CTE. The CTE would look like this:

WITH deactivated AS ( DELETE FROM employees WHERE id = 1 RETURNING id, name, department_id ) INSERT INTO deactivated_employees SELECT id, name, department_id, now() FROM deactivated;

Combining data-modifying statements with read statements opens up many use cases. But this is just the beginning. Next, we'll look at recursive queries and how near-impossible queries become simple when using CTEs.

Recursive Queries

Recursive CTEs are incredibly powerful, especially when dealing with hierarchical or tree-structured data, such as organizational charts, file systems, or categories with subcategories. They allow us to query data related to a parent-child relationship, iterating through the hierarchy levels until a particular condition is met. Only through a recursive common table expression can a relational database like PostgreSQL be turned into a graph database.

WITH can be given an optional modifier, RECURSIVE, to turn CTEs into recursive loops. The SQL structure looks like this:

WITH RECURSIVE [cte name] AS ( [base case] UNION ALL [recursive term] ) [primary query]

A recursive CTE consists of a base case and a recursive term. The base case defines the starting point of the recursive query (e.g., the first node in a graph or the parent in a hierarchy). The recursive term describes the "loop" that will run until some endpoint is determined.

This recursive term will reference the CTE itself to create the loop. The base case and recursive term are combined using a UNION ALL (or sometimes just UNION if you want to eliminate duplicates, but be cautious as it can be more expensive in terms of performance) to create the full recursive CTE.

Using Recursive CTEs

Returning to our employee management system from before, the employees table has a new manager_id column that references another employee.

CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, department_id INTEGER REFERENCES departments(id), salary DECIMAL(10, 2), hire_date DATE, manager_id INTEGER REFERENCES employees(id) );

Our task is to generate the company org chart starting with a given employee. Like the last example, breaking the task into small steps will help determine the recursive CTE query. The first step is to develop a base case, which would be to get the starting employee.

SELECT id, name, manager_id FROM employees WHERE id = [employee id];

Next, we need to figure out the recursive term or how to loop through the employee hierarchy. To do this, we can think of the employees table performing a JOIN against itself on current_employee.manager_id = manager.id.

SELECT manager.id, manager.name, manager.manager_id FROM employees manager JOIN [current employee] employee ON employee.manager_id = manager.id

We cannot reference the current iteration in the (recursive) loop yet, so I'll leave a placeholder [current employee]. Recursive terms will continue to execute until they reach a path when the recursive term returns no rows.

This is important to consider to prevent recursive queries from entering an infinite loop! In the recursive term, when gathering the employee hierarchy, the recursive query will stop executing when the manager_id is null (e.g., the CEO at the top of the org chart).

With the base case and the recursive term written, let's combine them into a recursive CTE. Our recursive CTE will have a name we can reference in the recursive term so that we can replace the [current employee] placeholder with the name of the CTE. The complete recursive query to gather the employee hierarchy would look like this:

WITH RECURSIVE employee_hierarchy AS ( -- Base case: select the starting employee SELECT id, name, manager_id FROM employees WHERE id = 1 -- employee id UNION ALL -- Recursive term: The recursion will stop when `manager_id` = NULL SELECT manager.id, manager.name, manager.manager_id FROM employees manager JOIN employee_hierarchy employee ON employee.manager_id = manager.id ) SELECT * FROM employee_hierarchy;

Column List

A CTE will return all the columns specified in its SELECT statement (in employee_hierarchy, it is id, name, and manager_id). But, we also have the option to explicitly define what columns CTEs return and their names—referred to as the column list. The names of columns can be changed by manually specifying each column after the CTE name in the WITH expression:

WITH RECURSIVE employee_hierarchy(employee_id, name, manager_id) AS ( SELECT id, name, manager_id ... query )

We can additionally add columns to column lists to provide generated data. For example, if we want to sort employee_hierarchy by what level the employee is in the org chart, we can add a level column to the column list. The base case would start with level set to one. Then, each iteration of the recursive term would increment level by one. This would look like:

WITH RECURSIVE employee_hierarchy(id, name, manager_id, level) AS ( SELECT id, name, manager_id, 1 -- level starting at one FROM employees WHERE id = 1 UNION ALL SELECT manager.id, manager.name, manager.manager_id, level+1 -- increment level FROM employees manager JOIN employee_hierarchy employee ON employee.manager_id = manager.id ) SELECT * FROM employee_hierarchy ORDER BY level DESC; -- order by level

Optimizations

Up to this point, we've explored using a common table expression to break down queries into simple steps and recursive CTEs to query data from graph or tree-like data structures. The last use case we'll examine is how CTEs can help us optimize our SQL queries. In Improving DISTINCT Query Performance Up to 8,000x on PostgreSQL, we discussed how recursive CTEs can improve DISTINCT queries. This is one example of CTEs creatively optimizing queries and can be handy if you're running PostgreSQL yourself. However, TimescaleDB supports skip index scans without relying on CTEs.

Until PostgreSQL 12, CTEs materialize by default. In other words, a common table expression is only computed once and then cached (similar to materialized views). Any additional reference to the CTE would refer to the cached result. CTEs being materialized provide excellent options for optimizing expensive queries that are called multiple times in a primary query.

Starting in PostgreSQL 12, a query that doesn't have side effects isn't recursive and only referenced once and can be inlined by the query planner. This means the CTE gets turned into a subquery.

For example, a query that looks like this:

WITH my_cte AS ( SELECT * FROM my_table ) SELECT * FROM other_table ot JOIN my_cte m ON ot.id = m.other_id; It could be inlined to behave like this:

SELECT * FROM other_table ot JOIN (SELECT * FROM my_table) m ON ot.id = m.other_id;

However, we can tell the query planner how we want it to behave. Materialization can be enabled or disabled by specifying MATERIALIZED or NOT MATERIALIZED after AS in the WITH expression.

WITH my_cte AS MATERIALIZED ( SELECT * FROM my_table ) SELECT * FROM other_table ot JOIN my_cte m ON ot.id = m.other_id;

CTEs can be a tool to optimize queries. But with every optimization, always ANALYZE queries before explicitly setting NOT MATERIALIZED or MATERIALIZED to ensure you're not accidentally making queries perform slower.

Hyperfunctions

Much like how a common table expression streamlines complex SQL queries into more manageable units, Timescale's hyperfunctions enable us to condense verbose SQL statements into more straightforward, concise queries.

For example, if we're looking to calculate the time-weighted average price of a stock, taking into account the duration each price was adequate, the query might look somewhat complex:

SELECT date_trunc('day', time) AS day, SUM(price * duration) / SUM(duration) AS time_weighted_average_price FROM ( SELECT time, price, lead(time) OVER (ORDER BY time) - time AS duration FROM stock_prices ) sub GROUP BY day ORDER BY day;

This query calculates the duration each price was in effect by using the lead() window function to get the next timestamp and subtracting the current timestamp from it. Then, it calculates the time-weighted average price by multiplying each price by its duration, summing those products, and dividing by the total duration.

Timescale offers the time_weight() hyperfunction, combined with the average() accessor, it makes calculating time-weighted averages straightforward:

SELECT time_bucket('1 day', time) AS one_day_bucket, average(time_weight('Linear', time, price)) AS time_weighted_average_price FROM stock_prices GROUP BY one_day_bucket ORDER BY one_day_bucket;

In this query, time_bucket() groups the data into daily intervals. The time_weight('Linear', time, price) function computes a linear time-weighted average, where time is the timestamp and price is the value to be averaged over time. The average() accessor extracts the average value from the time-weighted calculations.

This method abstracts away the complex window functions and arithmetic operations, significantly simplifying the query and improving readability, much like how CTEs simplify complex query logic into more manageable parts.

Conclusion

Common table expressions are a powerful tool when writing queries. This article shows that CTEs allow us to break down complex queries into manageable chunks, traverse graph-like structures through recursive queries, and optimize queries through optimization fences. Timescale's hyperfunctions provide some ancillary to CTEs by simplifying complex queries and speeding time series analysis.

You can create a free Timescale account and utilize common table expressions and hyperfunctions.

On this page