<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://illuminatedcomputing.com/">
  <id>https://illuminatedcomputing.com/</id>
  <title>Illuminated Computing</title>
  <updated>2026-07-21T00:00:00Z</updated>
  <link rel="alternate" href="https://illuminatedcomputing.com/" type="text/html"/>
  <link rel="self" href="https://illuminatedcomputing.com/tags/temporal/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Paul A. Jungwirth</name>
    <uri>https://illuminatedcommputing.com/</uri>
  </author>
  <entry>
    <id>tag:illuminatedcomputing.com,2026-07-21:/posts/2026/07/tquel-paper/</id>
    <title type="html">TQuel Paper: Implementing Temporal Operators in Postgres</title>
    <published>2026-07-21T00:00:00Z</published>
    <updated>2026-07-21T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2026/07/tquel-paper/" type="text/html"/>
    <content type="html">
&lt;p&gt;With &lt;a href="https://www.pgedge.com/blog/looking-forward-to-postgres-19-its-about-time"&gt;&lt;code&gt;UPDATE/DELETE FOR PORTION OF&lt;/code&gt;&lt;/a&gt; looking like it will land in Postgres 19, I’ve been &lt;a href="/pages/pgconf2026-temporal-roadmap/"&gt;thinking about next steps&lt;/a&gt;. I read a very helpful paper on temporal relational algebra last May by Richard Snodgrass. Here are some notes on it.&lt;/p&gt;

&lt;p&gt;The paper was “An Overview of TQuel”. It’s Chapter 6 in &lt;a href="https://www.amazon.com/dp/0805324135"&gt;&lt;em&gt;Temporal Databases: Theory, Design, and Implementation&lt;/em&gt;&lt;/a&gt; from 1993.&lt;/p&gt;

&lt;p&gt;TQuel was an extension to Quel, the query language for Ingres. Ingres, of course, was the predecessor to Postgres!&lt;/p&gt;

&lt;p&gt;My main motivation reading this paper was to learn more about the algebraic identities of temporal relational operators. A query planner depends on such identities to transform your query into a more efficient shape. For instance, if you can filter rows before joining tables instead of after, you’ll get a much faster execution. The useful identities for regular relational operators are well-known, but what about temporal operators? If we ever want to support temporal joins and setops in Postgres, we have to figure that out.&lt;/p&gt;

&lt;p&gt;I implemented some temporal operators in SQL in &lt;a href="https://github.com/pjungwir/temporal_ops"&gt;my &lt;code&gt;temporal_ops&lt;/code&gt; extension&lt;/a&gt;. Since they are just SQL, I don’t have to worry about optimizer correctness. But it would be better to have dedicated executor nodes. That should get us closer to an optimal implementation. I want to teach that extension to inject &lt;code&gt;CustomScans&lt;/code&gt; . . . somehow . . . maybe with a post-parser hook? Once I have that, I can experiment with planner transformations.&lt;/p&gt;

&lt;p&gt;But as a first step, I’m trying to see what is in the research already. One surprise in Snodgrass’s paper was that TQuel valid-times are not intervals (like Postgres rangetypes or SQL:2011 &lt;code&gt;PERIOD&lt;/code&gt;s). Instead they are sets of “chronons”: all the times that the tuple is true, whether contiguous or not. I can see how that might be a more “pure” representation. There is something artificial to forcing the valid-time to be only a single contiguous stretch of time. Fortunately a set of chronons is exactly a multirange, so it is still something you could represent in Postgres.&lt;/p&gt;

&lt;p&gt;A bigger surprise was that TQuel tracks valid-time for each &lt;em&gt;attribute&lt;/em&gt;, instead of for the overall tuple. That is a bit more complicated! Actually it reminds me of how &lt;a href="https://www.amazon.com/Time-Relational-Theory-Databases-Management/dp/0128006315"&gt;Date/Darwen/Lorentzos&lt;/a&gt; and &lt;a href="https://www.amazon.com/Bitemporal-Data-Practice-Tom-Johnston-ebook/dp/B00N9YPWD4"&gt;Johnston&lt;/a&gt; both propose a separate-table-per-attribute structure (which Date calls “Sixth Normal Form”). You can see &lt;a href="/posts/2017/12/temporal-databases-bibliography/"&gt;my temporal databases bibliography&lt;/a&gt; for more about that. There are hints in both of their books that they suspect this is asking too much from ordinary database users. And Snodgrass’s choice here made me worried that his results would be hard to apply to a practical RDBMS like Postgres.&lt;/p&gt;

&lt;p&gt;But then I realized that any SQL:2011 valid-time tuple can be trivially represented in TQuel’s format: just copy the tuple valid-time onto each attribute. In fact Snodgrass has a term for that: a “homogeneous relation”. (Such TQuel relations still have multirange valid-time though.) That makes me hopeful that identities that are true in TQuel might still be true in SQL:2011. They might not be though, if TQuel operators on homogeneous relations don’t always have homogeneous relation results: if for those operators homogeneous relations aren’t “closed”.&lt;/p&gt;

&lt;p&gt;Also TQuel has no nulls and no duplicates, but even that may not matter much. A valid-time never has null endpoints. Well with Postgres rangetypes we do use null to represent “unbounded”, but that has different behavior than a normal null. It is no different than using, say, &lt;code&gt;3000-01-01&lt;/code&gt;, except it works for any base type (unlike &lt;code&gt;'Infinity'&lt;/code&gt;). And I don’t think valid-time adds any further differences between duplicate and non-duplicate behavior. So the TQuel algebra should be as applicable as any non-temporal no-NULL no-duplicate algebra. At least it doesn’t add any &lt;em&gt;new&lt;/em&gt; issues.&lt;/p&gt;

&lt;p&gt;One result in the paper is that for homogeneous relations, TQuel operators are “snapshot reducible”. For such relations, “the valid-time operators ∪̂, −̂, ⨯̂, σ̂, and π̂ reduce to their snapshot counterparts.” In other words, if you want a final result at a single time &lt;code&gt;t&lt;/code&gt; (a snapshot), you can evaluate temporal operators and then take the snapshot (which is slow) or take snapshots first, from all your base tables, and then evaluate non-temporal operators. You can “push down the qual” that &lt;code&gt;valid_at @&amp;gt; now()&lt;/code&gt; (or whatever time you care about). It is a homomorphism:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;σ(R ⨯̂ S) = σ(R) × σ(S)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I think this is the most common query shape against temporal tables, especially for complicated reports. You don’t need the whole history; you just want the result at a given moment. It means that if you are converting an existing app to use application-time tables, you don’t need temporal operators yet. You don’t even need to rewrite the queries. Just declare views that filter your history tables by &lt;code&gt;now()&lt;/code&gt; (or by some variable), and run the queries on them. I talked about this in my &lt;a href="https://2026.pg-data.org/"&gt;PG DATA 2026&lt;/a&gt; talk &lt;a href="/pages/pgdata2026-migrating-to-a-temporal-schema"&gt;Migrating to a Temporal Schema&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The real reward of the paper though was Theorem 13. I found this a couple years ago from skimming, and I had wanted to read the whole thing ever since. Snodgrass says that &lt;em&gt;most&lt;/em&gt; traditional algebraic identities are preserved by his temporal operators, but not all. A big exception is that Cartesian product does not distribute over difference:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Q ⨯̂ (R −̂ S) ≢ (Q ⨯̂ R) −̂ (Q ⨯̂ S)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In SQL the name for “difference” is &lt;code&gt;EXCEPT&lt;/code&gt;. Since joins are all defined in terms of Cartesian product, this is a big one.&lt;/p&gt;

&lt;p&gt;Why doesn’t this work?&lt;/p&gt;

&lt;p&gt;To explore that question, I vibe-coded a “relational calculator”, &lt;a href="https://github.com/pjungwir/relsim"&gt;relsim&lt;/a&gt;. It is a Racket library that lets you evaluate tuples, relations, and operators on them. It supports all the traditional operators (even division!), and also temporal versions. There are temporal operators for SQL:2011 tuples (a range for the whole tuple), multirange tuples (the same but a multirange), and TQuel tuples (a multirange for each attribute). To make it more SQL-like, I allow nulls and duplicates.&lt;/p&gt;

&lt;p&gt;You don’t need to know Racket to use it. Actually I haven’t written Lisp since college myself. But I chose a Lisp so I could get a free parser and REPL, with syntax as math-like as possible. It is pretty easy to use, I think.&lt;/p&gt;

&lt;p&gt;I used relsim to find a counterexample to the Cartesian-product-over-difference identity, and then “step through it” to see where it went wrong. Claude and I wrote up &lt;a href="https://github.com/pjungwir/relsim/tree/master/identities"&gt;the investigation&lt;/a&gt;. Using TQuel-style tuples, suppose we have these relations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     Q
-------------
'a', {[0,20)}

     R
-------------
'b', {[0,20)}

     S
-------------
'b', {[5,10)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each relation has one attribute: a string with an attached multirange valid-time. And each relation has a single row.&lt;/p&gt;

&lt;p&gt;Then for the left-hand side of the identity, we have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Q ⨯̂ (R −̂ S)
{('a', {[0,20)})} ⨯̂ ({('b', {[0,20)})} −̂ {('b', {[5,10)})})
{('a', {[0,20)})} ⨯̂ {('b', {[0,5), [10,20)})}
{('a', {[0,20)}, 'b', {[0,5), [10,20)})}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here I’m using &lt;code&gt;{...}&lt;/code&gt; for a relation (since it’s a &lt;em&gt;set&lt;/em&gt; of tuples), and &lt;code&gt;(...)&lt;/code&gt; for a tuple. For the valid-time I’m using Postgres multirange notation.&lt;/p&gt;

&lt;p&gt;For the right-hand side, we have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;(Q ⨯̂ R) −̂ (Q ⨯̂ S)
({('a', {[0,20)})} ⨯̂ {('b', {[0,20)})}) −̂ ({('a', {[0,20)})} ⨯̂ {('b', {[5,10)})})
{('a', {[0,20)}, 'b', {[0,20)})} −̂ {('a', {[0,20)}, 'b', {[5,10)})}
{('a', {}, 'b', {[0,5), [10,20)})}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So &lt;code&gt;b&lt;/code&gt; is the same, but we lost all of &lt;code&gt;a&lt;/code&gt;! I’m not sure I have an intuitive explanation for “why” that happens.&lt;/p&gt;

&lt;p&gt;What about non-TQuel tuples? I tried those too, and sadly got the same result. For example with rangetypes (where two tuples might be needed for a difference result), the LHS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Q ⨯̂ (R −̂ S)
{('a', [0,20))} ⨯̂ ({('b', [0,20))} −̂ {('b', [5,10))})
{('a', [0,20))} ⨯̂ {('b', [0,5)),
                   ('b', [10,20))}
{('a', [0,20), 'b', [0,5), [0,5)),
 ('a', [0,20), 'b', [10,20), [10,20))}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that here for Cartesian product I’m keeping the inputs’ valid-times, as if they were regular attributes. Then I add a result valid-time that is the inputs’ intersection.&lt;/p&gt;

&lt;p&gt;Versus the RHS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;(Q ⨯̂ R) −̂ (Q ⨯̂ S)
({('a', [0,20))} ⨯̂ {('b', [0,20))}) −̂ ({('a', [0,20))} ⨯̂ {('b', [5,10))})
{('a', [0,20), 'b', [0,20), [0,20))} −̂ {('a', [0,20), 'b', [5,10), [5,10))}
{('a', [0,20), 'b', [0,20), [0,20))}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, different! Here the problem is that the RHS didn’t subtract anything. That’s because the old valid-times didn’t match, and we compare those like regular attributes.&lt;/p&gt;

&lt;p&gt;But if we &lt;em&gt;drop&lt;/em&gt; input valid-times, then the algebra works out! The LHS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Q ⨯̂ (R −̂ S)
{('a', [0,20))} ⨯̂ ({('b', [0,20))} −̂ {('b', [5,10))})
{('a', [0,20))} ⨯̂ {('b', [0,5)),
                   ('b', [10,20))}
{('a', 'b', [0,5)),
 ('a', 'b', [10,20))}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and the RHS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;(Q ⨯̂ R) −̂ (Q ⨯̂ S)
({('a', [0,20))} ⨯̂ {('b', [0,20))}) −̂ ({('a', [0,20))} ⨯̂ {('b', [5,10))})
{('a', 'b', [0,20))} −̂ {('a', 'b', [5,10))}
{('a', 'b', [0,5)),
 ('a', 'b', [10,20))}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We got the same answer! The same thing happens for multirange valid-times. Keeping the original valid-times spoils the algebra.&lt;/p&gt;

&lt;p&gt;I find several layers of irony here.&lt;/p&gt;

&lt;p&gt;One of the main features of Anton Dignös’s work is precisely preserving the input valid-times (for example the “extends” operator in &lt;a href="https://files.ifi.uzh.ch/boehlen/Papers/modf174-dignoes.pdf"&gt;his “Temporal Alignment” paper&lt;/a&gt;), and in &lt;a href="https://github.com/pjungwir/temporal_ops"&gt;&lt;code&gt;temporal_ops&lt;/code&gt;&lt;/a&gt; I tried to follow that. That can be useful for scaling aggregates, also for filtering (e.g. find all hotel reservations of a certain length) and joining (apply a discount depending on reservation length). But maybe it should not be the &lt;em&gt;default&lt;/em&gt;. If the user wants them, they can SELECT them explicitly. And indeed for these examples, the input valid-times are really used &lt;em&gt;internally&lt;/em&gt; by the temporal operator (by the aggregation function or the join’s theta condition), and they don’t need to be in the result.&lt;/p&gt;

&lt;p&gt;Another irony is that Snodgrass’s TSQL2 and SQL:2011 PERIODs have been criticized for not treating valid-time as an attribute within relational theory. And so for &lt;code&gt;PERIOD&lt;/code&gt;s you can’t &lt;code&gt;SELECT&lt;/code&gt; them, put them in a &lt;code&gt;VIEW&lt;/code&gt; or subquery, pass them to a function or return them, &lt;code&gt;GROUP BY&lt;/code&gt; them, etc. I’ve repeated that criticism myself. The lack of composability is annoying. But semantically, Snodgrass was right: valid-time is not an attribute, but a qualifier of the other attributes.&lt;/p&gt;

&lt;p&gt;To be fair to the critics, they don’t treat valid-times like regular attributes either. Since Date/Darwen/Lorentzos define temporal operators in terms of &lt;code&gt;PACK&lt;/code&gt; and &lt;code&gt;UNPACK&lt;/code&gt;, their inputs’ valid-times “disappear”, and the original values aren’t carried through to the result.&lt;/p&gt;

&lt;p&gt;It has been a big help to me to think that you can replace any valid-time tuple with a bunch of ordinary tuples, one for each moment in the valid-time, and that is how temporal features should behave. So valid-time really is something “magic” and different. I still don’t love how PERIODs are not &lt;em&gt;values&lt;/em&gt; in SQL, but somehow we do need to treat valid-time differently.&lt;/p&gt;

&lt;p&gt;I’m very interested in seeing some &lt;em&gt;proofs&lt;/em&gt; of temporal algebraic identities. Snodgrass cites a paper he wrote with E. McKenzie, “Supporting valid time in an historical relational algebra: Proofs and extensions.” But I haven’t been able to find a copy. It is “Technical Report TR-91-15, Department of Computer Science, University of Arizona, Tucson, AZ, August 1991.” That sounds like it might not have been widely distributed. Some other citations which I now have, but haven’t yet read, are Jeffrey D. Ullman, &lt;em&gt;Database and Knowledge-Base Systems&lt;/em&gt; Vol I &amp;amp; II and David Maier, &lt;em&gt;The Theory of Relational Databases&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It looks like the 80s and 90s were really a golden age for temporal research. There are a lot of different temporal models and algebraic systems. Already in 1990, &lt;a href="https://www.vldb.org/conf/1990/P013.PDF"&gt;this paper by Alexander Tuzhilin and James Clifford&lt;/a&gt; was complaining about how to compare them, and they give proofs to show that one is at least as powerful as another. But I’m doing my best to come to grips with what is out there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks to Jeff Davis, Noah Misch, Boris Novikov, and Kent Bulza for helpful conversations!&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-09-15:/posts/2024/09/benchmarking-temporal-foreign-keys/</id>
    <title type="html">Benchmarking Temporal Foreign Keys</title>
    <published>2024-09-15T00:00:00Z</published>
    <updated>2024-09-15T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/09/benchmarking-temporal-foreign-keys/" type="text/html"/>
    <content type="html">
&lt;p&gt;Way back in Februrary Peter Eisentraut &lt;a href="https://www.postgresql.org/message-id/7bd1c8f9-a91a-41a3-990e-0f796ba692ec%40eisentraut.org"&gt;asked me&lt;/a&gt; if I’d tested the performance of &lt;a href="https://commitfest.postgresql.org/49/4308/"&gt;my patch to add temporal foreign keys to Postgres&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Have you checked that the generated queries can use indexes and have suitable performance? Do you have example execution plans maybe?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is a report on the tests I made. I gave &lt;a href="/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/"&gt;a talk about this&lt;/a&gt; last month at &lt;a href="https://pdxpug.wordpress.com/"&gt;pdxpug&lt;/a&gt;, but this blog post will be easier to access, and I’ll focus just on the foreign key results.&lt;/p&gt;

&lt;h2 id="method"&gt;Method&lt;/h2&gt;

&lt;p&gt;As far as I know there are no published benchmark schemas or workflows for temporal data. Since the tables require start/end columns, you can’t use an existing benchmark like &lt;a href="https://www.tpc.org/tpch/"&gt;TCP-H&lt;/a&gt;. The tables built in to &lt;a href="https://www.postgresql.org/docs/current/pgbench.html"&gt;pgbench&lt;/a&gt; are no use either. I’m not even sure where to find a public dataset. The closest is something called “Incumben”, mentioned in the &lt;a href="https://www.zora.uzh.ch/id/eprint/62963/1/p433-dignos.pdf"&gt;“Temporal Alignment” paper&lt;/a&gt;. They authors say it has 85,857 entries for job assignments across 49,195 employees at the University of Arizona—but I can’t find any trace of it online. (I’ll update here if I hear back from them about it.)&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/pjungwir/benchbase/tree/temporal"&gt;a temporal benchmark of my own&lt;/a&gt; using &lt;a href="https://github.com/cmu-db/benchbase"&gt;CMU’s Benchbase framework&lt;/a&gt;. (Thanks to &lt;a href="https://markwkm.blogspot.com/"&gt;Mark Wong&lt;/a&gt; and &lt;a href="https://github.com/grantholly"&gt;Grant Holly&lt;/a&gt; for that recommendation!) It also uses employees and positions, both temporal tables with a &lt;code&gt;valid_at&lt;/code&gt; column (a &lt;code&gt;daterange&lt;/code&gt;). Each position has a reference to an employee, checked by a temporal foreign key. Primary and foreign keys have GiST indexes combining the integer part and the range part. Here is the DDL:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;TABLE&lt;/span&gt; employees (
    id          &lt;span class="predefined-type"&gt;int&lt;/span&gt; GENERATED &lt;span class="keyword"&gt;BY&lt;/span&gt; &lt;span class="directive"&gt;DEFAULT&lt;/span&gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; IDENTITY &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    valid_at    daterange &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    name        &lt;span class="predefined-type"&gt;text&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    salary      &lt;span class="predefined-type"&gt;int&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    &lt;span class="directive"&gt;PRIMARY&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (id, valid_at WITHOUT OVERLAPS)
);

