<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: davidkohn88</title>
    <description>The latest articles on DEV Community by davidkohn88 (@davidkohn88).</description>
    <link>https://dev.to/davidkohn88</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F675662%2F97d3c6f2-bb35-40a8-aaa5-c9734b822047.jpg</url>
      <title>DEV Community: davidkohn88</title>
      <link>https://dev.to/davidkohn88</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidkohn88"/>
    <language>en</language>
    <item>
      <title>How percentile approximation works (and why it's more useful than averages)</title>
      <dc:creator>davidkohn88</dc:creator>
      <pubDate>Wed, 24 Nov 2021 21:37:13 +0000</pubDate>
      <link>https://dev.to/tigerdata/how-percentile-approximation-works-and-why-its-more-useful-than-averages-4akj</link>
      <guid>https://dev.to/tigerdata/how-percentile-approximation-works-and-why-its-more-useful-than-averages-4akj</guid>
      <description>&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Things I forgot from 7th grade math: percentiles vs. averages&lt;/li&gt;
&lt;li&gt;Long tails, outliers, and real effects: Why percentiles are better than averages for understanding your data&lt;/li&gt;
&lt;li&gt;How percentiles work in PostgreSQL&lt;/li&gt;
&lt;li&gt;Percentile approximation: what it is and why we use it in TimescaleDB hyperfunctions&lt;/li&gt;
&lt;li&gt;Percentile approximation deep dive: approximation methods, how they work, and how to choose&lt;/li&gt;
&lt;li&gt;Wrapping it up&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Get a primer on percentile approximations, why they're useful for analyzing large time-series data sets, and how we created the percentile approximation hyperfunctions to be efficient to compute, parallelizable, and useful with continuous aggregates and other advanced TimescaleDB features.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In my recent post on &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-time-weighted-averages" rel="noopener noreferrer"&gt;time-weighted averages&lt;/a&gt;, I described how my early career as an electrochemist exposed me to the importance of time-weighted averages, which shaped how we built them into TimescaleDB hyperfunctions. A few years ago, soon after I started learning more about PostgreSQL internals (check out my &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-postgreSQL-aggregation-works" rel="noopener noreferrer"&gt;aggregation and two-step aggregates&lt;/a&gt; post to learn about them yourself!), I worked on backends for an ad analytics company, where I started using TimescaleDB.&lt;/p&gt;

&lt;p&gt;Like most companies, we cared a lot about making sure our website and API calls returned results in a reasonable amount of time for the user; we had billions of rows in our analytics databases, but we still wanted to make sure that the website was responsive and useful.&lt;/p&gt;

&lt;p&gt;There’s a direct correlation between website performance and business results: users get bored if they have to wait too long for results, which is obviously not ideal from a business and customer loyalty perspective. To understand how our website performed and find ways to improve, we tracked the timing of our API calls and used API call response time as a key metric.&lt;/p&gt;

&lt;p&gt;Monitoring an API is a common scenario and generally falls under the category of application performance monitoring (APM), but there are lots of similar scenarios in other fields including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictive maintenance for industrial machines&lt;/li&gt;
&lt;li&gt;Fleet monitoring for shipping companies&lt;/li&gt;
&lt;li&gt;Energy and water use monitoring and anomaly detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, analyzing raw (usually time-series) data only gets you so far. You want to analyze trends, understand how your system performs relative to what you and your users expect, and catch and fix issues before they impact production users, and so much more. We &lt;a href="https://blog.timescale.com/blog/introducing-hyperfunctions-new-sql-functions-to-simplify-working-with-time-series-data-in-postgresql/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-introducing-hyperfunctions" rel="noopener noreferrer"&gt;built TimescaleDB hyperfunctions to help solve this problem and simplify how developers work with time-series data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For reference, hyperfunctions are a series of SQL functions that make it easier to manipulate and analyze time-series data in PostgreSQL with fewer lines of code. You can use hyperfunctions to calculate percentile approximations of data, compute time-weighted averages, downsample and smooth data, and perform faster &lt;code&gt;COUNT DISTINCT&lt;/code&gt; queries using approximations. Moreover, hyperfunctions are “easy” to use: you call a hyperfunction using the same SQL syntax you know and love.&lt;/p&gt;

&lt;p&gt;We spoke with community members to understand their needs, and our initial release includes some of the most frequently requested functions, including &lt;strong&gt;percentile approximations&lt;/strong&gt; (see &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues/41" rel="noopener noreferrer"&gt;GitHub feature request and discussion&lt;/a&gt;). They’re very useful for working with large time-series data sets because they offer the benefits of using percentiles (rather than averages or other counting statistics) while still being quick and space-efficient to compute, parallelizable, and useful with continuous aggregates and other advanced TimescaleDB features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get started with the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=docs-percentile-approximation" rel="noopener noreferrer"&gt;percentile approximation hyperfunctions&lt;/a&gt; - and many more - right away, spin up a fully managed TimescaleDB service:&lt;/strong&gt; create an account to &lt;a href="https://console.cloud.timescale.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=cloud-signup" rel="noopener noreferrer"&gt;try it for free&lt;/a&gt; for 30 days. (Hyperfunctions are pre-loaded on each new database service on Timescale Cloud, so after you create a new service, you’re all set to use them).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;download and install the timescaledb_toolkit extension&lt;/a&gt;&lt;/strong&gt; on GitHub, after which you’ll be able to use percentile approximation and other hyperfunctions.&lt;/p&gt;

&lt;p&gt;Finally, we love building in public and continually improving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you have questions or comments on this blog post, &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions/185" rel="noopener noreferrer"&gt;we’ve started a discussion on our GitHub page, and we’d love to hear from you&lt;/a&gt;. And, if you like what you see, GitHub ⭐ are always welcome and appreciated too!&lt;/li&gt;
&lt;li&gt;You can view our &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;upcoming roadmap on GitHub&lt;/a&gt; for a list of proposed features, as well as features we’re currently implementing and those that are available to use today.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Things I forgot from 7th grade math: percentiles vs. averages&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I probably learned about averages, medians, and modes in 7th grade math class, but if you’re anything like me, they may periodically get lost in the cloud of “things I learned once and thought I knew, but actually, I don’t remember quite as well as I thought.”&lt;/p&gt;

&lt;p&gt;As I was researching this piece, I found a  number of good blog posts (see examples from the folks at &lt;a href="https://www.dynatrace.com/news/blog/why-averages-suck-and-percentiles-are-great/" rel="noopener noreferrer"&gt;Dynatrace&lt;/a&gt;, &lt;a href="https://www.elastic.co/blog/averages-can-dangerous-use-percentile" rel="noopener noreferrer"&gt;Elastic&lt;/a&gt;, &lt;a href="https://blog.appsignal.com/2018/12/04/dont-be-mean-statistical-means-and-percentiles-101.html" rel="noopener noreferrer"&gt;AppSignal&lt;/a&gt;, and &lt;a href="https://www.optimizely.com/insights/blog/why-cdn-balancing/" rel="noopener noreferrer"&gt;Optimizely&lt;/a&gt;) about how averages aren’t great for understanding application performance, or other similar things, and why it’s better to use percentiles.&lt;/p&gt;

&lt;p&gt;I won’t spend too long on this, but I think it’s important to provide a bit of background on why and how percentiles can help us better understand our data.&lt;/p&gt;

&lt;p&gt;First off, let’s consider how percentiles and averages are defined. To understand this, let’s start by looking at a &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Normal_distribution" rel="noopener noreferrer"&gt;normal distribution&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jjnt41d6t1hfxg12eyj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jjnt41d6t1hfxg12eyj.jpg" alt="Trulli" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;b&gt;A normal, or Gaussian, distribution describes many real-world processes that fall around a given value and where the probability of finding values that are further from the center decreases. The median, average, and mode are all the same for a normal distribution, and they fall on the dotted line at the center.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
The normal distribution is what we often think of when we think about statistics; it’s one of the most frequently used and often used in introductory courses. In a normal distribution, the median, the average (also known as the mean), and the mode are all the same, even though they’re defined differently.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;median&lt;/strong&gt; is the middle value, where half of the data is above and half is below. The &lt;strong&gt;mean&lt;/strong&gt; (aka average) is defined as the sum(value) / count(value),  and the &lt;strong&gt;mode&lt;/strong&gt; is defined as the most common or frequently occurring value.&lt;/p&gt;

&lt;p&gt;When we’re looking at a curve like this, the x-axis represents the value, while the y-axis represents the frequency with which we see a given value (i.e., values that are “higher” on the y-axis occur more frequently).&lt;/p&gt;

&lt;p&gt;In a normal distribution, we see a curve centered (the dotted line) at its most frequent value, with decreasing probability of seeing values further away from the most frequent one (the most frequent value is the mode). Note that the normal distribution is symmetric, which means that values to the left and right of the center have the same probability of occurring.&lt;/p&gt;

&lt;p&gt;The median, or the middle value, is also known as the 50th percentile (the middle percentile out of 100). This is the value at which 50% of the data is less than the value, and 50% is greater than the value (or equal to it).&lt;/p&gt;

&lt;p&gt;In the below graph, half of the data is to the left (shaded in blue), and a half is to the right (shaded in yellow), with the 50th percentile directly in the center.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnp0dhthdeb9ok4ceghe.jpg" alt="A normal distribution with the median/50th percentile depicted." width="800" height="472"&gt;&lt;b&gt;A normal distribution with the median/50th percentile depicted.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
This leads us to percentiles: a &lt;strong&gt;percentile&lt;/strong&gt; is defined as the value where x percent of the data falls below the value.&lt;/p&gt;

&lt;p&gt;For example, if we call something “the 10th percentile,” we mean that 10% of the data is less than the value and 90% is greater than (or equal to) the value.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frcd4hzxvvpsqy7i28bhf.jpg" alt="A normal distribution with the 10th percentile depicted." width="800" height="472"&gt;&lt;b&gt;A normal distribution with the 10th percentile depicted.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
And the 90th percentile is where 90% of the data is less than the value and 10% is greater:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8gsclrgosqi0ly9t2km.jpg" alt="A normal distribution with the 90th percentile depicted." width="800" height="472"&gt;&lt;b&gt;A normal distribution with the 90th percentile depicted.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
To calculate the 10th percentile, let’s say we have 10,000 values. We take all of the values, order them from smallest to largest, and identify the 1001st value (where 1000 or 10% of the values are below it), which will be our 10th percentile.&lt;/p&gt;

&lt;p&gt;We noted before that the median and average are the same in a normal distribution. This is because a normal distribution is symmetric. Thus, the magnitude and number of points with values larger than the median are completely balanced (both in magnitude and number of points smaller than the median).&lt;/p&gt;

&lt;p&gt;In other words, there is always the same number of points on either side of the median, but the average takes into account the actual value of the points.&lt;/p&gt;

&lt;p&gt;For the median and average to be equal, the points less than the median and greater than the median must have the same distribution (i.e., there must be the same number of points that are somewhat larger and somewhat smaller and much larger and much smaller). (&lt;strong&gt;Correction&lt;/strong&gt;: as pointed out to us in &lt;a href="https://news.ycombinator.com/item?id=28527954" rel="noopener noreferrer"&gt;a helpful comment on Hacker News&lt;/a&gt;, technically this is only true for symmetric distributions, asymmetric distributions it may or may not be true for and you can get odd cases of asymmetric distributions where these are equal, though they are less likely!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt; The fact that median and average are the same in the normal distribution can cause some confusion. Since a normal distribution is often one of the first things we learn, we (myself included!) can think it applies to more cases than it actually does.&lt;/p&gt;

&lt;p&gt;It’s easy to forget or fail to realize, that only the median guarantees that 50% of the values will be above, and 50% below – while the average guarantees that 50% of the &lt;strong&gt;weighted&lt;/strong&gt; values will be above and 50% below (i.e., the average is the &lt;a href="https://en.wikipedia.org/wiki/Centroid" rel="noopener noreferrer"&gt;centroid&lt;/a&gt;, while the median is the center).&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjwkdqpkva1ay1d3ai75.jpg" alt="The average and median are the same in a normal distribution, and they split the graph exactly in half. But they aren’t calculated the same way, don’t represent the same thing, and aren’t necessarily the same in other distributions." width="800" height="472"&gt;&lt;b&gt;The average and median are the same in a normal distribution, and they split the graph exactly in half. But they aren’t calculated the same way, don’t represent the same thing, and aren’t necessarily the same in other distributions.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
🙏 Shout out to the folks over at &lt;a href="https://www.desmos.com/" rel="noopener noreferrer"&gt;Desmos&lt;/a&gt; for their great graphing calculator, which helped make these graphs, and even allowed me to make an &lt;a href="https://www.desmos.com/calculator/ty3jt8ftgs" rel="noopener noreferrer"&gt;interactive demonstration of these concepts&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;But, to get out of the theoretical, let’s consider something more common in the real world, like the API response time scenario from my work at the ad analytics company.&lt;/p&gt;


&lt;h2&gt;
  
  
  Long tails, outliers, and real effects: Why percentiles are better than averages for understanding your data&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;We looked at how averages and percentiles are different – and now, we’re going to use a real-world scenario to demonstrate how using averages instead of percentiles can lead to false alarms or missed opportunities.&lt;/p&gt;

&lt;p&gt;Why? Averages don’t always give you enough information to distinguish between real effects and outliers or noise, whereas percentiles can do a much better job.&lt;/p&gt;

&lt;p&gt;Simply put, using averages can have a dramatic (and negative) impact on how values are reported, while percentiles can help you get closer to the “truth.”&lt;/p&gt;

&lt;p&gt;If you’re looking at something like API response time, you’ll likely see a frequency distribution curve that looks something like this:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fal5fkn4usyv5f4424jsx.jpg" alt="A frequency distribution for API response times with a peak at 250ms (all graphs are not to scale and are meant only for demonstration purposes)." width="800" height="492"&gt;&lt;b&gt;A frequency distribution for API response times with a peak at 250ms (all graphs are not to scale and are meant only for demonstration purposes).&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
In my former role at the ad analytics company, we’d aim for most of our API response calls to finish in under half a second, and many were much, much shorter than that. When we monitored our API response times, one of the most important things we tried to understand was how users were affected by changes in the code.  &lt;/p&gt;

&lt;p&gt;Most of our API calls finished in under half a second, but some people used the system to get data over very long time periods or had odd configurations that meant their dashboards were a bit less responsive (though we tried to make sure those were rare!).&lt;/p&gt;

&lt;p&gt;The type of curve that resulted is characterized as a &lt;strong&gt;long-tail distribution&lt;/strong&gt; where we have a relatively large spike at 250 ms, with a lot of our values under that and then an exponentially decreasing number of longer response times.&lt;/p&gt;

&lt;p&gt;We talked earlier about how in symmetric curves (like the normal distribution), but a long-tail distribution is an &lt;strong&gt;asymmetric&lt;/strong&gt; curve.&lt;/p&gt;

&lt;p&gt;This means that the largest values are much larger than the middle values, while the smallest values aren’t that far from the middle values. (In the API monitoring case,  you can never have an API call that takes less than 0 s to respond, but there’s no limit to how long they can take, so you get that long tail of longer API calls).&lt;/p&gt;

&lt;p&gt;Thus,  the average and the median of a long-tail distribution start to diverge:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmw72g5n4auugwygl5l3.jpg" alt="The API response time frequency curve with the median and average labeled. Graphs are not to scale and are meant for demonstration purposes only." width="800" height="492"&gt;&lt;b&gt;The API response time frequency curve with the median and average labeled. Graphs are not to scale and are meant for demonstration purposes only.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
In this scenario, the average is significantly larger than the median because there are enough “large” values in the long tail to make the average larger. Conversely, in some other cases, the average might be smaller than the median.&lt;/p&gt;

&lt;p&gt;But at the ad analytics company, we found that the average didn’t give us enough information to distinguish between important changes in how our API responded to software changes vs. noise/outliers that only affected a few individuals.&lt;/p&gt;

&lt;p&gt;In one case, we introduced a change to the code that had a new query. The query worked fine in staging, but there was a lot more data in the production system.&lt;/p&gt;

&lt;p&gt;Once the data was “warm” (in memory), it would run quickly, but it was very slow the first time. When the query went into production, the response time was well over a second for ~10% of the calls.&lt;/p&gt;

&lt;p&gt;In our frequency curve, a response time over a second (but less than 10s) for ~10% of the calls resulted in a second, smaller hump in our frequency curve and looked like this:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3kvl89sl8btsmxfklqyb.jpg" alt="A frequency curve showing the shift and extra hump that occurs when 10% of calls take a moderate amount of time, between 1 and 10s (graph still not to scale)." width="800" height="492"&gt;&lt;b&gt;A frequency curve showing the shift and extra hump that occurs when 10% of calls take a moderate amount of time, between 1 and 10s (graph still not to scale).&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
In this scenario, the average shifted a lot, while the median slightly shifted, it’s much less impacted.&lt;/p&gt;

&lt;p&gt;You might think that this makes the average a better metric than the median because it helped us identify the problem (too long API response times), and we could set up our alerting to notify when the average shifts.&lt;/p&gt;

&lt;p&gt;Let’s imagine that we’ve done that, and people will jump into action when the average goes above, say, 1 second(s).&lt;/p&gt;

&lt;p&gt;But now, we get a few users who start requesting 15 years of data from our UI...and those API calls take a really long time. This is because the API wasn’t really built to handle this “off-label” use.&lt;/p&gt;

&lt;p&gt;Just a few calls from these users easily shifted the average way over our 1s threshold.&lt;/p&gt;

&lt;p&gt;Why? The average (as a value) can be dramatically affected by outliers like this, even though they impact only a small fraction of our users. The average uses the sum of the data, so the magnitude of the outliers can have an outsized impact, whereas the median and other percentiles are based on the ordering of the data.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fniec8uhl3sea37z7vkmh.jpg" alt="Our curve with a few outliers, where less than 1% of the API call responses are over 100s (the response time has a break representing the fact that the outliers would be way to the right otherwise, still, the graph is not to scale)." width="800" height="366"&gt;&lt;b&gt;Our curve with a few outliers, where less than 1% of the API call responses are over 100s (the response time has a break representing the fact that the outliers would be way to the right otherwise, still, the graph is not to scale).&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
&lt;strong&gt;The point is that the average doesn’t give us a good way to distinguish between outliers and real effects and can give odd results when we have a long-tail or asymmetric distribution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why is this important to understand?&lt;/p&gt;

&lt;p&gt;Well, in the first case, we had a problem affecting 10% of our API calls, which could be 10% or more of our users (how could it affect more than 10% of the users? Well, if a user makes 10 calls on average, and 10% of API calls are affected, then, on average, all the users would be affected... or at least some large percentage of them).&lt;/p&gt;

&lt;p&gt;We want to respond very quickly to that type of urgent problem, affecting a large number of users. We built alerts and might even get our engineers up in the middle of the night and/or revert a change.&lt;/p&gt;

&lt;p&gt;But the second case, where “off-label” user behavior or minor bugs had a large effect on a few API calls, was much more benign. Because relatively few users are affected by these outliers, we wouldn’t want to get our engineers up in the middle of the night or revert a change. (Outliers can still be important to identify and understand, both for understanding user needs or potential bugs in the code, but they usually &lt;em&gt;aren’t an emergency&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Instead of using the average, we can instead use multiple percentiles to understand this type of behavior. Remember, unlike averages, percentiles rely on the ordering of the data rather than being impacted by the magnitude of data. If we use the 90th percentile, we know that 10% of users have values (API response times in our case) greater than it.  &lt;/p&gt;

&lt;p&gt;Let’s look at the 90th percentile in our original graph; it nicely captures some of the long tail behavior:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5spc0p7q8iud3x8fqjk.jpg" alt="Our original API response time graph showing the 90th percentile, median, and average. Graph not to scale." width="800" height="492"&gt;&lt;b&gt;Our original API response time graph showing the 90th percentile, median, and average. Graph not to scale.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
When we have some outliers caused by a few users who’re running super long queries or a bug affecting a small group of queries, the average shifts, but the 90th percentile is hardly affected.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flp6l804pd3yjfjs2g86s.jpg" alt="Outliers affect the average but don’t impact the 90th percentile or median. (Graph is not to scale.)" width="800" height="366"&gt;&lt;b&gt;Outliers affect the average but don’t impact the 90th percentile or median. (Graph is not to scale.)&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
But, when the tail is increased due to a problem affecting 10% of users, we see that the 90th percentile shifts outward pretty dramatically – which enables our team to be notified and respond appropriately:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpxqsm421j1vyu3hjeiq.jpg" alt="But when there are “real” effects from responses that impact more than 10% of users, the 90th percentile shifts dramatically (Graph not to scale.)" width="800" height="492"&gt;&lt;b&gt;But when there are “real” effects from responses that impact more than 10% of users, the 90th percentile shifts dramatically (Graph not to scale.)&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
This (hopefully) gives you a better sense of how and why percentiles can help you identify cases where large numbers of users are affected – but not burden you with false positives that might wake engineers up and give them alarm fatigue!&lt;/p&gt;

&lt;p&gt;So, now that we know why we might want to use percentiles rather than averages, let’s talk about how we calculate them.&lt;/p&gt;


&lt;h2&gt;
  
  
  How percentiles work in PostgreSQL&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;To calculate any sort of exact percentile, you take all your values, sort them, then find the nth value based on the percentile you’re trying to calculate.&lt;/p&gt;

&lt;p&gt;To see how this works in PostgreSQL, we’ll present a simplified case of our ad analytics company’s API tracking.&lt;/p&gt;

&lt;p&gt;We’ll start off with a table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;response_time&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In PostgreSQL we can calculate a percentile over the column &lt;code&gt;response_time&lt;/code&gt; using the &lt;a href="https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-ORDEREDSET-TABLE" rel="noopener noreferrer"&gt;&lt;code&gt;percentile_disc aggregate&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;percentile_disc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This doesn’t look the same as a normal aggregate; the &lt;code&gt;WITHIN GROUP (ORDER BY …)&lt;/code&gt; is a different syntax that works on special aggregates called &lt;a href="https://www.postgresql.org/docs/13/xaggr.html#XAGGR-ORDERED-SET-AGGREGATES" rel="noopener noreferrer"&gt;ordered-set aggregates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here we pass in the percentile we want (0.5 or the 50th percentile for the median) to the &lt;code&gt;percentile_disc&lt;/code&gt; function, and the column that we’re evaluating (&lt;code&gt;response_time&lt;/code&gt;) goes in the order by clause.&lt;/p&gt;

&lt;p&gt;It will be more clear why this happens when we understand what’s going on under the hood. Percentiles give a guarantee that x percent of the data will fall below the value they return. To calculate that, we need to sort all of our data in a list and then pick out the value where 50% of the data falls below it, and 50% falls above it.&lt;/p&gt;

&lt;p&gt;For those of you who read the section of our previous post on &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/#a-primer-on-postgresql-aggregation-through-pictures" rel="noopener noreferrer"&gt;how PostgreSQL aggregates work&lt;/a&gt;, we discussed how an aggregate like &lt;code&gt;avg&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;As it scans each row, the transition function updates some internal state (for &lt;code&gt;avg&lt;/code&gt; it’s the &lt;code&gt;sum&lt;/code&gt; and the &lt;code&gt;count&lt;/code&gt;), and then a final function processes the internal state to produce a result (for &lt;code&gt;avg&lt;/code&gt; divide &lt;code&gt;sum&lt;/code&gt; by &lt;code&gt;count&lt;/code&gt;).&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwgfm7jy63woav0mi1z2r.gif" alt="A GIF showing how the avg is calculated in PostgreSQL with the sum and count as the partial state as rows are processed and a final function that divides them when we’ve finished." width="600" height="602"&gt;&lt;b&gt;A GIF showing how the avg is calculated in PostgreSQL with the sum and count as the partial state as rows are processed and a final function that divides them when we’ve finished.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
The ordered set aggregates, like &lt;code&gt;percentile_disc&lt;/code&gt;, work somewhat similarly, with one exception: instead of the state being a relatively small fixed-size data structure (like &lt;code&gt;sum&lt;/code&gt; and &lt;code&gt;count&lt;/code&gt; for &lt;code&gt;avg&lt;/code&gt; ), it must keep all the values it has processed to sort them and calculate the percentile later.&lt;/p&gt;

&lt;p&gt;Usually, PostgreSQL does this by putting the values into a data structure called a &lt;code&gt;tuplestore&lt;/code&gt; that stores and sorts values easily.&lt;/p&gt;

&lt;p&gt;Then, when the final function is called, the &lt;code&gt;tuplestore&lt;/code&gt; will first sort the data. Then, based on the value input into the &lt;code&gt;percentile_disc&lt;/code&gt;), it will traverse to the correct point (0.5 of the way through the data for the median) in the sorted data and output the result.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxfp2tct5yc5mknqe77j.gif" alt="With the  raw `percentile_disc` endraw  ordered set aggregate, PostgreSQL has to store each value it sees in a  raw `tuplestore` endraw  then when it’s processed all the rows, it sorts them, and then goes to the right point in the sorted list to extract the percentile we need." width="600" height="602"&gt;&lt;b&gt;With the `percentile_disc` ordered set aggregate, PostgreSQL has to store each value it sees in a `tuplestore` then when it’s processed all the rows, it sorts them, and then goes to the right point in the sorted list to extract the percentile we need.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
Instead of performing these expensive calculations over very large data sets, &lt;strong&gt;many people find that approximate percentile calculations can provide a “close enough” approximation with significantly less work&lt;/strong&gt;...which is why we introduced percentile approximation hyperfunctions.&lt;/p&gt;


&lt;h2&gt;
  
  
  Percentile approximation: what it is and why we use it in TimescaleDB hyperfunctions&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In my experience, people often use averages and other summary statistics more frequently than percentiles because they are significantly “cheaper” to calculate over large datasets, both in computational resources and time.&lt;/p&gt;

&lt;p&gt;As we noted above, calculating the average in PostgreSQL has a simple, two-valued aggregate state. Even if we calculate a few additional, related functions like the standard deviation, we still just need a small, fixed number of values to calculate the function.&lt;/p&gt;

&lt;p&gt;In contrast, to calculate the percentile, we need all of the input values in a sorted list.&lt;/p&gt;

&lt;p&gt;This leads to a few issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory footprint:&lt;/strong&gt; The algorithm has to keep these values somewhere, which means keeping values in memory until they need to write some data to disk to avoid using too much memory (this is known as “spilling to disk”). This produces a significant memory burden and/or majorly slows down the operation because disk accesses are orders of magnitude slower than memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited Benefits from Parallelization:&lt;/strong&gt; Even though the algorithm can sort lists in parallel, the benefits from parallelization are limited because it still needs to merge all the sorted lists into a single, sorted list in order to calculate a percentile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High network costs:&lt;/strong&gt; In distributed systems (like TimescaleDB multi-node), all the values must be passed over the network to one node to be made into a single sorted list, which is slow and costly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No true partial states:&lt;/strong&gt; Materialization of partial states (e.g., for continuous aggregates) is not useful because the partial state is simply all the values that underlie it. This could save on sorting the lists, but the storage burden would be high and the payoff low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No streaming algorithm:&lt;/strong&gt; For streaming data, this is completely infeasible. You still need to maintain the full list of values (similar to the materialization of partial states problem above), which means that the algorithm essentially needs to store the entire stream!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these can be manageable when you’re dealing with relatively small data sets, while for high volume, time-series workloads, they start to become more of an issue.&lt;/p&gt;

&lt;p&gt;But, you only need the full list of values for calculating a percentile if you want exact percentiles. &lt;strong&gt;With relatively large datasets, you can often accept some accuracy tradeoffs to avoid running into any of these issues.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problems above, and the recognition of the tradeoffs involved in weighing whether to use averages or percentiles, led to the development of multiple algorithms to &lt;a href="https://en.wikipedia.org/wiki/Quantile#Approximate_quantiles_from_a_stream" rel="noopener noreferrer"&gt;approximate percentiles in high volume systems&lt;/a&gt;. Most percentile approximation approaches involve some sort of modified &lt;a href="https://en.wikipedia.org/wiki/Histogram" rel="noopener noreferrer"&gt;histogram&lt;/a&gt; to represent the overall shape of the data more compactly, while still capturing much of the shape of the distribution.&lt;/p&gt;

&lt;p&gt;As we were designing hyperfunctions, we thought about how we could capture the benefits of percentiles (e.g., robustness to outliers, better correspondence with real-world impacts) while avoiding some of the pitfalls that come with calculating exact percentiles (above).&lt;/p&gt;

&lt;p&gt;Percentile approximations seemed like the right fit for working with large, time-series datasets.&lt;/p&gt;

&lt;p&gt;The result is a whole family of &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/" rel="noopener noreferrer"&gt;percentile approximation hyperfunctions&lt;/a&gt;, built into TimescaleDB. The simplest way to call them is to use the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/percentile_agg/" rel="noopener noreferrer"&gt;&lt;code&gt;percentile_agg&lt;/code&gt; aggregate&lt;/a&gt; along with the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/approx_percentile/" rel="noopener noreferrer"&gt;&lt;code&gt;approx_percentile&lt;/code&gt; accessor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This query calculates approximate 10th, 50th, and 90th percentiles:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p90&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(If you’d like to learn more about aggregates, accessors, and two-step aggregation design patterns, check out &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/" rel="noopener noreferrer"&gt;our primer on PostgreSQL two-step aggregation&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;These percentile approximations have many benefits when compared to the normal PostgreSQL exact percentiles, especially when used for large data sets.&lt;/p&gt;
&lt;h3&gt;
  
  
  Memory footprint
&lt;/h3&gt;

&lt;p&gt;When calculating percentiles over large data sets, our percentile approximations limit the memory footprint (or need to spill to disk, as described above).&lt;/p&gt;

&lt;p&gt;Standard percentiles create memory pressure since they build up as much of the data set in memory as possible...and then slow down when forced to spill to disk.&lt;/p&gt;

&lt;p&gt;Conversely, hyperfunctions’ percentile approximations have fixed size representations based on the number of buckets in their modified histograms, so they limit the amount of memory required to calculate them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Parallelization in single and multi-node TimescaleDB
&lt;/h3&gt;

&lt;p&gt;All of our percentile approximation algorithms are parallelizable, so they can be computed using multiple workers in a single node; this can provide significant speedups because ordered-set aggregates like &lt;code&gt;percentile_disc&lt;/code&gt; are not parallelizable in PostgreSQL.&lt;/p&gt;

&lt;p&gt;Parallelizability provides a speedup in single node setups of TimescaleDB – and this can be even more pronounced in &lt;a href="https://blog.timescale.com/blog/timescaledb-2-0-a-multi-node-petabyte-scale-completely-free-relational-database-for-time-series/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-2-0-multi-node-setups#timescaledb-20-multi-node-petabyte-scale-and-completely-free" rel="noopener noreferrer"&gt;multi-node TimescaleDB setups&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why? To calculate a percentile in multi-node TimescaleDB using the &lt;code&gt;percentile_disc&lt;/code&gt; ordered-set aggregate (the standard way you would do this without our approximation hyperfunctions), you must send each value back from the data node to the access node, sort the data, and then provide an output.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcidn9azwzcey5si6lose.jpg" alt="When calculating the exact percentile in TimescaleDB multi-node, each data node must send all of the data back to the access node. The access node then sorts and calculates the percentile" width="800" height="520"&gt;&lt;b&gt;When calculating the exact percentile in TimescaleDB multi-node, each data node must send all of the data back to the access node. The access node then sorts and calculates the percentile&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
The “standard” way is very, very costly because all of the data needs to get sent to the access node over the network from each data node, which is slow and expensive.&lt;/p&gt;

&lt;p&gt;Even after the access node gets the data, it still needs to sort and calculate the percentile over all that data before returning a result to the user. (Caveat: there is the possibility that each data node could sort separately, and the access node would just perform a merge sort. But, this wouldn’t negate the need for sending all the data over the network, which is the most costly step.)&lt;/p&gt;

&lt;p&gt;With approximate percentile hyperfunctions, much more of the work can be &lt;a href="https://blog.timescale.com/blog/achieving-optimal-query-performance-with-a-distributed-time-series-database-on-postgresql/#pushing-down-work-to-data-nodes" rel="noopener noreferrer"&gt;pushed down to the data node&lt;/a&gt;. Partial approximate percentiles can be computed on each data node, and a fixed size data structure returned over the network.&lt;/p&gt;

&lt;p&gt;Once each data node calculates its partial data structure, the access node combines these structures, calculates the approximate percentile, and returns the result to the user.&lt;/p&gt;

&lt;p&gt;This means that more work can be done on the data nodes and, most importantly, far, far less data has to be passed over the network. With large datasets, this can result in orders of magnitude less time spent on these calculations.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz2wh9mkgsakfat58pfv.jpg" alt="Using our percentile approximation hyperfunctions, the data nodes no longer have to send all of the data back to the access node. Instead, they calculate a partial approximation and send them back to the access node, which then combines the partials and produces a result. This saves a lot of time on network calls since it parallelizes the computation over the data nodes, rather than performing much of the work on the access node." width="800" height="520"&gt;&lt;b&gt;Using our percentile approximation hyperfunctions, the data nodes no longer have to send all of the data back to the access node. Instead, they calculate a partial approximation and send them back to the access node, which then combines the partials and produces a result. This saves a lot of time on network calls since it parallelizes the computation over the data nodes, rather than performing much of the work on the access node.&lt;/b&gt;





&lt;h3&gt;
  
  
  Materialization in continuous aggregates
&lt;/h3&gt;

&lt;p&gt;TimescaleDB includes a feature called &lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/" rel="noopener noreferrer"&gt;continuous aggregates&lt;/a&gt;, designed to make queries on very large datasets run faster.&lt;/p&gt;

&lt;p&gt;TimescaleDB continuous aggregates continuously and incrementally store the results of an aggregation query in the background, so when you run the query, only the data that has changed needs to be computed, not the entire dataset.&lt;/p&gt;

&lt;p&gt;Unfortunately, exact percentiles using &lt;code&gt;percentile_disc&lt;/code&gt; cannot be stored in continuous aggregates because they cannot be broken down into a partial form, and would instead require storing the entire dataset inside the aggregate.&lt;/p&gt;

&lt;p&gt;We designed our percentile approximation algorithms to be usable with continuous aggregates. They have fixed-size partial representations that can be stored and re-aggregated inside of continuous aggregates.&lt;/p&gt;

&lt;p&gt;This is a huge advantage compared to exact percentiles because now you can do things like baselining and alerting on longer periods, without having to re-calculate from scratch every time.&lt;/p&gt;

&lt;p&gt;Let’s go back to our API response time example and imagine we want to identify recent outliers to investigate potential problems.&lt;/p&gt;

&lt;p&gt;One way to do that would be to look at everything that is, say, above the 99th percentile in the previous hour.&lt;/p&gt;

&lt;p&gt;As a reminder, we have a table:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;response_time&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'responses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- make it a hypertable so we can make continuous aggs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First, we’ll create a one hour aggregation:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;responses_1h_agg&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;continuous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that we don’t perform the accessor function in the continuous aggregate; we just perform the aggregation function.&lt;/p&gt;

&lt;p&gt;Now, we can find the data in the last 30s greater than the 99th percentile like so:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'30s'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses_1h_agg&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At the ad analytics company, we had a lot of users, so we’d have tens or hundreds of thousands of API calls every hour.&lt;/p&gt;

&lt;p&gt;By default, we have 200 buckets in our representation, so we’re getting a large reduction in the amount of data that we store and process by using a continuous aggregate. This means that it would speed up the response time significantly. If you don’t have as much data, you’ll want to increase the size of your buckets or decrease the fidelity of the approximation to achieve a large reduction in the data we have to process.&lt;/p&gt;

&lt;p&gt;We mentioned that we only performed the aggregate step in the continuous aggregate view definition; we didn’t use our &lt;code&gt;approx_percentile&lt;/code&gt; accessor function directly in the view. We do that because we want to be able to use other accessor functions and/or the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/rollup-percentile/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=docs-rollup-percentile" rel="noopener noreferrer"&gt;&lt;code&gt;rollup&lt;/code&gt;&lt;/a&gt; function, which you may remember as one of the main &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-how-percentile-aggregation-works#why-we-use-the-two-step-aggregate-design-pattern" rel="noopener noreferrer"&gt;reasons we chose the two-step aggregate approach&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s look at how that works, we can create a daily rollup and get the 99th percentile like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p_99_daily&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses_1h_agg&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We could even use the &lt;code&gt;approx_percentile_rank&lt;/code&gt; accessor function, which tells you what percentile a value would fall into.&lt;/p&gt;

&lt;p&gt;Percentile rank is the inverse of the percentile function; in other words, if normally you ask, what is the value of nth percentile? The answer is a value.&lt;/p&gt;

&lt;p&gt;With percentile rank, you ask what percentile would this value be in? The answer is a percentile.&lt;/p&gt;

&lt;p&gt;So, using &lt;code&gt;approx_percentile_rank&lt;/code&gt; allows us to see where the values that arrived in the last 5 minutes rank compared to values in the last day:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;last_day&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pct_daily&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;foo_1h_agg&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;approx_percentile_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct_daily&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pct_rank_in_day&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_day&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is another way continuous aggregates can be valuable.&lt;/p&gt;

&lt;p&gt;We performed a &lt;code&gt;rollup&lt;/code&gt; over a day, which just combined 24 partial states, rather than performing a full calculation over 24 hours of data with millions of data points.&lt;/p&gt;

&lt;p&gt;We then used the &lt;code&gt;rollup&lt;/code&gt; to see how that impacted just the last few minutes of data, giving us insight into how the last few minutes compare to the last 24 hours. These are just a few examples of how the percentile approximation hyperfunctions can give us some pretty nifty results and allow us to perform complex analysis relatively simply.&lt;/p&gt;


&lt;h2&gt;
  
  
  Percentile approximation deep dive: approximation methods, how they work, and how to choose&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Some of you may be wondering how TimescaleDB hyperfunctions’ underlying algorithms work, so let’s dive in! (For those of you who don’t want to get into the weeds, feel free to skip over this bit.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Approximation methods and how they work
&lt;/h3&gt;

&lt;p&gt;We implemented two different percentile approximation algorithms as TimescaleDB hyperfunctions: &lt;a href="https://arxiv.org/pdf/2004.08604.pdf" rel="noopener noreferrer"&gt;UDDSketch&lt;/a&gt; and &lt;a href="https://github.com/tdunning/t-digest" rel="noopener noreferrer"&gt;T-Digest&lt;/a&gt;. Each is useful in different scenarios, but first, let’s understand some of the basics of how they work.&lt;/p&gt;

&lt;p&gt;Both use a modified histogram to approximate the shape of a distribution. A histogram buckets nearby values into a group and tracks their frequency.&lt;/p&gt;

&lt;p&gt;You often see a histogram plotted like so:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzm1nuel9a1q9nx4ztpzy.png" alt="A histogram representing the same data as our response time frequency curve above, you can see how the shape of the graph is similar to the frequency curve. Not to scale." width="800" height="328"&gt;&lt;b&gt;A histogram representing the same data as our response time frequency curve above, you can see how the shape of the graph is similar to the frequency curve. Not to scale.&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
If you compare this to the frequency curve we showed above, you can see how this could provide a reasonable approximation of the  API response time vs frequency response. Essentially, a histogram has a series of bucket boundaries and a count of the number of values that fall within each bucket.&lt;/p&gt;

&lt;p&gt;To calculate the approximate percentile for, say, the 20th percentile, you first consider the fraction of your total data that would represent it. For our 20th percentile, that would be 0.2 * &lt;code&gt;total_points&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once you have that value, you can then sum the frequencies in each bucket, left to right, to find at which bucket you get the value closest to 0.2 * &lt;code&gt;total_points&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can even interpolate between buckets to get more exact approximations when the bucket spans a percentile of interest.&lt;/p&gt;

&lt;p&gt;When you think of a histogram, you may think of one that looks like the one above, where the buckets are all the same width.&lt;/p&gt;

&lt;p&gt;But choosing the bucket width, especially for widely varying data, can get very difficult or lead you to store a lot of extra data.&lt;/p&gt;

&lt;p&gt;In our API response time example, we could have data spanning from tens of milliseconds up to ten seconds or hundreds of seconds.&lt;/p&gt;

&lt;p&gt;This means that the right bucket size for a good approximation of the 1st percentile, e.g., 2ms, would be WAY smaller than necessary for a good approximation of the 99th percentile.&lt;/p&gt;

&lt;p&gt;This is why most percentile approximation algorithms use a modified histogram with a &lt;em&gt;variable bucket width&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For instance, the UDDSketch algorithm uses logarithmically sized buckets, which might look something like this:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1shqfsjf3faby1zo1bl.png" alt="A modified histogram showing how logarithmic buckets like the UDDSketch algorithm uses can still represent the data. (Note: we’d need to modify the plot to plot the frequency/bucket width so that the scale would remain similar; however, this is just for demonstration purposes and not drawn to scale)." width="800" height="331"&gt;&lt;b&gt;A modified histogram showing how logarithmic buckets like the UDDSketch algorithm uses can still represent the data. (Note: we’d need to modify the plot to plot the frequency/bucket width so that the scale would remain similar; however, this is just for demonstration purposes and not drawn to scale).&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
The designers of UDDSketch used a logarithmic bucket size like this because what they care about is the relative error.&lt;/p&gt;

&lt;p&gt;For reference, absolute error is defined as the difference between the actual and the approximated value:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;errabsolute=abs(vactual−vapprox)
err_\text{absolute} = abs(v_\text{actual} - v_\text{approx})
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;absolute&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ab&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;actual&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;approx&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;To get relative error, you divide the absolute error by the value:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;errrelative=errabsolutevactual
err_\text{relative} = \frac{err_\text{absolute}}{v_\text{actual}}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;relative&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;actual&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;absolute&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;If we had a constant absolute error, we might run into a situation like the following:&lt;/p&gt;

&lt;p&gt;We ask for the 99th percentile, and the algorithm tells us it’s 10s +/- 100ms. Then, we ask for the 1st percentile, and the algorithm tells us it’s 10ms +/- 100ms.&lt;/p&gt;

&lt;p&gt;The error for the 1st percentile is way too high!&lt;/p&gt;

&lt;p&gt;If we have a constant relative error, then we’d get 10ms +/- 100 microseconds.&lt;/p&gt;

&lt;p&gt;This is much, much more useful. (And 10s +/- 100 microseconds is probably too tight, we likely don’t really care about 100 microseconds if we’re already at 10s.)&lt;/p&gt;

&lt;p&gt;This is why the UDDSketch algorithm uses logarithmically sized buckets, where the width of the bucket scales with the size of the underlying data. This allows the algorithm to provide constant relative error across the full range of percentiles.&lt;/p&gt;

&lt;p&gt;As a result, you always know that the true value of the percentile will fall within some range &lt;br&gt;

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;([vapprox(1−err),vapprox(1+err)])
([v_\text{approx} (1-err), v_\text{approx} (1+err)])
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;([&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;approx&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;err&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;approx&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;err&lt;/span&gt;&lt;span class="mclose"&gt;)])&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/p&gt;

&lt;p&gt;On the other hand, T-Digest uses buckets that are variably sized, based on where they fall in the distribution. Specifically, it uses smaller buckets at the extremes of the distribution and larger buckets in the middle.&lt;/p&gt;

&lt;p&gt;So, it might look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2q3afzavb5gp85lx660d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2q3afzavb5gp85lx660d.png" alt="A modified histogram showing how variably sized buckets that are smaller at the extremes, like what the TDigest algorithm uses, can still represent the data (Note: for illustration purposes, not to scale.)" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;b&gt;A modified histogram showing how variably sized buckets that are smaller at the extremes, like what the TDigest algorithm uses, can still represent the data (Note: for illustration purposes, not to scale.)&lt;/b&gt;




&lt;p&gt;&lt;br&gt;
This histogram structure with variable-sized buckets optimizes for different things than UDDSketch. Specifically, it takes advantage of the idea that when you’re trying to understand the distribution, you likely care more about fine distinctions between extreme values than about the middle of the range.&lt;/p&gt;

&lt;p&gt;For example, I usually care a lot about distinguishing the 5th percentile from the 1st or the 95th from the 99th, while I don’t care as much about distinguishing between the 50th and the 55th percentile.&lt;/p&gt;

&lt;p&gt;The distinctions in the middle are less meaningful and interesting than the distinctions at the extremes. (Caveat: the TDigest algorithm is a bit more complex than this, and this doesn’t completely capture its behavior, but we’re trying to give a general gist of what’s going on. If you want more information, &lt;a href="https://arxiv.org/abs/1902.04023" rel="noopener noreferrer"&gt;we recommend this paper&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  Using advanced approximation methods in TimescaleDB hyperfunctions
&lt;/h3&gt;

&lt;p&gt;So far in this post, we’ve only used the general-purpose &lt;code&gt;percentile_agg&lt;/code&gt; aggregate. It uses the UDDSketch algorithm under the hood and is a good starting point for most users.&lt;/p&gt;

&lt;p&gt;We’ve also provided separate &lt;code&gt;uddsketch&lt;/code&gt; and &lt;code&gt;tdigest&lt;/code&gt; aggregates to allow for more customizability.&lt;/p&gt;

&lt;p&gt;Each takes the number of buckets as their first argument (which determines the size of the internal data structure), and &lt;code&gt;uddsketch&lt;/code&gt; also has an argument for the target maximum relative error.&lt;/p&gt;

&lt;p&gt;We can use the normal &lt;code&gt;approx_percentile&lt;/code&gt; accessor function  just as we used with &lt;code&gt;percentile_agg&lt;/code&gt;, so, we could compare median estimations like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uddsketch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;median_udd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;median_tdig&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of them also work with the &lt;code&gt;approx_percentile_rank&lt;/code&gt; hyperfunction we discussed above.&lt;/p&gt;

&lt;p&gt;If we wanted to see where 1000 would fall in our distribution, we could do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uddsketch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rnk_udd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approx_percentile_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rnk_tdig&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, each of the approximations have some accessors that only work with their items based on the approximation structure.&lt;/p&gt;

&lt;p&gt;For instance, &lt;code&gt;uddsketch&lt;/code&gt; provides an error accessor function. This will tell you the actual guaranteed maximum relative error based on the values that the &lt;code&gt;uddsketch&lt;/code&gt; saw.&lt;/p&gt;

&lt;p&gt;The UDDSketch algorithm guarantees a maximum relative error, while the T-Digest algorithm does not, so &lt;code&gt;error&lt;/code&gt; only works with &lt;code&gt;uddsketch&lt;/code&gt; (and &lt;code&gt;percentile_agg&lt;/code&gt; because it uses &lt;code&gt;uddsketch&lt;/code&gt; algorithm under the hood).&lt;/p&gt;

&lt;p&gt;This error guarantee is one of the main reasons we chose it as the default, because error guarantees are useful for determining whether you’re getting a good approximation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tdigest&lt;/code&gt;, on the other hand, provides &lt;code&gt;min_val&lt;/code&gt; &amp;amp; &lt;code&gt;max_val&lt;/code&gt; accessor functions because it biases its buckets to the extremes and can provide the exact min and max values at no extra cost. &lt;code&gt;Uddsketch&lt;/code&gt; can’t provide that.&lt;/p&gt;

&lt;p&gt;You can call these other accessors like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uddsketch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;median_udd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uddsketch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error_udd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;median_tdig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_val&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_val&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we discussed in the last post about &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=blog-how-postgresql-aggregation-works-hyperfunctions#why-we-use-the-two-step-aggregate-design-pattern" rel="noopener noreferrer"&gt;two-step aggregates&lt;/a&gt;, calls to all of these aggregates are automatically deduplicated and optimized by PostgreSQL so that you can call multiple accessors with minimal extra cost.&lt;/p&gt;

&lt;p&gt;They also both have &lt;code&gt;rollup&lt;/code&gt;  functions defined for them, so you can re-aggregate when they’re used in continuous aggregates or regular queries.&lt;/p&gt;

&lt;p&gt;(Note: &lt;code&gt;tdigest&lt;/code&gt; rollup can introduce some additional error or differences compared to calling the &lt;code&gt;tdigest&lt;/code&gt; on the underlying data directly. In most cases, this should be negligible and would often be comparable to changing the order in which the underlying data was ingested.)&lt;/p&gt;

&lt;p&gt;We’ve provided a few of the tradeoffs and differences between the algorithms here, but we have a &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/percentile-aggregation-methods/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=docs-advanced-percentile-aggregation#choosing-the-right-algorithm-for-your-use-case" rel="noopener noreferrer"&gt;longer discussion in the docs that can help you choose&lt;/a&gt;. You can also start with the default &lt;code&gt;percentile_agg&lt;/code&gt; and then experiment with different algorithms and parameters on your data to see what works best for your application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping it up&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;We’ve provided a brief overview of percentiles, how they can be more informative than more common statistical aggregates like average, why percentile approximations exist, and a little bit of how they generally work and within TimescaleDB hyperfunctions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get started with the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=docs-percentile-approximation" rel="noopener noreferrer"&gt;percentile approximation hyperfunctions&lt;/a&gt; - and many more - right away, spin up a fully managed TimescaleDB service:&lt;/strong&gt; create an account to &lt;a href="https://console.cloud.timescale.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=percentile-approximation-2021&amp;amp;utm_content=cloud-signup" rel="noopener noreferrer"&gt;try it for free&lt;/a&gt; for 30 days. (Hyperfunctions are pre-loaded on each new database service on Timescale Cloud, so after you create a new service, you’re all set to use them).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;download and install the &lt;code&gt;timescaledb_toolkit&lt;/code&gt; extension&lt;/a&gt;&lt;/strong&gt; on GitHub, after which you’ll be able to use percentile approximation and other hyperfunctions.&lt;/p&gt;

&lt;p&gt;We believe time-series data is everywhere, and making sense of it is crucial for all manner of technical problems. We built hyperfunctions to make it easier for developers to harness the power of time-series data.&lt;/p&gt;

&lt;p&gt;We’re always looking for feedback on what to build next and would love to know how you’re using hyperfunctions, problems you want to solve, or things you think should - or could - be simplified to make analyzing time-series data in SQL that much better. (To contribute feedback, comment on an &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;open issue&lt;/a&gt; or in a &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt; in GitHub.)&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>datascience</category>
      <category>opensource</category>
      <category>database</category>
    </item>
    <item>
      <title>Function pipelines: Building functional programming into PostgreSQL using custom operators</title>
      <dc:creator>davidkohn88</dc:creator>
      <pubDate>Wed, 24 Nov 2021 21:29:43 +0000</pubDate>
      <link>https://dev.to/tigerdata/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators-4e4n</link>
      <guid>https://dev.to/tigerdata/function-pipelines-building-functional-programming-into-postgresql-using-custom-operators-4e4n</guid>
      <description>&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Function pipelines: why are they useful?&lt;/li&gt;
&lt;li&gt;How we built function pipelines without forking PostgreSQL&lt;/li&gt;
&lt;li&gt;A custom data type: the &lt;code&gt;timevector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A custom operator: &lt;code&gt;-&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Custom functions: pipeline elements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timevector&lt;/code&gt; transforms&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timevector&lt;/code&gt; finalizers&lt;/li&gt;
&lt;li&gt;Aggregate accessors and mutators&lt;/li&gt;
&lt;li&gt;Next steps&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;We are announcing function pipelines, a new capability that introduces functional programming concepts inside PostgreSQL (and SQL) using custom operators.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Function pipelines&lt;/em&gt; radically improve the developer ergonomics of analyzing data in PostgreSQL and SQL, by applying principles from &lt;a href="https://en.wikipedia.org/wiki/Functional_programming" rel="noopener noreferrer"&gt;functional programming&lt;/a&gt; and popular tools like Python’s &lt;a href="https://pandas.pydata.org/docs/index.html" rel="noopener noreferrer"&gt;Pandas&lt;/a&gt; and &lt;a href="https://prometheus.io/docs/prometheus/latest/querying/basics/" rel="noopener noreferrer"&gt;PromQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At Timescale our mission is to serve developers worldwide, and enable them to build exceptional data-driven products that measure everything that matters: e.g., software applications, industrial equipment, financial markets, blockchain activity, user actions, consumer behavior, machine learning models, climate change, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.timescale.com/blog/why-sql-beating-nosql-what-this-means-for-future-of-data-time-series-database-348b777b847a/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-sql-beating-no-sql" rel="noopener noreferrer"&gt;We believe SQL is the best language for data analysis&lt;/a&gt;. We’ve championed the benefits of SQL for several years, even back when many were abandoning the language for custom domain-specific languages. And we were right - SQL has resurged and become the universal language for data analysis, and now many NoSQL databases are adding SQL interfaces to keep up.&lt;/p&gt;

&lt;p&gt;But SQL is not perfect, and at times can get quite unwieldy. For example,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;abs_delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;volatility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;abs_delta&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;calc_delta&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Pop quiz: What does this query do?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Even if you are a SQL expert, queries like this can be quite difficult to read - and even harder to express. Complex data analysis in SQL can be hard.&lt;/p&gt;

&lt;p&gt;Function pipelines let you express that same query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;abs_delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;volatility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;abs_delta&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;calc_delta&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it is much clearer what this query is doing. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gets the last day’s data from the measurements table, grouped by &lt;code&gt;device_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sorts the data by the time column&lt;/li&gt;
&lt;li&gt;Calculates the delta (or change) between values&lt;/li&gt;
&lt;li&gt;Takes the absolute value of the delta&lt;/li&gt;
&lt;li&gt;And then takes the sum of the result of the previous steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Function pipelines improve your own coding productivity, while also making your SQL code easier for others to comprehend and maintain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inspired by functional programming languages, function pipelines enable you to analyze data by composing multiple functions, leading to a simpler, cleaner way of expressing complex logic in PostgreSQL.&lt;/p&gt;

&lt;p&gt;And the best part: we built function pipelines in a way that is fully PostgreSQL compliant - we did not change any SQL syntax - meaning that any tool that speaks PostgreSQL will be able to support data analysis using function pipelines.&lt;/p&gt;

&lt;p&gt;How did we build this? By taking advantage of the incredible extensibility of PostgreSQL, in particular: &lt;a href="https://www.postgresql.org/docs/current/sql-createtype.html" rel="noopener noreferrer"&gt;custom types&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/sql-createoperator.html" rel="noopener noreferrer"&gt;custom operators&lt;/a&gt;, and &lt;a href="https://www.postgresql.org/docs/current/sql-createfunction.html" rel="noopener noreferrer"&gt;custom functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In our previous example, you can see the key elements of function pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom data types:&lt;/strong&gt; in this case, the &lt;code&gt;timevector&lt;/code&gt;, which is a set of &lt;code&gt;(time, value)&lt;/code&gt; pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom operator:&lt;/strong&gt; &lt;code&gt;-&amp;gt;&lt;/code&gt;, used to &lt;em&gt;compose&lt;/em&gt; and &lt;em&gt;apply&lt;/em&gt; function pipeline elements to the data that comes in.&lt;/li&gt;
&lt;li&gt;And finally, &lt;strong&gt;custom functions:&lt;/strong&gt; called pipeline elements. Pipeline elements can transform and analyze &lt;code&gt;timevector&lt;/code&gt;s (or other data types) in a function pipeline. For this initial release, we’ve built 60 custom functions! &lt;strong&gt;(&lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/function-pipelines/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-how-to-hyperfunctions" rel="noopener noreferrer"&gt;Full list here&lt;/a&gt;)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll go into more detail on function pipelines in the rest of this post, but if you just want to get started as soon as possible, the &lt;strong&gt;easiest way to try function pipelines is through a fully managed Timescale Cloud service&lt;/strong&gt;. &lt;a href="https://console.cloud.timescale.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=cloud-signup" rel="noopener noreferrer"&gt;Try it for free&lt;/a&gt; (no credit card required) for 30 days.&lt;/p&gt;

&lt;p&gt;Function pipelines are pre-loaded on each new database service on Timescale Cloud, available immediately - so after you’ve created a new service, you’re all set to use them!&lt;/p&gt;

&lt;p&gt;If you prefer to manage your own database instances, &lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/install-toolkit/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-hyperfunctions" rel="noopener noreferrer"&gt;you can install the &lt;code&gt;timescaledb_toolkit&lt;/code&gt;&lt;/a&gt; into your existing PostgreSQL installation, completely for free.&lt;/p&gt;

&lt;p&gt;We’ve been working on this capability for a long time, but in line with our belief of “&lt;a href="https://blog.timescale.com/blog/move-fast-but-dont-break-things-introducing-the-experimental-schema-with-new-experimental-features-in-timescaledb-2-4/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-introducing-experimental-features" rel="noopener noreferrer"&gt;move fast but don’t break things&lt;/a&gt;”, we’re initially releasing function pipelines as an &lt;a href="https://docs.timescale.com/api/latest/api-tag-overview/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-experimental-feature#experimental-timescaledb-toolkit" rel="noopener noreferrer"&gt;experimental feature&lt;/a&gt; - and we would absolutely love to &lt;strong&gt;get your feedback&lt;/strong&gt;. You can &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or join a &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt; in GitHub (And, if you like what you see, GitHub ⭐ are always welcome and appreciated too!).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We’d also like to take this opportunity to give a huge shoutout to &lt;code&gt;pgx&lt;/code&gt;, &lt;a href="https://github.com/zombodb/pgx" rel="noopener noreferrer"&gt;the Rust-based framework for building PostgreSQL extensions&lt;/a&gt;  it handles a lot of the heavy lifting for this project. We have over 600 custom types, operators, and functions in the &lt;code&gt;timescaledb_toolkit&lt;/code&gt; extension at this point; managing this without &lt;code&gt;pgx&lt;/code&gt; (and the ease of use that comes from working with Rust) would be a real bear of a job.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Function pipelines: why are they useful?&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In the Northern hemisphere (where most of Team Timescale sits, including your authors), it is starting to get cold at this time of the year.&lt;/p&gt;

&lt;p&gt;Now imagine a restaurant in New York City whose owners care about their customers and their customers’ comfort. And you are working on an IoT product designed to help small businesses like these owners minimize their heating bill while maximizing their customers happiness. So you install two thermometers, one at the front measuring the temperature right by the door, and another at the back of the restaurant.&lt;/p&gt;

&lt;p&gt;Now, as many of you may know (if you’ve ever had to sit by the door of a restaurant in the fall or winter), when someone enters, the temperature drops - and once the door is closed, the temperature warms back up. The temperature at the back of the restaurant will vary much less than at the front, right by the door. And both of them will drop slowly down to a lower set point during non-business hours and warm back up sometime before business hours based on the setpoints on our thermostat. So overall we’ll end up with a graph that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwynl4d3hm8orykb2apkl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwynl4d3hm8orykb2apkl.jpg" alt="A graph of the temperature at the front (near the door) and back. The back is much steadier, while the front is more volatile. Graph is for illustrative purposes only, data is fabricated. No restaurants or restaurant patrons were harmed in the making of this post." width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;
A graph of the temperature at the front (near the door) and back. The back is much steadier, while the front is more volatile. Graph is for illustrative purposes only, data is fabricated. No restaurants or restaurant patrons were harmed in the making of this post.




&lt;p&gt;&lt;br&gt;
As we can see, the temperature by the front door varies much more than at the back of the restaurant. Another way to say this is the temperature by the front door is more &lt;em&gt;volatile&lt;/em&gt;. Now, the owners of this restaurant want to measure this because frequent temperature changes means uncomfortable customers.&lt;/p&gt;

&lt;p&gt;In order to measure volatility, we could first subtract each point from the point before to calculate a delta. If we add this up directly, large positive and negative deltas will cancel out. But, we only care about the magnitude of the delta, not its sign - so what we really should do is take the absolute value of the delta, and then take the total sum of the previous steps.&lt;/p&gt;

&lt;p&gt;We now have a metric that might help us measure customer comfort, and also the efficacy of different weatherproofing methods (for example, adding one of those little vestibules that acts as a windbreak).&lt;/p&gt;

&lt;p&gt;To track this, we collect measurements from our thermometers and store them in a table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;device_id&lt;/code&gt; identifies the thermostat, &lt;code&gt;ts&lt;/code&gt; the time of reading and &lt;code&gt;val&lt;/code&gt; the temperature.&lt;/p&gt;

&lt;p&gt;Using the data in our measurements table, let’s look at how we calculate volatility using function pipelines.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Because all of the function pipeline features are still experimental, they exist in the &lt;code&gt;toolkit_experimental&lt;/code&gt; schema. Before running any of the SQL code in this post you will need to set your &lt;code&gt;search_path&lt;/code&gt; to include the experimental schema as we do in the example below, we won’t repeat this throughout the post so as not to distract.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;toolkit_experimental&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;--still experimental, so do this to make it easier to read&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
        &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;volatility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we have the same query that we used as our example in the introduction.&lt;/p&gt;

&lt;p&gt;In this query, the function pipeline&lt;br&gt;
&lt;code&gt;timevector(ts, val) -&amp;gt; sort() -&amp;gt; delta() -&amp;gt; abs() -&amp;gt; sum()&lt;/code&gt; succinctly expresses the following operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;timevector&lt;/code&gt;s (more detail on this later) out of the &lt;code&gt;ts&lt;/code&gt; and &lt;code&gt;val&lt;/code&gt; columns&lt;/li&gt;
&lt;li&gt;Sort each &lt;code&gt;timevector&lt;/code&gt; by the time column&lt;/li&gt;
&lt;li&gt;Calculate the delta (or change) between each pair in the &lt;code&gt;timevector&lt;/code&gt; by subtracting the previous &lt;code&gt;val&lt;/code&gt; from the current&lt;/li&gt;
&lt;li&gt;Take the absolute value of the delta&lt;/li&gt;
&lt;li&gt;Take the sum of the result from the previous steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;GROUP BY&lt;/code&gt; clauses do the rest of the work telling us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We’re getting data &lt;em&gt;FROM&lt;/em&gt; the &lt;code&gt;measurements&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;WHERE&lt;/em&gt; the &lt;code&gt;ts&lt;/code&gt;, or timestamp column, contains values over the last day&lt;/li&gt;
&lt;li&gt;Showing one pipeline output per &lt;code&gt;device_id&lt;/code&gt; (the &lt;em&gt;GROUP BY&lt;/em&gt; column)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we noted before, if you were to do this same calculation using SQL and PostgreSQL functionality, your query would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;abs_delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;volatility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;abs_delta&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;calc_delta&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does the same 5 steps as the above, but is much harder to understand, because we have to use a &lt;a href="https://www.postgresql.org/docs/current/functions-window.html" rel="noopener noreferrer"&gt;window function&lt;/a&gt; and aggregate the results - but also, because aggregates are performed before window functions, we need to actually execute the window function in a subquery.&lt;/p&gt;

&lt;p&gt;As we can see, function pipelines make it significantly easier to comprehend the overall analysis of our data. There’s no need to completely understand what’s going on in these functions just yet, but for now it’s enough to understand that we’ve essentially implemented a small functional programming language inside of PostgreSQL. You can still use all of the normal, expressive SQL you’ve come to know and love. Function pipelines just add new tools to your SQL toolbox that make it easier to work with time-series data.&lt;/p&gt;

&lt;p&gt;Some avid SQL users might find the syntax a bit foreign at first, but for many people who work in other programming languages, especially using tools like &lt;a href="https://pandas.pydata.org/docs/index.html" rel="noopener noreferrer"&gt;Python’s Pandas Package&lt;/a&gt;, this type of &lt;a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html" rel="noopener noreferrer"&gt;successive operation on data sets&lt;/a&gt; will feel natural.&lt;/p&gt;

&lt;p&gt;And again, this is still fully PostgreSQL compliant: We introduce no changes to the parser or anything that should break compatibility with PostgreSQL drivers.&lt;/p&gt;




&lt;h2&gt;
  
  
  How we built function pipelines without forking PostgreSQL&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;We built function pipelines- without modifying the &lt;a href="https://www.postgresql.org/docs/10/parser-stage.html" rel="noopener noreferrer"&gt;parser&lt;/a&gt; or anything that would require a fork of PostgreSQL- by taking advantage of three of the many ways that PostgreSQL enables extensibility: &lt;a href="https://www.postgresql.org/docs/current/sql-createtype.html" rel="noopener noreferrer"&gt;custom types&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/sql-createfunction.html" rel="noopener noreferrer"&gt;custom functions&lt;/a&gt;, and &lt;a href="https://www.postgresql.org/docs/current/sql-createoperator.html" rel="noopener noreferrer"&gt;custom operators&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom data types&lt;/strong&gt;, starting with the &lt;code&gt;timevector&lt;/code&gt;, which is a set of &lt;code&gt;(time, value)&lt;/code&gt; pairs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A custom operator&lt;/strong&gt;: &lt;code&gt;-&amp;gt;&lt;/code&gt;, which is used to &lt;em&gt;compose&lt;/em&gt; and &lt;em&gt;apply&lt;/em&gt; function pipeline elements to the data that comes in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom functions&lt;/strong&gt;, called &lt;em&gt;pipeline elements&lt;/em&gt;, which can transform and analyze &lt;code&gt;timevectors&lt;/code&gt; (or other data types) in a function pipeline (with 60 functions in this initial release)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We believe that new idioms like these are exactly what PostgreSQL was meant to enable. That’s why it has supported custom types, functions and operators from its earliest days. (And is one of the many reasons why we love PostgreSQL.)&lt;/p&gt;




&lt;h2&gt;
  
  
  A custom data type: the timevector&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;timevector&lt;/code&gt; is a collection of &lt;code&gt;(time, value)&lt;/code&gt; pairs. As of now, the times must be &lt;code&gt;TIMESTAMPTZ&lt;/code&gt;s and the values must be &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; numbers. (But this may change in the future as we continue to develop this data type. If you have ideas/input, please &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;file feature requests on GitHub&lt;/a&gt; explaining what you’d like!)&lt;/p&gt;

&lt;p&gt;You can think of the &lt;code&gt;timevector&lt;/code&gt; as something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvr7whthoombtlcjl4my.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvr7whthoombtlcjl4my.jpg" alt="A depiction of a  raw `timevector` endraw ." width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;
A depiction of a `timevector`.




&lt;p&gt;&lt;br&gt;
One of the first questions you might ask is: how does a &lt;code&gt;timevector&lt;/code&gt; relate to time-series data? (If you want to know more about time-series data, we have a &lt;a href="https://blog.timescale.com/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-what-the-heck-is-time-series-data" rel="noopener noreferrer"&gt;great blog post on that&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Let’s consider our example from above, where we were talking about a restaurant that was measuring temperatures, and we had a &lt;code&gt;measurements&lt;/code&gt; table like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we can think of a single time-series dataset as all historical and future time and temperature measurements from a device. &lt;/p&gt;

&lt;p&gt;Given this definition, we can think of a &lt;code&gt;timevector&lt;/code&gt; as a &lt;strong&gt;finite subset of a time-series dataset&lt;/strong&gt;. The larger time-series dataset may extend back into the past and it may extend into the future, but the &lt;code&gt;timevector&lt;/code&gt; is bounded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2hj19hq3of6p5nsblyz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2hj19hq3of6p5nsblyz.jpg" alt="A  raw `timevector` endraw  is a finite subset of a time-series and contains all the  raw `(time, value)` endraw  pairs in some region of the time-series." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;
A `timevector` is a finite subset of a time-series and contains all the `(time, value)` pairs in some region of the time-series.




&lt;p&gt;&lt;br&gt;
In order to construct a &lt;code&gt;timevector&lt;/code&gt;  from the data gathered from a thermometer, we use a custom aggregate and pass in the columns we want to become our &lt;code&gt;(time, value)&lt;/code&gt; pairs. We can use the &lt;code&gt;WHERE&lt;/code&gt; clause to define the extent of the &lt;code&gt;timevector&lt;/code&gt; (i.e., the limits of this subset), and the &lt;code&gt;GROUP BY&lt;/code&gt; clause to provide identifying information about the time-series that’s represented. &lt;/p&gt;

&lt;p&gt;Building on our example, this is how we construct a &lt;code&gt;timevector&lt;/code&gt; for each thermometer in our dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a &lt;code&gt;timevector&lt;/code&gt; doesn't provide much value by itself. So now, let’s also consider some complex calculations that we can apply to the &lt;code&gt;timevector&lt;/code&gt;, starting with a custom operator used to apply these functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  A custom operator: -&amp;gt;&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In function pipelines, the &lt;code&gt;-&amp;gt;&lt;/code&gt; operator is used to apply and compose multiple functions, in an easy to write and read format. &lt;/p&gt;

&lt;p&gt;Fundamentally, &lt;code&gt;-&amp;gt;&lt;/code&gt; means: “apply the operation on the right to the inputs on the left”, or, more simply “do the next thing”. &lt;/p&gt;

&lt;p&gt;We created a general-purpose operator for this because we think that too many operators meaning different things can get very confusing and difficult to read.&lt;/p&gt;

&lt;p&gt;One thing that you’ll notice about the pipeline elements is that the arguments are in an unusual place in a statement like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
 &lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;volatility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It appears (from the semantics) that the &lt;code&gt;timevector(ts, val)&lt;/code&gt; is an argument to &lt;code&gt;sort()&lt;/code&gt;, the resulting &lt;code&gt;timevector&lt;/code&gt; is an argument to &lt;code&gt;delta()&lt;/code&gt; and so on. &lt;/p&gt;

&lt;p&gt;The thing is that &lt;code&gt;sort()&lt;/code&gt; (and the others) are regular function calls; they can’t see anything outside of their parentheses and don’t know about anything to their left in the statement; so we need a way to get the &lt;code&gt;timevector&lt;/code&gt; into the &lt;code&gt;sort()&lt;/code&gt; (and the rest of the pipeline). &lt;/p&gt;

&lt;p&gt;The way we solved this is by taking advantage of one of the same fundamental computing insights that functional programming languages use: code and data are really the same thing. &lt;/p&gt;

&lt;p&gt;Each of our functions returns a special type that describes the function and its arguments. We call these types pipeline elements (more later). &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-&amp;gt;&lt;/code&gt; operator then performs one of two different types of actions depending on the types on its right and left sides.  It can either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Apply&lt;/em&gt; a pipeline element to the left hand argument - perform the function described by the pipeline element on the incoming data type directly.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Compose&lt;/em&gt; pipeline elements into a combined element that can be applied at some point in the future (this is an optimization that allows us to apply multiple elements in a “nested” manner so that we don’t perform multiple unnecessary passes).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The operator determines the action to perform based on its left and right arguments. &lt;/p&gt;

&lt;p&gt;Let’s look at our&lt;code&gt;timevector&lt;/code&gt; from before: &lt;code&gt;timevector(ts, val) -&amp;gt; sort() -&amp;gt; delta() -&amp;gt; abs() -&amp;gt; sum()&lt;/code&gt;. If you remember from before, I noted that this function pipeline performs the following steps: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;timevector&lt;/code&gt;s out of the &lt;code&gt;ts&lt;/code&gt; and &lt;code&gt;val&lt;/code&gt; columns&lt;/li&gt;
&lt;li&gt;Sort it by the time column &lt;/li&gt;
&lt;li&gt;Calculate the delta (or change) between each pair in the &lt;code&gt;timevector&lt;/code&gt; by subtracting the previous &lt;code&gt;val&lt;/code&gt; from the current&lt;/li&gt;
&lt;li&gt;Take the absolute value of the delta&lt;/li&gt;
&lt;li&gt;Take the sum of the result from the previous steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And logically, at each step, we can think of the &lt;code&gt;timevector&lt;/code&gt; being materialized and passed to the next step in the pipeline. &lt;/p&gt;

&lt;p&gt;However, while this will produce a correct result, it’s not the most efficient way to compute this. Instead, it would be more efficient to compute as much as possible in a single pass over the data. &lt;/p&gt;

&lt;p&gt;In order to do this, we allow not only the apply operation, but also the compose operation. Once we’ve composed a pipeline into a logically equivalent higher order pipeline with all of the elements we can choose the most efficient way to execute it internally. (Importantly, even if we have to perform each step sequentially, we don’t need to materialize it and pass it between each step in the pipeline so it has significantly less overhead even without other optimization). &lt;/p&gt;




&lt;h2&gt;
  
  
  Custom functions: pipeline elements&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Now let’s discuss the third, and final, key piece that makes up function pipelines: custom functions, or as we call them, pipeline elements.&lt;/p&gt;

&lt;p&gt;We have implemented over 60 individual pipeline elements, which fall into 4 categories (with a few subcategories):&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;timevector&lt;/code&gt; transforms&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;These elements take in a &lt;code&gt;timevector&lt;/code&gt; and produce a&lt;code&gt;timevector&lt;/code&gt;. They are the easiest to compose, as they produce the same type.&lt;/p&gt;

&lt;p&gt;Example pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lttb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Organized by sub-category:&lt;/p&gt;

&lt;h3&gt;
  
  
  Unary mathematical
&lt;/h3&gt;

&lt;p&gt;Simple mathematical functions applied to the value in each point in a &lt;code&gt;timevector&lt;/code&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abs()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the absolute value of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cbrt()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the cube root of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ceil()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the first integer greater than or equal to each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;floor()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the first integer less than or equal to each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ln()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the natural logarithm of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log10()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the base 10 logarithm of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the closest integer to each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sign()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes +/-1 for each positive/negative value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqrt()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the square root for each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trunc()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes only the integer portion of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Binary mathematical
&lt;/h3&gt;

&lt;p&gt;Simple mathematical functions with a scalar input applied to the value in each point in a &lt;code&gt;timevector&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes each value plus &lt;code&gt;N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;div(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes each value divided by &lt;code&gt;N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logn(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the logarithm base &lt;code&gt;N&lt;/code&gt; of each value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mod(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the remainder when each number is divided by &lt;code&gt;N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mul(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes each value multiplied by &lt;code&gt;N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;power(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes each value taken to the &lt;code&gt;N&lt;/code&gt; power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes each value less &lt;code&gt;N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Compound transforms
&lt;/h3&gt;

&lt;p&gt;Transforms involving multiple points inside of a &lt;code&gt;timevector&lt;/code&gt; - &lt;em&gt;&lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/function-pipelines/#compound-transforms" rel="noopener noreferrer"&gt;see here for more information&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delta()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subtracts each value from the previous`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fill_to(interval, fill_method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fills gaps larger than &lt;code&gt;interval&lt;/code&gt; with points at &lt;code&gt;interval&lt;/code&gt; from the previous using &lt;code&gt;fill_method&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lttb(resolution)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downsamples a &lt;code&gt;timevector&lt;/code&gt; using the largest triangle three buckets algorithm at `resolution, requires sorted input.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sorts the &lt;code&gt;timevector&lt;/code&gt; by the &lt;code&gt;time&lt;/code&gt; column ascending&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Lambda elements
&lt;/h3&gt;

&lt;p&gt;These elements use lambda expressions, which allows the user to write small functions to be evaluated over each point in a &lt;code&gt;timevector&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Lambda expressions can return a &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; value like &lt;code&gt;$$ $value^2 + $value + 3 $$&lt;/code&gt;. They can return a &lt;code&gt;BOOL&lt;/code&gt; like &lt;code&gt;$$ $time &amp;gt; ‘2020-01-01’t $$&lt;/code&gt; .  They can also return a &lt;code&gt;(time, value)&lt;/code&gt; pair like &lt;code&gt;$$ ($time + ‘1 day’i, sin($value) * 4)$$&lt;/code&gt;. You can apply them using the elements below:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filter(lambda (bool) )&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes points from the &lt;code&gt;timevector&lt;/code&gt; where the lambda expression evaluates to &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map(lambda (value) )&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies the lambda expression to all the values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map(lambda (time, value) )&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies the lambda expression to all the times and values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;timevector&lt;/code&gt; finalizers&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;These elements end the &lt;code&gt;timevector&lt;/code&gt; portion of a pipeline, they can either help with output or  produce an aggregate over the entire &lt;code&gt;timevector&lt;/code&gt;. They are an optimization barrier to composition as they (usually) produce types other than &lt;code&gt;timevector&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Example pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finalizer pipeline elements organized by sub-category:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;timevector&lt;/code&gt; output
&lt;/h3&gt;

&lt;p&gt;These elements help with output, and can produce a set of &lt;code&gt;(time, value)&lt;/code&gt; pairs or a Note: this is an area where we’d love further feedback, are there particular data formats that would be especially useful for, say graphing that we can add? &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;File an issue in our GitHub!&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unnest( )&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Produces a set of &lt;code&gt;(time, value)&lt;/code&gt; pairs. You can wrap and expand as a composite type to produce separate columns &lt;code&gt;(pipe -&amp;gt; unnest()).*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;materialize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Materializes a &lt;code&gt;timevector&lt;/code&gt; to pass to an application or other operation directly, blocks any optimizations that would materialize it lazily.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;timevector&lt;/code&gt; aggregates
&lt;/h3&gt;

&lt;p&gt;Aggregate all the points in a &lt;code&gt;timevector&lt;/code&gt; to produce a single value as a result. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;average()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the average of the values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;couter_agg()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the counter_agg aggregate over the times and values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stats_agg()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes a range of statistical aggregates and returns a &lt;code&gt;1DStatsAgg&lt;/code&gt; over the values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the sum of the values in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_vals()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counts the points in the &lt;code&gt;timevector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Aggregate accessors and mutators&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;These function pipeline elements act like the accessors that I described in our &lt;a href="https://blog.timescale.com/blog/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-2/" rel="noopener noreferrer"&gt;previous post on aggregates&lt;/a&gt;. You can use them to get a value from the aggregate part of a function pipeline like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;timevector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stats_agg&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these don’t just work on &lt;code&gt;timevector&lt;/code&gt;s - they also work on a normally produced aggregate as well. &lt;/p&gt;

&lt;p&gt;When used instead of normal function accessors and mutators they can make the syntax more clear by getting rid of nested functions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, we can use the arrow accessor to convey the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;percentile_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;approx_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By aggregate family:&lt;/p&gt;

&lt;h3&gt;
  
  
  Counter aggregates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/counter-aggregation/counter-aggs/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-hyperfunctions-counter-aggregates#counter-aggregates" rel="noopener noreferrer"&gt;Counter aggregates&lt;/a&gt; deal with resetting counters, (and were stabilized in our 1.3 release this week!). Counters are a common type of metric in the application performance monitoring and metrics world. All values have resets accounted for. These elements must have a &lt;code&gt;CounterSummary&lt;/code&gt; to their left when used in a pipeline, from a &lt;code&gt;counter_agg()&lt;/code&gt; aggregate or pipeline element.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;counter_zero_time()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The time at which the counter value is predicted to have been zero based on the least squares fit of the points input to the &lt;code&gt;CounterSummary&lt;/code&gt;(x intercept)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corr()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The correlation coefficient of the least squares fit line of the adjusted counter value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delta()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the last - first value of the counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extapolated_delta(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computes the delta extrapolated using the provided method to bounds of range. Bounds must have been provided in the aggregate or a &lt;code&gt;with_bounds&lt;/code&gt; call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;idelta_left()&lt;/code&gt; / &lt;code&gt;idelta_right()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Computes the instantaneous difference between the second and first points (left) or last and next-to-last points (right)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;intercept()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The y-intercept of the least squares fit line of the adjusted counter value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;irate_left()&lt;/code&gt; / &lt;code&gt;irate_right()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Computes the instantaneous rate of change between the second and first points (left) or last and next-to-last points (right)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_changes()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of times the counter changed values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_elements()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of items - any with the exact same time will have been counted only once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_changes()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of times the counter reset.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slope()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The slope of the least squares fit line of the adjusted counter value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;with_bounds(range)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies bounds using the &lt;code&gt;range&lt;/code&gt; (a &lt;code&gt;TSTZRANGE&lt;/code&gt;) to the &lt;code&gt;CounterSummary&lt;/code&gt; if they weren’t provided in the aggregation step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Percentile approximation
&lt;/h3&gt;

&lt;p&gt;These aggregate accessors deal with &lt;a href="https://blog.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-how-percentile-approximation-works" rel="noopener noreferrer"&gt;percentile approximation&lt;/a&gt;. For now we’ve only implemented them for &lt;code&gt;percentile_agg&lt;/code&gt; and &lt;code&gt;uddsketch&lt;/code&gt; based aggregates. We have not yet implemented them for &lt;code&gt;tdigest&lt;/code&gt;. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approx_percentile(p)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The approximate value at percentile &lt;code&gt;p&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approx_percentile_rank(v)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The approximate percentile a value &lt;code&gt;v&lt;/code&gt; would fall in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The maximum relative error guaranteed by the approximation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mean()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The exact average of the input values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_vals()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The number of input values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Statistical aggregates
&lt;/h3&gt;

&lt;p&gt;These aggregate accessors add support for common &lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/function-pipelines/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-hyperfunctions-function-pipelines#statistical-aggregates" rel="noopener noreferrer"&gt;statistical aggregates&lt;/a&gt; (and were stabilized in our 1.3 release this week!). These allow you to compute and &lt;code&gt;rollup()&lt;/code&gt; common statistical aggregates like &lt;code&gt;average&lt;/code&gt;, &lt;code&gt;stddev&lt;/code&gt; and more advanced ones like &lt;code&gt;skewness&lt;/code&gt; as well as 2 dimensional aggregates like &lt;code&gt;slope&lt;/code&gt; and &lt;code&gt;covariance&lt;/code&gt;.  Because there are both 1D and 2D versions of these, the accessors can have multiple forms, for instance, &lt;code&gt;average()&lt;/code&gt; calculates the average on a 1D aggregate while &lt;code&gt;average_y()&lt;/code&gt; &amp;amp; &lt;code&gt;average_x()&lt;/code&gt; do so on each dimension of a 2D aggregate. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;average() / average_y() / average_x()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The average of the values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corr()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The correlation coefficient of the least squares fit line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;covariance(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The covariance of the values using either &lt;code&gt;population&lt;/code&gt; or &lt;code&gt;sample&lt;/code&gt; method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;determination_coeff()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The determination coefficient (aka R squared)  of the values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kurtosis(method) / kurtosis_y(method) / kurtosis_x(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The kurtosis (4th moment) of the values using either &lt;code&gt;population&lt;/code&gt; or &lt;code&gt;sample&lt;/code&gt; method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;intercept()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The intercept of the least squares fit line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num_vals()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The number of (non-null) values seen.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum() / sum_x() / sum_y()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The sum of the values seen.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skewness(method) / skewness_y(method) / skewness_x(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The skewness (3rd moment) of the values using either &lt;code&gt;population&lt;/code&gt; or &lt;code&gt;sample&lt;/code&gt; method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slope()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The slope of the least squares fit line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stddev(method) / stddev_y(method) / stddev_x(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The standard deviation of the values using either &lt;code&gt;population&lt;/code&gt; or &lt;code&gt;sample&lt;/code&gt; method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;variance(method) / variance_y(method) / variance_x(method)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The variance of the values using either &lt;code&gt;population&lt;/code&gt; or &lt;code&gt;sample&lt;/code&gt; method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x_intercept()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The x intercept of the least squares fit line.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Time weighted averages
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-time-weighted-averages" rel="noopener noreferrer"&gt;&lt;code&gt;average()&lt;/code&gt;&lt;/a&gt; accessor may be called on the output of a &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=blog-time-weighted-averages" rel="noopener noreferrer"&gt;&lt;code&gt;time_weight()&lt;/code&gt;&lt;/a&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Approximate count distinct (Hyperloglog)
&lt;/h3&gt;

&lt;p&gt;This is an &lt;a href="(https://docs.timescale.com/timescaledb/latest/how-to-guides/hyperfunctions/approx-count-distincts/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=docs-hyperfunctions-aprox-count-distinct)"&gt;approximation for distinct counts&lt;/a&gt; that was stabilized in our 1.3 release! The &lt;code&gt;distinct_count()&lt;/code&gt; accessor may be called on the output of a &lt;code&gt;hyperloglog()&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;hyperloglog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;distinct_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Next steps&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;We hope this post helped you understand how function pipelines leverage PostgreSQL extensibility to offer functional programming concepts in a way that is fully PostgreSQL compliant. And how function pipelines can improve the ergonomics of your code making it easier to write, read, and maintain. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://console.cloud.timescale.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=function-pipeline-2021&amp;amp;utm_content=cloud-signup" rel="noopener noreferrer"&gt;You can try function pipelines today&lt;/a&gt;&lt;/strong&gt; with a fully-managed Timescale Cloud service (no credit card required, free for 30 days). Function pipelines are available now on every new database service on Timescale Cloud, so after you’ve created a new service, you’re all set to use them!&lt;/p&gt;

&lt;p&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;download and install the &lt;code&gt;timescaledb_toolkit&lt;/code&gt; extension&lt;/a&gt; on GitHub for free, after which you’ll be able to use function pipelines. &lt;/p&gt;

&lt;p&gt;We love building in public. You can view our &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;upcoming roadmap on GitHub&lt;/a&gt; for a list of proposed features, as well as features we’re currently implementing and those that are available to use today. We also welcome feedback from the community (it helps us prioritize the features users really want). To contribute feedback, comment on an &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;open issue&lt;/a&gt; or in a &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt; in GitHub.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>opensource</category>
      <category>database</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How PostgreSQL aggregation works and how it inspired our hyperfunctions’ design</title>
      <dc:creator>davidkohn88</dc:creator>
      <pubDate>Mon, 30 Aug 2021 09:04:30 +0000</pubDate>
      <link>https://dev.to/tigerdata/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-33k6</link>
      <guid>https://dev.to/tigerdata/how-postgresql-aggregation-works-and-how-it-inspired-our-hyperfunctions-design-33k6</guid>
      <description>&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A primer on PostgreSQL aggregation (through pictures)&lt;/li&gt;
&lt;li&gt;Two-step aggregation in TimescaleDB hyperfunctions&lt;/li&gt;
&lt;li&gt;Why we use the two-step aggregate design pattern&lt;/li&gt;
&lt;li&gt;Two-step aggregation + continuous aggregates in TimescaleDB&lt;/li&gt;
&lt;li&gt;An example of how the two-step aggregate design impacts hyperfunctions’ code&lt;/li&gt;
&lt;li&gt;Summing it up&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Get a primer on PostgreSQL aggregation, how PostgreSQL’s implementation inspired us as we built TimescaleDB hyperfunctions and its integrations with advanced TimescaleDB features – and what this means for developers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At Timescale, our goal is to always focus on the developer experience, and we take great care to design our products and APIs to be developer-friendly. We believe that when our products are easy to use and accessible to a wide range of developers, we enable them to solve a breadth of different problems – and thus build solutions that solve big problems.&lt;/p&gt;

&lt;p&gt;This focus on developer experience is why we made the decision &lt;a href="https://blog.timescale.com/blog/when-boring-is-awesome-building-a-scalable-time-series-database-on-postgresql-2900ea453ee2/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=tsdb-beta-blog" rel="noopener noreferrer"&gt;early in the design of TimescaleDB to build on top of PostgreSQL&lt;/a&gt;. We believed then, as we do now, that building on &lt;a href="https://db-engines.com/en/ranking" rel="noopener noreferrer"&gt;the world’s fastest-growing database&lt;/a&gt; would have numerous benefits for our users. &lt;/p&gt;

&lt;p&gt;Perhaps the biggest of these advantages is developer productivity: developers can use the tools and frameworks they know and love and bring all of their SQL skills and expertise. &lt;/p&gt;

&lt;p&gt;Today, there are nearly three million active TimescaleDB databases running mission-critical time-series workloads across industries. Time-series data comes at you fast, sometimes generating millions of data points per second (&lt;a href="https://blog.timescale.com/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-series-blog" rel="noopener noreferrer"&gt;read more about time-series data&lt;/a&gt;). Because of this volume and rate of information, time-series data is complex to query and analyze. We built TimescaleDB as a purpose-built relational database for time-series to reduce that complexity so that developers can focus on their applications.&lt;/p&gt;

&lt;p&gt;So, we’re built with developer experience at our core, and we’ve continually released functionality to further this aim, including &lt;a href="https://blog.timescale.com/blog/timescaledb-2-0-a-multi-node-petabyte-scale-completely-free-relational-database-for-time-series/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=tsdb-2.0-blog" rel="noopener noreferrer"&gt;continuous aggregates, user-defined actions, informational views&lt;/a&gt;, and most recently, &lt;a href="https://blog.timescale.com/blog/introducing-hyperfunctions-new-sql-functions-to-simplify-working-with-time-series-data-in-postgresql/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=hyperfunctions-blog" rel="noopener noreferrer"&gt;TimescaleDB hyperfunctions&lt;/a&gt;: a series of SQL functions within TimescaleDB that make it easier to manipulate and analyze time-series data in PostgreSQL with fewer lines of code.&lt;/p&gt;

&lt;p&gt;To ensure we stay focused on developer experience as we plan new hyperfunctions features, we established a set of “design constraints” that guide our development decisions. Adhering to these guidelines ensures our APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work within the SQL language (no new syntax, just functions and aggregates)&lt;/li&gt;
&lt;li&gt;Intuitive for new and experienced SQL users&lt;/li&gt;
&lt;li&gt;Useful for just a few rows of data and high-performance with billions of rows&lt;/li&gt;
&lt;li&gt;Play nicely with all TimescaleDB features, and ideally, makes them &lt;em&gt;more&lt;/em&gt; useful to users&lt;/li&gt;
&lt;li&gt;Make fundamental things simple to make more advanced analyses possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does this look like in practice? In this post, I explain how these constraints led us to adopt two-step aggregation throughout TimescaleDB hyperfunctions, how two-step aggregates interact with other TimescaleDB features, and how PostgreSQL's internal aggregation API influenced our implementation. &lt;/p&gt;

&lt;p&gt;When we talk about two-step aggregation, we mean the following calling convention:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffk65hi08fowhbu3n8qgi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffk65hi08fowhbu3n8qgi.jpg" alt="code: SELECT average(time_weight('LOCF', value)) as time_weighted_average FROM foo;&amp;lt;br&amp;gt;
-- or&amp;lt;br&amp;gt;
SELECT approx_percentile(0.5, percentile_agg(value)) as median FROM bar;&amp;lt;br&amp;gt;
" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where we have an inner aggregate call:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucyq3dmed7kf24n4rtfq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucyq3dmed7kf24n4rtfq.jpg" alt="The same as the previous in terms of code, except the sections: time_weight('LOCF', value) and percentile_agg(value) are highlighted " width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And an outer accessor call:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9x6h15xut38k0s3b670.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9x6h15xut38k0s3b670.jpg" alt="The same as the previous in terms of code, except the sections: average(time_weight('LOCF', value)) and approx_percentile(0.5, percentile_agg(value)) are highlighted" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We chose this design pattern over the more common - and seemingly simpler - one-step aggregation approach, in which a single function encapsulates the behavior of both the inner aggregate and outer accessor:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvf9b3qwtnpzrrmorl5f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvf9b3qwtnpzrrmorl5f.jpg" alt="code: -- NB: THIS IS AN EXAMPLE OF AN API WE DECIDED NOT TO USE, IT DOES NOT WORK SELECT time_weighted_average('LOCF', value) as time_weighted_average FROM foo; -- or SELECT approx_percentile(0.5, value) as median FROM bar;" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read on for more on why the one-step aggregate approach quickly breaks down as you start doing more complex things (like composing functions into more advanced queries) and how, under the hood, almost all PostgreSQL aggregates do a version of two-step aggregation. You’ll learn how the PostgreSQL implementation inspired us as we built TimescaleDB hyperfunctions, continuous aggregates, and other advanced features – and what this means for developers. &lt;/p&gt;

&lt;p&gt;If you’d like to get started with hyperfunctions right away, &lt;a href="https://console.forge.timescale.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=signup" rel="noopener noreferrer"&gt;create your free trial account&lt;/a&gt; and start analyzing 🔥. (TimescaleDB hyperfunctions are pre-installed on every Timescale Forge instance, our hosted cloud-native relational time-series data platform).&lt;/p&gt;


&lt;h2&gt;
  
  
  A primer on PostgreSQL aggregation (through pictures)&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;When I first started learning about PostgreSQL 5 or 6 years ago (I was an electrochemist, and dealing with lots of battery data, as mentioned in &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weight-blog" rel="noopener noreferrer"&gt;my last post on time-weighted averages&lt;/a&gt;), I ran into some performance issues. I was trying to better understand what was going on inside the database in order to improve its performance – and that’s when I found &lt;a href="https://momjian.us" rel="noopener noreferrer"&gt;Bruce Momjian&lt;/a&gt;’s talks on &lt;a href="https://momjian.us/main/presentations/internals.html" rel="noopener noreferrer"&gt;PostgreSQL Internals Through Pictures&lt;/a&gt;. Bruce is well known in the community for his insightful talks (and his penchant for bow ties), and his sessions were a revelation for me. &lt;/p&gt;

&lt;p&gt;They’ve served as a foundation for my understanding of how PostgreSQL works ever since. He explained things so clearly, and I’ve always learned best when I can visualize what’s going on, so the “through pictures” part really helped - and stuck with - me. &lt;/p&gt;

&lt;p&gt;So this next bit is my attempt to channel Bruce by explaining some PostgreSQL internals through pictures. Cinch up your bow ties and get ready for some learnin’.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwckpfux0elhtialp4f1.gif" alt="A GIF of the author finishing tying a bow tie and fixing his shirt." width="600" height="337"&gt;The author pays homage to Bruce Momjian (and looks rather pleased with himself because he’s managed to tie a bow tie on the first try).





&lt;h3&gt;
  
  
  PostgreSQL aggregates vs. functions
&lt;/h3&gt;

&lt;p&gt;We have written about &lt;a href="https://blog.timescale.com/blog/introducing-hyperfunctions-new-sql-functions-to-simplify-working-with-time-series-data-in-postgresql/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=hyperfunctions-blog" rel="noopener noreferrer"&gt;how we use custom functions and aggregates to extend SQL&lt;/a&gt;, but we haven’t exactly explained the difference between them.&lt;/p&gt;

&lt;p&gt;The fundamental difference between an aggregate function and a “regular” function in SQL is that an &lt;strong&gt;aggregate&lt;/strong&gt; produces a single result from a group of related rows, while a regular &lt;strong&gt;function&lt;/strong&gt; produces a result for each row:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn89rz7olep6g62wxewx6.jpg" alt="A side-by-side diagram depicting an “aggregate” side and a “function” side and how each product results. There are three individual rows on the aggregate side, with arrows that point to a single result; on the function side, there are three individual rows, with arrows that point to three different results (one per row). " width="800" height="333"&gt;In SQL, aggregates produce a result from multiple rows, while functions produce a result per row.




&lt;p&gt;&lt;br&gt;
This is not to say that a function can’t have inputs from multiple columns; they just have to come from the same row. &lt;/p&gt;

&lt;p&gt;Another way to think about it is that functions often act on rows, whereas aggregates act on columns. To illustrate this, let’s consider a theoretical table ’foo’ with two columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;baz&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And just a few values, so we can easily see what’s going on:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The function &lt;a href="https://www.postgresql.org/docs/13/functions-conditional.html#FUNCTIONS-GREATEST-LEAST" rel="noopener noreferrer"&gt;&lt;code&gt;greatest()&lt;/code&gt;&lt;/a&gt; will produce the largest of the values in columns &lt;code&gt;bar&lt;/code&gt; and &lt;code&gt;baz&lt;/code&gt; for each row:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;greatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;greatest&lt;/span&gt; 
&lt;span class="c1"&gt;----------&lt;/span&gt;
        &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="mi"&gt;4&lt;/span&gt;
        &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Whereas the aggregate &lt;a href="https://www.postgresql.org/docs/current/functions-aggregate.html" rel="noopener noreferrer"&gt;&lt;code&gt;max()&lt;/code&gt;&lt;/a&gt; will produce the largest value from each column:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bar_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;baz_max&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;bar_max&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;baz_max&lt;/span&gt; 
&lt;span class="c1"&gt;----|--------&lt;/span&gt;
       &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="nv"&gt;`&lt;/span&gt;&lt;span class="se"&gt;``&lt;/span&gt;&lt;span class="nv"&gt;



Using the above data, here’s a picture of what happens when we aggregate something: 
&amp;lt;figure&amp;gt;
&amp;lt;img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwomr9qpp8o3p461swg4.jpg" alt="A diagram showing how the statement: `&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;` works: multiple rows with values of “bar equal to” 1.0, 2.0, and 3.0, go through the `&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;` aggregate to ultimately produce a result of 3.0. " style="width:100%"&amp;gt;
&amp;lt;figcaption align = "center"&amp;gt;The `&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nv"&gt;` aggregate gets the largest value from multiple rows.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;
&amp;lt;p&amp;gt;
The aggregate takes inputs from multiple rows and produces a single result. That’s the main difference between it and a function, but how does it do that? Let’s look at what it’s doing under the hood.

### Aggregate internals: row-by-row
Under the hood, aggregates in PostgreSQL work row-by-row. But, then how does an aggregate know anything about the previous rows? 

Well, an aggregate stores some state about the rows it has previously seen, and as the database sees new rows, it updates that internal state. 

For the `&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nv"&gt;` aggregate we’ve been discussing, the internal state is simply the largest value we’ve collected so far. 

Let’s take this step-by-step. 

When we start, our internal state is `&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="nv"&gt;` because we haven’t seen any rows yet:

![Flowchart arrow diagram representing the max open parens bar close parens aggregate, with three rows below the arrow where bar is equal to 1.0, 2.0, and 3.0, respectively. There is a box in the arrow in which the state is equal to NULL. ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edvpx1w1ga0qori6dwul.jpg)

Then, we get our first row in: 
![The same flowchart arrow diagram, except that row one, with bar equal to 1.0, has moved from below the arrow_into_ the arrow. ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7q3ejwkxieanvy4idem.jpg)

Since our state is `&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="nv"&gt;`, we initialize it to the first value we see:
![The same flowchart diagram, except that row one has moved _out_ of the arrow, and the state has been updated from NULL to the 1.0, row one’s value.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6izuxd45sevfvwjvh8ct.jpg)

Now, we get our second row: 
![The same flowchart diagram, except that row two has moved into the arrow representing the max aggregate. ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/37jr6glndj13fdim3ehr.jpg)

And we see that the value of bar (2.0) is greater than our current state (1.0), so we update the state:
![The same diagram, except that row two has moved out of the max aggregate, and the state has been updated to the largest value (the value of row two, 2.0). ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qvr3udolmos6v01wo59e.jpg)

Then, the next row comes into the aggregate:
![The same diagram, except that the row three has moved into the arrow representing the max aggregate. ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7pjbniuy0xrdqsg5ufja.jpg)

We compare it to our current state, take the greatest value, and update our state: 
![The same diagram, expect that row three has moved out of the max aggregate, and the state has been updated to the largest value, the value of the third row, 3.0.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfkp9q9wf5gtey3k752b.jpg)

Finally, we don’t have any more rows to process, so we output our result:
![The same diagram, now noting that there are “no more rows” to process, and including a final result, 3.0, being output at the end of the arrow.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ye5u1cdnqk3tslrpd225.jpg)

So, to summarize, each row comes in, gets compared to our current state, and then the state gets updated to reflect the new greatest value. Then the next row comes in, and we repeat the process until we’ve processed all our rows and output the result.
&amp;lt;figure&amp;gt;
&amp;lt;img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbqf9uvopm9xq9skfwwd.gif" alt="A GIF depicting the previous diagrams, one after the other, as the rows move through the aggregate." style="width:100%"&amp;gt;
&amp;lt;figcaption align = "center"&amp;gt;The max aggregate aggregation process, told in GIFs.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;
&amp;lt;p&amp;gt;
There’s a name for the function that processes each row and updates the internal state: the **[state transition function](https://www.postgresql.org/docs/current/sql-createaggregate.html)** (or just “transition function” for short.) The transition function for an aggregate takes the current state and the value from the incoming row as arguments and produces a new state. 

It’s defined like this, where `&lt;/span&gt;&lt;span class="n"&gt;current_value&lt;/span&gt;&lt;span class="nv"&gt;` represents values from the incoming row, `&lt;/span&gt;&lt;span class="n"&gt;current_state&lt;/span&gt;&lt;span class="nv"&gt;` represents the current aggregate state built up over the previous rows (or NULL if we haven’t yet gotten any), and `&lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="nv"&gt;` represents the output after analyzing the incoming row:


&lt;/span&gt;&lt;span class="se"&gt;``&lt;/span&gt;&lt;span class="nv"&gt;`&lt;/span&gt;&lt;span class="k"&gt;SQL&lt;/span&gt;
&lt;span class="n"&gt;next_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transition_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;`&lt;/span&gt;&lt;span class="se"&gt;``&lt;/span&gt;&lt;span class="nv"&gt;



### Aggregate internals: composite state
So, the `&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nv"&gt;` aggregate has a straightforward state that contains just one value (the largest we’ve seen). But not all aggregates in PostgreSQL have such a simple state. 

Let’s consider the aggregate for average (`&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="nv"&gt;`):


&lt;/span&gt;&lt;span class="se"&gt;``&lt;/span&gt;&lt;span class="nv"&gt;`&lt;/span&gt;&lt;span class="k"&gt;SQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;`&lt;/span&gt;&lt;span class="se"&gt;``&lt;/span&gt;&lt;span class="nv"&gt;



To refresh, an average is defined as: 


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;avg(x)=sum(x)count(x)avg(x) = \frac{sum(x)}{count(x)}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;vg&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;co&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;To calculate it, we store the sum and the count as our internal state and update our state as we process rows: &lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pjuxskwee16306gnl48o.gif" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pjuxskwee16306gnl48o.gif&lt;/a&gt;" alt="A GIF of the aggregation process for the statement &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, with diagrams similar to the previous. Three rows with values of bar equal to 1.0, 2.0, and 3.0 go through the aggregate, and the transition function updates the state, which has two values, each starting NULL, the sum is updated at each step by adding the value of the incoming row, and the count is incremented.&lt;br&gt;&lt;br&gt;
" style="width:100%"&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;The &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregation process, told in GIFs. For &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, the transition function must update a more complex state since the sum and count are stored separately at each aggregation step.&amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;
But, when we’re ready to output our result for &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, we need to divide &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; by &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;count&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;:&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/to9t3tjqp6bj8rpol45m.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/to9t3tjqp6bj8rpol45m.jpg&lt;/a&gt;" alt="An arrow flowchart diagram similar to those before, showing the end state of the avg aggregate. The rows have moved through the aggregate, and the state is 6.0 - the sum and three - the count. There are then some question marks and an end result of 2.0." style="width:100%"&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt; For some aggregates, we can output the state directly – but for others, we need to perform an operation on the state before calculating our final result. &amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;
There’s another function inside the aggregate that performs this calculation: the &lt;strong&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createaggregate.html" rel="noopener noreferrer"&gt;final function&lt;/a&gt;&lt;/strong&gt;. Once we’ve processed all the rows, the final function takes the state and does whatever it needs to produce the result. &lt;/p&gt;

&lt;p&gt;It’s defined like this, where &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;final_state&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; represents the output of the transition function after it has processed all the rows:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;result&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;final_func&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;final_state&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;And, through pictures:&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f8ecxml9yt9lwxa28dih.gif" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f8ecxml9yt9lwxa28dih.gif&lt;/a&gt;" alt="A GIF that starts the same as the previous GIF, the avg aggregate state is updated as rows pass through the aggregate. Once all the rows are processed, a final function step divides the final sum - 6.0 by the final count - 3 and outputs the result - 2.0. " style="width:100%"&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt; How the average aggregate works, told in GIFs. Here, we’re highlighting the role of the final function. &amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;&lt;br&gt;
To summarize: as an aggregate scans over rows, its &lt;strong&gt;transition function&lt;/strong&gt; updates its internal state. Once the aggregate has scanned all of the rows, its &lt;strong&gt;final function&lt;/strong&gt; produces a result, which is returned to the user.&lt;/p&gt;
&lt;h3&gt;
  
  
  Improving the performance of aggregate functions
&lt;/h3&gt;

&lt;p&gt;One interesting thing to note here: the transition function is called many, many more times than the final function: once for each row, whereas the final function is called once per group of rows. &lt;/p&gt;

&lt;p&gt;Now, the transition function isn’t inherently more expensive than the final function on a per-call basis – but because there are usually orders of magnitude more rows going into the aggregate than coming out, the transition function step becomes the most expensive part very quickly. This is especially true when you have high volume time-series data being ingested at high rates; optimizing aggregate transition function calls is important for improving performance. &lt;/p&gt;

&lt;p&gt;Luckily, PostgreSQL already has ways to optimize aggregates.    &lt;/p&gt;
&lt;h3&gt;
  
  
  Parallelization and the combine function
&lt;/h3&gt;

&lt;p&gt;Because the transition function is run on each row, &lt;a href="https://www.postgresql.org/message-id/flat/CA%2BTgmoYSL_97a--qAvdOa7woYamPFknXsXX17m0t2Pwc%2BFOvYw%40mail.gmail.com#fb9f2ae2a52ac605a4439a1879ff3c10" rel="noopener noreferrer"&gt;some enterprising PostgreSQL developers&lt;/a&gt; asked: &lt;em&gt;what if we parallelized the transition function calculation?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s revisit our definitions for transition functions and final functions:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;next_state&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;transition_func&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;current_state&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;current_value&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;result&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;final_func&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;final_state&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;We can run this in parallel by instantiating multiple copies of the transition function and handing a subset of rows to each instance. Then, each parallel aggregate will run the transition function over the subset of rows it sees, producing multiple (partial) states, one for each parallel aggregate. But, since we need to aggregate over the entire data set, we can’t run the final function on each parallel aggregate separately because they only have some of the rows. &lt;/p&gt;

&lt;p&gt;So, now we’ve ended up in a bit of a pickle: we have multiple partial aggregate states, and the final function is only meant to work on the single, final state - right before we output the result to the user. &lt;/p&gt;

&lt;p&gt;To solve this problem, we need a new type of function that takes two partial states and combines them into one so that the final function can do its work. This is (aptly) called the &lt;strong&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createaggregate.html" rel="noopener noreferrer"&gt;combine function&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;We can run the combine function iteratively over all of the partial states that are created when we parallelize the aggregate.&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;combined_state&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;combine_func&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;partial_state_1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;partial_state_2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;For instance, in &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, the combine function will add up the counts and sums.&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ei54hrl4gy30hajmboys.gif" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ei54hrl4gy30hajmboys.gif&lt;/a&gt;" alt="A GIF that starts the same as the previous GIF, the avg aggregate state is updated as rows pass through the aggregate. Once all the rows are processed, a final function step divides the final sum - 6.0 by the final count - 3 and outputs the result - 2.0. " style="width:100%"&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt; How parallel aggregation works, told in GIFs. Here, we’re highlighting the combine function (We’ve added a couple more rows to illustrate parallel aggregation.)&lt;br&gt;
&amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;&lt;br&gt;
Then, after we have the combined state from all of our parallel aggregates, we run the final function and get our result.&lt;/p&gt;
&lt;h3&gt;
  
  
  Deduplication &amp;lt;a name="deduplication"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h3&gt;

&lt;p&gt;Parallelization and the combine function are one way to reduce the cost of calling an aggregate, but it’s not the only way. &lt;/p&gt;

&lt;p&gt;One other built-in PostgreSQL optimization that reduces an aggregate’s cost occurs in a statement like this:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;/&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;2&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;AS&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;half_avg&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;PostgreSQL will optimize this statement to evaluate the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; calculation only once and then use that result twice. &lt;/p&gt;

&lt;p&gt;And, if we have different aggregates with the same transition function but different final functions? PostgreSQL further optimizes by calling the transition function (the expensive part) on all the rows and then doing both final functions! Pretty neat!&lt;/p&gt;

&lt;p&gt;Now, that’s not all that PostgreSQL aggregates can do, but it’s a pretty good tour, and it’s enough to get us where we need to go today. &lt;/p&gt;


&lt;h2&gt;
  
  
  Two-step aggregation in TimescaleDB hyperfunctions&amp;lt;a name="two-step-in-tsdb"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h2&gt;

&lt;p&gt;In TimescaleDB, we’ve implemented the two-step aggregation design pattern for our aggregate functions. This generalizes the PostgreSQL internal aggregation API and exposes it to the user via our aggregates, accessors, and rollup functions. (In other words, each of the internal PostgreSQL functions has an equivalent function in TimescaleDB hyperfunctions.)&lt;/p&gt;

&lt;p&gt;As a refresher, when we talk about the two-step aggregation design pattern, we mean the following convention, where we have an inner aggregate call:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucyq3dmed7kf24n4rtfq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucyq3dmed7kf24n4rtfq.jpg" alt="The same as the previous in terms of code, except the sections: time_weight('LOCF', value) and percentile_agg(value) are highlighted " width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And an outer accessor call:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9x6h15xut38k0s3b670.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9x6h15xut38k0s3b670.jpg" alt="The same as the previous in terms of code, except the sections: average(time_weight('LOCF', value)) and approx_percentile(0.5, percentile_agg(value)) are highlighted" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The inner aggregate call returns the internal state, just like the transition function does in PostgreSQL aggregates. &lt;/p&gt;

&lt;p&gt;The outer accessor call takes the internal state and returns a result to the user, just like the final function does in PostgreSQL. &lt;/p&gt;

&lt;p&gt;We also have special &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/rollup-percentile/#sample-usage" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;&lt;/a&gt; functions &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/rollup-timeweight/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=roll-up-timeweight-docs" rel="noopener noreferrer"&gt;defined for each of our aggregates&lt;/a&gt; that work much like PostgreSQL combine functions. &lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fgz5f9gw8cbanb0an8wg.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fgz5f9gw8cbanb0an8wg.jpg&lt;/a&gt;" alt="A table with columns labeled: the PostgreSQL internal aggregation API, Two-step aggregate equivalent, and TimescaleDB hyperfunction example. In the first row, we have the transition function equivalent to the aggregate, and the examples are time_weight() and percentile_agg(). In the second row, we have the final function, equivalent to the accessor, and the examples are average() and approx_percentile(). In the third row, we have the combine function equivalent to rollup in two-step aggregates, and the example is rollup()."&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;PostgreSQL internal aggregation APIs and their TimescaleDB hyperfunctions’ equivalent&amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt; &lt;/p&gt;


&lt;h2&gt;
  
  
  Why we use the two-step aggregate design pattern&amp;lt;a name="3"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h2&gt;

&lt;p&gt;There are four basic reasons we expose the two-step aggregate design pattern to users rather than leave it as an internal structure: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Allow multi-parameter aggregates to re-use state, making them more efficient&lt;/li&gt;
&lt;li&gt;Cleanly distinguish between parameters that affect aggregates vs. accessors, making performance implications easier to understand and predict&lt;/li&gt;
&lt;li&gt;Enable easy to understand rollups, with logically consistent results, in continuous aggregates and window functions (one of our most common requests on continuous aggregates)&lt;/li&gt;
&lt;li&gt;Allow easier retrospective analysis of downsampled data in continuous aggregates as requirements change, but the data is already gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s a little theoretical, so let’s dive in and explain each one.&lt;/p&gt;
&lt;h3&gt;
  
  
  Re-using state
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is very good at optimizing statements (as we saw earlier in this post, through pictures 🙌), but you have to give it things in a way it can understand. &lt;/p&gt;

&lt;p&gt;For instance, when we talked about deduplication, we saw that PostgreSQL could “figure out” when a statement occurs more than once in a query (i.e., &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;) and only run the statement a single time to avoid redundant work:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;/&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;2&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;AS&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;half_avg&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;This works because the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; occurs multiple times without variation. &lt;/p&gt;

&lt;p&gt;However, if I write the equation in a slightly different way and move the division inside the parentheses so that the expression &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; doesn’t repeat so neatly, PostgreSQL can’t figure out how to optimize it:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;bar&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;/&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;AS&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;half_avg&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;It doesn’t know that the division is commutative, or that those two queries are equivalent. &lt;/p&gt;

&lt;p&gt;This is a complicated problem for database developers to solve, and thus, as a PostgreSQL user, you need to make sure to write your query in a way that the database can understand. &lt;/p&gt;

&lt;p&gt;Performance problems caused by equivalent statements that the database doesn’t understand are equal (or that are equal in the specific case you wrote, but not in the general case) can be some of the trickiest SQL optimizations to figure out as a user. &lt;/p&gt;

&lt;p&gt;Therefore, &lt;strong&gt;when we design our APIs, we try to make it hard for users to unintentionally write low-performance code: in other words, the default option should be the high-performance option.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the next bit, it’ll be useful to have a simple table defined as:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;CREATE&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;TABLE&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;ts&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;timestamptz&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt; &amp;lt;span class="nb"&amp;gt;DOUBLE&amp;lt;/span&amp;gt; &amp;lt;span class="nb"&amp;gt;PRECISION&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;Let’s look at an example of how we use two-step aggregation in the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=percentile-approx-docs" rel="noopener noreferrer"&gt;percentile approximation hyperfunction&lt;/a&gt; to allow PostgreSQL to optimize performance.&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p10&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p50&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;9&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p90&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;...is treated as the same as:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;pct_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p10&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;pct_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p50&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;9&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;pct_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p90&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;pct_agg&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;pct&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;This calling convention allows us to use identical aggregates so that, under the hood, PostgreSQL can deduplicate calls to the identical aggregates (and is faster as a result).&lt;/p&gt;

&lt;p&gt;Now, let’s compare this to the one-step aggregate approach. &lt;/p&gt;

&lt;p&gt;PostgreSQL can’t deduplicate aggregate calls here because the extra parameter in the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregate changes with each call:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="c1"&amp;gt;-- NB: THIS IS AN EXAMPLE OF AN API WE DECIDED NOT TO USE, IT DOES NOT WORK&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p10&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p50&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;9&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p90&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;So, even though all of those functions could use the same approximation built up over all the rows, PostgreSQL has no way of knowing that. The two-step aggregation approach enables us to structure our calls so that PostgreSQL can optimize our code, and it enables developers to understand when things will be more expensive and when they won't. Multiple different aggregates with different inputs will be expensive, whereas multiple accessors to the same aggregate will be much less expensive. &lt;/p&gt;
&lt;h3&gt;
  
  
  Cleanly distinguishing between aggregate/accessor parameters
&lt;/h3&gt;

&lt;p&gt;We also chose the two-step aggregate approach because some of our aggregates can take multiple parameters or options themselves, and their accessors can also take options:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;uddsketch&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;001&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&amp;lt;span class="c1"&amp;gt;--1000 buckets, 0.001 target err&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;9&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;uddsketch&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;001&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p90&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;uddsketch&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;100&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;01&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;less_accurate_median&amp;lt;/span&amp;gt; &amp;lt;span class="c1"&amp;gt;-- modify the terms for the aggregate get a new approximation&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;That’s an example of &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/percentile-aggregation-methods/uddsketch/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=uddsketch-docs" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;uddsketch&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;&lt;/a&gt;, an &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/percentile-approximation/percentile-aggregation-methods/#choosing-the-right-algorithm-for-your-use-case" rel="noopener noreferrer"&gt;advanced aggregation method&lt;/a&gt; for percentile approximation that can take its own parameters. &lt;/p&gt;

&lt;p&gt;Imagine if the parameters were jumbled together in one aggregate:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="c1"&amp;gt;-- NB: THIS IS AN EXAMPLE OF AN API WE DECIDED NOT TO USE, IT DOES NOT WORK&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;001&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;It’d be pretty difficult to understand which argument is related to which part of the functionality.&lt;/p&gt;

&lt;p&gt;Conversely, the two-step approach separates the arguments to the accessor vs. aggregate very cleanly, where the aggregate function is defined in parenthesis within the inputs of our final function:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;uddsketch&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;001&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;By making it clear which is which, users can know that if they change the inputs to the aggregate, they will get more (costly) aggregate nodes, =while inputs to the accessor are cheaper to change. &lt;/p&gt;

&lt;p&gt;So, those are the first two reasons we expose the API - and what it allows developers to do as a result. The last two reasons involve continuous aggregates and how they relate to hyperfunctions, so first, a quick refresher on what they are. &lt;/p&gt;


&lt;h2&gt;
  
  
  Two-step aggregation + continuous aggregates in TimescaleDB&amp;lt;a name="4"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h2&gt;

&lt;p&gt;TimescaleDB includes a feature called &lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=cont-aggs-docs" rel="noopener noreferrer"&gt;continuous aggregates&lt;/a&gt;, which are designed to make queries on very large datasets run faster. TimescaleDB continuous aggregates continuously and incrementally store the results of an aggregation query in the background, so when you run the query, only the data that has changed needs to be computed, not the entire dataset. &lt;/p&gt;

&lt;p&gt;In our discussion of the combine function above, we covered how you could take the expensive work of computing the transition function over every row and split the rows over multiple parallel aggregates to speed up the calculation. &lt;/p&gt;

&lt;p&gt;TimescaleDB continuous aggregates do something similar, except they spread the computation work over time rather than between parallel processes running simultaneously. The continuous aggregate computes the transition function over a subset of rows inserted some time in the past, stores the result, and then, at query time, we only need to compute over the raw data for a small section of recent time that we haven’t yet calculated. &lt;/p&gt;

&lt;p&gt;When we designed TimescaleDB hyperfunctions, we wanted them to work well within continuous aggregates and even open new possibilities for users.  &lt;/p&gt;

&lt;p&gt;Let’s say I create a continuous aggregate from the simple table above to compute the sum, average, and percentile (the latter using a hyperfunction) in 15 minute increments:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;CREATE&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;MATERIALIZED&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;VIEW&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;WITH&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;timescaledb&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;continuous&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;AS&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'15 min'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;ts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;GROUP&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;BY&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'15 min'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;ts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;And then what if I come back and I want to re-aggregate it to hours or days, rather than 15-minute buckets – or need to aggregate my data across all ids? Which aggregates can I do that for, and which can’t I? &lt;/p&gt;
&lt;h3&gt;
  
  
  Logically consistent rollups
&lt;/h3&gt;

&lt;p&gt;One of the problems we wanted to solve with two-step aggregation was how to convey to the user when it is “okay” to re-aggregate and when it’s not. (By “okay,” I mean you would get the same result from the re-aggregated data as you would running the aggregate on the raw data directly.) &lt;/p&gt;

&lt;p&gt;For instance:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tab&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="c1"&amp;gt;-- is equivalent to:&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tab&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="k"&amp;gt;GROUP&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;BY&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;s&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tab&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="c1"&amp;gt;-- is NOT equivalent to:&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tab&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="k"&amp;gt;GROUP&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;BY&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;s&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;Why is re-aggregation okay for &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; but not for &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;avg&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;? &lt;/p&gt;

&lt;p&gt;Technically, it’s logically consistent to re-aggregate when: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The aggregate returns the internal aggregate state. The internal aggregate state for sum is &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, whereas for average, it is&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;count&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The aggregate’s combine and transition functions are equivalent. For &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, the states and the operations are the same. For &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;count&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;, the states are the same, but the transition and combine functions perform &lt;em&gt;different operations&lt;/em&gt; on them. &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;’s transition function adds the incoming value to the state, and its combine function adds two states together, or a sum of sums.  Conversely, &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;count&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;s transition function increments the state for each incoming value, but its combine function adds two states together, or a sum of counts. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But, you have to have in-depth (and sometimes rather arcane) knowledge about each aggregate’s internals to know which ones meet the above criteria – and therefore, which ones you can re-aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With the two-step aggregate approach, we can convey when it is logically consistent to re-aggregate by exposing our equivalent of the combine function when the aggregate allows it.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;We call that function &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;. &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;Rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; takes multiple inputs from the aggregate and combines them into a single value. &lt;/p&gt;

&lt;p&gt;All of our aggregates that can be combined have &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; functions that will combine the output of the aggregate from two different groups of rows. (Technically, &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; is an aggregate function because it acts on multiple rows. For clarity, I’ll call them rollup functions to distinguish them from the base aggregate).  Then you can call the accessor on the combined output! &lt;/p&gt;

&lt;p&gt;So using that continuous aggregate we created to get a 1 day re-aggregation of our &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; becomes as simple as:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'1 day'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;GROUP&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;BY&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'1 day'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;(We actually suggest that you create your continuous aggregates without calling the accessor function for this very reason. Then, you can just create views over top or put the accessor call in your query). &lt;/p&gt;

&lt;p&gt;This brings us to our final reason.&lt;/p&gt;
&lt;h3&gt;
  
  
  Retrospective analysis using continuous aggregates
&lt;/h3&gt;

&lt;p&gt;When we create a continuous aggregate, we’re defining a view of our data that we then could be stuck with for a very long time. &lt;/p&gt;

&lt;p&gt;For example, we might have a data retention policy that deletes the underlying data after X time period. If we want to go back and re-calculate anything, it can be challenging, if not impossible, since we’ve “dropped” the data. &lt;/p&gt;

&lt;p&gt;But, we understand that in the real world, you don’t always know what you’re going to need to analyze ahead of time. &lt;/p&gt;

&lt;p&gt;Thus, we designed hyperfunctions to use the two-step aggregate approach, so they would better integrate with continuous aggregates. As a result, users store the aggregate state in the continuous aggregate view and modify accessor functions without requiring them to recalculate old states that might be difficult (or impossible) to reconstruct (because the data is archived, deleted, etc.). &lt;/p&gt;

&lt;p&gt;The two-step aggregation design also allows for much greater flexibility with continuous aggregates. For instance, let’s take a continuous aggregate where we do the aggregate part of the two-step aggregation like this:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;CREATE&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;MATERIALIZED&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;VIEW&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;WITH&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;timescaledb&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;continuous&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;AS&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'15 min'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;ts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;val&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;GROUP&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;BY&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;time_bucket&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="s1"&amp;gt;'15 min'&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;::&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;interval&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;ts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;When we first create the aggregate, we might only want to get the median:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;But then, later, we decide we want to know the 95th percentile as well. &lt;/p&gt;

&lt;p&gt;Luckily, we don’t have to modify the continuous aggregate; we &lt;strong&gt;just modify the parameters to the accessor function in our original query to return the data we want from the aggregate state:&lt;/strong&gt;&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;95&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p95&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;And then, if a year later, we want the 99th percentile as well, we can do that too:&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;SELECT&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;median&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;95&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p95&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;approx_percentile&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;99&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;percentile_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;p99&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;FROM&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;foo_15_min_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;That’s just scratching the surface. Ultimately, our goal is to provide a high level of developer productivity that enhances other PostgreSQL and TimescaleDB features, like aggregate deduplication and continuous aggregates. &lt;/p&gt;


&lt;h2&gt;
  
  
  An example of how the two-step aggregate design impacts hyperfunctions’ code&amp;lt;a name="5"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h2&gt;

&lt;p&gt;To illustrate how the two-step aggregate design pattern impacts how we think about and code hyperfunctions, let’s look at the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weight-avgs-docs" rel="noopener noreferrer"&gt;time-weighted average family of functions&lt;/a&gt;. (Our &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weight-avgs-blogpost" rel="noopener noreferrer"&gt;what time-weighted averages are and why you should care&lt;/a&gt; post provides a lot of context for this next bit, so if you haven’t read it, we recommend doing so. You can also skip this next bit for now.)&lt;/p&gt;

&lt;p&gt;The equation for the time-weighted average is as follows:&lt;/p&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;timeweightedaverage=areaundercurveΔTtime\\_weighted\\_average = \frac{area\\_under\\_curve}{ \Delta T}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;im&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mspace newline"&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;g&lt;/span&gt;&lt;span class="mord mathnormal"&gt;h&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mspace newline"&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;a&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;g&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mspace newline"&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mspace newline"&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;As we noted in the table above: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; is TimescaleDB hyperfunctions’ aggregate and corresponds to the transition function in PostgreSQL’s internal API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; is the accessor, which corresponds to the PostgreSQL final function.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; for re-aggregation corresponds to the PostgreSQL combine function. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; function returns an aggregate type that has to be usable by the other functions in the family.&lt;/p&gt;

&lt;p&gt;In this case, we decided on a &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; type that is defined like so (in pseudocode):&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;first_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;last_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; is the weighted sum (another name for the area under the curve), and &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;first_pt&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;last_pt&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; are the first and last (time, value) pairs in the rows that feed into the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregate. &lt;/p&gt;

&lt;p&gt;Here’s a graphic depiction of those elements, which builds on our &lt;a href="https://blog.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/#mathy-bits-how-to-derive-a-time-weighted-average" rel="noopener noreferrer"&gt;how to derive a time-weighted average theoretical description&lt;/a&gt;:&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yt3d7d06r1fajo7iiued.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yt3d7d06r1fajo7iiued.jpg&lt;/a&gt;" alt="A graph showing value on the y-axis and time on the x-axis. There are four points:  open parens t 1 comma v 1 close parens, labeled first point to open parens t 4 comma  v 4 close parens, labeled last point. The points are spaced unevenly in time on the graph. The area under the graph is shaded, and labeled w underscore sum. The time axis has a brace describing the total distance between the first and last points labeled Delta T. "&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;Depiction of the values we store in the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; representation. &amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;&lt;br&gt;
So, the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregate does all of the calculations as it receives each of the points in our graph and builds a weighted sum for the time period (ΔT) between the first and last points it “sees.” It then outputs the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; accessor function performs simple calculations to return the time-weighted average from the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;  (in pseudocode where &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nb"&amp;gt;time&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; returns the time from the point):&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;func&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="o"&amp;gt;-&amp;amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="nb"&amp;gt;float&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;delta_t&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;last_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nb"&amp;gt;time&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;-&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;first_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nb"&amp;gt;time&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
    &amp;lt;span class="n"&amp;gt;time_weighted_average&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;/&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;delta_t&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;return&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;time_weighted_average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;But, as we built the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; hyperfunction, ensuring the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; function worked as expected was a little more difficult – and introduced constraints that impacted the design of our &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; data type. &lt;/p&gt;

&lt;p&gt;To understand the rollup function, let’s use our graphical example and imagine the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; function returns two &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummaries&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; from different regions of time like so: &lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdesd8okhaobc9lk1imk.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdesd8okhaobc9lk1imk.jpg&lt;/a&gt;" alt="A similar graph to the previous, except that now there are two sets of shaded regions. The first is similar to the previous and is labeled with first sub 1 open parens t 1 comma v 1 close parens, last  1 open parens t 4 comma  v 4 close parens , and w underscore sum  1.  The second is similar, with points first 2 open parens t 5 comma  v 4 close parens and last 2 open parens t 8 comma  v 8 close parens and the label w underscore sum 2 on the shaded portion."&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;What happens when we have multiple &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummaries&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; representing different regions of the graph. &amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;
The &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; function needs to take in and return the same &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; data type so that our &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; accessor can understand it. (This mirrors how PostgreSQL’s combine function takes in two states from the transition function and then returns a single state for the final function to process). &lt;/p&gt;

&lt;p&gt;We also want the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; output to be the same as if we had computed the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; over all the underlying data. The output should be a &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; representing the full region.  &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; we output should also account for the area in the gap between these two weighted sum states:&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/en9qxli39gr6khvcopkl.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/en9qxli39gr6khvcopkl.jpg&lt;/a&gt;" alt="A similar picture to the previous, with the area between the points open parens t 4 comma  v 4 close parens aka last 1 and open parens t 5 comma  v 5 close parens aka first 2, down to the time axis highlighted. This is called w underscore sum gap. "&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;Mind the gap! (between one &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; and the next).&amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;
The gap area is easy to get because we have the last_1 and first_2 points - and it’s the same as the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; we’d get by running the&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregate on them.&lt;/p&gt;

&lt;p&gt;Thus, the overall &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; function needs to do something like this (where &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; extracts the weighted sum from the &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;):&lt;/p&gt;

&lt;/span&gt;&lt;span class="se"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;SQL&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;func&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &lt;br&gt;
&amp;lt;span class="o"&amp;gt;-&amp;amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;w_sum_gap&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;tws1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;last_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;first_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;).&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="n"&amp;gt;w_sum_total&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;w_sum_gap&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;+&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;+&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="k"&amp;gt;return&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;w_sum_total&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws1&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;first_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="n"&amp;gt;tws2&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;last_pt&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&lt;br&gt;
&amp;lt;span class="nv"&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;span class="nv"&gt;

&lt;p&gt;Graphically, that means we’d end up with a single &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; representing the whole area:&lt;br&gt;
&amp;lt;figure&amp;gt;&lt;br&gt;
&amp;lt;img src="&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/czw7y3lgsot8q3sppaa2.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/czw7y3lgsot8q3sppaa2.jpg&lt;/a&gt;" alt="Similar to the previous graphs, except that now there is only one region that has been shaded, the combined area of the w underscore sum 1, w underscore sum 2, and w underscore sum gap has become one area, w underscore sum. Only the overall first open parens t 1 comma  v 1 close parens and last open parens t 8 comma  v 8 close parens points are shown."&amp;gt;&lt;br&gt;
&amp;lt;figcaption align = "center"&amp;gt;The combined &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;TimeWeightSummary&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;. &amp;lt;/figcaption&amp;gt;&lt;br&gt;
&amp;lt;/figure&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;&lt;br&gt;
So that’s how the two-step aggregate design approach ends up affecting the real-world implementation of our time-weighted average hyperfunctions. The above explanations are a bit condensed, but they should give you a more concrete look at how &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; aggregate, &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;average&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; accessor, and &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;rollup&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; functions work.&lt;/p&gt;


&lt;h2&gt;
  
  
  Summing it up&amp;lt;a name="6"&amp;gt;&amp;lt;/a&amp;gt;
&lt;/h2&gt;

&lt;p&gt;Now that you’ve gotten a tour of the PostgreSQL aggregate API,  how it inspired us to make the TimescaleDB hyperfunctions two-step aggregate API, and a few examples of how this works in practice, we hope you'll try it out yourself and tell us what you think :). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get started with hyperfunctions right away, &lt;a href="https://console.forge.timescale.com/signup/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=sign-up" rel="noopener noreferrer"&gt;spin up a fully managed TimescaleDB service and try it for free&lt;/a&gt;&lt;/strong&gt;. Hyperfunctions are pre-loaded on each new database service on Timescale Forge, so after you create a new service, you’re all set to use them!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;download and install the timescaledb_toolkit extension&lt;/a&gt;&lt;/strong&gt; on GitHub, after which you’ll be able to use &lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;time_weight&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt; and all other hyperfunctions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you have questions or comments on this blog post, &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions/196" rel="noopener noreferrer"&gt;we’ve started a discussion on our GitHub page, and we’d love to hear from you&lt;/a&gt;&lt;/strong&gt;. (And, if you like what you see, GitHub ⭐ are always welcome and appreciated too!)&lt;/p&gt;

&lt;p&gt;We love building in public, and you can view our &lt;a href="https://github.com/timescale/timescaledb-toolkit" rel="noopener noreferrer"&gt;upcoming roadmap on GitHub&lt;/a&gt; for a list of proposed features, features we’re currently implementing, and features available to use today. For reference, the two-step aggregate approach isn’t just used in the stabilized hyperfunctions covered here; it’s also used in many of our experimental features, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/timescale/timescaledb-toolkit/blob/main/docs/stats_agg.md" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;stats_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;&lt;/a&gt; uses two-step aggregation to make simple statistical aggregates, like average and standard deviation, easier to work with in continuous aggregates and to &lt;a href="https://github.com/timescale/timescaledb-toolkit/blob/main/docs/rolling_average_api_working.md" rel="noopener noreferrer"&gt;simplify computing rolling averages&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/timescale/timescaledb-toolkit/blob/main/docs/counter_agg.md" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;counter_agg&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;&lt;/a&gt; uses two-step aggregation to make working with counters more efficient and composable.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/timescale/timescaledb-toolkit/blob/main/docs/hyperloglog.md" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;/span&amp;gt;&amp;lt;span class="n"&amp;gt;Hyperloglog&amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;&lt;/code&gt;&lt;/a&gt; uses two-step aggregation in conjunction with continuous aggregates to give users faster approximate COUNT DISTINCT rollups over longer periods of time. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features will be stabilized soon, but we’d love your feedback while the APIs are still evolving. What would make them more intuitive? Easier to use? &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; or &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions" rel="noopener noreferrer"&gt;start a discussion&lt;/a&gt;!&lt;br&gt;
&lt;/p&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>timeseries</category>
    </item>
    <item>
      <title>What time-weighted averages are and why you should care</title>
      <dc:creator>davidkohn88</dc:creator>
      <pubDate>Thu, 29 Jul 2021 12:41:56 +0000</pubDate>
      <link>https://dev.to/tigerdata/what-time-weighted-averages-are-and-why-you-should-care-1ge6</link>
      <guid>https://dev.to/tigerdata/what-time-weighted-averages-are-and-why-you-should-care-1ge6</guid>
      <description>&lt;p&gt;&lt;em&gt;Learn how time-weighted averages are calculated, why they’re so powerful for data analysis, and how to use TimescaleDB hyperfunctions to calculate them faster – all using SQL.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Many people who work with time-series data have nice, regularly sampled datasets. Data could be sampled every few seconds, or milliseconds, or whatever they choose, but by regularly sampled, we mean the time between data points is basically constant. Computing the average value of data points over a specified time period in a regular dataset is a relatively well-understood query to compose. But for those who don't have regularly sampled data, getting a representative average over a period of time can be a complex and time-consuming query to write. &lt;strong&gt;Time-weighted averages are a way to get an unbiased average when you are working with irregularly sampled data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Time-series data comes at you fast, sometimes generating millions of data points per second (&lt;a href="https://blog.timescale.com/blog/what-the-heck-is-time-series-data-and-why-do-i-need-a-time-series-database-dcf3b1b18563/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-series-blog" rel="noopener noreferrer"&gt;read more about time-series data&lt;/a&gt;). Because of the sheer volume and rate of information, time-series data can already be complex to query and analyze, which is why we built &lt;a href="https://blog.timescale.com/blog/timescaledb-2-0-a-multi-node-petabyte-scale-completely-free-relational-database-for-time-series/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=tsdb-2-0-blog" rel="noopener noreferrer"&gt;TimescaleDB, a multi-node, petabyte-scale, completely free relational database for time-series&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Irregularly sampled time-series data just adds another level of complexity – and is more common than you may think. For example, irregularly sampled data, and thus the need for time-weighted averages, frequently occurs in: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Industrial IoT&lt;/strong&gt;, where teams “compress” data by only sending points when the value changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote sensing&lt;/strong&gt;, where sending data back from the edge can be costly, so you only send high-frequency data for the most critical operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger-based systems&lt;/strong&gt;, where the sampling rate of one sensor is affected by the reading of another (i.e., a security system that sends data more frequently when a motion sensor is triggered)&lt;/li&gt;
&lt;li&gt;...and many, many more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Timescale, we’re always looking for ways to make developers’ lives easier, especially when they’re working with time-series data. To this end, &lt;a href="https://blog.timescale.com/blog/introducing-hyperfunctions-new-sql-functions-to-simplify-working-with-time-series-data-in-postgresql/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=hyperfunctions-blog" rel="noopener noreferrer"&gt;we introduced hyperfunctions&lt;/a&gt;, new SQL functions that simplify working with time-series data in PostgreSQL. &lt;strong&gt;One of these hyperfunctions enables you to &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weighted-avg-docs" rel="noopener noreferrer"&gt;compute time-weighted averages&lt;/a&gt; quickly and efficiently&lt;/strong&gt;, so you gain hours of productivity. &lt;/p&gt;

&lt;p&gt;Read on for examples of time-weighted averages, how they’re calculated, how to use the time-weighted averages hyperfunctions in TimescaleDB, and some ideas for how you can use them to get a productivity boost for your projects, no matter the domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get started with the &lt;code&gt;time_weight&lt;/code&gt; hyperfunction - and many more - right away, spin up a fully managed TimescaleDB service&lt;/strong&gt;: create an account to &lt;a href="https://console.forge.timescale.com/signup/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=signup" rel="noopener noreferrer"&gt;try it for free&lt;/a&gt; for 30 days. Hyperfunctions are pre-loaded on each new database service on Timescale Forge, so after you create a new service, you’re all set to use them!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-toolkit" rel="noopener noreferrer"&gt;download and install the timescaledb_toolkit extension&lt;/a&gt;&lt;/strong&gt; on GitHub, after which you’ll be able to use &lt;code&gt;time_weight&lt;/code&gt; and other hyperfunctions.&lt;/p&gt;

&lt;p&gt;Finally, we love building in public and continually improving: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you have questions or comments on this blog post, &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions/185/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-issue-185" rel="noopener noreferrer"&gt;we’ve started a discussion on our GitHub page, and we’d love to hear from you&lt;/a&gt;. (And, if you like what you see, GitHub ⭐ are always welcome and appreciated too!)&lt;/li&gt;
&lt;li&gt;You can view our &lt;a href="https://github.com/timescale/timescaledb-toolkit/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-toolkit" rel="noopener noreferrer"&gt;upcoming roadmap on GitHub&lt;/a&gt; for a list of proposed features, as well as features we’re currently implementing and those that are available to use today.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What are time-weighted averages?
&lt;/h2&gt;

&lt;p&gt;I’ve been a developer at Timescale for over 3 years and worked in databases for about 5 years, but I was an electrochemist before that. As an electrochemist, I worked for a battery manufacturer and saw a lot of charts like these: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2Fg-z2z19BotekB_mzML194picBe7KrJzXlogwVaO89mxM1tirT6KDbrKNwgiCDFKQ09Q4b1ICc1eo7vnIuVUIfFN4Oulayypa4U-pR-Y9UAH9CAFIdftMWvRlma2vWl77IqsKyPm5" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2Fg-z2z19BotekB_mzML194picBe7KrJzXlogwVaO89mxM1tirT6KDbrKNwgiCDFKQ09Q4b1ICc1eo7vnIuVUIfFN4Oulayypa4U-pR-Y9UAH9CAFIdftMWvRlma2vWl77IqsKyPm5" alt="Battery discharge curve showing cell voltage on the y-axis and capacity in amp-hours on the x-axis. The curve starts high, decreases relatively rapidly through the exponential zone, then stays relatively constant for a long period through the nominal zone, after which the voltage drops quite rapidly as it reaches its fully discharged state." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
Example battery discharge curve, which describes how long a battery can power something. (Also a prime example of where time-weighted averages are 💯 necessary) Source: &lt;a href="https://www.nrel.gov/docs/fy17osti/67809.pdf" rel="noopener noreferrer"&gt;https://www.nrel.gov/docs/fy17osti/67809.pdf&lt;/a&gt;



&lt;p&gt;&lt;br&gt;&lt;br&gt;
That’s a battery discharge curve, which describes how long a battery can power something. The x-axis shows capacity in Amp-hours, and since this is a constant current discharge, the x-axis is really just a proxy for time. The y-axis displays voltage, which determines the battery’s power output; as you continue to discharge the battery, the voltage drops until it gets to a point where it needs to be recharged. &lt;/p&gt;

&lt;p&gt;When we’d do R&amp;amp;D for new battery formulations, we’d cycle many batteries many times to figure out which formulations make batteries last the longest. &lt;/p&gt;

&lt;p&gt;If you look more closely at the discharge curve, you’ll notice that there are only two “interesting” sections:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FsoW4o8ltBce2Y3rOIgAtZrXjiwSELKXZFdbsldW0_YcovNLUsb7ZHqF3N60J2FP-dTmsuNfp_TXYfinKzgJ7rQzd6vCLUWYFK_zP3o1YP8iQYFur_Uvmqxn6l9yznAxpPaXskg97" alt="The same battery discharge curve as in the previous image but with the “interesting bits” circled, namely where the voltage decreases rapidly at the beginning and the end of the discharge curve." width="800" height="400"&gt;Example battery discharge curve, calling out the “interesting bits” (the points in time where data changes rapidly)



&lt;p&gt;&lt;br&gt;&lt;br&gt;
These are the parts at the beginning and end of the discharge where the voltage changes rapidly. Between these two sections, there’s that long period in the middle, where the voltage hardly changes at all:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2Fk-EPSQ86LqI4j6wggTjxnXqzO4R9VZNzOeMSRItnBoLjDaHS6iHmXL9qor0V85Jw9pIBWD1TrEKtIJW_y0hBXqsE32erCaB-QDjxj5chQNXIp1scSeTwvFYdhg2cpm95Sy-LwL_w" alt="The same battery discharge curve again, except now the “boring” part of the curve is highlighted, which is the middle section where the voltage hardly changes." width="800" height="400"&gt;Example battery discharge curve, calling out the “boring bits” (the points in time where the data remains fairly constant)



&lt;p&gt;&lt;br&gt;&lt;br&gt;
Now, when I said before that I was an electrochemist, I will admit that I was exaggerating a little bit. I knew enough about electrochemistry to be dangerous, but I worked with folks with PhDs who knew a lot more than I did. &lt;/p&gt;

&lt;p&gt;But, I was often better than them at working with data, so I’d do things like programming the &lt;a href="https://en.wikipedia.org/wiki/Potentiostat" rel="noopener noreferrer"&gt;potentiostat&lt;/a&gt;, the piece of equipment you hook the battery up to in order to perform these tests. &lt;/p&gt;

&lt;p&gt;For the interesting parts of the discharge cycle (those parts at the start and end), we could have the potentiostat sample at its max rate, usually a point every 10 milliseconds or so. We didn’t want to sample as many data points during the long, boring parts where the voltage didn’t change because it would mean saving lots of data with unchanging values and wasting storage.&lt;/p&gt;

&lt;p&gt;To reduce the boring data we’d have to deal with without losing the interesting bits, we’d set up the program to sample every 3 minutes, or when the voltage changed by a reasonable amount, say more than 5 mV. &lt;/p&gt;

&lt;p&gt;In practice, what would happen is something like this: &lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2F93ByMmTk0hl4bWvxwFcmx-eteEk6JEKNDFVuzR5z5rtX2q-Y0RAWqQ92cIPsDCYSrm4Y2g1dWsZW9UPBjQ_lVjV567eQTEH-TLB5Ny7eEs9QTafrU1mNJKq3yyZpqKS5zm1lhyNQ" alt="The same battery discharge curve again, this time with data points superimposed on the image. The data points are spaced close together in the “interesting bits,” where the voltage changes quickly at the beginning and end of the discharge curve. The data points are spaced further apart during the “boring” part in the middle, where the voltage hardly changes at all." width="800" height="400"&gt;Example battery discharge curve with data points superimposed to depict rapid sampling during the interesting bits and slower sampling during the boring bits.



&lt;p&gt;&lt;br&gt;&lt;br&gt;
By sampling the data in this way, we'd get more data during the interesting parts and less data during the boring middle section. That’s great! &lt;/p&gt;

&lt;p&gt;It let us answer more interesting questions about the quickly changing parts of the curve and gave us all the information we needed about the slowly changing sections – without storing gobs of redundant data. &lt;strong&gt;But, here’s a question: given this dataset, how do we find the average voltage during the discharge?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question is important because it was one of the things we could compare between this discharge curve and future ones, say 10 or 100 cycles later. As a battery ages, its average voltage drops, and how much it dropped over time could tell us how well the battery’s storage capacity held up during its lifecycle – and if it could turn into a useful product. &lt;/p&gt;

&lt;p&gt;The problem is that the data in the interesting bits is sampled more frequently (i.e., there are more data points for the interesting bits), which would give it more weight when calculating the average, even though it shouldn't.&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2F9ScWi6ABrcZG6Ej1dGa7CH3S2UuDJZHscUwlnmL9j4q-RKMWLjpxjx9GHSr7PjFfnQJEY4zyI4ib9vxyBhbMhwgv9Jwe7nnCKNxszi5fUZ5t1qE0XE6Q3NvLDpmPHAoJLdsxJ3GU" alt="The same battery curve again, with the same data points superimposed and the “interesting bits” circled again, however this time noting that the “interesting bits” shouldn’t count extra even though there are more data points included in the circled area." width="800" height="400"&gt;Example battery discharge curve, with illustrative data points to show that while we collect more data during the interesting bits, they shouldn’t count “extra.”




&lt;p&gt;&lt;br&gt;&lt;br&gt;
If we just took a naive average over the whole curve, adding the value at each point and dividing by the number of points, it would mean that a change to our sampling rate could change our calculated average...even though the underlying effect was really the same! &lt;/p&gt;

&lt;p&gt;We could easily overlook any of the differences we were trying to identify – and any clues about how we could improve the batteries could just get lost in the variation of our sampling protocol. &lt;/p&gt;

&lt;p&gt;Now, some people will say: well, why not just sample at max rate of the potentiostat, even during the boring parts? Well, these discharge tests ran really long. They’d take 10 to 12 hours to complete, but the interesting bits could be pretty short, from seconds or minutes. If we sampled at the highest rate, one every 10ms or so, it would mean orders of magnitude more data to store even though we would hardly use any of it! And orders of magnitude more data would mean more cost, more time for analysis, all sorts of problems.&lt;/p&gt;

&lt;p&gt;So the big question is: &lt;strong&gt;how do we get a representative average when we’re working with irregularly spaced data points?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s get theoretical for a moment here:&lt;/p&gt;

&lt;p&gt;(This next bit is a little equation-heavy, but I think they’re relatively simple equations, and they map very well onto their graphical representation. I always like it when folks give me the math and graphical intuition behind the calculations – but if you want to skip ahead to just see how time-weighted average is used, the mathy bits end here.)&lt;/p&gt;


&lt;h2&gt;
  
  
  Mathy Bits: How to derive a time-weighted average &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s say we have some points like this:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FPTO8f0OV-QODjgzJYjdM2_6qCVCqmfTu3VZFRIGPq5ce7NqdnBlQymtsj6jW3-2GcJSLJaw4ZxrZJh-mHXPT6uzzGr8UQodTSpeqO-osnvym4A1XI_quxKfAzCP2oe_GltM4Gi3E" alt="A graph showing value on the y-axis and time on the x-axis. There are four points:  open parens t 1 comma v 1 close parens to open parens t 4 comma  v 4 close parens spaced unevenly in time on the graph." width="800" height="400"&gt;A theoretical, irregularly sampled time-series dataset



&lt;p&gt;&lt;br&gt;&lt;br&gt;
Then, the normal average would be the sum of the values, divided by the total number of points:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;avg=(v1+v2+v3+v4)4
 avg = \frac{(v_1 + v_2 + v_3 + v_4)}{4}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;vg&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;But, because they’re irregularly spaced, we need some way to account for that. &lt;/p&gt;

&lt;p&gt;One way to think about it would be to get a value at every point in time, and then divide it by the total amount of time. This would be like getting the total area under the curve and dividing by the total amount of time ΔT. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FGJCgIqjl7ntHqKzmXGu2OUAkecSKTO0SHyeUOx1Cb4VnZZojq8h0jiHurtdEq2l50bkzICA9YRaXrXM7Ay8o-9o9985VKDzJpHde8T5BCDhrAh05F4ysy8SaV6xGSH0iIsApPGgT" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2FGJCgIqjl7ntHqKzmXGu2OUAkecSKTO0SHyeUOx1Cb4VnZZojq8h0jiHurtdEq2l50bkzICA9YRaXrXM7Ay8o-9o9985VKDzJpHde8T5BCDhrAh05F4ysy8SaV6xGSH0iIsApPGgT" alt="The same graph as above but with the area under the curve shaded in gray. The area under the curve is drawn by drawing a line through each pair of points and then shading down to the x-axis. The total time spanned by the points from t 1 to t 4 is denoted as Delta T." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
The area under an irregularly sampled time-series dataset




&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;better_avg=area_under_curveΔT
better\_avg = \frac{area\_under\_curve}{\Delta T}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;tt&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;_&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;vg&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord"&gt;_&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;er&lt;/span&gt;&lt;span class="mord"&gt;_&lt;/span&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;(In this case, we’re doing a linear interpolation between the points). So, let’s focus on finding that area. The area between the first two points is a trapezoid: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2F3dYoqjN0mxcj3ZOtSF8AwwM35d_zKjeArrmY4qdrkyo8Iu5TEhbX6HdllPj0jYf64QrAN5av_gHrFOzr-ODAsIWEipwlKLKo_4UK0ZrFRgevF_-cj68ttSSyvOUfpFM7Tep1YA5L" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2F3dYoqjN0mxcj3ZOtSF8AwwM35d_zKjeArrmY4qdrkyo8Iu5TEhbX6HdllPj0jYf64QrAN5av_gHrFOzr-ODAsIWEipwlKLKo_4UK0ZrFRgevF_-cj68ttSSyvOUfpFM7Tep1YA5L" alt="The same graph as above, except there is a trapezoid shaded in blue bounded on top by the line connecting the first two points and vertical lines connecting the points to the x-axis. The distance between the two points on the x-axis is denoted delta t 1." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
A trapezoid representing the area under the first two points



&lt;p&gt;&lt;br&gt;&lt;br&gt;
Which is really a rectangle plus a triangle: &lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh6.googleusercontent.com%2FUIL-A8wJdaniVdeMxzV-GWGeJsQi4lxCVuLyQ1hgNOr9aYjGJnMfM90kQ1Qm6yQO4ROIAr6kK4LwoUrLvRncUNEDlkU-FO9YlnQUtb425mF_wwbR-rlrmV98VRhupFUJVomonAGc" alt="The same graph as the previous, except now the trapezoid, has been divided into a rectangle and a triangle. The rectangle is the height of the first point v 1. The triangle is a right triangle with the line connecting the first two points as the hypotenuse. The distance on the y-axis between the first two points is denoted as delta v 1. " width="800" height="400"&gt;That same trapezoid broken down into a rectangle and a triangle.



&lt;p&gt;&lt;br&gt;&lt;br&gt;
Okay, let's calculate that area:&lt;br&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;area=Δt1v1+Δt1Δv12
area = \Delta t_1 v_1 + \frac{\Delta t_1 \Delta v_1}{2}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
So just to be clear, that's:&lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;area=Δt1v1⏞area of rectangle+Δt1Δv12⏞area of triangle
area = \overbrace{\Delta t_1 v_1}^{\text{area of rectangle}} + \overbrace{\frac{\Delta t_1 \Delta v_1}{2}}^{\text{area of triangle}} 
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="stretchy"&gt;&lt;span class="brace-left"&gt;&lt;/span&gt;&lt;span class="brace-center"&gt;&lt;/span&gt;&lt;span class="brace-right"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;area of rectangle&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="stretchy"&gt;&lt;span class="brace-left"&gt;&lt;/span&gt;&lt;span class="brace-center"&gt;&lt;/span&gt;&lt;span class="brace-right"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;area of triangle&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
 &lt;br&gt;
Okay. So now if we notice that: &lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Δv1=v2−v1
\Delta v_1 = v_2 - v_1
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
We can simplify this equation pretty nicely, start with:&lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Δt1v1+Δt1(v2−v1)2
\Delta t_1 v_1 + \frac{\Delta t_1 (v_2 - v_1)}{2}
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
Factor out 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Δt12
\frac{\Delta t_1}{2} 
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;Δ&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 to get:&lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Δt12(2v1+(v2−v1))
\frac{\Delta t_1}{2} (2v_1 + (v_2 - v_1))
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
Simplify:&lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;Δt12(v1+v2)
\frac{\Delta t_1}{2} (v_1 + v_2)
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
One cool thing to note is that this gives us a new way to think about this solution: it's the average of each pair of adjacent values, weighted by the time between them:&lt;br&gt;

&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;area=(v1+v2)2⏞average of v1 and v2Δt1
area = \overbrace{\frac{(v_1 + v_2)}{2}}^{\text{average of    } v_1 \text{ and } v_2} \Delta t_1 
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mover"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="stretchy"&gt;&lt;span class="brace-left"&gt;&lt;/span&gt;&lt;span class="brace-center"&gt;&lt;/span&gt;&lt;span class="brace-right"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;average of &lt;/span&gt;&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt; and &lt;/span&gt;&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;It’s also equal to the area of the rectangle drawn to the midpoint between v1 and v2:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2F-yQ8VWjY_eVgIibISFjezgjxsx59bVGJCP2iX91n29nkpu3aFD1nxfuVjx81azGODw4_nkj0ELzm38k7PLNyWjMm68hgc4wXbLRpRZ9AoAu1v1HS8cm8mmftaCtU8Vfpza1egiJM" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2F-yQ8VWjY_eVgIibISFjezgjxsx59bVGJCP2iX91n29nkpu3aFD1nxfuVjx81azGODw4_nkj0ELzm38k7PLNyWjMm68hgc4wXbLRpRZ9AoAu1v1HS8cm8mmftaCtU8Vfpza1egiJM" alt="The same graph as the previous, except that now there is a rectangle imposed on the trapezoid. The rectangle is the same width as the others and goes to a height of v 1 plus v 2 over 2. " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
The area of the trapezoid and of the rectangle, drawn to the midpoint between the two points, is the same.



&lt;p&gt;&lt;br&gt;&lt;br&gt;
Now that we’ve derived the formula for two adjacent points, we can repeat this for every pair of adjacent points in the dataset. Then all we need to do is sum that up, and that will be the time-weighted sum, which is equal to the area under the curve. (Folks who have studied calculus may actually remember some of this from when they were learning about integrals and integral approximations!) &lt;/p&gt;

&lt;p&gt;With the total area under the curve calculated,  all we have to do is divide the time-weighted sum by the overall  ΔT and we have our time-weighted average. 💥&lt;/p&gt;

&lt;p&gt;Now that we've worked through our time-weighted average in theory, let’s test it out in SQL. &lt;/p&gt;


&lt;h2&gt;
  
  
  How to compute time-weighted averages in SQL &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s consider the scenario of an ice cream manufacturer or shop owner who is monitoring their freezers. It turns out that ice cream needs to stay in a relatively narrow range of temperatures (~0-10℉)&lt;sup id="fnref1"&gt;1&lt;/sup&gt; so that it doesn’t melt and re-freeze, causing those weird crystals that no one likes. Similarly, if ice cream gets too cold, it’s too hard to scoop. &lt;/p&gt;

&lt;p&gt;The air temperature in the freezer will vary a bit more dramatically as folks open and close the door, but the ice cream temperature takes longer to change. Thus, problems (melting, pesky ice crystals) will only happen if it's exposed to extreme temperatures for a prolonged period. By measuring this data, the ice cream manufacturer can impose quality controls on each batch of product they’re storing in the freezer.&lt;/p&gt;

&lt;p&gt;Taking this into account, the sensors in the freezer measure temperature in the following way: when the door is closed and we’re in the optimal range, the sensors take a measurement every 5 minutes; when the door is opened, the sensors take a measurement every 30 seconds until the door is closed, and the temperature has returned below 10℉.&lt;/p&gt;

&lt;p&gt;To model that we might have a simple table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;freezer_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some data like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:00:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:05:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:10:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:15:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:20:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:25:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:30:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:31:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;-- door opened!&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:31:30+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:32:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:32:30+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;-- door closed&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:33:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:33:30+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:34:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:34:30+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:35:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:35:30+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:36:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;-- temperature stabilized&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:40:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01 00:45:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The period after the door opens, minutes 31-36, has a lot more data points. If we were to take the average of all the points, we would get a misleading value. The freezer was only above the threshold temperature for 5 out of 45 minutes (11% of the time period), but those minutes make up 10 out of 20 data points (50%!) because we sample freezer temperature more frequently after the door is opened. &lt;/p&gt;

&lt;p&gt;To find the more accurate, time-weighted average temperature, let’s write the SQL for the formula above that handles that case. We’ll also get the normal average just for comparison’s sake. (Don’t worry if you have trouble reading it, we’ll write a much simpler version later).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;prev_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'epoch'&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ts_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'epoch'&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;prev_ts_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="o"&gt;*&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt;  &lt;span class="n"&gt;freezer_temps&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
&lt;span class="n"&gt;nextstep&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;prev_temp&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; 
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_temp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts_e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev_ts_e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;weighted_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="o"&gt;*&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;-- the regular average&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weighted_sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts_e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts_e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;time_weighted_average&lt;/span&gt; &lt;span class="c1"&gt;-- our derived average&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nextstep&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; freezer_id |  avg  | time_weighted_average 
------------+-------+-----------------------
          1 | 10.2  |     6.636111111111111
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does return what we want, and gives us a much better picture of what happened, but it’s not exactly fun to write, is it? &lt;/p&gt;

&lt;p&gt;We’ve got a few window functions in there, some case statements to deal with nulls, and several CTEs to try to make it reasonably clear what’s going on. &lt;strong&gt;This is the kind of thing that can really lead to code maintenance issues when people try to figure out what’s going on and tweak it.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Code is all about managing complexity, lots of complex queries to accomplish a relatively simple task makes it much less likely that the developer who comes along next (or you in 3 months) will understand what’s going on, how to use it, or how to change it if they (or you!) need a different result. Or, worse, it means that the code will never get changed because people don’t quite understand what the query’s doing, and it just becomes a black box that no one wants to touch (including you). &lt;/p&gt;




&lt;h2&gt;
  
  
  TimescaleDB hyperfunctions to the rescue!
&lt;/h2&gt;

&lt;p&gt;This is why we created &lt;strong&gt;&lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=hyperfunctions-docs" rel="noopener noreferrer"&gt;hyperfunctions&lt;/a&gt;&lt;/strong&gt;, to make complicated time-series data analysis less complex. Let’s look at what the time-weighted average freezer temperature query looks like if we use the &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weighted-avg-docs" rel="noopener noreferrer"&gt;hyperfunctions for computing time-weighted averages&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;time_weighted_average&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
 freezer_id |  avg  | time_weighted_average 
------------+-------+-----------------------
          1 | 10.2  |     6.636111111111111
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn’t that so much more concise?! Calculate a &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/time_weight/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-weight-docs" rel="noopener noreferrer"&gt;&lt;code&gt;time_weight&lt;/code&gt;&lt;/a&gt; with a &lt;code&gt;’Linear’&lt;/code&gt; weighting method (that’s the kind of weighting derived above &lt;sup id="fnref2"&gt;2&lt;/sup&gt;), then take the average of the weighted values, and we’re done. I like that API much better (and I’d better, because I designed it!). &lt;/p&gt;

&lt;p&gt;What’s more, not only do we save ourselves from writing all that SQL, but it also becomes far, far easier to &lt;strong&gt;compose&lt;/strong&gt; (build up more complex analyses over top of the time-weighted average). This is a huge part of the design philosophy behind hyperfunctions; we want to make fundamental things simple so that you can easily use them to build more complex, application-specific analyses.&lt;/p&gt;

&lt;p&gt;Let’s imagine we’re not satisfied with the average over our entire dataset, and we want to get the time-weighted average for every 10-minute bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10 mins'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;time_weighted_average&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added a &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time_bucket/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=time-bucket-docs" rel="noopener noreferrer"&gt;&lt;code&gt;time_bucket&lt;/code&gt;&lt;/a&gt;, grouped by it, and done! Let’s look at some other kinds of sophisticated analysis that hyperfunctions enable.&lt;/p&gt;

&lt;p&gt;Continuing with our ice cream example, let’s say that we’ve set our threshold because we know that if the ice cream spends more than 15 minutes above 15 ℉, it’ll develop those ice crystals[^ice-cream-footnote] that make it all sandy/grainy tasting. We can use the time-weighted average in a &lt;a href="https://www.postgresql.org/docs/current/functions-window.html" rel="noopener noreferrer"&gt;window function&lt;/a&gt; to see if that happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="n"&gt;fifteen_min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rolling_twa&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt;
&lt;span class="k"&gt;WINDOW&lt;/span&gt; &lt;span class="n"&gt;fifteen_min&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt;  &lt;span class="s1"&gt;'15 minutes'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; freezer_id |           ts           | temperature |    rolling_twa     
------------+------------------------+-------------+--------------------
          1 | 2020-01-01 00:00:00+00 |           4 |                   
          1 | 2020-01-01 00:05:00+00 |         5.5 |               4.75
          1 | 2020-01-01 00:10:00+00 |           3 |                4.5
          1 | 2020-01-01 00:15:00+00 |           4 |  4.166666666666667
          1 | 2020-01-01 00:20:00+00 |         3.5 | 3.8333333333333335
          1 | 2020-01-01 00:25:00+00 |           8 |  4.333333333333333
          1 | 2020-01-01 00:30:00+00 |           9 |                  6
          1 | 2020-01-01 00:31:00+00 |        10.5 |  7.363636363636363
          1 | 2020-01-01 00:31:30+00 |          11 |  7.510869565217392
          1 | 2020-01-01 00:32:00+00 |          15 |  7.739583333333333
          1 | 2020-01-01 00:32:30+00 |          20 |               8.13
          1 | 2020-01-01 00:33:00+00 |        18.5 |  8.557692307692308
          1 | 2020-01-01 00:33:30+00 |          17 |  8.898148148148149
          1 | 2020-01-01 00:34:00+00 |        15.5 |  9.160714285714286
          1 | 2020-01-01 00:34:30+00 |          14 |   9.35344827586207
          1 | 2020-01-01 00:35:00+00 |        12.5 |  9.483333333333333
          1 | 2020-01-01 00:35:30+00 |          11 | 11.369047619047619
          1 | 2020-01-01 00:36:00+00 |          10 | 11.329545454545455
          1 | 2020-01-01 00:40:00+00 |           7 |             10.575
          1 | 2020-01-01 00:45:00+00 |           5 |  9.741666666666667
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window here is over the previous 15 minutes, ordered by time. And it looks like we stayed below our ice-crystallization temperature!&lt;/p&gt;

&lt;p&gt;We also provide a special &lt;a href="https://docs.timescale.com/api/latest/hyperfunctions/time-weighted-averages/rollup-timeweight/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=rolllup-docs" rel="noopener noreferrer"&gt;&lt;code&gt;rollup&lt;/code&gt;&lt;/a&gt; function so you can re-aggregate time-weighted values from subqueries. For instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;time_weighted_average&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10 mins'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time_weighted_average 
-----------------------
    6.636111111111111
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give us the same output as a grand total of the first equation because we’re just re-aggregating the bucketed values.&lt;/p&gt;

&lt;p&gt;But this is mainly there so that you can do more interesting analysis, like, say, normalizing each ten-minute time-weighted average by freezer to the overall time-weighted average.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10 mins'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Linear'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;freezer_temps&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;freezer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bucketed_twa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;overall_twa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_weight&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;normalized_twa&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of feature (storing the time-weight for analysis later) is most useful in a &lt;a href="https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=co-aggs-docs" rel="noopener noreferrer"&gt;continuous aggregate&lt;/a&gt;, and it just so happens that we’ve designed our time-weighted average to be usable in that context! &lt;/p&gt;

&lt;p&gt;We’ll be going into more detail on that in a future post, so be sure to &lt;a href="https://www.timescale.com/signup/newsletter/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=newsletter" rel="noopener noreferrer"&gt;subscribe to our newsletter&lt;/a&gt; so you can get notified when we publish new technical content.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try time-weighted averages today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get started with the time_weight hyperfunction - and many more - right away, spin up a fully managed TimescaleDB service&lt;/strong&gt;: create an account to &lt;a href="https://console.forge.timescale.com/signup?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=signup" rel="noopener noreferrer"&gt;try it for free&lt;/a&gt; for 30 days. Hyperfunctions are pre-loaded on each new database service on Timescale Forge, so after you create a new service, you’re all set to use them!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you prefer to manage your own database instances, you can &lt;a href="https://github.com/timescale/timescaledb-toolkit/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-toolkit" rel="noopener noreferrer"&gt;download and install the timescaledb_toolkit extension&lt;/a&gt;&lt;/strong&gt; on GitHub, after which you’ll be able to use &lt;code&gt;time_weight&lt;/code&gt; and all other hyperfunctions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you have questions or comments on this blog post, &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions/185/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-issue-185" rel="noopener noreferrer"&gt;we’ve started a discussion on our GitHub page, and we’d love to hear from you&lt;/a&gt;&lt;/strong&gt;. (And, if you like what you see, GitHub ⭐ are always welcome and appreciated too!)&lt;/li&gt;
&lt;li&gt;We love building in public, and you can view our &lt;a href="https://github.com/timescale/timescaledb-toolkit/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hyperfunctions-1-0-2021&amp;amp;utm_content=github-toolkit" rel="noopener noreferrer"&gt;upcoming roadmap on GitHub&lt;/a&gt; for a list of proposed features, features we’re currently implementing, and features available to use today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’d like to give a special thanks to &lt;a href="https://github.com/inselbuch" rel="noopener noreferrer"&gt;@inselbuch&lt;/a&gt;, who &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues/46" rel="noopener noreferrer"&gt;submitted the GitHub issue&lt;/a&gt; that got us started on this project (as well as the other folks who 👍’d it and let us know they wanted to use it.) &lt;/p&gt;

&lt;p&gt;We believe time-series data is everywhere, and making sense of it is crucial for all manner of technical problems. We built hyperfunctions to make it easier for developers to harness the power of time-series data. We’re always looking for feedback on what to build next and would love to know how you’re using hyperfunctions, problems you want to solve, or things you think should - or could - be simplified to make analyzing time-series data in SQL that much better. (To contribute feedback, comment on an &lt;a href="https://github.com/timescale/timescaledb-toolkit/issues" rel="noopener noreferrer"&gt;open issue&lt;/a&gt; or in a &lt;a href="https://github.com/timescale/timescaledb-toolkit/discussions" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt; in GitHub.)&lt;/p&gt;

&lt;p&gt;Lastly, in future posts, we’ll give some more context around our design philosophy, decisions we’ve made around our APIs for time-weighted averages (and other features), and detailing how other hyperfunctions work. So, if that’s your bag, you’re in luck – but you’ll have to wait a week or two.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;I don’t know that these times or temperatures are accurate per se; however, the phenomenon of ice cream partially melting and refreezing causing larger ice crystals to form - and coarsening the ice cream as a result - is well documented. See, for instance, &lt;a href="https://peoplegetreadybooks.com/?q=h.tviewer&amp;amp;using_sb=status&amp;amp;qsb=keyword&amp;amp;qse=OqerFF92q0vIs_NOprdwmw" rel="noopener noreferrer"&gt;Harold McGee’s On Food And Cooking&lt;/a&gt; (p 44 in the 2004 revised edition). So, just in case you are looking for advice on storing your ice cream from a blog about time-series databases: for longer-term storage, you would likely want the ice cream to be stored below 0℉. Our example is more like a scenario you’d see in an ice cream display (e.g., in an ice cream parlor or factory line) since the ice cream is kept between 0-10℉ (ideal for scooping, because lower temperatures make ice cream too hard to scoop). ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;We also offer ’LOCF’ or last observation carried forward weighting, which is best suited to cases where you record data points whenever the value changes (i.e., the old value is valid until you get a new one.) The derivation for that is similar, except the rectangles have the height of the first value, rather than the linear weighting we’ve discussed in this post (i.e., where we do linear interpolation between adjacent data points):&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.timescale.com%2Fcontent%2Fimages%2F2021%2F07%2FLOCF-Weighting.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.timescale.com%2Fcontent%2Fimages%2F2021%2F07%2FLOCF-Weighting.png" alt="LOCF Weighting. A graph showing value on the y-axis and time on the x-axis.  There are four points:  open parens t 1 comma v 1 close parens to open parens t 4 comma  v 4 close parens spaced unevenly in time on the graph. There is a shaded area on the graph drawn as a series of rectangles. Each rectangle extends from one point to the next in the series and the rectangle is the height of the first point. So the rectangle under points 1 and 2 has the height of point 1 et cetera." width="" height=""&gt;&lt;/a&gt;LOCF weighting is useful when you know the value is constant until the following point. Rather than: &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.timescale.com%2Fcontent%2Fimages%2F2021%2F07%2FLinear-Weighting.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.timescale.com%2Fcontent%2Fimages%2F2021%2F07%2FLinear-Weighting.png" alt="Linear Weighting. A graph showing value on the y-axis and time on the x-axis. There are four points:  open parens t 1 comma v 1 close parens to open parens t 4 comma  v 4 close parens spaced unevenly in time on the graph. The area under the graph is shaded, much like the previous graph, except now it is a series of trapezoids and the top of each trapezoid is the line drawn between successive points. " width="" height=""&gt;&lt;/a&gt;Linear weighting is useful when you are sampling a changing value at irregular intervals. In general, linear weighting is appropriate for cases where the sampling rate is variable, but there are no guarantees provided by the system about only providing data when it changes. LOCF works best when there’s some guarantee that your system will provide data only when it changes, and you can accurately carry the old value until you receive a new one.  ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>datascience</category>
      <category>database</category>
      <category>postgres</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
