Project: YugabyteDB (App Definition and Development / Database)
File: stopwatch.md

title: > Case study: implementing a stopwatch with SQL [YSQL] headerTitle: > Case study: implementing a stopwatch with SQL linkTitle: > Case study: SQL stopwatch description: > Case study: using YSQL to implement a stopwatch menu: v2.18: identifier: stopwatch parent: api-ysql-datatypes-datetime weight: 130 type: docs You sometimes want to time a sequence of several SQL statements, issued from ysqlsh, and to record the time in the spool file. The \\timing on meta-command doesn't help here because it reports the time after every individual statement and, on Unix-like operating systems, does this using stderr. The \\o meta-command doesn't redirect stderr to the spool file. This case study shows you how to implement a SQL stopwatch that allows you to start it with a procedure call before starting what you want to time and to read it with a select statement when what you want to time finishes. This reading goes to the spool file along with all other select results. There are various built-in SQL functions for reading the wall-clock time because there are different notions of currency: "right now at the instant of reading , independently of statements and transactions", "as of the start of the current individual SQL statement", or "as of the start of the current transaction". A generic elapsed time stopwatch is best served by reading the wall-clock as it stands at the instant of reading, even as time flows on while a SQL statement executes. The clocktimestamp()_ function does just this. Try this: ```plpgsql set timezone = 'UTC'; select tochar(clocktimestamp(), 'hh24:mi:ss') as t0, (pg_sleep(5)::text = '')::text as slept, tochar(clocktimestamp(), 'hh24:mi:ss') as t1; ``` Here's a typical result: ```output t0 | slept | t1 -+-+- 20:13:54 | true | 20:13:59 ``` (The pgsleep() built-in function returns void, and the ::text typecast of void is the empty string.) You can see that t1 is five seconds later than t0, just as the invocation of pgsleep() requests. The return data type of clocktimestamp() is timestamptz. (You can confirm this with the \\df_ meta-command.) Try this. ```plpgsql select tochar((clocktimestamp() at time zone 'America/Los_Angeles'), 'yyyy-mm-dd hh24:mi:ss') as "LAX", tochar((clocktimestamp() at time zone 'Europe/Helsinki'), 'yyyy-mm-dd hh24:mi:ss') as "HEL"; ``` Notice the use of the operator to interpret a timestamptz value as a plain timestamp value local to the specified timezone. In this use of the operator (converting from timestamptz to plain timestamp), its effect is insensitive to the session's TimeZone setting. Here's a typical result: ```output LAX | HEL 2021-07-30 10:26:39 | 2021-07-30 20:26:39 ``` The reported local time in Helsinki is ten hours later than the reported local time in Los Angeles, as is expected around the middle of the summer. You might be worried by the fact that the text rendition of the clocktimestamp() return value depends on the session's TimeZone setting. And you might think that this could confound the attempt to measure elapsed time by subtracting the reading at the start from the reading at the finish of the to-be-timed statements unless care is taken to ensure that the TimeZone_ setting is identical at the start and the end of the timed

The simplest way to side-step this concern is to record the value of this expression at the start and at the finish: ```output extract(epoch from clock_timestamp()) ``` This evaluates to the number of seconds (as a double precision value with microsecond precision) of the specified moment from the so-called start of the epoch (00:00:00 on 1-Jan-1970 UTC). The section explains, and demonstrates, that the result of "extract(epoch from timestamptzvalue)" is insensitive to the session's TimeZone setting. Try this, using the LAX and HEL results from the previous query as the timestamptz_ literals. ```ppgsql with c as ( select '2021-07-30 10:26:39 America/Los_Angeles'::timestamptz as t1, '2021-07-30 20:26:39 Europe/Helsinki' ::timestamptz as t2 ) select ( extract(epoch from t1) = extract(epoch from t2) )::text as same from c; ``` The result is true. This reflects the fact that extract() accesses the internal representation of the timestamptz value and this is normalized to UTC when it's recorded. Notice that, if you insist, you could subtract the start timestamptz value directly from the finish timestamptz value to produce an interval value. But youd have to reason rather more carefully to prove that your timing results are unaffected by the sessions timezone settingparticularly in the rare, but perfectly possible, event that a daylight savings boundary is crossed, in the regime of the sessions TimeZone setting, while the to-be-timed operations execute. (Reasoning does show that these tricky details dont affect the outcome. But it would be unkind to burden readers of your code with needing to understand this when a more obviously correct approach is available.) Further, the durationastext() user-defined function (below) to format the result using appropriate units would be a little bit harder to write when you start with an interval value than when you start with a scalar seconds value. PostgreSQL doesn't provide any construct to encapsulate PL/pgSQL functions and procedures and to represent state that these can jointly set and read that persists across the boundaries of invocations of these subprograms. YSQL inherits this limitation. (Other database programming environments do provide such schemes. For example, Oracle Database's PL/SQL provides the package for this purpose.) However, PostgreSQL and YSQL do support the so-called user-defined run-time parameter. This supports the representation of session-duration, session-private, state as a text value. Here's a simple example: ```plpgsql -- Record a value. set mynamespace.myvalue to 'Some text.'; -- Read it back later. select currentsetting('mynamespace.myvalue') as "current value of 'mynamespace.my_value'"; ``` This is the result: ```output current value of 'mynamespace.myvalue' Some text. ``` You don't have to declare the namespace or items within it. They come in to being implicitly when you first write to them. Notice that the effect of the set statement is governed by ordinary transaction semantics. Try this (with autocommit set to on, following the usual convention): ```plpgsql set mynamespace.myvalue to 'First value.'; begin; set mynamespace.myvalue to 'Second value.'; rollback; select currentsetting('mynamespace.myvalue') as "current value of 'mynamespace.my_value'"; ``` This is the result: ```output current value of 'mynamespace.myvalue' First value. ``` The implementation and testing of this function are utterly straightforwardbut a fair amount of typing is needed. The code presented here can save you that effort. You may prefer to use it as a model for implementing your own formatting rules. The return data type of the extract() operator is double