&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;TABLE&lt;/span&gt; positions (
    id          &lt;span class="predefined-type"&gt;int&lt;/span&gt; GENERATED &lt;span class="keyword"&gt;BY&lt;/span&gt; &lt;span class="directive"&gt;DEFAULT&lt;/span&gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; IDENTITY &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    valid_at    daterange &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    name        &lt;span class="predefined-type"&gt;text&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    employee_id &lt;span class="predefined-type"&gt;int&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    &lt;span class="directive"&gt;PRIMARY&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (id, valid_at WITHOUT OVERLAPS),
    &lt;span class="directive"&gt;FOREIGN&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (employee_id, PERIOD valid_at) &lt;span class="keyword"&gt;REFERENCES&lt;/span&gt; employees (id, PERIOD valid_at)
);
&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;INDEX&lt;/span&gt; idx_positions_employee_id &lt;span class="keyword"&gt;ON&lt;/span&gt; positions &lt;span class="keyword"&gt;USING&lt;/span&gt; gist (employee_id, valid_at);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Naturally you can’t run that unless you’ve compiled Postgres with the temporal patches above.&lt;/p&gt;

&lt;p&gt;The benchmark has procedures that exercise foreign keys (update/delete employee, insert/update position). There are other procedures too: selecting one row, selecting many rows, inner join, outer join, semijoin, antijoin. I plan to add aggregates and set operations (union/except/intersect), as well as better queries for sequenced vs non-sequenced semantics. But right now the foreign key procedures are better developed than anything else. I also plan to change the SQL from rangetypes to standard SQL:2011 PERIODs, at least for non-Postgres RDBMSes. I’ll write more about all that later; this post is about foreign keys.&lt;/p&gt;

&lt;h3 id="_implementation"&gt;
&lt;code&gt;range_agg&lt;/code&gt; Implementation&lt;/h3&gt;

&lt;p&gt;Temporal foreign keys in Postgres are implemented like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt; &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="keyword"&gt;FROM&lt;/span&gt;    (
  &lt;span class="class"&gt;SELECT&lt;/span&gt; pkperiodatt &lt;span class="keyword"&gt;AS&lt;/span&gt; r
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;   [ONLY] pktable x
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;  pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;    pkperiodatt &amp;amp;&amp;amp; &lt;span class="error"&gt;$&lt;/span&gt;n
&lt;span class="keyword"&gt;FOR&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; SHARE &lt;span class="keyword"&gt;OF&lt;/span&gt; x
) x1
&lt;span class="keyword"&gt;HAVING&lt;/span&gt; &lt;span class="error"&gt;$&lt;/span&gt;n &amp;lt;&lt;span class="error"&gt;@&lt;/span&gt; range_agg(x1.r)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is very similar to non-temporal checks. The main difference is we use &lt;code&gt;range_agg&lt;/code&gt; to aggregate referenced records, since it may require their combination to satisfy the reference. For example if the employee got a raise in the middle of the position, neither employee record alone covers the position’s valid time:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/employee_position_fk.png" alt="Temporal foreign key"&gt;&lt;/p&gt;

&lt;p&gt;In our query, the &lt;code&gt;HAVING&lt;/code&gt; checks that the “sum” of the employee times covers the position time.&lt;/p&gt;

&lt;p&gt;A subquery is not logically required, but Postgres currently doesn’t allow &lt;code&gt;FOR KEY SHARE&lt;/code&gt; in a query with aggregations.&lt;/p&gt;

&lt;p&gt;I like this query because it works not just for rangetypes, but multiranges too. In fact we could easily support arbitrary types, as long as the user provides an opclass with an appropriate support function (similar to the &lt;code&gt;stratnum&lt;/code&gt; support function introduced for temporal primary keys). We would call that function in place of &lt;code&gt;range_agg&lt;/code&gt;. But how does it perform?&lt;/p&gt;

&lt;h3 id=""&gt;&lt;code&gt;EXISTS implementation&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;I compared this query with two others. The original implementation for temporal foreign keys appears on pages 128–129 of &lt;a href="https://www2.cs.arizona.edu/~rts/tdbbook.pdf"&gt;&lt;em&gt;Developing Time-Oriented Database Applications in SQL&lt;/em&gt; by Richard Snodgrass&lt;/a&gt;. I call this the “&lt;code&gt;EXISTS&lt;/code&gt; implementation”. Here is the SQL I used:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt; &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="comment"&gt;-- There was a PK when the FK started:&lt;/span&gt;
&lt;span class="keyword"&gt;WHERE&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt;
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt;
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;= COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
)
&lt;span class="comment"&gt;-- There was a PK when the FK ended:&lt;/span&gt;
&lt;span class="keyword"&gt;AND&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt;
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;= COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
)
&lt;span class="comment"&gt;-- There are no gaps in the PK:&lt;/span&gt;
&lt;span class="comment"&gt;-- (i.e. there is no PK that ends early,&lt;/span&gt;
&lt;span class="comment"&gt;-- unless a matching PK record starts right away)&lt;/span&gt;
&lt;span class="keyword"&gt;AND&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; pk1
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
    &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
    &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; pk2
    &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pk1.pkatt1 = pk2.pkatt1 [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
            &lt;span class="comment"&gt;-- but skip pk1.pkperiodatt &amp;amp;&amp;amp; pk2.pkperiodatt&lt;/span&gt;
    &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pk2.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
         &amp;lt;= COALESCE(upper(pk1.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
            COALESCE(upper(pk1.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
         &amp;lt;  COALESCE(upper(pk2.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  )
);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The main idea here is that we check three things: (1) the referencing row is covered in the beginning, (2) it is covered in the end, (3) in between, the referenced row(s) have no gaps.&lt;/p&gt;

&lt;p&gt;I made a few changes to the original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It can’t be a &lt;code&gt;CHECK&lt;/code&gt; constraint, since it references other rows.&lt;/li&gt;

&lt;li&gt;There is less nesting. The original is wrapped in a big &lt;code&gt;NOT EXISTS&lt;/code&gt; and looks for bad rows. Essentially it says “there are no invalid records.” In Postgres we check one referencing row at a time, and we give a result if it is valid. You could say we look for good rows. This also requires inverting the middle-layer &lt;code&gt;EXISTS&lt;/code&gt; and &lt;code&gt;NOT EXISTS&lt;/code&gt; predicates, and changing &lt;code&gt;OR&lt;/code&gt;s to &lt;code&gt;AND&lt;/code&gt;s. I’ve &lt;a href="https://www.cybertec-postgresql.com/en/avoid-or-for-better-performance/"&gt;often run into trouble with &lt;code&gt;OR&lt;/code&gt;&lt;/a&gt;, so this is probably fortunate.&lt;/li&gt;

&lt;li&gt;We have to “unwrap” the start/end times since they are stored in a rangetype. I could have used rangetype operators here, but I wanted to keep the adaptation as straightforward as possible, and the previous changes felt like a lot already. Unwrapping requires dealing with unbounded ranges, so I’m using plus/minus &lt;code&gt;Infinity&lt;/code&gt; as a sentinel. This is not perfectly accurate, since in ranges a null bound is “further out” than a plus/minus &lt;code&gt;Infinity&lt;/code&gt;. (Try &lt;code&gt;select '{(,)}'::datemultirange - '{(-Infinity,Infinity)}'::datemultirange&lt;/code&gt;.) But again, solving that was taking me too far from the original, and it’s fine for a benchmark.&lt;/li&gt;

&lt;li&gt;We need to lock the rows with &lt;code&gt;FOR KEY SHARE&lt;/code&gt; in the same way as above. We need to do this in each branch, since they may use different rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given the complexity, I didn’t expect this query to perform very well.&lt;/p&gt;

&lt;h3 id="_implementation_2"&gt;
&lt;code&gt;lag&lt;/code&gt; implementation&lt;/h3&gt;

&lt;p&gt;Finally there is an implementation in &lt;a href="https://github.com/xocolatl/periods"&gt;Vik Fearing’s &lt;code&gt;periods&lt;/code&gt; extension&lt;/a&gt;. This is a lot like the &lt;code&gt;EXISTS&lt;/code&gt; implementation, except to check for gaps it uses the &lt;code&gt;lag&lt;/code&gt; window function. Here is the SQL I tested:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="keyword"&gt;FROM&lt;/span&gt;    (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  uk.uk_start_value,
          uk.uk_end_value,
          NULLIF(LAG(uk.uk_end_value) &lt;span class="keyword"&gt;OVER&lt;/span&gt;
            (&lt;span class="keyword"&gt;ORDER&lt;/span&gt; &lt;span class="keyword"&gt;BY&lt;/span&gt; uk.uk_start_value), uk.uk_start_value) &lt;span class="keyword"&gt;AS&lt;/span&gt; x
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;   (
    &lt;span class="class"&gt;SELECT&lt;/span&gt;  coalesce(lower(x.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk_start_value,
            coalesce(upper(x.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk_end_value
    &lt;span class="keyword"&gt;FROM&lt;/span&gt;    pktable &lt;span class="keyword"&gt;AS&lt;/span&gt; x
    &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
    &lt;span class="keyword"&gt;AND&lt;/span&gt;     uk.pkperiodatt &amp;amp;&amp;amp; &lt;span class="error"&gt;$&lt;/span&gt;n
    &lt;span class="keyword"&gt;FOR&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; SHARE &lt;span class="keyword"&gt;OF&lt;/span&gt; x
  ) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk
) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk
&lt;span class="keyword"&gt;WHERE&lt;/span&gt;   uk.uk_start_value &amp;lt; upper(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     uk.uk_end_value &amp;gt;= lower(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;HAVING&lt;/span&gt;  &lt;span class="predefined"&gt;MIN&lt;/span&gt;(uk.uk_start_value) &amp;lt;= lower(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     &lt;span class="predefined"&gt;MAX&lt;/span&gt;(uk.uk_end_value) &amp;gt;= upper(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     array_agg(uk.x) FILTER (&lt;span class="keyword"&gt;WHERE&lt;/span&gt; uk.x &lt;span class="keyword"&gt;IS&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;) &lt;span class="keyword"&gt;IS&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again I had to make some adaptations to &lt;a href="https://github.com/xocolatl/periods/blob/328c1aaac731f44958b725fb02ca75186f501ce7/periods--1.2.sql#L2230-L2252"&gt;the original&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is less nesting, for similar reasons as before.&lt;/li&gt;

&lt;li&gt;We unwrap the ranges, much like the &lt;code&gt;EXISTS&lt;/code&gt; version. Again there is an &lt;code&gt;Infinity&lt;/code&gt;-vs-null discrepancy, but it is harder to deal with since the query uses null entries in the &lt;code&gt;lag&lt;/code&gt; result to indicate gaps.&lt;/li&gt;

&lt;li&gt;I couldn’t resist using &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;=&lt;/code&gt; and &lt;code&gt;&amp;gt;=&lt;/code&gt; in the most-nested part to find relevant rows. The change was sufficiently obvious, and if it makes a difference it should speed things up, so it makes the comparison a bit more fair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I made a new branch rooted in my &lt;a href="https://github.com/pjungwir/postgresql/tree/valid-time"&gt;valid-time branch&lt;/a&gt;, and added &lt;a href="https://github.com/pjungwir/postgresql/tree/temporal-fk-comparison"&gt;an extra commit&lt;/a&gt; to switch between each implementation with a compile-tag flag. By default we still use &lt;code&gt;range_agg&lt;/code&gt;, but instead you can say &lt;code&gt;‑DRI_TEMPORAL_IMPL_LAG&lt;/code&gt; or &lt;code&gt;‑DRI_TEMPORAL_IMPL_EXISTS&lt;/code&gt;. I installed each implementation in a separate cluster, listening on port 5460, 5461, and 5462 respectively.&lt;/p&gt;

&lt;p&gt;I also included procedures in Benchbase to simply run the above queries as &lt;code&gt;SELECT&lt;/code&gt;s. Since we are doing quite focused microbenchmarking here, I thought that would be less noisy than doing the same DML for each implementation. It also means we can run a mix of all three implementations together: they use the same cluster, and if there is any noise on the machine it affects them all. If you look at my temporal benchmark code, you’ll see the same SQL but adapted for the &lt;code&gt;employees&lt;/code&gt;/&lt;code&gt;positions&lt;/code&gt; tables.&lt;/p&gt;

&lt;h2 id="hypothesis"&gt;Hypothesis&lt;/h2&gt;

&lt;p&gt;Here is the query plan for the &lt;code&gt;range_agg&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Aggregate
  Filter: ('[2020-10-10,2020-12-12)'::daterange &amp;lt;@ range_agg(x1.r))
  -&amp;gt;  Subquery Scan on x1
    -&amp;gt;  LockRows
      -&amp;gt;  Index Scan using employees_pkey on employees x
        Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It uses the index, and it all seems like what we’d want. It is not an &lt;code&gt;Index Only Scan&lt;/code&gt;, but that’s because we lock the rows. Non-temporal foreign keys are the same way. This should perform pretty well.&lt;/p&gt;

&lt;p&gt;Here is the query plan for the &lt;code&gt;EXISTS&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Result
  One-Time Filter: ((InitPlan 1).col1 AND (InitPlan 2).col1 AND (NOT (InitPlan 4).col1))
  InitPlan 1
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees x
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= '2020-10-10'::date) AND ('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 2
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees x_1
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt; '2020-12-12'::date) AND ('2020-12-12'::date &amp;lt;= COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 4
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees pk1
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: (('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)) AND (COALESCE(upper(valid_at), 'infinity'::date) &amp;lt; '2020-12-12'::date) AND (NOT EXISTS(SubPlan 3)))
      SubPlan 3
      -&amp;gt;  LockRows
        -&amp;gt;  Index Scan using employees_pkey on employees pk2
          Index Cond: (id = pk1.id)
          Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= COALESCE(upper(pk1.valid_at), 'infinity'::date)) AND (COALESCE(upper(pk1.valid_at), 'infinity'::date) &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That looks like a lot of work!&lt;/p&gt;

&lt;p&gt;And here is the plan for the &lt;code&gt;lag&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Aggregate
  Filter: ((array_agg(uk.x) FILTER (WHERE (uk.x IS NOT NULL)) IS NULL) AND (min(uk.uk_start_value) &amp;lt;= '2020-10-10'::date) AND (max(uk.uk_end_value) &amp;gt;= '2020-12-12'::date))
  -&amp;gt;  Subquery Scan on uk
    Filter: ((uk.uk_start_value &amp;lt; '2020-12-12'::date) AND (uk.uk_end_value &amp;gt;= '2020-10-10'::date))
    -&amp;gt;  WindowAgg
      -&amp;gt;  Sort
        Sort Key: uk_1.uk_start_value
        -&amp;gt;  Subquery Scan on uk_1
          -&amp;gt;  LockRows
            -&amp;gt;  Index Scan using employees_pkey on employees x
              Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This looks a lot like the &lt;code&gt;range_agg&lt;/code&gt; version. We still use our index. There is an extra &lt;code&gt;Sort&lt;/code&gt; step, but internally the &lt;code&gt;range_agg&lt;/code&gt; function must do much the same thing (if not something worse). Maybe the biggest difference (though a slight one) is aggregating twice.&lt;/p&gt;

&lt;p&gt;So I expect &lt;code&gt;range_agg&lt;/code&gt; to perform the best, with &lt;code&gt;lag&lt;/code&gt; a close second, and &lt;code&gt;EXISTS&lt;/code&gt; far behind.&lt;/p&gt;

&lt;p&gt;One exception may be a single referencing row that spans many referenced rows. If &lt;code&gt;range_agg&lt;/code&gt; is O(n&lt;sup&gt;2&lt;/sup&gt;), it should fall behind as the referenced rows increase.&lt;/p&gt;

&lt;h2 id="results"&gt;Results&lt;/h2&gt;

&lt;p&gt;I started by running a quick test on my laptop, an M2 Macbook Air with 16 GB of RAM. I tested the DML commands on each cluster, one after another. Then I checked the benchbase summary file for the throughput. The results were what I expected:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/throughput-comparison-2024-07-28.png" alt="early results"&gt;&lt;/p&gt;

&lt;p&gt;Similarly, &lt;code&gt;range_agg&lt;/code&gt; had the best latency at the 25th, 50th, 75th, 90th, and 99th percentiles:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/latency-comparison-2024-07-28.png" alt="latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;But the difference throughout is pretty small, and at the time my Benchbase procedures used a lot of &lt;code&gt;synchronized&lt;/code&gt; blocks to ensure there were few foreign key failures, and that kind of locking seemed like it might throw off the results. I needed to do more than this casual check.&lt;/p&gt;

&lt;p&gt;I ran all the future benchmarks on my personal desktop, running Ubuntu 22.04.&lt;/p&gt;
&lt;!--
TODO: fill in these details
cpu
ram
nvme
linux version
postgres 18devel
--&gt;
&lt;p&gt;It was hard to make things reproducible, but I wrote various scripts as I went, and I tried to capture results. The repo for all that is &lt;a href="https://github.com/pjungwir/benchmarking-temporal-tables"&gt;here&lt;/a&gt;. My pdxpug talk above contains some reflections about improving my benchmark methodology.&lt;/p&gt;

&lt;p&gt;I also removed the &lt;code&gt;synchronized&lt;/code&gt; blocks and dealt with foreign key failures a better way (by categorizing them as errors but not raising an exception).&lt;/p&gt;

&lt;p&gt;The first more careful tests used the direct &lt;code&gt;SELECT&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;Again, the 95th percentile latency was what I expected:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/95th-latency-comparison-half-invalid.png" alt="95% latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;But the winner for mean latency was &lt;code&gt;EXISTS&lt;/code&gt;!:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/mean-latency-comparison-half-invalid.png" alt="mean latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;A clue was in the Benchbase output showing successful transactions vs errors. (The &lt;code&gt;Noop&lt;/code&gt; procedure is so can make the proportions 33/33/33/1 instead of 33/33/34.):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Completed Transactions:
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyRangeAgg/01      [72064] ********************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyLag/02           [71479] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyExists/03        [71529] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.Noop/04                         [ 4585] *****
Aborted Transactions:
&amp;lt;EMPTY&amp;gt;

Rejected Transactions (Server Retry):
&amp;lt;EMPTY&amp;gt;

Rejected Transactions (Retry Different):
&amp;lt;EMPTY&amp;gt;

Unexpected SQL Errors:
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyRangeAgg/01      [80861] ********************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyLag/02           [80764] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyExists/03        [80478] *******************************************************************************&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;More than half of the transactions were an invalid reference.&lt;/p&gt;

&lt;p&gt;And if we put one of those into &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, we see that most of the plan was never executed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Result (actual time=0.034..0.035 rows=0 loops=1)
  One-Time Filter: ((InitPlan 1).col1 AND (InitPlan 2).col1 AND (NOT (InitPlan 4).col1))
  InitPlan 1
  -&amp;gt;  LockRows (actual time=0.033..0.033 rows=0 loops=1)
    -&amp;gt;  Index Scan using employees_pkey on employees x (actual time=0.033..0.033 rows=0 loops=1)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= '2020-10-10'::date) AND ('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 2
  -&amp;gt;  LockRows (never executed)
    -&amp;gt;  Index Scan using employees_pkey on employees x_1 (never executed)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt; '2020-12-12'::date) AND ('2020-12-12'::date &amp;lt;= COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 4
  -&amp;gt;  LockRows (never executed)
    -&amp;gt;  Index Scan using employees_pkey on employees pk1 (never executed)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: (('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)) AND (COALESCE(upper(valid_at), 'infinity'::date) &amp;lt; '2020-12-12'::date) AND (NOT EXISTS(SubPlan 3)))
      SubPlan 3
      -&amp;gt;  LockRows (never executed)
        -&amp;gt;  Index Scan using employees_pkey on employees pk2 (never executed)
          Index Cond: (id = pk1.id)
          Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= COALESCE(upper(pk1.valid_at), 'infinity'::date)) AND (COALESCE(upper(pk1.valid_at), 'infinity'::date) &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this example, the beginning of the referencing range wasn’t covered, so Postgres never had to check the rest. Essentially the query is &lt;code&gt;a AND b AND c&lt;/code&gt;, so Postgres can short-circuit the evaluation as soon as it finds &lt;code&gt;a&lt;/code&gt; to be false. Using &lt;code&gt;range_agg&lt;/code&gt; or &lt;code&gt;lag&lt;/code&gt; doesn’t allow this, because an aggregate/window function has to run to completion to get a result.&lt;/p&gt;

&lt;p&gt;As confirmation (a bit gratuitous to be honest), I ran the &lt;code&gt;EXISTS&lt;/code&gt; benchmark with this bpftrace script:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Count how many exec nodes per query were required,
// and print a histogram of how often each count happens.
// Run this for each FK implementation separately.
// My hypothesis is that the EXISTS implementation calls ExecProcNode far fewer times,
// but only if the FK is invalid.

u:/home/paul/local/bench-*/bin/postgres:standard_ExecutorStart {
  @nodes[tid] = 0
}
u:/home/paul/local/bench-*/bin/postgres:ExecProcNode {
  @nodes[tid] += 1
}
u:/home/paul/local/bench-*/bin/postgres:standard_ExecutorEnd {
  @calls = hist(@nodes[tid]);
  delete(@nodes[tid]);
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For &lt;code&gt;EXISTS&lt;/code&gt; I got this histogram when there were no invalid references:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;@calls:
[0]                    6 |                                                    |
[1]                    0 |                                                    |
[2, 4)                 0 |                                                    |
[4, 8)            228851 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16)                1 |                                                    |
[16, 32)               1 |                                                    |
[32, 64)               2 |                                                    |
[64, 128)              2 |                                                    |
[128, 256)             2 |                                                    |
[256, 512)             5 |                                                    |&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But with 50%+ errors I got this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;@calls:
[0]                    6 |                                                    |
[1]                    0 |                                                    |
[2, 4)            218294 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[4, 8)            183438 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@         |
[8, 16)              231 |                                                    |
[16, 32)               1 |                                                    |
[32, 64)               2 |                                                    |
[64, 128)              2 |                                                    |
[128, 256)             2 |                                                    |
[256, 512)             5 |                                                    |&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So more than half the time, Postgres ran the query with half the steps (maybe one-fourth).&lt;/p&gt;

&lt;p&gt;After tuning the random numbers to bring errors closer to 1%, I got results more like the original ones. Mean latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/mean-latency-comparison-mostly-valid.png" alt="mostly valid mean latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;Median latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/median-latency-comparison-mostly-valid.png" alt="mostly valid median latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;95th percentile latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/95th-latency-comparison-mostly-valid.png" alt="mostly valid 95% latency comparison"&gt;&lt;/p&gt;

&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;All foreign key implementations have expected query plans. We use indexes where we should, etc.&lt;/p&gt;

&lt;p&gt;When most foreign key references are valid, &lt;code&gt;range_agg&lt;/code&gt; outperforms the other two implementations by a small but consistent amount. But with a large number of invalid references, &lt;code&gt;EXISTS&lt;/code&gt; is a lot faster.&lt;/p&gt;

&lt;p&gt;In most applications I’ve seen, foreign keys are used as guardrails, and we expect 99% of checks to pass (or more really). When using &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; the situation is different, but these benchmarks are for &lt;code&gt;NO ACTION&lt;/code&gt; or &lt;code&gt;RESTRICT&lt;/code&gt;, and I don’t think &lt;code&gt;CASCADE&lt;/code&gt; affords the &lt;code&gt;EXISTS&lt;/code&gt; implementation the same shortcuts. So it seems right to optimize for the mostly-valid case, not the more-than-half-invalid case.&lt;/p&gt;

&lt;p&gt;These results are good news, because &lt;code&gt;range_agg&lt;/code&gt; is also more general: it supports multiranges and custom types.&lt;/p&gt;

&lt;h2 id="further_work"&gt;Further Work&lt;/h2&gt;

&lt;p&gt;There are more things I’d like to benchmark (and if I do I’ll update this post):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace separate start/end comparisons with range operators in the &lt;code&gt;EXISTS&lt;/code&gt; and &lt;code&gt;lag&lt;/code&gt; implementations. I just need to make sure they still pass all the tests when I do that.&lt;/li&gt;

&lt;li&gt;Correct the &lt;code&gt;Infinity&lt;/code&gt;-vs-null discrepancy.&lt;/li&gt;

&lt;li&gt;Monitor the CPU and disk activity under each implementation and compare the results. I don’t think I’ll see any difference in disk, but CPU might be interesting.&lt;/li&gt;

&lt;li&gt;Compare different scale factors (i.e. starting number of employees/positions).&lt;/li&gt;

&lt;li&gt;Compare implementations when an employee is chopped into many small records, and a single position spans all of them. If &lt;code&gt;range_agg&lt;/code&gt; is O(n&lt;sup&gt;2&lt;/sup&gt;) that should be worse than the sorting in the other options.&lt;/li&gt;

&lt;li&gt;Compare temporal foreign keys to non-temporal foreign keys (based on B-tree indexes, not GiST). I’m not sure yet how to do this in a meaningful way. Of course b-trees are faster in general, but how do I use them to achieve the same primary key and foreign key constraints? Maybe the best way is to create the tables without constraints, give them only b-tree indexes, and run the direct &lt;code&gt;SELECT&lt;/code&gt; statements, not the DML.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-08-26:/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/</id>
    <title type="html">PDXPUG Talk: Benchbase and Temporal Foreign Keys</title>
    <published>2024-08-26T00:00:00Z</published>
    <updated>2024-08-26T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/" type="text/html"/>
    <content type="html">
&lt;p&gt;Last Thursday I gave &lt;a href="https://illuminatedcomputing.com/pages/pdxpug2024-benchbase-and-temporal-foreign-keys/"&gt;a talk at PDXPUG about using Benchbase to compare the performance of temporal foreign keys&lt;/a&gt;. It was a lot of fun, and a really good turnout. There were even folks from Seattle and Bend. After listening for an hour, people stuck around and talked about databases and benchmarks for another two, then the last few holdouts went out for drinks for another hour and a half. At least half the audience were way more qualified to give the talk than me. To my surprise &lt;a href="http://smalldatum.blogspot.com/"&gt;Mark Callaghan&lt;/a&gt; was there, who has published database benchmarks non-stop for years.&lt;/p&gt;

&lt;p&gt;I had two major goals: &lt;a href="/posts/2024/08/benchbase-documentation/"&gt;to document how to use Benchbase&lt;/a&gt; and to report on comparing three implementations of temporal foreign keys. A couple minor goals were to share the start of a broader general-purpose benchmark for temporal databases and to talk about a benchmarking methodology, especially mistakes I made and how I tried to improve.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-07-17:/posts/2024/07/temporal-ops/</id>
    <title type="html">Temporal Ops</title>
    <published>2024-07-17T00:00:00Z</published>
    <updated>2024-07-17T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/07/temporal-ops/" type="text/html"/>
    <content type="html">
&lt;p&gt;One silver lining of &lt;a href="/posts/2024/07/temporal-reverted/"&gt;temporal primary &amp;amp; foreign keys getting reverted&lt;/a&gt; is I got to meet &lt;a href="https://github.com/hettie-d"&gt;Hettie Dombrovskaya&lt;/a&gt; and &lt;a href="https://www.red-gate.com/simple-talk/author/borisnovikov/"&gt;Boris Novikov&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve been working with them to write SQL for various temporal operations not covered by the SQL:2011 standard. There is no support there for outer joins, semijoins, antijoins, aggregates, or set operations (&lt;code&gt;UNION&lt;/code&gt;, &lt;code&gt;INTERSECT&lt;/code&gt;, &lt;code&gt;EXCEPT&lt;/code&gt;). As far as I know no one has ever shown how to implement those operations in SQL. I have queries so far for outer join, semijoin, and antijoin, and I’m planning to include aggregates based on &lt;a href="https://www.red-gate.com/simple-talk/databases/postgresql/making-temporal-databases-work-part-2-computing-aggregates-across-temporal-versions/"&gt;this article by Boris&lt;/a&gt;. The set operations look pretty easy to me, so hopefully I’ll have those soon too.&lt;/p&gt;

&lt;p&gt;If you’re interested, the repo is &lt;a href="https://github.com/pjungwir/temporal_ops"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-07-05:/posts/2024/07/temporal-reverted/</id>
    <title type="html">Temporal Reverted</title>
    <published>2024-07-05T00:00:00Z</published>
    <updated>2024-07-05T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/07/temporal-reverted/" type="text/html"/>
    <content type="html">
&lt;p&gt;My work adding temporal primary keys and foreign keys to Postgres was &lt;a href="https://www.postgresql.org/message-id/47550967-260b-4180-9791-b224859fe63e@illuminatedcomputing.com"&gt;reverted from v17&lt;/a&gt;. The problem is empty ranges (and multiranges). An empty range doesn’t overlap anything, including another empty range. So &lt;code&gt;'empty' &amp;amp;&amp;amp; 'empty'&lt;/code&gt; is false. But temporal PKs are essentially an exclusion constraint using &lt;code&gt;(id WITH =, valid_at WITH &amp;amp;&amp;amp;)&lt;/code&gt;. Therefore you can insert duplicates, as long as the range is empty:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;INSERT INTO t (id, valid_at, name) VALUES (5, 'empty', 'foo');
INSERT INTO t (id, valid_at, name) VALUES (5, 'empty', 'bar');&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That might be okay for some users, but it surely breaks expectations for others. And it’s a questionable thing to do that we should probably just forbid. The SQL standard forbids empty &lt;code&gt;PERIOD&lt;/code&gt;s, so we should make sure that using plain ranges does the same. Adding a record with an empty application time doesn’t really have a meaning in the temporal model.&lt;/p&gt;

&lt;p&gt;I think this is a pretty small bump in the road. At &lt;a href="https://2024.pgconf.dev"&gt;the Postgres developers conference&lt;/a&gt; we found a good solution to excluding empty ranges. My original attempt used &lt;code&gt;CHECK&lt;/code&gt; constraints, but that had a lot of complications. Forbidding them in the executor is a lot simpler. I’ve already sent in &lt;a href="https://www.postgresql.org/message-id/56de0a38-77cc-48a8-bfa7-eb92fa57830b%40illuminatedcomputing.com"&gt;a new set of patches for v18&lt;/a&gt; that implement that change.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-01-24:/posts/2024/01/temporal-pks-merged/</id>
    <title type="html">Temporal PKs Merged!</title>
    <published>2024-01-24T00:00:00Z</published>
    <updated>2024-01-24T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/01/temporal-pks-merged/" type="text/html"/>
    <content type="html">
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; My temporal patches &lt;a href="https://illuminatedcomputing.com/posts/2024/07/temporal-reverted/"&gt;were reverted from v17&lt;/a&gt;. Hopefully they will be accepted for v18 instead.&lt;/p&gt;

&lt;p&gt;Today first thing in the morning I saw that &lt;a href="https://www.postgresql.org/message-id/88518c81-dcdc-4c5b-9200-146b74a520ab%40eisentraut.org"&gt;the first part of my temporal tables work for Postgres got merged&lt;/a&gt;. It was two patches actually: a little one to add a new GiST support function and then the main patch adding support for temporal primary keys and unique constraints based on range types. The support for SQL:2011 PERIODs comes later; for now you must use ranges—although in my opinion that is better anyway. Also this patch allows multiranges or, keeping with Postgres’s long history of extensibility, any type with an overlaps operator. So unless some big problem appears, PKs and UNIQUE constraints are on track to be released in Postgres 17.&lt;/p&gt;

&lt;p&gt;Probably I can get (basic) foreign keys into v17 too. Temporal update/delete, foreign keys with CASCADE, and PERIODs will more likely take ’til 18.&lt;/p&gt;

&lt;p&gt;If you are interested in temporal features, early testing is always appreciated! :-)&lt;/p&gt;

&lt;p&gt;Getting this into Postgres has been a ten-year journey, and the rest of this post is going to be a self-indulgent history of that work. You’ve been warned. :-)&lt;/p&gt;

&lt;p&gt;It started in 2013 when I kept noticing my clients needed a better way to track the history of things that change over time, and I discovered &lt;a href="https://www2.cs.arizona.edu/~rts/publications.html"&gt;Richard Snodgrass’s book &lt;em&gt;Developing Time-Oriented Database Applications in SQL&lt;/em&gt;&lt;/a&gt;. He offered a rigorous, systematic approach, with working SQL solutions for everything. This was exactly what I needed. His approach was vastly better than the ad hoc history-tracking I’d seen so far. But no one had implemented any of it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/message-id/CA+renyVepHxTO1c7dFbVjP1GYMUc0-3qDNWPN30-noo5MPyaVQ@mail.gmail.com"&gt;My first Postgres patch&lt;/a&gt; in 2015 was motivated by temporal databases: I added UUID support to the &lt;code&gt;btree_gist&lt;/code&gt; extension. A temporal primary key is basically an exclusion constraint on &lt;code&gt;(id WITH =, valid_at WITH &amp;amp;&amp;amp;)&lt;/code&gt;, and I had a project with UUID ids. But that exclusion constraint requires a GiST index that knows how to perform equal comparisons against the &lt;code&gt;id&lt;/code&gt; column and overlap comparisons against the &lt;code&gt;valid_at&lt;/code&gt; column. Out-the-box GiST indexes can’t do that (unless your ids are something weird like range types). If your ids are integers, you can install &lt;code&gt;btree_gist&lt;/code&gt; to create a GiST opclass that knows what integer &lt;code&gt;=&lt;/code&gt; means, but at the time UUIDs were not supported. So I started there. I liked that temporal databases had a manageable feature set and a manageable body of literature, so that even a working programmer like me could break new ground (not like Machine Learning or even Time Series databases). Nonetheless that patch took a year and a half to get committed, and it was really other people like Chris Bandy who finished it.&lt;/p&gt;

&lt;p&gt;I kept reading about temporal databases, and in 2017 I wrote &lt;a href="https://github.com/pjungwir/time_for_keys/commit/b0146278cf0d25a6d3f05a38e2ab2c6e8001c93c"&gt;a proof-of-concept for temporal foreign keys&lt;/a&gt;, mostly at AWS Re:Invent. I happened to be given a free registration &amp;amp; hotel room, but it was too late to register for any of the good talks. But all that time with nothing to do was fantastically productive, and I remember by the flight home I was adding tons of tests, trying to cover every feature permutation—ha, as if. A few days after I returned I also published my &lt;a href="https://illuminatedcomputing.com/posts/2017/12/temporal-databases-bibliography/"&gt;annotated bibliography&lt;/a&gt;, which I’ve updated many times since.&lt;/p&gt;

&lt;p&gt;In Snodgrass a temporal foreign key is a page-and-a-half of SQL, mostly because a referencing row may need more than one referenced row to completely cover its time span. But I realized we could make the check much simpler if we used an aggregate function to combine all the relevant rows in the referenced table first. So I wrote &lt;code&gt;range_agg&lt;/code&gt;, first &lt;a href="https://github.com/pjungwir/range_agg"&gt;as an extension&lt;/a&gt;, then &lt;a href="https://www.postgresql.org/message-id/16d71dc8-34cf-5ebd-1ce5-ccd93c0a14f9@illuminatedcomputing.com"&gt;as a core patch&lt;/a&gt;. Jeff Davis (who laid the foundation for temporal support with range types and exclusion constraints) said my function was too narrow and pushed me to implement &lt;a href="https://commitfest.postgresql.org/31/2112/"&gt;multiranges&lt;/a&gt;, a huge improvement. Again it took a year and a half, and I had trouble making consistent progress. There was a lot of work at the end by Alvaro Herrera and Alexander Korotkov (and I’m sure others) to get it committed. That was a few days before Christmas 2020.&lt;/p&gt;

&lt;p&gt;Although the Postgres review process can take a long time, I cherish how it pushes me to do better. As a consultant/freelancer I encounter codebases of, hmm, varying quality, and Postgres gives me an example of what high standards look like.&lt;/p&gt;

&lt;p&gt;One thing I still remember from reading &lt;a href="https://www.amazon.com/Programmers-Work-Interviews-Computer-Industry/dp/1556152116"&gt;&lt;em&gt;Programmers at Work&lt;/em&gt;&lt;/a&gt; many years ago was how many inteviewees said they tried to build things at a higher level of abstraction than they thought they’d need. I’ve seen enough over-engineered tangles and inner-platform effects that my own bias is much closer to YAGNI and keeping things concrete, but the advice in those interviews still prods me to discover good abstractions. The Postgres codebase is full of things like that, and really it’s such a huge project that strong organizing ideas are essential. Multiranges was a great example of how to take a concrete need and convert it into something more general-purpose. And I thought I was doing that already with &lt;code&gt;range_agg&lt;/code&gt;! I think one thing that makes an abstraction good is a kind of definiteness, something opinionated. So it is not purely general, but really adds something new. It always requires an act of creation.&lt;/p&gt;

&lt;p&gt;The coolest thing I’ve heard of someone doing with multiranges was &lt;a href="https://iopscience.iop.org/article/10.3847/1538-3881/ac5ab8"&gt;using them in astronomy to search for neutrinos, gravitational waves, and gamma-ray bursts&lt;/a&gt;. By using multiranges, they were able to compare observations with maps of the night sky “orders of magnitude faster” than with other implementations. (Hopefully I’ve got that right: I read a pre-print of the paper but it was not all easy for me to understand!)&lt;/p&gt;

&lt;p&gt;My first patch for an actual temporal feature was &lt;a href="https://www.postgresql.org/message-id/CA%2BrenyWxfXpThaOXiNuo6dEJQPYOWjysnXQw7_m7WJnNHVn_-g%40mail.gmail.com"&gt;primary keys&lt;/a&gt; back in 2018. Then foreign keys followed in 2019, just a couple weeks before I gave a talk at PgCon about temporal databases. By the end of the year I had &lt;code&gt;FOR PORTION OF&lt;/code&gt; as well. At first &lt;code&gt;FOR PORTION OF&lt;/code&gt; was implemented in the Executor Phase, but when I gave a progress report for PgCon 2020 I was already working on a trigger-based reimplementation, though it wasn’t submitted until June 2021. I also pulled in &lt;a href="https://www.postgresql.org/message-id/mSRBIYry-zk13wGeWVYCGw5o0LqZ4dyectlawH43VoLzQ70Tqa2oClVdkmQ1MlhG2lToRBkyY77g1o7vSGUwMS9BXvE-H-bg_x9bjx0DKNI%3D%40protonmail.com"&gt;work by Vik Fearing from 2018&lt;/a&gt; to support &lt;code&gt;ADD/DROP PERIOD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Soon after that progress got harder: my wife and I had our sixth baby in August, and somehow he seemed to be more work than the others. I took over daily math lessons (we homeschool), and I had to let go my biggest client, who needed more hours than I could give. (I’m proud to have given them an orderly transition over several months though.) In January 2022 Peter Eisentraut gave me a thorough review, but I went silent. Still, I had a lot of encouragement from the community, especially Corey Huinker, and eventually doing Postgres got easier again. I had a talk accepted for PgCon 2023, and I worked hard to submit new patches, which I did only weeks before the conference.&lt;/p&gt;

&lt;p&gt;The best part of PgCon was getting everyone who cared about my work together in the hallway to agree on the overall approach. I had worried for years about using ranges as well as PERIODs, since the standard doesn’t know anything about ranges. The second-best part was when someone told me I should stop calling myself a Postgres newbie.&lt;/p&gt;

&lt;p&gt;At PgCon Peter asked me to re-organize the patches, essentially implementing PERIODs as &lt;code&gt;GENERATED&lt;/code&gt; range columns. It made the code much nicer. I also went back to an Executor Phase approach for &lt;code&gt;FOR PORTION OF&lt;/code&gt;. Using triggers had some problems around updateable views and &lt;code&gt;READ COMMITTED&lt;/code&gt; transaction isolation.&lt;/p&gt;

&lt;p&gt;Since May I’ve felt more consistent than during my other Postgres work. I’ve been kept busy by excellent feedback by a meticulous reviewer, Jian He, who has caught many bugs. Often as soon as I get caught up, before I’ve written the email with the new patch files, he finds more things!&lt;/p&gt;

&lt;p&gt;Another thing that’s helped is going out once a week (for nearly a year now) to get early dinner then work on Postgres at a local bar. Somehow it’s much easier to do Postgres from somewhere besides my home office, where I do all my normal work. Getting dinner lets me read something related (lately &lt;a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321"&gt;&lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt; by Martin Klepmann&lt;/a&gt; and &lt;a href="https://postgrespro.com/community/books/internals"&gt;&lt;em&gt;PostgreSQL 14 Internals&lt;/em&gt; by Egor Rogov&lt;/a&gt;), and it’s fun. Doing just a little every week helps me keep momentum, so that fitting in further progress here and there seems easy. I’m lucky to have a wife who has supported it so often, despite leaving her with the kids and dishes.&lt;/p&gt;

&lt;p&gt;I think I have years more work of temporal features to add, first finishing SQL:2011 then going beyond (e.g. temporal outer joins, temporal aggregates, temporal upsert). It’s been a great pleasure!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2019-09-04:/posts/2019/08/sql2011-survey/</id>
    <title type="html">Survey of SQL:2011 Temporal Features</title>
    <published>2019-09-04T00:00:00Z</published>
    <updated>2019-09-04T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2019/08/sql2011-survey/" type="text/html"/>
    <content type="html">
&lt;h1 id="introduction"&gt;Introduction&lt;/h1&gt;

&lt;p&gt;This blog post is a survey of SQL:2011 Temporal features from MariaDB, IBM DB2, Oracle, and MS SQL Server. I’m working on adding temporal features to Postgres, so I wanted to see how other systems interpret the standard.&lt;/p&gt;

&lt;p&gt;If you’re new to temporal databases, you also might enjoy &lt;a href="https://github.com/pjungwir/postgres-temporal-talk"&gt;this talk&lt;/a&gt; I gave at PGCon 2019.&lt;/p&gt;

&lt;p&gt;In this post I cover both application-time (aka valid-time) and system-time, but I focus more on valid-time. Valid-time tracks the history of the thing “out there”, e.g. when a house was remodeled, when an employee got a raise, etc. System-time tracks the history of when you changed the database. In general system-time is more widely available, both as native SQL:2011 features and as extensions/plugins/etc., but is less interesting. It is great for compliance/auditing, but you’re unlikely to build application-level features on it. Also since it’s generated automatically you don’t need special DML commands for it, and it is less important to protect yourself with temporal primary and foreign keys.&lt;/p&gt;

&lt;p&gt;At this point all the major systems I survey have &lt;em&gt;some&lt;/em&gt; temporal support, although none of them support it completely. On top of that the standard itself is quite modest, although in some ways it can be interpreted more or less expansively.&lt;/p&gt;

&lt;h1 id="the_standard"&gt;The Standard&lt;/h1&gt;

&lt;p&gt;I’ll start by giving a quick overview of the standard. Here I’m working from the draft documents (downloaded from &lt;a href="https://modern-sql.com/standard"&gt;here&lt;/a&gt;), and my interpretation may not be correct. If you have any corrections please let me know! Also you can find a more complete description of the standard at &lt;a href="https://sigmodrecord.org/publications/sigmodRecord/1209/pdfs/07.industry.kulkarni.pdf"&gt;this article by Kulkarni and Michels&lt;/a&gt; (pdf).&lt;/p&gt;

&lt;p&gt;In SQL:2011 the gateway to temporal features is a &lt;code&gt;PERIOD&lt;/code&gt;, which is something you declare on your table. It is a range-like structure derived from two existing &lt;code&gt;date&lt;/code&gt; columns. (Actually the standard also supports &lt;code&gt;timestamp&lt;/code&gt; and &lt;code&gt;timestamp with time zone&lt;/code&gt;, but I’ll use &lt;code&gt;date&lt;/code&gt; as a synecdoche throughout this post.)&lt;/p&gt;

&lt;h2 id="periods"&gt;Periods&lt;/h2&gt;

&lt;p&gt;You can declare a valid-time &lt;code&gt;PERIOD&lt;/code&gt; when you create the table or afterwards:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id          INTEGER,
  valid_from  DATE,
  valid_til   DATE,
  PERIOD FOR valid_at (valid_from, valid_til)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can call the &lt;code&gt;PERIOD&lt;/code&gt; whatever you like &lt;em&gt;except&lt;/em&gt; &lt;code&gt;SYSTEM_TIME&lt;/code&gt;, which is magical and enables system-time features. Both of the &lt;code&gt;PERIOD&lt;/code&gt;‘s source columns must be &lt;code&gt;NOT NULL&lt;/code&gt;, and if not they are automatically converted to it. (Most databases do the same thing with a &lt;code&gt;PRIMARY KEY&lt;/code&gt;.) Note that the &lt;code&gt;NOT NULL&lt;/code&gt; requirement means to represent “forever” or “until further notice” you must use a sentinel value like &lt;code&gt;3000-01-01&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Naturally a &lt;code&gt;PERIOD&lt;/code&gt; adds an implicit constraint that &lt;code&gt;valid_from&lt;/code&gt; must be less than &lt;code&gt;valid_til&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also define a &lt;code&gt;SYSTEM_TIME&lt;/code&gt; period and ask the database to track changes for you:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id        INTEGER,
  sys_from  TIMESTAMP GENERATED ALWAYS AS ROW START,
  sys_til   TIMESTAMP GENERATED ALWAYS AS ROW END,
  PERIOD FOR system_time (valid_from, valid_til)
) WITH SYSTEM VERSIONING;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Technically the standard lets you use &lt;code&gt;DATE&lt;/code&gt; columns for system-time periods, but it’s hard to imagine how that would work in practice. Really anything short of the RDBMS’s finest granularity could “squeeze out” some history.&lt;/p&gt;

&lt;h2 id="primary_keys"&gt;Primary Keys&lt;/h2&gt;

&lt;p&gt;If you have a valid-time &lt;code&gt;PERIOD&lt;/code&gt; then you can declare a temporal primary key when you create the table:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id          INTEGER,
  valid_from  DATE,
  valid_til   DATE,
  PERIOD FOR valid_at (valid_from, valid_til),
  CONSTRAINT tpk_t PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A temporal primary key is a lot like a normal primary key, except the scalar part (here just &lt;code&gt;id&lt;/code&gt;) &lt;em&gt;does not have to be unique&lt;/em&gt;, as long as rows with the same key don’t overlap in time. In other words you can give product 5 one price today and another tomorrow, and there’s no contradiction. But if you have two rows with the same scalar key covering the same date, that’s a violation of temporal entity integrity.&lt;/p&gt;

&lt;h2 id="foreign_keys"&gt;Foreign Keys&lt;/h2&gt;

&lt;p&gt;Temporal referential integrity is like ordinary referential integrity, except the non-unique nature of temporal primary keys makes it trickier. In a temporal foreign key, the child row’s lifespan must be completely “covered” by one (or more!) rows in the parent table. In other words some parent record must exist for every moment the child record exists. You can declare a temporal foreign key between two tables that both have &lt;code&gt;PERIOD&lt;/code&gt;s, e.g.:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE ch (
  id          INTEGER,
  valid_from  DATE,
  valid_til   DATE,
  t_id        INTEGER,
  PERIOD FOR valid_at (valid_from, valid_til),
  CONSTRAINT tpk_ch PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
  CONSTRAINT tfk_ch_to_t FOREIGN KEY (id, PERIOD valid_at)
    REFERENCES t (id, PERIOD valid_at)
);&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="projecting"&gt;Projecting&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;PERIOD&lt;/code&gt; is not included in the projection when you &lt;code&gt;SELECT * FROM t&lt;/code&gt;. It is questionable whether you can project it explicitly with &lt;code&gt;SELECT *, valid_at FROM t&lt;/code&gt;, but since it’s not a full-fledged data type I’d say probably not.&lt;/p&gt;

&lt;h2 id="filtering"&gt;Filtering&lt;/h2&gt;

&lt;p&gt;Also you can’t reference a &lt;code&gt;PERIOD&lt;/code&gt; in most other contexts, e.g. as a function input, or a &lt;code&gt;GROUP BY&lt;/code&gt; criterion, or when &lt;code&gt;ORDER&lt;/code&gt;ing, or joining. You &lt;em&gt;can&lt;/em&gt; use it in a “period predicate”, which lets you test these period relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overlaps&lt;/li&gt;

&lt;li&gt;equals&lt;/li&gt;

&lt;li&gt;contains&lt;/li&gt;

&lt;li&gt;precedes&lt;/li&gt;

&lt;li&gt;succeeds&lt;/li&gt;

&lt;li&gt;immediately precedes&lt;/li&gt;

&lt;li&gt;immediately succeeds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either side of the relationship can use a previously-named &lt;code&gt;PERIOD&lt;/code&gt; or an anonymous dynamically-constructed one, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;x.valid_at OVERLAPS PERIOD(y.valid_from, y.valid_til)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s not clear to me where you can use a period predicate, although the standard groups it with other kinds of predicate under the &lt;code&gt;&amp;lt;predicate&amp;gt;&lt;/code&gt; object, so maybe anywhere you like? This &lt;a href="https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html"&gt;browsable BNF grammer&lt;/a&gt; makes it easy to see that a &lt;code&gt;&amp;lt;predicate&amp;gt;&lt;/code&gt; can go anywhere that accepts a &lt;a href="https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#_6_39_boolean_value_expression"&gt;boolean expression&lt;/a&gt;, which can be used in a &lt;code&gt;&amp;lt;search condition&amp;gt;&lt;/code&gt;, which is what you put into your &lt;code&gt;WHERE&lt;/code&gt; clause, or a join’s &lt;code&gt;ON&lt;/code&gt;, or a &lt;code&gt;CASE WHEN&lt;/code&gt;, or lots of other places. If you have a firmer read of the standard here, let me know!&lt;/p&gt;

&lt;p&gt;Also there is a special syntax for querying based on system-time. The standard doesn’t mention using it for valid-time, although you could imagine doing it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t FOR SYSTEM_TIME AS OF t1
SELECT * FROM t FOR SYSTEM_TIME BETWEEN t1 AND t2
SELECT * FROM t FOR SYSTEM_TIME BETWEEN SYMMETRIC t1 AND t2
SELECT * FROM t FOR SYSTEM_TIME FROM t1 TO t2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you ask for a limited time range, the stard/end columns do &lt;em&gt;not&lt;/em&gt; get truncated to match your request. In other words, if you query &lt;code&gt;FOR SYSTEM_TIME BETWEEN '2000-01-01' AND '2020-01-01'&lt;/code&gt;, your result records’ &lt;code&gt;sys_til&lt;/code&gt; attributes are still &lt;code&gt;3000-01-01&lt;/code&gt; (or whatever your sentinel is).&lt;/p&gt;

&lt;h2 id="dml"&gt;DML&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; commands you can restrict the timespan you want changed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;UPDATE  t
FOR PORTION OF valid_at FROM t1 TO t2
SET     ...
...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DELETE FROM t
FOR PORTION OF valid_at FROM t1 TO t2
...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These commands may require special transformations if they “hit” only part of an existing record. For example if you delete the middle of a longer timespan, then you need to replace the old big record with your new version &lt;em&gt;plus&lt;/em&gt; two short records (one on each end). An update is the same: after changing the targeted portion, you’d have to insert new records to preserve each end of the original. The standard gives careful instructions here: the RDBMS should include these extra inserts within the “primary effect” of the operation.&lt;/p&gt;

&lt;p&gt;There is no need for any special syntax for &lt;code&gt;INSERT&lt;/code&gt;, nor for special transformations.&lt;/p&gt;

&lt;p&gt;The standard doesn’t have anything to say about a &lt;code&gt;MERGE&lt;/code&gt; statement (in Postgres &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;), except in the case of system-time tables, where there is no new syntax and it does what you’d expect.&lt;/p&gt;

&lt;h2 id="questions"&gt;Questions&lt;/h2&gt;

&lt;p&gt;Since a &lt;code&gt;PERIOD&lt;/code&gt; is attached to a table and isn’t part of the relational model, it isn’t part of a result set. It gets lost when you query a table. That makes it hard to query non-table temporal data, like views, subqueries, CTEs, and set-returning functions. (This was &lt;a href="/posts/2017/12/temporal-databases-bibliography/"&gt;a major criticism of the original TSQL2 proposal&lt;/a&gt; from the 90s.) Nonetheless I can imagine how SQL:2011 leaves open some workarounds, e.g. by letting you use anonymous &lt;code&gt;PERIOD&lt;/code&gt;s inside period predicates, and letting you use period predicates as widely as possible. Also you could argue that projecting a &lt;code&gt;PERIOD&lt;/code&gt; is unnecessary since you already have the start and end columns. So &lt;em&gt;if&lt;/em&gt; an RDBMS gave you deep support for period predicates, composing temporal results would still be possible—albeit awkward. In practice though, no one does this, as we will see.&lt;/p&gt;

&lt;p&gt;SQL:2011 also has no support for joining temporal results. You can effect an inner join with the &lt;code&gt;OVERLAPS&lt;/code&gt; operator, but not the other kinds.&lt;/p&gt;

&lt;p&gt;Snodgrass suggested that temporal databases should “coalesce” results before presenting them or at least before saving them to a table. Coalescing means that when two rows have adjacent or overlapping timespans and all other attributs are identical, they get merged to become just one row. Duplicates are removed. This seems like good behavior, both for clarity and to avoid cutting up your data more and more finely as time goes on, but SQL:2011 doesn’t mention it.&lt;/p&gt;

&lt;p&gt;There is also no explicit mention of how triggers combine with the new temporal DML operations.&lt;/p&gt;

&lt;h1 id="mariadb"&gt;MariaDB&lt;/h1&gt;

&lt;p&gt;MySQL doesn’t support any temporal features, but recent versions of MariaDB &lt;a href="https://mariadb.com/kb/en/library/temporal-data-tables/"&gt;have started to add support&lt;/a&gt;. Version 10.3.4 (released Jan 2018) included system-time support; Version 10.4.3 (Feb 2019), valid-time.&lt;/p&gt;

&lt;h2 id="system_time"&gt;System Time&lt;/h2&gt;

&lt;p&gt;MariaDB supports the normal syntax for declaring system-time tables, but you can also use this abbreviated syntax if you like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id INT
) WITH SYSTEM VERSIONING;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That will automatically add pseudo-columns named &lt;code&gt;ROW_START&lt;/code&gt; and &lt;code&gt;ROW_END&lt;/code&gt; (which also don’t appear in &lt;code&gt;SELECT * FROM t&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or the standard syntax works too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id        INTEGER,
  sys_from  TIMESTAMP(6) GENERATED ALWAYS AS ROW START,
  sys_til   TIMESTAMP(6) GENERATED ALWAYS AS ROW END,
  PERIOD FOR SYSTEM_TIME (valid_from, valid_til)
) WITH SYSTEM VERSIONING;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Either way, for a &lt;code&gt;timestamp(6)&lt;/code&gt; column (which is what the docs use) it looks like the max future date is 2038:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;MariaDB [temporal]&amp;gt; insert into t (id) values (2);
Query OK, 1 row affected (0.008 sec)

MariaDB [temporal]&amp;gt; select * from t2;
+------+----------------------------+----------------------------+
| id   | valid_from                 | valid_til                  |
+------+----------------------------+----------------------------+
|    2 | 2019-07-27 17:07:51.849190 | 2038-01-18 19:14:07.999999 |
+------+----------------------------+----------------------------+
1 row in set (0.004 sec)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That seems awfully soon to me.&lt;/p&gt;

&lt;p&gt;You can use these three ways of asking for system-time filters:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t FOR SYSTEM_TIME AS OF '2020-01-01';
SELECT * FROM t FOR SYSTEM_TIME FROM '2020-01-01' TO '2030-01-01';
SELECT * FROM t FOR SYSTEM_TIME BETWEEN '2020-01-01' AND '2030-01-01';&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;MariaDB doesn’t know about &lt;code&gt;BETWEEN SYMMETRIC&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also say &lt;code&gt;FOR SYSTEM_TIME ALL&lt;/code&gt;, which is useful because the default (with no &lt;code&gt;FOR SYSTEM_TIME&lt;/code&gt; at all) is to filter &lt;code&gt;AS OF NOW()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;MariaDB partially addresses the composability problem by letting you say &lt;code&gt;FOR SYSTEM_TIME&lt;/code&gt; against a view, which “pushes down” the filter to the underlying tables. This even works if the view queries some non-system-time tables. Since every system-time &lt;code&gt;PERIOD&lt;/code&gt; is named the same thing, the database can sensibly interpret &lt;code&gt;FOR SYSTEM_TIME&lt;/code&gt; against your view.&lt;/p&gt;

&lt;h3 id="systemtime_partitions"&gt;System-Time Partitions&lt;/h3&gt;

&lt;p&gt;To prevent tables getting too large, you can automatically partition a table by its &lt;code&gt;SYSTEM_TIME&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id INT
) WITH SYSTEM VERSIONING
  PARTITION BY SYSTEM_TIME (
    PARTITION p_hist HISTORY,
    PARTITION p_curr CURRENT
  );&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That will keep current records in one partition and historical records in another. You can also have multiple historical partitions and ask the system to switch to the next one every &lt;code&gt;n&lt;/code&gt; rows. You can also drop older partitions to keep your data growth under control.&lt;/p&gt;

&lt;h3 id="excluded_columns"&gt;Excluded Columns&lt;/h3&gt;

&lt;p&gt;To further economize on disk, you can qualify specific columns as &lt;code&gt;WITHOUT SYSTEM VERSIONING&lt;/code&gt; to exclude them from history.&lt;/p&gt;

&lt;h2 id="application_time"&gt;Application Time&lt;/h2&gt;

&lt;p&gt;Declaring an application-time &lt;code&gt;PERIOD&lt;/code&gt; works, but you can’t include a temporal &lt;code&gt;PRIMARY KEY&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id          INTEGER,
  valid_from  DATE,
  valid_til   DATE,
  PERIOD FOR valid_at (valid_from, valid_til),
  -- This next line breaks!:
  CONSTRAINT tpk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Naturally you can’t create temporal foreign keys either.&lt;/p&gt;

&lt;p&gt;If you omit the &lt;code&gt;NOT NULL&lt;/code&gt; for the &lt;code&gt;PERIOD&lt;/code&gt; source columns (as above), they become &lt;code&gt;NOT NULL&lt;/code&gt; automatically.&lt;/p&gt;

&lt;h3 id="dml_2"&gt;DML&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; statements you can use &lt;code&gt;FOR PORTION OF valid_at&lt;/code&gt;, per the standard. You can’t use an anonymous period:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;UPDATE  t
FOR PORTION OF PERIOD (valid_from, valid_til)
SET     ...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(It’s hard to imagine why you’d want to though.)&lt;/p&gt;

&lt;h3 id="projecting_2"&gt;Projecting&lt;/h3&gt;

&lt;p&gt;You can’t &lt;code&gt;SELECT&lt;/code&gt; a period, named or anonymous:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t;
SELECT *, valid_at FROM t;
SELECT *, PERIOD (valid_from, valid_til) FROM t;&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="filtering_2"&gt;Filtering&lt;/h3&gt;

&lt;p&gt;In a &lt;code&gt;SELECT&lt;/code&gt; you can’t use &lt;code&gt;FOR valid_at&lt;/code&gt; to filter things. That’s a little sad but perhaps understandable since arguably the standard only requires &lt;code&gt;FOR SYSTEM_TIME&lt;/code&gt;. But period predicates don’t work either. These were all errors for me:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t WHERE valid_at CONTAINS '2020-01-01';
SELECT * FROM t WHERE valid_at OVERLAPS PERIOD('2020-01-01', '2030-01-01');&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So if you want to ask questions about your valid-time history, you need to query against the scalar date columns.&lt;/p&gt;

&lt;h3 id="triggers"&gt;Triggers&lt;/h3&gt;

&lt;p&gt;You can declare triggers on valid-time tables, and the triggers &lt;em&gt;do&lt;/em&gt; fire for the extra inserts. Here is what I did to test things:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE thist (
  id INTEGER,
  old_valid_from DATE,
  old_valid_til DATE,
  new_valid_from DATE,
  new_valid_til DATE, op CHAR(1)
);

CREATE TRIGGER tins AFTER INSERT ON t
FOR EACH ROW
INSERT INTO thist VALUES 
(NEW.id, NULL, NULL, NEW.valid_from, NEW.valid_til, 'i');

CREATE TRIGGER tupd AFTER UPDATE ON t
FOR EACH ROW
INSERT INTO thist VALUES
(NEW.id, OLD.valid_from, OLD.valid_til, NEW.valid_from, NEW.valid_til, 'u');

CREATE TRIGGER tdel AFTER DELETE ON t
FOR EACH ROW
INSERT INTO thist VALUES
(OLD.id, OLD.valid_from, OLD.valid_til, NULL, NULL, 'd');&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you &lt;code&gt;UPDATE&lt;/code&gt; in the middle of a larger record, you get two &lt;code&gt;INSERT&lt;/code&gt;s for the unaltered ends followed by an &lt;code&gt;UPDATE&lt;/code&gt; of the middle. (The &lt;code&gt;INSERT&lt;/code&gt;s come first.) The &lt;code&gt;NEW.valid_from&lt;/code&gt; and &lt;code&gt;NEW.valid_til&lt;/code&gt; mark the part that is being inserted/updated, as you’d expect.&lt;/p&gt;

&lt;p&gt;If you &lt;code&gt;DELETE&lt;/code&gt; in the middle of a larger record, you also get two &lt;code&gt;INSERTS&lt;/code&gt; followed by a &lt;code&gt;DELETE&lt;/code&gt; of the part you touched. In the delete trigger the &lt;code&gt;OLD.valid_{from,til}&lt;/code&gt; columns have their actual old values, not the slice you’re deleting. (This probably makes sense, but it feels a little too mechanical/literal. It means your &lt;code&gt;DELETE&lt;/code&gt; trigger doesn’t know what slice of history you’re actually removing.)&lt;/p&gt;

&lt;h2 id="bitemporal"&gt;Bitemporal&lt;/h2&gt;

&lt;p&gt;You can also define bitemporal tables!&lt;/p&gt;

&lt;h1 id="ibm_db2"&gt;IBM DB2&lt;/h1&gt;

&lt;p&gt;DB2 has the fullest temporal support of all the databases I examined. My tests used version 11.5.0.0 on Linux.&lt;/p&gt;

&lt;h2 id="system_time_2"&gt;System Time&lt;/h2&gt;

&lt;p&gt;System-time works with a few syntax differences:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id        INTEGER NOT NULL PRIMARY KEY,
  sys_from  TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW BEGIN,
  sys_til   TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW END,
  PERIOD SYSTEM_TIME (sys_from, sys_til)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You have to omit &lt;code&gt;WITH SYSTEM VERSIONING&lt;/code&gt;, and you have to explicitly make the period source columns &lt;code&gt;NOT NULL&lt;/code&gt;. Also you say &lt;code&gt;GENERATED ALWAYS AS ROW BEGIN&lt;/code&gt; not &lt;code&gt;GENERATED ALWAYS AS ROW START&lt;/code&gt;. Finally it is &lt;code&gt;PERIOD SYSTEM_TIME&lt;/code&gt; not &lt;code&gt;PERIOD FOR SYSTEM_TIME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sentinel for “forever” is &lt;code&gt;9999-12-30-00.00.00.000000000000&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id="application_time_2"&gt;Application Time&lt;/h2&gt;

&lt;p&gt;DB2 supports many valid-time features—but only if you name the period &lt;code&gt;BUSINESS_TIME&lt;/code&gt;. At IBM, it’s always business time! (I am shamelessly stealing this joke from my audience at PGCon 2019.)&lt;/p&gt;

&lt;p&gt;Valid-time periods have the same syntax quirks as system-time.&lt;/p&gt;

&lt;p&gt;You can define temporal primary keys!&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://www.ibm.com/support/knowledgecenter/en/SSEPEK_10.0.0/intro/src/tpc/db2z_integrity.html"&gt;the docs&lt;/a&gt; you can define temporal foreign keys, but I couldn’t make it work:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; create table t2 (id integer not null, valid_from date not null, valid_til date not null, \
db2 (cont.) =&amp;gt; t_id integer, period business_time (valid_from, valid_til), \
db2 (cont.) =&amp;gt; constraint t2pk primary key (id, business_time without overlaps), \
db2 (cont.) =&amp;gt; constraint tfk foreign key (t_id, period business_time) \
db2 (cont.) =&amp;gt; references t (id, period business_time));
DB21034E  The command was processed as an SQL statement because it was not a 
valid Command Line Processor command.  During SQL processing it returned:
SQL0104N  An unexpected token "business_time" was found following "gn key 
(t_id, period".  Expected tokens may include:  "&amp;lt;space&amp;gt;".  SQLSTATE=42601&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Someome else can’t make it work either, according to &lt;a href="https://web.archive.org/web/20191223062812/https://www.ibm.com/developerworks/community/forums/html/topic?id=440e07ad-23ee-4b0a-ae23-8c747abca819"&gt;this forum thread&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt; failed for me too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; create table t2 (id integer not null, valid_from date not null, valid_til date not null, \
db2 (cont.) =&amp;gt; t_id integer, period business_time (valid_from, valid_til), \
db2 (cont.) =&amp;gt; constraint t2pk primary key (id, business_time without overlaps));
DB20000I  The SQL command completed successfully.
db2 =&amp;gt; alter table t2 add constraint tfk foreign key (t_id, period business_time) \
db2 (cont.) =&amp;gt; references t (id, period business_time);
DB21034E  The command was processed as an SQL statement because it was not a 
valid Command Line Processor command.  During SQL processing it returned:
SQL0104N  An unexpected token "business_time" was found following "gn key 
(t_id, period".  Expected tokens may include:  "&amp;lt;space&amp;gt;".  SQLSTATE=42601&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If I learn a way to make it work, I’ll update this article.&lt;/p&gt;

&lt;h3 id="projecting_3"&gt;Projecting&lt;/h3&gt;

&lt;p&gt;As usual &lt;code&gt;SELECT * FROM t&lt;/code&gt; does not give you the period, and &lt;code&gt;SELECT *, valid_at FROM t&lt;/code&gt; is an error. Periods are not first-class types.&lt;/p&gt;

&lt;h3 id="filtering_3"&gt;Filtering&lt;/h3&gt;

&lt;p&gt;DB2 nicely interprets the standard generously and lets you use the system-time &lt;code&gt;SELECT&lt;/code&gt; syntax for application-time too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t FOR business_time FROM t1 TO t2
SELECT * FROM t FOR business_time AS OF t1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;but not:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t FOR business_time BETWEEN t1 AND t2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I couldn’t get any of the period predicates to work, e.g.:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; select * from t where business_time contains '2015-01-01';
SQL0104N  An unexpected token "contains" was found following "where 
business_time".  Expected tokens may include:  "CONCAT".  SQLSTATE=42601&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I also couldn’t do anything creative with anonymous periods, e.g.:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; select * from t for period(valid_from, valid_til) as of '2015-01-01';
SQL0104N  An unexpected token "period" was found following "select * from t 
for".  Expected tokens may include:  "&amp;lt;space&amp;gt;".  SQLSTATE=42601&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;IBM doesn’t even care if you call it &lt;code&gt;business_time&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; select * from t for period business_time(valid_from, valid_til) as of '2015-01-01';
SQL0104N  An unexpected token "period business_time" was found following 
"select * from t for".  Expected tokens may include:  "&amp;lt;space&amp;gt;".  
SQLSTATE=42601&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That means temporal features are going to break down when used with views, subqueries, CTEs, and set-returning functions. A period is tied to a table, but not a result set.&lt;/p&gt;

&lt;h3 id="dml_3"&gt;DML&lt;/h3&gt;

&lt;p&gt;IBM DML is pretty standard. You can &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; &lt;code&gt;FOR PORTION OF BUSINESS_TIME FROM '2010-06-01' TO '2010-06-15'&lt;/code&gt;. The extra &lt;code&gt;INSERT&lt;/code&gt;s happen as expected.&lt;/p&gt;

&lt;h3 id="triggers_2"&gt;Triggers&lt;/h3&gt;

&lt;p&gt;Like MariaDB, DB2 does call triggers for the derived &lt;code&gt;INSERT&lt;/code&gt;s. Here is some setup to add a row to &lt;code&gt;thist&lt;/code&gt; whenever a trigger gets called:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;create table thist (id integer, old_valid_from date, old_valid_til date, new_valid_from date, new_valid_til date, op char(1));

create trigger tins after insert on t referencing new as new \
for each row insert into thist values \
(NEW.id, null, null, NEW.valid_from, NEW.valid_til, 'i');

create trigger tupd after update on t referencing old as old new as new \
for each row insert into thist values \
(NEW.id, OLD.valid_from, OLD.valid_til, NEW.valid_from, NEW.valid_til, 'u');

create trigger tdel after delete on t referencing old as old \
for each row insert into thist values \
(OLD.id, OLD.valid_from, OLD.valid_til, null, null, 'd');&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If we &lt;code&gt;UPDATE FOR PORTION OF&lt;/code&gt; in the middle of a larger record, our &lt;code&gt;INSERT&lt;/code&gt; trigger is called twice:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;db2 =&amp;gt; update t \
db2 (cont.) =&amp;gt; for portion of business_time \
db2 (cont.) =&amp;gt; from '2015-01-01' to '2016-01-01' \
db2 (cont.) =&amp;gt; set foo = 'bar';
DB20000I  The SQL command completed successfully.
db2 =&amp;gt; select * from t;

ID          VALID_FROM VALID_TIL  FOO       &lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;          1 01/01/2015 01/01/2016 bar       
          1 01/01/2020 01/01/2030 -         
          1 01/01/2010 01/01/2015 -         
          1 01/01/2016 01/01/2020 -         

  4 record(s) selected.

db2 =&amp;gt; select * from thist;

ID          OLD_VALID_FROM OLD_VALID_TIL NEW_VALID_FROM NEW_VALID_TIL OP&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;          1 -              -             01/01/2010     01/01/2015    i 
          1 -              -             01/01/2016     01/01/2020    i 
          1 01/01/2010     01/01/2020    01/01/2015     01/01/2016    u 

  3 record(s) selected.&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="bitemporal_2"&gt;Bitemporal&lt;/h2&gt;

&lt;p&gt;Bitemporal works too!&lt;/p&gt;

&lt;h1 id="oracle"&gt;Oracle&lt;/h1&gt;

&lt;p&gt;For my tests I used Oracle 19c (version 19.3) for Linux and ran it on CentOS 7.&lt;/p&gt;

&lt;h2 id="system_time_3"&gt;System time&lt;/h2&gt;

&lt;p&gt;Oracle has its own way of tracking table history, so it doesn’t bother with SQL:2011 system-time.&lt;/p&gt;

&lt;h2 id="application_time_3"&gt;Application time&lt;/h2&gt;

&lt;p&gt;Oracle lets you declare a &lt;code&gt;PERIOD&lt;/code&gt;, but like MariaDb you can’t define a temporal primary key:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE t (
  id          INTEGER,
  valid_from  DATE,
  valid_til   DATE,
  PERIOD FOR valid_at (valid_from, valid_til),
  -- This next line breaks!:
  CONSTRAINT tpk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Of course that means no foreign keys either.&lt;/p&gt;

&lt;p&gt;One interesting thing is that a &lt;code&gt;PERIOD&lt;/code&gt; doesn’t force your columns to &lt;code&gt;NOT NULL&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; desc t;        
 Name                                      Null?    Type&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt; ID                                                 NUMBER(38)
 VALID_FROM                                         DATE
 VALID_TIL                                          DATE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And that’s because nulls &lt;em&gt;are&lt;/em&gt; allowed in &lt;code&gt;PERIOD&lt;/code&gt;-source columns:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; insert into t values (6, null, null);

1 row created.

SQL&amp;gt; select * from t where id = 6;

        ID VALID_FRO VALID_TIL&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;         6&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="projecting_4"&gt;Projecting&lt;/h3&gt;

&lt;p&gt;When you say &lt;code&gt;SELECT * FROM t&lt;/code&gt; you don’t get the period. You also can’t say this either, but in Oracle’s case it’s a parser error:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT *, valid_at FROM t;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn’t work either:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT *, 1+1 FROM t;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But if you avoid the &lt;code&gt;*&lt;/code&gt; you can select it!:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; SELECT id, valid_from, valid_til, valid_at FROM t;

        ID VALID_FRO VALID_TIL   VALID_AT&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;         1 01-JAN-00 01-JAN-30      33426
         2 01-JAN-10 01-JAN-30      33426
         3 01-JAN-20 01-JAN-30      33426
         4 01-JAN-00 01-JAN-10      33426&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The result doesn’t mean much to me though. Anyone have any ideas?&lt;/p&gt;

&lt;h3 id="filtering_4"&gt;Filtering&lt;/h3&gt;

&lt;p&gt;Like in DB2 you &lt;em&gt;are&lt;/em&gt; able to filter by a valid-time period, although the syntax is a little non-standard (and wordy):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; SELECT * FROM t
  2  AS OF PERIOD FOR valid_at DATE '2005-01-01';

        ID VALID_FRO VALID_TIL&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;         1 01-JAN-00 01-JAN-30
         4 01-JAN-00 01-JAN-10
         6&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Incidentally, you can see here that &lt;code&gt;NULL&lt;/code&gt; in a period means “unbounded”. You can also make just one of the bounds &lt;code&gt;NULL&lt;/code&gt;, and &lt;code&gt;AS OF&lt;/code&gt; queries give the expected results. This is just like Postgres ranges! If Oracle does this for &lt;code&gt;PERIOD&lt;/code&gt;s, perhaps Postgres should too?&lt;/p&gt;

&lt;p&gt;You can use &lt;code&gt;BETWEEN&lt;/code&gt; too, but its syntax is similarly garbled:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; SELECT * FROM t
  2  VERSIONS PERIOD FOR valid_at
  3  BETWEEN DATE '2025-01-01' AND DATE '2035-01-01';

        ID VALID_FRO VALID_TIL&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code&gt;         2 01-JAN-10 01-JAN-30
         1 01-JAN-00 01-JAN-30
         3 01-JAN-20 01-JAN-30
         6&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Anonymous periods don’t seem to work though:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; SELECT * FROM t
  2  AS OF PERIOD FOR (valid_from, valid_til) DATE '2005-01-01';
AS OF PERIOD FOR (valid_from, valid_til) DATE '2005-01-01'
                 *
ERROR at line 2:
ORA-00904: : invalid identifier&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You also can’t use standard period predicates:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; SELECT * FROM t WHERE valid_at CONTAINS DATE '2015-01-01';
SELECT * FROM t WHERE valid_at CONTAINS DATE '2015-01-01'
                               *
ERROR at line 1:
ORA-00920: invalid relational operator


SQL&amp;gt; SELECT * FROM t WHERE valid_at OVERLAPS PERIOD('2015-01-01', '2020-01-01');
SELECT * FROM t WHERE valid_at OVERLAPS PERIOD('2015-01-01', '2020-01-01')
                               *
ERROR at line 1:
ORA-00920: invalid relational operator&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="dml_4"&gt;DML&lt;/h3&gt;

&lt;p&gt;Oracle doesn’t understand &lt;code&gt;FOR PORTION OF&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SQL&amp;gt; UPDATE t FOR PORTION OF valid_at
  2  FROM DATE '2005-01-01' TO DATE '2006-01-01'
  3  SET id = 8 WHERE id = 1;
UPDATE t FOR PORTION OF valid_at
             *
ERROR at line 1:
ORA-00905: missing keyword


SQL&amp;gt; DELETE FROM t FOR PORTION OF valid_at
  2  FROM DATE '2005-01-01' TO DATE '2006-01-01'
  3  WHERE id = 1;
DELETE FROM t FOR PORTION OF valid_at
              *
ERROR at line 1:
ORA-00933: SQL command not properly ended&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="triggers_3"&gt;Triggers&lt;/h3&gt;

&lt;p&gt;In Oracle you can define triggers on tables with a valid-time period, but without temporal DML there are no interesting questions about how they should behave. Nonetheless here are the same triggers as above but in Oracle syntax (in case I ever want to test this in the future):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE thist (
  id INTEGER,
  old_valid_from DATE,
  old_valid_til DATE,
  new_valid_from DATE,
  new_valid_til DATE, op CHAR(1)
);

CREATE TRIGGER tins AFTER INSERT ON t
FOR EACH ROW
BEGIN
INSERT INTO thist VALUES 
(:NEW.id, NULL, NULL, :NEW.valid_from, :NEW.valid_til, 'i');
END;
/

CREATE TRIGGER tupd AFTER UPDATE ON t
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
INSERT INTO thist VALUES
(:NEW.id, :OLD.valid_from, :OLD.valid_til, :NEW.valid_from, :NEW.valid_til, 'u');
END;
/

CREATE TRIGGER tdel AFTER DELETE ON t
REFERENCING OLD AS OLD
FOR EACH ROW
BEGIN
INSERT INTO thist VALUES
(:OLD.id, :OLD.valid_from, :OLD.valid_til, NULL, NULL, 'd');
END;
/&lt;/code&gt;&lt;/pre&gt;

&lt;h1 id="ms_sql_server"&gt;MS SQL Server&lt;/h1&gt;

&lt;p&gt;I tested an evaluation copy of MS SQL Server 2017 (version &lt;code&gt;14.0.1000.169, RTM&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;SQL Server doesn’t support application-time periods at all, just system-time.&lt;/p&gt;

&lt;h2 id="system_time_4"&gt;System Time&lt;/h2&gt;

&lt;p&gt;The syntax for system-time tables is just a little non-standard:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE dbo.t (
  id INTEGER PRIMARY KEY,
  valid_from datetime2 GENERATED ALWAYS AS ROW START,
  valid_til datetime2 GENERATED ALWAYS AS ROW END,
  PERIOD FOR SYSTEM_TIME (valid_from, valid_til)
) WITH (
  SYSTEM_VERSIONING = ON
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note the parens, the underscore, and the &lt;code&gt;= ON&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The history is stored in a separate invisible table with a generated name. But you can query that table like any other, so if you want to give it a nicer name you can:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;WITH (
  SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.thist)
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;valid_til&lt;/code&gt; sentinel will be &lt;code&gt;9999-12-31 23:59:59.9999999&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To ask about a certain time you can say any of these:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT * FROM t FOR SYSTEM_TIME AS OF '2020-01-01';
SELECT * FROM t FOR SYSTEM_TIME BETWEEN '2020-01-01' AND '2030-01-01';
SELECT * FROM t FOR SYSTEM_TIME FROM '2020-01-01' TO '2030-01-01';&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;but not &lt;code&gt;BETWEEN SYMMETRIC&lt;/code&gt;.&lt;/p&gt;

&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;So basically everyone has at least one kind of &lt;code&gt;PERIOD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everyone but Oracle has system-time (and they have another &lt;a href="https://www.oracle.com/database/technologies/flashback/"&gt;older approach&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The only database with temporal primary keys is DB2. They claim to have temporal foreign keys too, but I couldn’t make it work.&lt;/p&gt;

&lt;p&gt;I was pleased that two databases let you select with &lt;code&gt;FOR&lt;/code&gt; and a valid-time period. No one lets you build anonymous periods (in &lt;code&gt;FOR&lt;/code&gt;, &lt;code&gt;FOR PORTION OF&lt;/code&gt;, or elsewhere), and no one supports period predicates.&lt;/p&gt;

&lt;p&gt;With temporal DML, the extra inserts seem to be consistent (between MariaDB and DB2), and both databases fire triggers on them the same way.&lt;/p&gt;

&lt;p&gt;I hope this helps the Postgres community work out their own temporal behavior with respect to the standard. I think it was an interesting study in its own right, too. One thing I learned is that “every other RDBMS supports SQL:2011” is only sort of true, at least as of today. :-)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2017-12-05:/posts/2017/12/temporal-databases-bibliography/</id>
    <title type="html">Temporal Databases Annotated Bibliography</title>
    <published>2017-12-05T00:00:00Z</published>
    <updated>2017-12-05T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2017/12/temporal-databases-bibliography/" type="text/html"/>
    <content type="html">
&lt;p&gt;I’ve been reading about temporal databases for a few years now, so I think it’s time I share my bibliography and notes. This is presented in “narrative order”, so that you can get a sense of how the research has developed. This article somewhat overlaps a mini literature review I wrote on the &lt;a href="https://www.mail-archive.com/pgsql-hackers@postgresql.org/msg324631.html"&gt;Postgres hackers mailing list&lt;/a&gt;, but this article is more complete and in a place where I can keep it updated.&lt;/p&gt;

&lt;p&gt;Temporal databases let you track the history of things over time: both the history of changes to the database (e.g. for auditing) and the history of the thing itself. They are not the same thing as time-series databases: whereas a time-series database has time-stamped &lt;em&gt;events&lt;/em&gt;, a temporal database stores the history of &lt;em&gt;things&lt;/em&gt;, typically by adding a start/end time to each row (so two timestamps, not one). With time-series the challenge is typically scale; with temporal the challenge is with complexity and correctness.&lt;/p&gt;

&lt;h2 id="research"&gt;Research&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Abdullah Tansel, James Clifford, Shashi Gadia, Sushil Jajodia, Arie Segev, and Richard T. Snodgrass (editors). &lt;em&gt;Temporal Databases: Theory, Design, and Implementation.&lt;/em&gt; 1993.&lt;/strong&gt; I only became aware of this in 2023. I own a copy now but haven’t read it cover to cover. I’m curious to see the variety of ideas back then, before things got standardized. It’s amazing how many old ideas are just forgotten. Codd’s &lt;a href="https://dl.acm.org/doi/10.1145/320107.320109"&gt;“Extending the data base relational model to capture more meaning”&lt;/a&gt; from 1979 is famous for introducing &lt;code&gt;NULL&lt;/code&gt; and outer joins, but no one seems to remember the second half, with graph databases and queries that dynamically build themselves by querying the system catalog. So I bet this book has creative ideas that could inspire modern developments.&lt;/p&gt;

&lt;p&gt;Already this book answered one question I had. Can a planner reorder/transform temporal operations the same way it can non-temporal ones? In other words, do they have the same algebraic properties? On page 175 in the chapter on TQuel I found this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Theorem 12&lt;/strong&gt; The following equivalences hold for the valid-time algebra.&lt;/p&gt;

&lt;p&gt;Q ∪̂ R ≡ R ∪̂ Q&lt;/p&gt;

&lt;p&gt;Q ⨯̂ R ≡ R ⨯̂ Q&lt;/p&gt;

&lt;p&gt;σ̂&lt;sub&gt;F&lt;sub&gt;1&lt;/sub&gt;&lt;/sub&gt;(σ̂&lt;sub&gt;F&lt;sub&gt;2&lt;/sub&gt;&lt;/sub&gt;(Q)) ≡ σ̂&lt;sub&gt;F&lt;sub&gt;2&lt;/sub&gt;&lt;/sub&gt;(σ̂&lt;sub&gt;F&lt;sub&gt;1&lt;/sub&gt;&lt;/sub&gt;(Q))&lt;/p&gt;

&lt;p&gt;Q ∪̂ (R ∪̂ S) ≡ (Q ∪̂ R) ∪̂ S&lt;/p&gt;

&lt;p&gt;Q ⨯̂ (R ⨯̂ S) ≡ (Q ⨯̂ R) ⨯̂ S&lt;/p&gt;

&lt;p&gt;Q ⨯̂ (R ∪̂ S) ≡ (Q ⨯̂ R) ∪̂ (Q ⨯̂ S)&lt;/p&gt;

&lt;p&gt;σ̂&lt;sub&gt;F&lt;/sub&gt;(Q ∪̂ R) ≡ σ̂&lt;sub&gt;F&lt;/sub&gt;(Q) ∪̂ σ̂&lt;sub&gt;F&lt;/sub&gt;(R)&lt;/p&gt;

&lt;p&gt;σ̂&lt;sub&gt;F&lt;/sub&gt;(Q −̂ R) ≡ σ̂&lt;sub&gt;F&lt;/sub&gt;(Q) −̂ σ̂&lt;sub&gt;F&lt;/sub&gt;(R)&lt;/p&gt;

&lt;p&gt;π̂&lt;sub&gt;X&lt;/sub&gt;(Q ∪̂ R) ≡ π̂&lt;sub&gt;X&lt;/sub&gt;(Q) ∪̂ π̂&lt;sub&gt;X&lt;/sub&gt;(R)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theorem 13&lt;/strong&gt; The distributive property of Cartesian product over difference, or Q ⨯̂ (R −̂ S) ≡ (Q ⨯̂ R) −̂ (Q ⨯̂ S), does &lt;em&gt;not&lt;/em&gt; hold for the valid-time algebra.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I discuss this in more depth in &lt;a href="https://illuminatedcomputing.com/posts/2026/07/tquel-paper/"&gt;“TQuel Paper: Implementing Temporal Operators in Postgres”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snodgrass, Richard T. &lt;em&gt;Developing Time-Oriented Database Applications in SQL&lt;/em&gt;. 1999.&lt;/strong&gt; The seminal work on temporal databases and still the most useful introduction I know. Covers the “combinatorial explosion” of non-temporal/state-temporal/system-temporal/bi-temporal tables, current/sequenced/non-sequenced queries, &lt;code&gt;SELECT&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;, different RDBMS vendors, etc. Very similar to the proposed TSQL2 standard that was ultimately not accepted but still influenced &lt;a href="https://info.teradata.com/htmlpubs/DB_TTU_16_00/index.html#page/SQL_Reference%2FB035-1186-160K%2Fjst1472240640825.html%23"&gt;Teradata’s temporal support&lt;/a&gt;. Available as a free PDF from &lt;a href="https://www2.cs.arizona.edu/~rts/publications.html"&gt;his website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hugh Darwen and C. J. Date. “An Overview and Analysis of Proposals Based on the TSQL2 Approach.”&lt;/strong&gt; Latest draft 2005, but originally written earlier. Criticizes the TSQL2 proposal’s use of “statement modifiers”, especially their problems with composability when a view/subquery/CTE/function returns a temporal result. Available &lt;a href="https://web.archive.org/web/20240728071358/https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.116.7598"&gt;as a PDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ralph Kimball and Margy Ross. &lt;em&gt;The Data Warehouse Toolkit&lt;/em&gt;. 3rd edition, 2013.&lt;/strong&gt; (2nd edition 2002, 1st 1996.) (My notes are based on reading the 2nd edition, but I don’t think there are major relevant changes.) This book is not about temporal databases per se, but in Chapter 4 (and scattered around elsewhere) he talks about dealing with data that changes over time (“Slowly Changing Dimensions”). His first suggestion (Type 1) is to ignore the problem and overwrite old data with new. His Type 2 approach (make a new row) is better but loses the continuity between the old row and the new. Type 3 fixes that but supports only one change, not several. This writing is evidence for the need to handle temporal data, and the contortions that result from not having a systematic approach. (These criticisms and the realization that Kimball is trying to solve the same problems as temporal databases come from Johnston’s second book below; I’m glad he made the connection between Kimball and temporal databases!) (&lt;a href="http://www.dsc.ufcg.edu.br/~sampaio/Livros/alph%20Kimball.%20The%20Data%20Warehouse%20Toolkit..%20The%20Complete%20Guide%20to%20Dimensional%20Modelling%20%28Wiley%2C2002%29%28ISBN%200471200247%29%28449s%29.pdf"&gt;pdf&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C. J. Date, Hugh Darwen, Nikos Lorentzos. &lt;em&gt;Time and Relational Theory, Second Edition: Temporal Databases in the Relational Model and SQL&lt;/em&gt;. 2nd edition, 2014.&lt;/strong&gt; (First edition published in 2002.) I read this in March 2018. If you’ve read Date &amp;amp; Darwen’s other works you know they are quite rigorous, often critical of SQL’s compromises vs the pure relational model (e.g. &lt;code&gt;NULL&lt;/code&gt; and non-distinct results), and not always very practical. Nonetheless this book is full of great ideas, and I hope anyone thinking about implementing temporal features in an RDBMS product will read it. They mostly deal with state-time, not transaction-time, which is great since that’s where the field most needs attention, although they also show how to apply their ideas to transaction-time tables too.&lt;/p&gt;

&lt;p&gt;The core idea is that a temporal table should really have a row for each second (or millisecond or day or whatever is your finest granularity of time), saying what was true at that moment. Then the primary key is just the second plus the regular primary key. This idea isn’t new, but they use it to define everything else in the book. This makes thinking about temporal queries a lot easier, so that if you ever have any hesitation about something, you can think about it in terms of one-row-per-second and it starts to be easy.&lt;/p&gt;

&lt;p&gt;Of course no database could adopt that in practice, so they offer operators to transform rows with start/end ranges to one-row-per-second and back again. In almost every case you don’t have to use those operators, but they are a solid theoretical basis for the next step: defining interval-aware primary keys, foreign keys, and all the relational operators. Finally they show how you implement things without “materializing” the expanded version of the data.&lt;/p&gt;

&lt;p&gt;Another idea implicit in that is that temporal concerns belong in the SQL &lt;em&gt;operators&lt;/em&gt;, not as “statement modifiers” like in TSQL2. Really that might be the most important idea in the whole book, so I’m glad to see it being applied to Postgres in the papers by Dignös et al (below).&lt;/p&gt;

&lt;p&gt;They also argue (vs SQL:2011) that a single interval column type is better than separate start/end columns, which I agree with. Strangely they go against all the existing research by using closed/closed intervals (e.g. &lt;code&gt;[Jan2017,Dec2017]&lt;/code&gt;) instead of closed/open (&lt;code&gt;[Jan2017,Jan2018)&lt;/code&gt;), without really giving much justification. They also avoid using &lt;code&gt;NULL&lt;/code&gt; for an unbounded side (e.g. &lt;code&gt;[Jan2017,NULL)&lt;/code&gt;), preferring a magic “end of time” date (&lt;code&gt;[Jan2017,Jan3000)&lt;/code&gt;). I wasn’t surprised by the second decision, given the authors’ history, but the first was less explicable. In both cases their approach sadly marginalizes their work and imposes barriers to adopting it in real SQL products.&lt;/p&gt;

&lt;p&gt;I really appreciate how these authors have insisted that valid time (and the same with transaction time) should be a regular column, not something different. That was a big part of their complaint against TSQL2. To be honest when I first read Snodgrass the idea of using pseudo-columns seemed so suboptimal it was hard to take seriously (“Of course they don’t mean it.”), so I’m glad these authors have insisted on pointing out that approach’s shortcomings. Unfortunately in SQL:2011 valid-time is still a pseudo-column made up of two regular date/time columns. The problem is in composability: using a temporal query as a subquery, view, or function result: it all works cleanly if the interval is just another input to your interval-aware operators, but not if you need some kind of extra pseudo-column metadata. I hope implementers will take their advice seriously and not build temporal features on such a distorting idea.&lt;/p&gt;

&lt;p&gt;Something I disagreed with was their suggestion to use tables in sixth-normal form—basically every column gets its own table—since attributes can have different lifespans. I can see how that is purer, but it seems like just too much complexity. They probably suspected the same because they always show how to do things with either that approach or tables in a more traditional third-normal form (or &lt;a href="https://en.wikipedia.org/wiki/Boyce%E2%80%93Codd_normal_form"&gt;BCNF&lt;/a&gt; if you prefer). Even that is slightly distorted in order to avoid &lt;code&gt;NULL&lt;/code&gt;s, but you can easily look past that.&lt;/p&gt;

&lt;p&gt;Finally, I appreciated that on page 282 they mention DDL on temporal databases. Like everyone they say it’s beyond the scope of their current discussion, but it’s a penetrating insight to say, “the database catalog might itself need to be treated as a temporal database.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;SQL:2011 Draft standard&lt;/em&gt;.&lt;/strong&gt; (&lt;a href="https://web.archive.org/web/20160402092111/http://jtc1sc32.org/doc/N1951-2000/32N1964T-text_for_ballot-FCD_9075-2.pdf"&gt;pdf&lt;/a&gt;) Personally I find the standard pretty disappointing. It uses separate start/end columns instead of built-in range types, although range types offer benefits like exclusion constraints and convenient operators for things like “overlaps” that are verbose to code correctly by hand. It only mentions inner joins, not the various outer joins, semi-joins (&lt;code&gt;EXISTS&lt;/code&gt;), anti-joins (&lt;code&gt;NOT EXISTS&lt;/code&gt;), or aggregates. Many of its features apply only to system-time, not application-time, even though applicaion-time is the more interesting and less-available feature. (There are lots of auditing add-ons, but almost nothing for tracking the history of things.) The syntax seems too specific, lacking appropriate generality. A lot of these drawbacks seem motivated by a goal that goes back to TSQL2: to let people add temporal support to old tables without breaking any existing queries. That has always seemed to me like an unlikely possibility, and an unfortunate source of distortions. I don’t expect something for free, and I don’t mind doing work to migrate a table to a temporal format, as long as the result is good. Instead we get an (ostensible) one-time benefit for a prolonged compromise in functionality and ease-of-use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Krishna Kulkarni and Jan-Eike Michels. “Temporal Features in SQL:2011”. &lt;em&gt;SIGMOD Record&lt;/em&gt;, September 2012.&lt;/strong&gt; Nice overview of the temporal features included in the SQL:2011 standard. &lt;a href="https://sigmodrecord.org/publications/sigmodRecord/1209/pdfs/07.industry.kulkarni.pdf"&gt;Here is a PDF&lt;/a&gt; of the paper. See also &lt;a href="https://web.archive.org/web/20230524033629/http://metadata-standards.org/Document-library/Documents-by-number/WG2-N1501-N1550/WG2_N1536_koa046-Temporal-features-in-SQL-standard.pdf"&gt;these slides by Kulkani&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peter Vanroose. “Temporal Data &amp;amp; Time Travel in PostgreSQL,” FOSDEM 2015.&lt;/strong&gt; (&lt;a href="https://wiki.postgresql.org/images/6/64/Fosdem20150130PostgresqlTemporal.pdf"&gt;Slides as a pdf&lt;/a&gt;) Lots of good detail here about SQL:2011. I’d love to see a recording of this talk if it’s available, but I haven’t found it yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tom Johnston and Randall Weis. &lt;em&gt;Managing Time in Relational Databases: How to Design, Update and Query Temporal Data&lt;/em&gt;. 2010.&lt;/strong&gt; I finished this in May/June 2019. Although it concentrates on the authors’ proprietary middleware add-on to MS SQL Server called Asserted Versioning, it nonetheless has many ideas for building practical temporal features on any RDBMS. I liked their creativity and willingness to improve on SQL:2011 when they could. (Technically their book predates the final form of the standard.) Their middleware approach did sometimes lead them to behavior that would be strange in a more SQL-oriented solution. For example their temporal updates and deletes can only target a single record by ID (but they also mention they have a fix for this), and if that ID is not found you get an error. In ordinary SQL you would not get an error if an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; didn’t change any rows, and on the other hand you could hit more rows than one by not using the primary key. They mention that at the time of writing they were trying to replace their middleware approach with an implemention using views and instead-of triggers (something that has been done elsewhere too), and I expect that would wind up behaving more like ordinary SQL. They also introduce an interesting idea of future-dating records’ system-time (“assertion-time” in their lingo) so that the database will not make them “effective” until you’re ready. This has some nice practical use-cases, but there are pretty serious drawbacks. In their system a future-dated record is essentially immutable, so you had better not change your mind! Despite this book’s many contributions, I think everything here was also covered in Johnston’s second book, which is probably what I’d read if I had to choose one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tom Johnston. &lt;em&gt;Bitemporal Data: Theory and Practice&lt;/em&gt;. 2014.&lt;/strong&gt; I felt like I found a kindred soul when I read how he connects database design and ontology, as I’ve always thought of programming as “applied philosophy.” Databases as Aristotelian propositional logic is inseparable from the mathematical set-based theory. Johnston gives helpful distinctions between the physical rows in the table, the assertions they represent, and the things themselves. Eventually this leads to a grand vision of connecting every assertion’s bitemporal (or tritemporal) history to its speaker, somewhat like some ideas in the Semantic Web, although this doesn’t sound very practical. Like Date he seems to be landing on something like sixth-normal form, with a view-like presentation layer to bring all the attributes back together again. (Also like Date he seems to acknowledge 6NF may not be practical.) He points out how unsatisfactory Kimball’s suggestions are. He also criticizes the limitations of SQL:2011 and offers some amendments to make it more useful. Describes a (patented) idea of “episodes” to optimize certain temporal queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anton Dignös, Michael H. Böhlen, and Johann Gamper. “Temporal Alignment”, SIGMOD ’12.&lt;/strong&gt; Amazing! Shows how to define temporal versions of every relational operator by a combination of the traditional operators and just two simple transforms, which they call “align” and “split”. Gives a very readable exposition of the new theory and then describes how they patched Postgres 9.0 and benchmarked the performance. I think this solves the composability problems Date objected to in TSQL2, and unlike SQL:2011 it is general and comprehensive. The focus is on state-time, and I’m not sure how it will map onto bi-temporal, but even just having good state-time functionality would be tremendous. And the paper is only 12 easy-to-read pages! (&lt;a href="https://files.ifi.uzh.ch/boehlen/Papers/modf174-dignoes.pdf"&gt;pdf&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anton Dignös, Michael Hanspeter Böhlen, Johann Gamper, and Christian S. Jensen. “Extending the kernal of a relational DBMS with comprehensive support for sequenced temporal queries,” &lt;em&gt;ACM Transactions on Database Systems&lt;/em&gt;, 41(4):1-46.&lt;/strong&gt; Continues the previous paper but adds support for scaling the inputs to aggregate groups according to how much of their time period goes into each group. Gives more benchmarks against a patched Postgres 9.5. (&lt;a href="http://www.zora.uzh.ch/id/eprint/130374/1/Extending_the_kernel.pdf"&gt;pdf&lt;/a&gt;) These researchers are now trying to contribute their work to the Postgres core project, of which I am very much in favor. :-)&lt;/p&gt;

&lt;h2 id="tools"&gt;Tools&lt;/h2&gt;

&lt;p&gt;Finally here are some tools for temporal support in Postgres. The sad theme is that pretty much everything gives audit support but not history support:&lt;/p&gt;

&lt;h3 id="postgres"&gt;Postgres&lt;/h3&gt;

&lt;p&gt;The most complete implementation of bi-temporal tables is Vik Fearing’s &lt;a href="https://github.com/xocolatl/periods"&gt;periods extension&lt;/a&gt;. Since Postgres uses a bison grammar, an extension can’t add new syntax, but this one does a great job working around that limitation to provide almost everything from the SQL:2011 standard.&lt;/p&gt;

&lt;p&gt;Henrietta (Hettie) Dombrovskaya has a &lt;a href="https://github.com/hettie-d/pg_bitemporal"&gt;&lt;code&gt;pg_temporal&lt;/code&gt; repository&lt;/a&gt; with talks, slides, and code. I haven’t tried building it but I believe her company is using this in production. She is an awesome speaker, so her talks are great intros to this topic too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pgaudit/pgaudit"&gt;The pgaudit extension&lt;/a&gt; looks pretty useful but I haven’t tried it yet. According to the AWS docs you can even use this &lt;a href="http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html"&gt;on RDS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vlad Arkhipov’s &lt;a href="https://pgxn.org/dist/temporal_tables/"&gt;temporal tables extension&lt;/a&gt; only supports system-time (auditing). Also &lt;a href="https://github.com/arkhipov/temporal_tables"&gt;on Github&lt;/a&gt; and &lt;a href="http://clarkdave.net/2015/02/historical-records-with-postgresql-and-temporal-tables-and-sql-2011/"&gt;a nice writeup by Clark Dave&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Magnus Hagander presented an approach to capturing system-time history in a separate schema at PGConf US 2015 and PGDay’15 Russia. Here are &lt;a href="https://www.hagander.net/talks/tardis_orm.pdf"&gt;slides&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=TRgni5q0YM8"&gt;video&lt;/a&gt;. Quite elegant if you want to ask questions like “what did we think as of time t?” If I recall correctly this is similar to one of the ideas proposed at the end of Snodgrass, although I haven’t compared them carefully. Hagander points out that DDL changes against temporal databases are challenging and hopefully infrequent. This is a topic that is almost completely absent from the literature, except for a brief mention in Johnston 2014.&lt;/p&gt;

&lt;p&gt;I’ve heard about a &lt;a href="https://github.com/Actuarial-Sciences-for-Africa-ASA/BitemporalPostgres.jl"&gt;Julia project for temporal data in Postgres&lt;/a&gt;. I’m not sure how it works, but it looks interesting!&lt;/p&gt;

&lt;h3 id="ruby"&gt;Ruby&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/ifad/chronomodel"&gt;Chronomodel&lt;/a&gt; extends the ActiveRecord ORM to record system-time history. The &lt;a href="https://github.com/Casecommons/pg_audit_log"&gt;&lt;code&gt;pg_audit_log&lt;/code&gt; gem&lt;/a&gt; works fine but like many audit solutions is rather write-only. I wouldn’t want to build any functionality that has to query its tables to reconstruct history. You could also try &lt;a href="https://github.com/airblade/paper_trail"&gt;&lt;code&gt;paper_trail&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/collectiveidea/audited"&gt;&lt;code&gt;audited&lt;/code&gt; (formerly &lt;code&gt;acts_as_audited&lt;/code&gt;)&lt;/a&gt;. Of these projects only Chronomodel seems to be aware of temporal database research.&lt;/p&gt;

&lt;h2 id="further_research"&gt;Further Research&lt;/h2&gt;

&lt;p&gt;Temporal databases are exciting because there is still so much to do. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;What should the UI look like? Even one dimension adds a lot of complexity, let alone bi-temporal. How do you present this to users? As usual an audit history is easier, and it’s possible to find existing examples, whereas a state-time history is more challenging but probably more valuable. How should we let people view and edit the history of something? How does it work if there is a “Save” button vs save-as-you-type?&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;What does “full stack” temporal support look like? Do we extend REST? What would be a nice ORM interface? Should we use triggers to hide the temporal behavior behind regular-looking SQL? Or maybe extend SQL so you can more explicitly say what you want to do?&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; support for “as of” semantics or “over time” semantics.&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;Temporal foreign keys. I have &lt;a href="https://github.com/pjungwir/time_for_keys"&gt;a POC Postgres extension&lt;/a&gt; and &lt;a href="https://commitfest.postgresql.org/45/4308/"&gt;am making progress adding it to core&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;DDL changes. For example if you want to add a &lt;code&gt;NOT NULL&lt;/code&gt; column, what do you do with the old data? Could there be built-in support to apply constraints only to a given time span?&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;Future time. There are some cool ideas about this in Johnson’s books, and I’ve heard of fintech folks doing something here.&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;Temporal upsert. This would be an &lt;code&gt;INSERT&lt;/code&gt; that falls back to &lt;code&gt;FOR PORTION OF&lt;/code&gt; for spans that already exist. Quite often this is what an application really wants. Johnson talks about how the standard should have included something here.&lt;/p&gt;
&lt;/li&gt;

&lt;li&gt;
&lt;p&gt;Outer joins and aggregates. Dignös et al show how to do this, but it’s not in the standard. I’d love to get their old patch working again and try to get something similar into Postgres, once the basics are finished. I’ve written &lt;a href="https://github.com/pjungwir/temporal_ops"&gt;SQL implementations&lt;/a&gt; of temporal outer join, semi-join, anti-join, &lt;code&gt;UNION&lt;/code&gt;, &lt;code&gt;EXCEPT&lt;/code&gt;, and &lt;code&gt;INTERSECT&lt;/code&gt;, but those won’t perform as well as built-in RDBMS support. If Postgres gets &lt;a href="https://commitfest.postgresql.org/patch/5083/"&gt;inlining PL/pgSQL functions&lt;/a&gt; it will be faster. Even better would be a C implementation using &lt;code&gt;CustomScan&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2017-11-08:/posts/2017/11/javascript-timezones/</id>
    <title type="html">Javascript Daylight Savings Time: One Weird Trick Your Application Hates</title>
    <published>2017-11-08T00:00:00Z</published>
    <updated>2017-11-08T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2017/11/javascript-timezones/" type="text/html"/>
    <content type="html">
&lt;p&gt;I’ve talked in the past about how to handle &lt;a href="https://illuminatedcomputing.com/posts/2014/04/timezones/"&gt;timezones in Rails&lt;/a&gt;, so here is a tip for handling timezones in Javascript, in particular around Daylight Savings Time.&lt;/p&gt;

&lt;p&gt;Suppose you have a time: April 3, 2017, at midnight Pacific Time. You want to express it as UTC in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString"&gt;ISO 8601 format&lt;/a&gt;, for instance to send it over the wire as JSON. The result is &lt;code&gt;"2017-04-03T07:00:00.000Z"&lt;/code&gt;. Note the 07:00. Pacific Time is -8 hours from UTC during Standard Time, and -7 hours during Daylight Savings Time. April 3 falls in Daylight Savings Time.&lt;/p&gt;

&lt;p&gt;Now suppose we change the year: April 3, &lt;strong&gt;1969&lt;/strong&gt;, still at midnight Pacific Time. DST started later that year, so now the answer is &lt;code&gt;"1969-04-03T08:00:00.000Z"&lt;/code&gt;. But if we run &lt;code&gt;new Date(1969, 3, 3).toISOString()&lt;/code&gt; your browser gives us: &lt;code&gt;&lt;script&gt;document.write(new Date(1969, 3, 3).toISOString());&lt;/script&gt;&lt;/code&gt;. That might look correct, or you might see a 07:00 again.&lt;/p&gt;

&lt;p&gt;Believe it or not, the original Javascript specification said that browsers should use the &lt;strong&gt;current year’s&lt;/strong&gt; Daylight Savings Time transition dates when building dates from any year. If you just re-read that sentence in disbelief and still think it is too crazy to be real, here is &lt;a href="https://stackoverflow.com/questions/16946002/javascript-time-zone-is-wrong-for-past-daylight-saving-time-transition-rules"&gt;a conversation with links to the old and new spec&lt;/a&gt;. I think it’s crazy too!&lt;/p&gt;

&lt;p&gt;Right now, some browsers do the right thing (ignore the old spec), some do the wrong thing (follow the old spec), and it also depends on what version you’re running. It even seems to depend on what year you’re asking about. For instance modern Chrome seems to give me the right answers back to 1970, but then is wrong before that. Also, even if your browser does the wrong thing, you might still get lucky based on the current year and the date you’re trying to build. I wrote &lt;a href="http://jsbin.com/quqajehaqu/edit?html,js,output"&gt;a jsbin page&lt;/a&gt; you can load in multiple browsers to see if they agree.&lt;/p&gt;

&lt;p&gt;I think the only safe answer is to use &lt;a href="https://momentjs.com/timezone/"&gt;moment-timezone&lt;/a&gt; to build your dates. For instance if you know the timezone:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;moment.tz([y, m, d], tz)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or if you don’t:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;moment.tz([y, m, d], moment.tz.guess())&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(And don’t forget the &lt;code&gt;m&lt;/code&gt; is off by one.)&lt;/p&gt;

&lt;p&gt;If you need to force that back into a regular &lt;code&gt;Date&lt;/code&gt; object, you could do:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-javascript"&gt;&lt;span class="keyword"&gt;new&lt;/span&gt; Date(moment.tz([y, m, d], tz).toJSON())&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Just make sure that you’re using &lt;code&gt;moment-timezone-with-data.js&lt;/code&gt;, not plain &lt;code&gt;moment-timezone.js&lt;/code&gt;, or you’ll still be relying on the browser’s own idiosyncratic behavior.&lt;/p&gt;

&lt;p&gt;I hope this is helpful to someone. If your users enter birthdays with some kind of date picker, you probably suffer from this bug!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2014-04-18:/posts/2014/04/timezones/</id>
    <title type="html">Dates and Time Zones in Rails</title>
    <published>2014-04-18T00:00:00Z</published>
    <updated>2014-04-18T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2014/04/timezones/" type="text/html"/>
    <content type="html">
&lt;p&gt;I work on a video education app that needs to report how many lessons were assigned each day. I have a timetracking app that needs to invoice based on when one month starts and another ends. And I’ve got another app for watching cronjobs, that counts how many failures happened in a given day. All these apps share a problem: if they get time zones wrong, they will give incorrect results. For instance, if a video lesson was assigned on Tuesday after 5pm PDT, its UTC time will be Wednesday. Even though all three apps only show dates, not times, time zones still matter. So here are some lessons I’ve learned dealing with Rails time zones, both programming myself and leading a team.&lt;/p&gt;

&lt;p&gt;But first, here are two articles that lay some groundwork:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://danilenko.org/2012/7/6/rails_timezones/"&gt;The Exhaustive Guide to Rails Time Zones&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://web.archive.org/web/20160619104315/http://www.elabs.se:80/blog/36-working-with-time-zones-in-ruby-on-rails"&gt;Working with time zones in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both those posts describe some nice methods provided by ActiveSupport/Rails, as well as why you’d want to use them instead of built-in Ruby methods. For instance, saying &lt;code&gt;Time.now&lt;/code&gt; will get you the time in your OS’s default time zone, whereas &lt;code&gt;Time.zone.now&lt;/code&gt; will get you the time in whatever timezone you put in &lt;code&gt;config/application.rb&lt;/code&gt;. It’s a good idea to use the Rails timezone, since you have more control over that and it’s part of your source control.&lt;/p&gt;

&lt;p&gt;If you want to explore, take a look at &lt;code&gt;ActiveSupport::TimeZone&lt;/code&gt;. You can get a specific time zone like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tz = &lt;span class="constant"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="constant"&gt;TimeZone&lt;/span&gt;[&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Pacific Time (US &amp;amp; Canada)&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;tz&lt;/code&gt; will be the start of just about everything you do with time. An &lt;code&gt;ActiveSupport::TimeZone&lt;/code&gt; is also what you get from &lt;code&gt;Time.zone&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those other articles are great, but this post talks about time zones from a higher level, aspiring to share best practices rather than helpful methods. In my examples I’ll assume a Postgres database, although there’s nothing here that won’t work on MySQL as well.&lt;/p&gt;

&lt;h2 id="scope_your_time_zones"&gt;Scope Your Time Zones&lt;/h2&gt;

&lt;p&gt;The first thing to remember is that all times are relative to somebody. Probably that’s a user, but maybe it’s something else. For my video education app, sometimes it’s a teacher and sometimes it’s a student. For my cronjob app, it’s a job (and sometimes a user). But there is always a reference point. In that sense, the articles above that recommend &lt;code&gt;Time.zone.now&lt;/code&gt; are wrong. &lt;code&gt;Time.zone.now&lt;/code&gt; is no better than &lt;code&gt;Time.now&lt;/code&gt; if you need many time zones—and who doesn’t? So it should really be &lt;code&gt;user.time_zone.now&lt;/code&gt;. I’d recommend adding this method to your &lt;code&gt;User&lt;/code&gt; class right from the beginning, so that even if you haven’t yet implemented user-specific time zones, you are still writing the rest of your app correctly:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="keyword"&gt;def&lt;/span&gt; &lt;span class="function"&gt;time_zone&lt;/span&gt;
  &lt;span class="constant"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="constant"&gt;TimeZone&lt;/span&gt;[&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Pacific Time (US &amp;amp; Canada)&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;]
&lt;span class="keyword"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="ignore_time_zones_as_much_as_possible"&gt;Ignore Time Zones as Much as Possible&lt;/h2&gt;

&lt;p&gt;Now that you’ve got time zones everwhere, the next step is to get rid of them. Thinking about time zones is hard, and finding time zone bugs is hard. You want to avoid them as much as possible!&lt;/p&gt;

&lt;p&gt;It’s been said that in C, if you start thinking about big endian vs little endian, you’re probably doing things wrong. Oftentimes when programmers half-understand something, they do more than they should, and that’s very true with time zones. Your code will be a lot easier to understand if you tackle time zones with a few well-placed strokes, rather than lots of fiddling all over the place.&lt;/p&gt;

&lt;p&gt;One thing to remember is that 07:05:00 PDT and 14:05:00 UTC are the &lt;em&gt;same instant&lt;/em&gt;. If you converted both to seconds since the epoch, you’d get the same number. So in that sense, changing the time zone doesn’t change anything: it’s just a bit of extra metadata hanging onto the time representing someone’s perspective. Knowing that x PDT and y UTC are the same instant is really helpful when you feel the urge to fiddle with timezones. Is your fiddling just a noop?&lt;/p&gt;

&lt;p&gt;By default, Rails comes configured with its default time zone as UTC. Leave it that way! Since every time needs a reference point anyway, your code shouldn’t care about the Rails-wide setting. UTC is a good neutral choice. For one thing, it doesn’t have daylight savings time. And if you see it, you know you’re dealing with a time-zone-less value.&lt;/p&gt;

&lt;p&gt;You should also leave your OS time zone as UTC, if possible. Keeping it consistent with Rails will remove one chance for abiguiuty. And again, it’s a good neutral.&lt;/p&gt;

&lt;p&gt;You also want UTC in your database. If you use a migration to create columns with &lt;code&gt;t.timestamps&lt;/code&gt; or &lt;code&gt;t.datetime :foo&lt;/code&gt;, Rails will make a &lt;code&gt;TIMESTAMP WITHOUT TIME ZONE&lt;/code&gt; column. You can think of this as a time in UTC if you like. Really it’s an int of (micro)seconds since the epoch. Whenever you give a time to ActiveRecord, it will convert it to UTC before it hits Postgres. Or more correctly, it will strip off the timezone part and give Postgres the int. Remember, it’s the same instant! But it’s nice to imagine the column as UTC. If you’re in psql and type &lt;code&gt;SELECT created_at FROM lessons&lt;/code&gt;, that’s what you’re seeing.&lt;/p&gt;

&lt;p&gt;When everything in your stack is UTC, it’s like looking through nice clear glass. You don’t have to think about conversions at each layer.&lt;/p&gt;

&lt;p&gt;You should also strive to make your app code as time-zone-less as possible. The big principle here is to handle time zones once, hopefully at the beginning of the HTTP request, when you decide the correct reference point for all times. Usually that’s &lt;code&gt;current_user.time_zone&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second article above suggests you add this to your controller:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;around_filter &lt;span class="symbol"&gt;:user_time_zone&lt;/span&gt;, &lt;span class="key"&gt;if&lt;/span&gt;: &lt;span class="symbol"&gt;:current_user&lt;/span&gt;

&lt;span class="keyword"&gt;def&lt;/span&gt; &lt;span class="function"&gt;user_time_zone&lt;/span&gt;(&amp;amp;block)
  &lt;span class="constant"&gt;Time&lt;/span&gt;.use_zone(current_user.time_zone, &amp;amp;block)
&lt;span class="keyword"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That makes me pretty uncomfortable. The idea is that you can say &lt;code&gt;Time.zone&lt;/code&gt; everywhere else in your app and always get the current user’s time zone. But I’d rather be explicit about where the time zone is coming from. (Also see below for why &lt;code&gt;use_time&lt;/code&gt; doesn’t help at all for &lt;em&gt;parsing&lt;/em&gt; times.) Instead, write code to take a &lt;code&gt;tz&lt;/code&gt; argument if necessary (emphasis on the if necessary). This will make your code less surprising, less coupled, and easier to test.&lt;/p&gt;

&lt;p&gt;Even better is to write your code to take just a time. Almost always that’s really what you want. Remember that no matter the time zone, it’s all the same instant. Usually a single time is sufficient, because it can be a reference point for creating other times, using &lt;code&gt;t + 1.day&lt;/code&gt; or &lt;code&gt;t + 2.weeks&lt;/code&gt; or whatever. Use a (user|cronjob|foo)-scoped time zone to get your first time, and then forget about time zones for the rest of the stack. If you implemented the &lt;code&gt;User#time_zone&lt;/code&gt; method above, your time zone code is already well-encapsulated, so there’s no need for an &lt;code&gt;around_filter&lt;/code&gt; to further abbreviate things.&lt;/p&gt;

&lt;p&gt;Here is another approach I don’t like. This &lt;a href="https://web.archive.org/web/20150308140507/http://icu.iorahealth.com:80/blog/2012/05/07/expressing-postgresql-timestamps-without-zones-in-local-timee/"&gt;blog post&lt;/a&gt; suggests you deal with time zones in Postgres like so:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT created_at AT TIME ZONE 'UTC' AT TIME ZONE 'US/Pacific'&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Does that look strange to you? What’s happening is that you start with a &lt;code&gt;TIMESTAMP WITHOUT TIME ZONE&lt;/code&gt;, so first you tack on a time zone (just metadata), then you convert it to Pacific time. In other words, you’re doing this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;time without zone -&amp;gt; assumed to be UTC -&amp;gt; converted to Pacific Time&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s good to know that this is how to get a Postgres timestamp converted to whatever time zone you want, but I wouldn’t recommend it in a Rails app (which is the article’s context). For one thing, Rails and Postgres don’t use the same names for all timezones, so you have to maintain your own mapping between the two. This is one of those chores that will keep nagging you for the life of your app, so I’d rather just avoid it. But more important, this approach means you have to pass your timezone all the way down to the database layer. I’d rather deal with time zones early, get to a TimeWithZone (or even an int), and then forget about time zones for the rest of the code.&lt;/p&gt;

&lt;h2 id="remember_daylight_savings_time"&gt;Remember Daylight Savings Time&lt;/h2&gt;

&lt;p&gt;This is a small tip, but be careful about DST. You should avoid ever writing a fixed offset or a string like “PDT”. This is wrong:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Time&lt;/span&gt;.new(&lt;span class="integer"&gt;2014&lt;/span&gt;, &lt;span class="integer"&gt;4&lt;/span&gt;, &lt;span class="integer"&gt;8&lt;/span&gt;, &lt;span class="integer"&gt;3&lt;/span&gt;, &lt;span class="integer"&gt;15&lt;/span&gt;, &lt;span class="integer"&gt;0&lt;/span&gt;, &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-07:00&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you were after PDT, your code is broken during PST. If you were after MST, your code is broken during MDT. Similarly in SQL if you say &lt;code&gt;AT TIME ZONE 'PDT'&lt;/code&gt;, you’ve broken PST.&lt;/p&gt;

&lt;p&gt;If you stick with the ActiveSupport::TimeZone instances, you can forget about DST, and things will just work. That’s why there is no such thing as this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="constant"&gt;TimeZone&lt;/span&gt;[&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;PDT&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;only this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="constant"&gt;TimeZone&lt;/span&gt;[&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Pacific Time (US &amp;amp; Canada)&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="even_dates_are_times"&gt;Even Dates are Times&lt;/h2&gt;

&lt;p&gt;When you think you’re just dealing with dates, time zones probably still matter. Was the lesson assigned on Tuesday or Wednesday? It depends. From the teacher’s perspective? The student’s? The principal’s? Unless you’re really sure, I’d recommend always storing a full date+time in your database. Also, in Ruby avoid converting things to &lt;code&gt;Date&lt;/code&gt;. Of course if you read the articles above you know you don’t want this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Date&lt;/span&gt;.today&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But this isn’t any better:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Time&lt;/span&gt;.zone.today&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or even this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;current_user.time_zone.today&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here is a query I’ve seen:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Lesson&lt;/span&gt;.where(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;date(created_at) &amp;gt;= ?&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;, tz.today - &lt;span class="integer"&gt;3&lt;/span&gt;.days)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But that’s wrong, because you’re stripping off all the time zone information. It’s going to report Tuesday’s lessons in Wednesday. (It also means you need a database index on the expression &lt;code&gt;date(created_at)&lt;/code&gt;, which is probably less often useful than a normal index on just the column.)&lt;/p&gt;

&lt;p&gt;To improve this query, I’d write it like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Lesson&lt;/span&gt;.where(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;created_at &amp;gt;= ?&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;, tz.now.midnight - &lt;span class="integer"&gt;3&lt;/span&gt;.days)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If today is Wednesday (for you), that will give all the lessons created since the beginning of Sunday. Because we’re carrying a full time all the way down to the database query, we don’t have problems with the definition of “today.”&lt;/p&gt;

&lt;p&gt;If you are ever tempted to use &lt;code&gt;today&lt;/code&gt; or &lt;code&gt;to_date&lt;/code&gt;, you probably want these methods instead:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tz.now.midnight     &lt;span class="comment"&gt;# the start of today, 00:00:00&lt;/span&gt;
tz.now.end_of_day   &lt;span class="comment"&gt;# 23:59:59&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="parsing_times"&gt;Parsing times&lt;/h2&gt;

&lt;p&gt;Now that you’ve got a per-user time zone, you want to use that to parse date/time inputs from that user. If you user types “3:15”, you want to interpret that as 3:15 in the user’s time zone.&lt;/p&gt;

&lt;p&gt;There is a &lt;code&gt;parse&lt;/code&gt; method on &lt;code&gt;ActiveSupport::TimeZone&lt;/code&gt;, but unfortunately no &lt;code&gt;strptime&lt;/code&gt;. If you say this, you get the wrong result:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Time&lt;/span&gt;.strptime(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;2014-04-08 03:15&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;).in_time_zone(tz)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s because &lt;code&gt;strptime&lt;/code&gt; assumes the time is in the default time zone (hopefully UTC), and &lt;code&gt;in_time_zone&lt;/code&gt; does not change the “instant” the time represents, only the perspective used to view it. If you’re lucky to have a standardish format, this will work:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;tz.parse(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;2014-04-08 03:15&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If your format is something weirder, or you just don’t trust &lt;code&gt;parse&lt;/code&gt; with its heuristic-based approach, then you’re probably out of luck. A previous version of this article recommended &lt;code&gt;Time.use_zone(tz) { Time.strptime(...) }&lt;/code&gt;, but that doesn’t work, because &lt;code&gt;use_time&lt;/code&gt; only changes &lt;code&gt;Time.zone&lt;/code&gt;. It doesn’t change &lt;code&gt;Time.strptime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Another approach with &lt;code&gt;strptime&lt;/code&gt; that doesn’t work is to concatenate the time zone to your string and use &lt;code&gt;%Z&lt;/code&gt; (and friends) in your format, like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="constant"&gt;Time&lt;/span&gt;.strptime(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;2014-04-08 03:15&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt; + &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt; &lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt; + tz.formatted_offset, &lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;%Y-%m-%d %H:%M %:z&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The problem here is that &lt;code&gt;tz.formatted_offset&lt;/code&gt; really depends on the time of year because of Daylight Savings Time. But you don’t know that until you parse the string. You could pass the timezone abbreviation instead, like &lt;code&gt;PDT&lt;/code&gt;, but that has the same problem. And &lt;code&gt;%Z&lt;/code&gt; doesn’t understand long names like &lt;code&gt;Pacific Time (US &amp;amp; Canada)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The only approach I know that works is to parse the time with your default time zone, then feed the bits into &lt;code&gt;tz.local&lt;/code&gt;, like so:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;t = &lt;span class="constant"&gt;Time&lt;/span&gt;.strptime(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;2014-04-08 03:15&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;)
tz.local(t.year, t.month, t.mday, t.hour, t.min)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Sorry, that’s the best I can do!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;: I &lt;a href="https://github.com/rails/rails/pull/19618"&gt;added a &lt;code&gt;strptime&lt;/code&gt; method to the &lt;code&gt;TimeZone&lt;/code&gt; class&lt;/a&gt; (my first Rails code contribution), so hopefully you’ll start seeing it in future versions!&lt;/p&gt;

&lt;h2 id="displaying_times"&gt;Displaying times&lt;/h2&gt;

&lt;p&gt;So much for times as inputs. Times as outputs is a lot easier. Your rule should be to ignore time zones until the last minute, when you actually format the value for rendering. So it should go right into your view:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;= lesson.created_at.in_time_zone(current_user.time_zone).strftime("%A, %B %-d, %Y")&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You might want a helper for this though, something like:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="keyword"&gt;def&lt;/span&gt; &lt;span class="function"&gt;format_time&lt;/span&gt;(time, tz, format)
  time.in_time_zone(tz).strftime(format)
&lt;span class="keyword"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then your Haml can be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;= format_time(lesson.created_at, current_user.time_zone, "%A, %B %-d, %Y")&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or de-parameterize that as much as you like:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-ruby"&gt;&lt;span class="keyword"&gt;def&lt;/span&gt; &lt;span class="function"&gt;format_time&lt;/span&gt;(time)
  time.in_time_zone(current_user.time_zone).strftime(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;span class="content"&gt;%A, %B %-d, %Y&lt;/span&gt;&lt;span class="delimiter"&gt;"&lt;/span&gt;&lt;/span&gt;)
&lt;span class="keyword"&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;= format_time(lesson.created_at)&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;So in general, my principles for handling times in Rails are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even dates are times.&lt;/li&gt;

&lt;li&gt;Set “global” time zones to UTC everywhere you can.&lt;/li&gt;

&lt;li&gt;Don’t ever use global time zones; scope it to a user or whatever is appropriate.&lt;/li&gt;

&lt;li&gt;To most of your code, a time is just an instant.&lt;/li&gt;

&lt;li&gt;Push time zone inputs to the very beginning of your request, to the top of your stack.&lt;/li&gt;

&lt;li&gt;Push time zone outputs to the very end of your request, when rendering the view.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good luck!&lt;/p&gt;
</content>
  </entry>
</feed>