But its better to implement durationastext() with a numeric input formal parameter because of this datatypes greater precision and accuracy. This makes behavior with input values that are very close to the units boundaries that the inequality tests define more accurate than if double precision is used. Of course, this doesnt matter when the function is used for its ultimate purpose because ordinary stochastic timing variability will drown any concerns about the accuracy of the formatting. But testing is helped when results with synthetic data agree reliably with what you expect. As is often the case with formatting code, the readability benefits from trivial encapsulations of the SQL built-in functions that it usesin this case tochar() and ltrim()_. First create this overload-pair of helpers: ```plpgsql drop function if exists fmt(numeric, text) cascade; create function fmt(n in numeric, template in text) returns text stable language plpgsql as $body$ begin return ltrim(to_char(n, template)); end; $body$; drop function if exists fmt(int, text) cascade; create function fmt(i in int, template in text) returns text stable language plpgsql as $body$ begin return ltrim(to_char(i, template)); end; $body$; ``` Now create durationastext() itself: ```plpgsql drop function if exists durationastext(numeric) cascade; create function durationastext(t in numeric) returns text stable language plpgsql as $body$ declare msprsec constant numeric not null := 1000.0; secsprmin constant numeric not null := 60.0; minsprhour constant numeric not null := 60.0; secsprhour constant numeric not null := minsprhour*secsprmin; secsprday constant numeric not null := 24.0*secsprhour; confidence_limit constant numeric not null := 0.02; ms_limit constant numeric not null := 5.0; cs_limit constant numeric not null := 10.0; result text not null := ''; begin case when t < confidence_limit then result := 'less than ~20 ms'; when t >= confidencelimit and t < mslimit then result := fmt(t*msprsec, '9999')||' ms'; when t >= mslimit and t < cslimit then result := fmt(t, '90.99')||' ss'; when t >= cslimit and t < secspr_min then result := fmt(t, '99.9')||' ss'; when t >= secsprmin and t < secsprhour then declare ss constant numeric not null := round(t); mins constant int not null := trunc(ss/secsprmin); secs constant int not null := ss - mins*secsprmin; begin result := fmt(mins, '09')||':'||fmt(secs, '09')||' mi:ss'; end; when t >= secsprhour and t < secsprday then declare mi constant numeric not null := round(t/secsprmin); hours constant int not null := trunc(mi/minsprhour); mins constant int not null := round(mi - hours*minsprhour); begin result := fmt(hours, '09')||':'||fmt(mins, '09')||' hh:mi'; end; when t >= secsprday then declare days constant int not null := trunc(t/secsprday); mi constant numeric not null := (t - days*secsprday)/secsprmin; hours constant int not null := trunc(mi/minsprhour); mins constant int not null := round(mi - hours*minsprhour); begin result := fmt(days, '99')||' days '|| fmt(hours, '09')||':'||fmt(mins, '09')||' hh:mi'; end; end case; return result; end; $body$; ``` Its convenient to encapsulate the tests for the function durationastext() using a table function to generate a nicely formatted report. The readability of this reporting function, too, benefits from a dedicated helper function along the same lines as the helpers for the durationastext() function. ```plpgsql drop function if exists display(numeric) cascade; create function display(n in numeric) returns text stable language plpgsql as $body$ begin return lpad(tochar(n,

15)||' :: '||durationas_text(n); end; $body$; ``` Now create and execute the durationastexttestreport() table function itself: ```plpgsql drop function if exists durationastexttestreport() cascade; create function durationastexttestreport() returns table (z text) stable language plpgsql as $body$ declare secsprmin constant numeric not null := 60.0; secsprhour constant numeric not null := 60.0*secsprmin; secsprday constant numeric not null := 24.0*secsprhour; confidence_limit constant numeric not null := 0.02; ms_limit constant numeric not null := 5.0; cs_limit constant numeric not null := 10.0; delta constant numeric not null := 0.000001; cs constant numeric not null := 0.1; sec constant numeric not null := 1; min constant numeric not null := secsprmin; t numeric not null := 0; begin z := ' s (in seconds) durationastext(s)'; return next; z := ' -- -'; return next; t := confidence_limit - delta; z := display(t); return next; z := ''; return next; t := confidence_limit; z := display(t); return next; t := ms_limit - delta; z := display(t); return next; z := ''; return next; t := ms_limit; z := display(t); return next; t := cs_limit - delta; z := display(t); return next; z := ''; return next; t := cs_limit; z := display(t); return next; t := secsprmin - cs; z := display(t); return next; t := secsprmin - delta; z := display(t); return next; z := ''; return next; t := secsprmin; z := display(t); return next; t := secsprhour - sec; z := display(t); return next; t := secsprhour - delta; z := display(t); return next; z := ''; return next; t := secsprhour; z := display(t); return next; t := secsprday - min; z := display(t); return next; t := secsprday - delta; z := display(t); return next; z := ''; return next; t := secsprday; z := display(t); return next; t := secsprday*2.345; z := display(t); return next; end; $body$; select z from durationastexttestreport(); ``` This is the result: ```output s (in seconds) durationastext(s) -- - 0.019999 :: less than ~20 ms 0.020000 :: 20 ms 4.999999 :: 5000 ms 5.000000 :: 5.00 ss 9.999999 :: 10.00 ss 10.000000 :: 10.0 ss 59.900000 :: 59.9 ss 59.999999 :: 60.0 ss 60.000000 :: 01:00 mi:ss 3599.000000 :: 59:59 mi:ss 3599.999999 :: 60:00 mi:ss 3600.000000 :: 01:00 hh:mi 86340.000000 :: 23:59 hh:mi 86399.999999 :: 24:00 hh:mi 86400.000000 :: 1 days 00:00 hh:mi 202608.000000 :: 2 days 08:17 hh:mi ``` Create a the procedure startstopwatch(), the function stopwatchreadingasdp(), and the function stopwatchreading()_. This reads the wall-clock time and records it in a dedicated user-defined run-time parameter: ```plpgsql drop procedure if exists start_stopwatch() cascade; create procedure start_stopwatch() language plpgsql as $body$ declare -- Record the current wall-clock time as (real) seconds -- since midnight on 1-Jan-1970. starttime constant text not null := extract(epoch from clocktimestamp())::text; begin execute 'set stopwatch.starttime to '''||starttime||''''; end; $body$; ``` This reads the current wall-clock time, reads the start time from the user-defined run-time parameter, and returns the elapsed time as a double precision value: ```plpgsql drop function if exists stopwatchreadingas_dp() cascade; create function stopwatchreadingas_dp() returns double precision -- It's critical to use "volatile" because "clock_timestamp()" is volatile. -- "volatile" is the default. Spelled out here for self-doc. volatile language plpgsql as $body$ declare starttime constant double precision not null :=

currtime constant double precision not null := extract(epoch from clocktimestamp()); diff constant double precision not null := currtime - starttime; begin return diff; end; $body$; ``` This is a simple wrapper for stopwatchreadingasdp() that returns the elapsed time since the stopwatch was started as a text value by applying the function durationastext() to the stopwatchreadingasdp() return value: ```plpgsql drop function if exists stopwatch_reading() cascade; create function stopwatch_reading() returns text -- It's critical to use "volatile" because "stopwatchreadingas_dp()" is volatile. volatile language plpgsql as $body$ declare t constant text not null := durationastext(stopwatchreadingas_dp()::numeric); begin return t; end; $body$; ``` The first execution of a PL/pgSQL unit in a session can be slower than subsequent executions because of the cost of compiling it. In a timing test like this, it therefore makes sense deliberately to warm up before recording the timings. ```plpgsql \timing off -- Warm up do $body$ begin for j in 1..3 loop call start_stopwatch(); perform pg_sleep(0.1); declare t constant text not null := stopwatch_reading(); begin end; end loop; end; $body$; call start_stopwatch(); \timing on do $body$ begin perform pg_sleep(6.78); end; $body$; \timing off \o test-stopwatch.txt select stopwatch_reading(); \o ``` Here is a typical result, as reported in the file stopwatch.txt: ```output stopwatch_reading 6.79 ss ``` As explained, the "\timing on" output isn't captured in the spool file. But here's what you typically see in the terminal window: ```output Time: 6783.047 ms (00:06.783) ``` The elapsed times that "\timing on" and the SQL stopwatch report are in good agreement with each other. And both are a little longer that the argument that was used for pgsleep()_ because of the times for the client-server round trips. Sometimes you want to time operations that are done in two or several successive sessions. Installation scripts do this when they need to create objects with two or several different owners. It won't work, in such scenarios, to record the starting wall-clock time using a user-defined run-time parameter because this has only session duration. Both psql and (therefore) ysqlsh support client-side variablesand these survive across session boundaries. You can assign the result column(s) of a select statement to such variables with the \\gset meta-command. The select statement isn't terminated by the usual semicolon. Rather, the \\gset meta-command acts: both as the directive to assign the value of the select list item s0 to the variable stopwatchs0; and as the terminator for the select_ statement. ```plpgsql select extract(epoch from clock_timestamp())::text as s0 \gset stopwatch_ -- See what the effect was. \echo :stopwatch_s0 ``` Create a new double precision overload for the function stopwatchreading()_ to return the elapsed time like this: ```plpgsql drop function if exists stopwatch_reading(double precision) cascade; create function stopwatchreading(starttime in double precision) returns text volatile language plpgsql as $body$ declare currtime constant double precision not null := extract(epoch from clocktimestamp()); diff constant double precision not null := currtime - starttime; begin return durationastext(diff::numeric); end; $body$; ``` Test it like this: ```plpgsql select stopwatchreading(:stopwatchs0); ``` You'll get a result that reflects the time that it took you to read the few sentences above and to copy-and-paste the

It's convenient to assign the SQL statements that start and read the stopwatch to variables so that you can then use these as shortcut commands, to save typing: ```plpgsql \set startstopwatch 'select extract(epoch from clocktimestamp())::text as s0 \\gset stopwatch_' ``` and: ```plpgsql \set stopwatchreading 'select stopwatchreading(:stopwatch_s0);' ``` You can define these shortcuts in the psqlrc file (on the postgres/etc directory under the directory where you've installed the YugabyteDB client code). {{< tip title="'ysqlsh' meta-command syntax" >}} The section describes the \\gset and the \\set meta-commands. (\\gset is allowed on the same line as the SQL statement that it terminates, just as a new SQL statement is allowed on the same line as a previous SQL statement that's terminated with a semicolon.) Such a client-side variable acts like a macro: the text that it stands for is eagerly substituted and only then is the command, as it now stands, processed in the normal way. The argument of the \\set meta-command that follows the variable name needs to be surrounded with single quotes when it contains spaces that you intend to be respected. If you want to include a single quote within the argument, then you escape it with a backslash. And if you want to include a backslash within the argument, then you escape that with a backslash too. Try this: ```plpgsql \set x 'select \'Hello \\world\' as v;' \echo :x ``` This is the result: ```output select 'Hello \world' as v; ``` Now try this: ```plpgsql :x ``` This is the result: ```output v -- Hello \world ``` When you want to define a command as the variable x and within that expand another variable y for use within that command, do it like this: ```plpgsql \set x 'select \'Hello \'||:y as v;' \set y 42 :x ``` This is the result: ```output v Hello 42 ``` If you subsequently redefine the variable y without redefining the variable x, and then use x, like this: ```plpgsql \set y 17 :x ``` then you'll get this new result: ```output v Hello 17 ``` {{< /tip >}} To be convincing, the end-to-end test needs to connect as at least two different users in the same database. Because session creation brings a noticeable cost, this drowns the first-use cost of invoking PL/pgSQL codeand so the test has no warm-up code. The test assumes that two ordinary users, u1 and u2 exist in the database demo, that the stopwatch code is on the search path for both of these users, and that both of these have sufficient privileges to execute the code. Try this: ```plpgsql \o test-cross-session-stopwatch.txt \c demo u1 :start_stopwatch do $body$ begin perform pg_sleep(2); end; $body$; \c demo u2 do $body$ begin perform pg_sleep(3); end; $body$; \c demo u1 do $body$ begin perform pg_sleep(1.5); end; $body$; :stopwatch_reading \o ``` The test-cross-session-stopwatch.txt spool file will contain a result like this: ```output stopwatch_reading 6.86 ss ``` In this example, session creation cost explains the fact that the reported time, 6.86 seconds, is longer than the total requested sleep time, 6.5 seconds. The code that this page explains isn't referenced elsewhere in the overall section. You might like to install it in any database that you use for development and testing. (This is strongly encouraged.) For this reason, it's bundled for a one-touch installation, separately from the . {{< tip title="Download the code kit" >}} Get the code kit from . It has a README that points you to the master-install script and that recommends installing it centrally for use by any user of that database. {{< /tip >}}