Wednesday, January 03, 2024

UI5 Tips: Buffering Events to avoid a request-storm

Standard UI5 event handling will usually go a long way. Yet sometimes, certain user actions can cause ui5 objects to generate a lot of similar events within a small period of time, and it is often not useful to handle each and all of them: only the last event needs handling. A very common scenario is doing a search in response to the liveChange event: if you'd attach a handler to handle the liveChange event, and do the backend query from there, then a backend request would be sent for each keystroke while the user is typing in the search field. This causes a storm of requests that the backend must somehow handle. But most of these requests will be for naught, as the user is only interested in the result of the query that matches the last complete search term they typed. So, rather than firing a query to the backend for each and every keystroke, it makes more sense to buffer these events, and react to only the last one. The bufferedEventHandler utility helps you to do just that in a generic and reusable way. This ui5tip describes the bufferedEventHandler utility. It is available on github under terms of the Apache 2.0 License. There's also a sample application so you can try it out yourself.

The BufferedEventHandler sample app

The bufferedEventHandler sample application illustrates the scenario from the introduction. It consists of a single page showing mockup company data in a sap.ui.table.Table. A screenshot is shown below: Screenshot of the BufferedEventHandler sample app. At the top left of the grid, there's a sap.m.SearchField labeled "Search in Name". The user can type some search term into the searchfield, and the grid will automatically refresh and show only the rows for which the CompanyName has a case-insensitive match with the entered search term. While the search happens automatically, it does not happen immediately as the search term changes at every keystroke. Rather, about 1 second after the user stops typing, the data grid is filtered. At the top right of the grid, there's a sap.m.ProgressIndicator labeled "Event buffer Timeout". The progress indicator reflects how much time has passed since the last keystroke. When the progress indicator reaches a 100%, the filter action is executed.

The bufferedEventHandler Utility

To buffer events we provide a bufferedEventHandler utility object with just one bufferEvents function. You can find this in the bufferedEventHandler file in the utils directory. To use it, we need to import it into the source file where we want to use it. This will usually be in a ui5 controller and in the sample app we do this in MainPage.controller.js:
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/table/Column",
  "sap/m/Text",
  "sap/ui/model/Filter",
  "sap/ui/model/FilterOperator",
  "sap/ui/model/FilterType",
  "ui5tips/utils/bufferedEventHandler"
], 
function(
  Controller,
  Column,
  Text,
  Filter,
  FilterOperator,
  FilterType,
  bufferedEventHandler
){
 "use strict";  
  var controller = Controller.extend("ui5tips.components.mainpage.MainPage", {
    ...
  });
  return controller;
}
We can now refer to the bufferedEventHandler utility through the local variable that is also called bufferedEventHandler. The controller uses the bufferedEventHandler utility in the initSearchField() method. This called from the controller's standard onInit() lifecycle method, which is called just once for the Controller instance:
  ...
  onInit: function() {
    this.initSearchField();
  },
  initSearchField: function(){
    var searchField = this.byId('searchField');
    bufferedEventHandler.bufferEvents(
      // event provider
      searchField,
      // timeInterval
      1000, 
      // eventId
      'liveChange', 
      // data
      null, 
      // handler
      this.doSearch, 
      // listener
      this,
      // progressHandler
      this.searchFieldProgress,
      // progressUpdateInterval
      50
    );
  },
  ...

The bufferEvents() Method

The meat of the initSearchField() method is the call to the bufferEvents method of the bufferedEventHandler utility. This method has the following arguments:
  • eventProvider: the 1st argument should be the object that emits the events - in our example this is the sap.m.SearchField. This object should be a subclass of sap.ui.base.EventProvider. (bufferEvents will throw an error if it's not!)
  • timeInterval: the 2nd argument is the timeout, in milliseconds. This is the amount of time that should pass between the occurrence of the last event and the call to the actual handler of the event. If a new event occurs during the wait period, the timeout is reset, and a new waiting period is started. In the example, we use a timeInterval of 1000 - that is, we will wait 1000 milliseconds (1 second) before handling the last event.
Choosing the timeInterval is a balancing act. In the case of the example, where the events are generated in response to user actions, the timeInterval should not be too short, as the user should be given enough time to type a meaningful searchterm before the actual query kicks in. But if the timeInterval is too long, the application may appear unresponsive to the user. If the application appears unresponsive, the user may try to retype their search term, which will only postpone the reaction even more. (There's more about this in the section about the ProgressIndicator). The next 4 arguments of bufferEvents correspond to sap.ui.base.EventProvider's attachEvent() method:
  • eventId: a string that identifies the event to listen to. In our example this is 'liveChange'. Some ui5 objects, (for example, sap.ui.base.ManagedObjects, which includes all sap.ui.core.Controls) expose the events they expose through their metadata. In these cases, bufferEvents will verify whether the passed eventId is in fact exposed by the object, and it will throw an error in case it doesn't. EventProviders that do not expose their events through metadata can still be used with the bufferedEventHandler, but you'll just need to make sure yourself the value for eventId is valid, as bufferEvent has no way of checking it.
  • data: an optional argument to pass any "extra" data that the event handler might need. In the example, we pass null as we have no need for any additional data.
  • handler: this should be the callback-function that will be called upon to actually handle the event. The callback function will receive an instance of an sap.ui.base.Event as single argument, which typically provides access to all relevant information pertaining to the event. In the example, we pass this.doSearch, which is a method of the controller that will perform the actual filtering of the data grid.
  • listener: this is an optional argument which you can use to specify the scope in which the handler will be called. Typically the handler will not be completely standalone, but it will refer to a this object, one way or another. If the handler function is not already bound (for example, by using the function's bind() method), then you should pass whatever object should act as this for the handler function via the listener argument. In the example, we simply use this which refers to the controller instance itself. This makes sense as the handler function is also a method of the controller. (Remember: we passed this.doSearch as handler.)
In the call to bufferEvents, these arguments will be used to create an actual handler for the event, and also automatically attach it to the eventProvider for the specified eventId. But rather then calling the passed handler, it will start a javascript timeout for a duration of the passed timeInterval. If the timeout was already initiated, it is cleared, thus canceling the previous event, and initiating a new waiting period.

Monitoring wait progress

The final 2 arguments to bufferEvents are optional, and may be used by the application to monitor the waiting period between the occurrence of the last event and the time when the handler will actually be called:
  • progressHandler: when passed, this should be a callback function which is to be called at the start and during the waiting period. If the progressHandler callback is called, it will be called using the listener as scope. The callback will be passed a floating point number between 0 and 1, indicating the fraction of the time that has passed between the last event and now. If a progressHandler is specified, it is always called at least once and passed 0 whenever a new waiting period is initiated. In this example we passed this.searchFieldProgress, which is a method of the controller that updates the sap.m.ProgressIndicator that sits in the right top of the data grid.
  • progressUpdateInterval: this should be in integer, indicating the number of milliseconds between the calls to the progressHandler. In our example it is 50, which means we will get 1000 / 50 = 20 updates during the waiting period, which ensures a smooth and regular update of the ProgressIndicator control.

The ProgressIndicator

The sample application provides a sap.m.ProgressIndicator to indicate when the entered search term will be used to filter the data. A progress indicator may not be necessary in case the timeInterval is so short that it will appear to the user as if the event is handled immediately. But when the timeInterval exceeds 200 or 250 milliseconds, most users will start to experience a noticeable lag. Now, there is this strange psychological phenomenon happening here - as the user is still typing their search term, they will be happy that the backend query is not already fired. It would make them feel rushed if the grid was constantly being updated while they were typing. But once the user is done typing their search term, they want to have the result as quickly as possible. Obviously, the software cannot read the user's mind (yet!), so once the user stops typing, the application needs to let the user know they acknowledged their action, and that it is 'working on it'. Hence the need for a progress indicator: by having a visual indicator that "something's happening", the user will be assured the application has acknowledged their input, and this will make the wait period before actually handling the event more acceptable. If the wait is sufficiently short, a simple busyIndicator might do the trick, but since the progressHandler gets passed an exacte estimate of how much longer the user will need to wait, our progress indicator can communicate this to the user. This will make the application's behavior more predictable and hopefully more satisfying to use. Of course, it is not absolutely necessary to use the sap.m.ProgressIndicator to give this kind of feedback to the user. It's just that for this sample, this was the easiest, most straighforward illustration of this principle. You can use the `progressHandler` callback to do anything you like to fit your need.

Detaching

The bufferEvents method will create and attach a handler to the eventProvider. bufferEvents will also return that generated handler so you can detach it explicitly from the eventProvider if you need to. As a convenience, the returned handler provides its own detach() method for this purpose:
var bufferedEventHandlerInstance = bufferedEventHandler.bufferEvents(...);
...
bufferedEventHandlerInstance.detach();
(Note that in a typical scenario, the eventHandler and the eventProvider will almost certainly be in the same scope and lifecycle, so there is rarely a need to explicitly do this.)

Other Use Cases

The liveSearch scenario may not always be a convincing use case. For example, if the query is done against a client model rather than a remote backend system then it might not actually be a problem to re-issue the query for every keystroke. But there are some other scenarios that benefit from event buffering. We will encounter one such case in the tip about Persisting UI State.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Manipulating the sap.m.TabContainer close buttons with custom CSS

Here's a ui5tip to show how you can change the look and feel of the sap.m.TabContainer with a minimal amount of custom CSS. If you want to try this for yourself, be sure to check out the sample application from github.

The sap.m.TabContainer

The sap.m.TabContainer provides a simple, no-nonsense widget to build a tabbed user interface (check out the samples). Tabs can be added via the items aggregation, which should contain a collection of sap.m.TabContainerItem's. While this control generally suits my needs, it has one feature I find problematic: each tabs always has a close button, which appears as a little 'X' icon in the right side of the tab. If the user clicks it, it will actually 'close' the tab, that is: the respective sap.m.TabContainerItem will be removed from the TabContainer. See the screenshot below to see what the default looks like (close buttons highligthed in red):default sap.m.TabContainer with close buttons on each tab.

Suppressing the close action

The openui5 samples show how you can suppress that behavior: you can write an event handler for the itemClose event, and then call the preventDefault() method on the event: (In the view xml:)
<m:TabContainer itemClose="onTabContainerItemClose">
  <m:items>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
    <m:TabContainerItem>
       ...
    </m:TabContainerItem>
  </m:items>
</m:TabContainer>
(In the controller javascript:)
  onTabContainerItemClose: function(event){
    event.preventDefault();
  }
Obviously, it would be strange if we'd always prevent the tab from being closed: Suppressing the default action of closing the tab only makes sense in a context where the user is supposed to be able to close the tab at all, and in such a case this could be used to pop up a dialog to let the user choose if they really meant to close the tab or want to keep it open. But the use case I frequently encounter is that the tab should not be closeable in the first place. While suppressing the close action would ensure the tab is never closed, it would confuse and anger the user, as the close button itself would still be there, inviting users to perform an action that can never be fulfilled.

Using the other Tab widget

One might suggest to use the sap.m.IconTabBar widget instead of the sap.m.TabContainer. The sap.m.IconTabBar takes sap.m.IconTabFilter's in its items collection, and these do not have a close button. Now, in some cases the sap.m.IconTabBar/sap.m.IconTabFilter may suit your needs and then you're fine. However I find that it has a number of other drawbacks (which I won't get into here). Besides, the sap.m.IconTabBar introduces a similar problem, but the other way around: whereas we cannot get rid of the close button in the sap.m.TabContainer, we cannot ever have a close button in the sap.m.IconTabBar. What we really want, is a property or something like that, which will let us control whether the tab will have a close button or not.

CSS to the rescue

In the previous section we argued that we'd really like to be able to control for each individual tab whether they have a close action at all, for example, by setting a property. To add a property one would normally have to extend a ui5 control, and attach some code so that the property setting can somehow influence the behavior of the control - in this case, control whether or not the close button will be displayed. While this is probably possible (I haven't tried it for this case), it does seem like an extraordinary measure for such a humble request. I found that a similar effect can be achieved by applying some custom CSS in combination with standard ui5 features. That's what this entire sample is about. With this tip you can:
  • hide all close buttons for an entire sap.m.TabContainer
  • hide the close button on an individual sap.m.TabContainerItem
  • show the close button on an individual sap.m.TabContainerItem in case the close buttons are hidden by default on the sap.m.TabContainer
All this functionality requires the inclusion of some css. In the sample this is all isolated in a single ui5-customization.css file, which is included into the application by declaring it the manifest.json.

Hiding all close buttons

To hide all the close buttons for all sap.m.TabContainerItem in the items collection of a particular sap.m.TabContainer, simply add the noCloseButtons style class:
<m:TabContainer 
  class="noCloseButtons" 
>
  <m:items>
    <!-- note: close button will be hidden by default for each m:TabContainerItem -->
    ...
  </m:items>
</m:TabContainer>
This works because the class property in the ui5 xml view is rendered to the html dom directly. So whatever html elements that ui5 creates to implement the TabControl widget will then be selectable with a class selector in css, and this is how we can relatively simply influence the look of our TabContainer through css. In our ui5-customization.css file, this is how we use noCloseButtons class to hide the buttons:
div.sapMTabContainer.noCloseButtons > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
(Note the initial selector, div.sapMTabContainer.noCloseButtons and the chain of > child selectors which target the actual bit of html that is used to render the close button, which is simply hidden by setting the css visibility property to hidden.) In the sample application, you can see this behavior in action in the App.view.xml. This contains the code for the outermost tabcontainer. A screenshot is shown below and as you can see both tabs ("Hide individually" and "Show individually") do not have a close button: Hiding the close buttons on a sap.m.TabContainer by applying the noCloseButtons css class.

Hiding an individual close button

If we can add a custom css class to sap.m.TabContainer to hide all close buttons, then surely it should be possible to follow the same approach for an individual sap.m.TabContainerItem, right? Yes, it should, but sadly, we cannot. (The reason is that sap.m.TabContainer is a subclass of sap.m.Control, which provides a addStyleClass() method, whereas sap.m.TabContainerItem is a subclass of sap.ui.core.Element, which does not have such a method) Now, let's take a step back and think about how we used the css style class on the sap.m.TabContainer to hide all the close buttons. By setting the custom css style class on the sap.m.TabContainer, the html dom was changed to include the custom class, and we could then use that in a css selector. So even if we cannot apply a css style class to a sap.m.TabContainerItem, might there be another way that would allow us to influence how ui5 writes the html dom so we may write a css selector in our custom css? It turns out that such a feature exists in the shape of a feature called ui5 custom data. The custom data aggregation is provided by sap.ui.core.Element and thus available to its subclasses, including sap.m.TabContainerItem. A custom data item is an arbitrary key/value pair, and by setting its writeToDom property, ui5 will render it to the html dom as a html data attribute. To see what it looks like in our sample, take a look at TabContainerItemWithHiddenCloseButton.fragent.xml, which uses it to hide the close button in the second sap.m.TabContainerItem, in an otherwise normal sap.m.TabContainer:
<m:TabContainerItem
  id="item2"
  name="No Close Button"
>
  <m:customData>
    <core:CustomData writeToDom="true" key="noCloseButton" value="true"/>
  </m:customData>
  ...
</m:TabContainerItem>
Because the CusomtData's writeToDom property is set to true, ui5 will generate a html data attribute to the html dom that looks something like this:
data-noclosebutton='true'
And in our ui5-customization.css file, the following rule is intended to pick that up and hide the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='true'] > .sapMTSItemCloseBtnCnt {
  visibility: hidden;
}
As you can see it is very similar to the rule we used to hide all close buttons on any sap.m.TabContainer having the noCloseButtons class, except now the class is missing and instead we use a css predicate selector based on the data attribute:
.sapMTabStripItem[data-noclosebutton='true']
The screenshot below shows what it looks like in the app. Note that the tab named "Default" has the close button as usual, but the one named "No Close Button - the one with the custom data attribute - does not show a close button: Individual TabContainerItem with a hidden close button.

Showing an individual close button

The final hack in this sample combines the style class and the custom data attribute. CSS allows us to write a selector that takes both the presence of the css style class as well as the presence of a html data attribute into account. We can put this to good use if we want to have a sap.m.TabContainer that hides all close buttons by default, but undo the hiding of the close button of specific sap.m.TabContainerItem's, based on the value of a data attribute (which is in turn controlled by the ui5 Custom Data feature). You an see this in action in the TabContainerItemWithHiddenCloseButtons.fragment.xml file of the example:
<m:TabContainer 
  class="noCloseButtons"
>
  <m:items>

    <m:TabContainerItem name="Default">
     ...
    </m:TabContainerItem>

    <m:TabContainerItem name="Show Close Button">
      <m:customData>
        <core:CustomData key="noCloseButton" value="false" writeToDom="true" />
      </m:customData>
      ..
    </m:TabContainerItem>

  </m:items>
</m:TabContainer>
Again the second sap.m.TabContainerItem has a CustomData item with the key noCloseButton, but now the value is true so as to override the effect of the noCloseButtons style class applied to the sap.m.TabContainer. In our our ui5-customization.css file, the following rule is intended to pick that up and show the close button:
div.sapMTabContainer > .sapMTabStripContainer > .sapMTabStrip > .sapMTSTabsContainer > .sapMTSTabs > .sapMTabStripItem[data-noclosebutton='false'] > .sapMTSItemCloseBtnCnt {
  visibility: visible;
}
The screenshot below shows what it looks like when you run the sample application: Show the close button in an individual TabContainerItem, overriding the effect of the noCloseButtons style class of the sap.m.TabContainer.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

UI5 Tips: Adding a Splash-Screen / Loading indicator

UI5 apps always take a little time to load. If you don't take any precautions, the user will be looking at a blank screen while the app is loading. It is fairly simple to add a splash screen and loading indicator to improve the user experience. This will give your app a more professional appearance, and it will cost minimal effort. This post shows you exactly how to do this. You might also be interested in checking out the sample app from github so you can run it yourself.

Running the app

The loadingscreen sample app is in the loadingscreen subdirectory of this repository. Simply expose the folder and all its contents with a webserver, and navigate to its index.html

How does it work?

In various tutorials and the ui5 walkthrough, the <body> is usually left empty. This is for a good reason - any content that is in the body would simply show up on the page, as is shown in the walkthrough's "Hello world!" example. But there is an exception: if an element has an id attribute with the value busyIndicator, then this content will be hidden after the ui5 bootstrap code is finished and the app content is placed inside the body. In the loadingscreen sample, the index.html page has a static <div> element with an id attribute with the value busyIndicator as static content in the <body> element. Indside that <div> we can place anything we like. In the example, the div contains a header and a footer with static text to indicate that the application is loading. Between the header and the footer is an image of the ui5 logo, and a css animation, which superficially resembles the ui5 busy indicator animation:
<body class="sapUiBody" id="content">

  <!-- Loading splash screen -->
  <div id="busyIndicator" style="text-align: center; font-family: Sans, Arial">
    <!-- static header text -->
    <h3>My UI5 App is loading</h3>
    
    <!-- static image of the ui5logo  -->
    <img src="images/openui5-logo.png" class="logo"/>

    <!-- 
         loader animation 
         CC0 licensed code used with permission from 
         https://loading.io/css/
    -->
    <div class="lds-ellipsis">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
    <!-- end of loader animation -->

    <!-- static footer text  -->
    <center><h5>This may take a few moments...</h5></center>
  </div>
  
</body>
The css animation requires some css, and this is included simply as a static css resource by including an appropriate <link> element in the <head> of the page:
<head>

  <!-- css required for the loading screen css anumation -->
  <link id="animation" rel="stylesheet" type="text/css" href="css/progress-animation.css"/>
   
</head>
In this case, we pulled the css animation from the excellent site https://loading.io/css/, which provides many different free css animations. Note that any css required by the loading screen must really be included statically via the <link> or <style> element. The standard ui5 mechanism to include css by declaring it in the manifest.json is no good as it will be loaded as part of the ui5 bootstrap, and the whole idea of the loading screen is to show something before the ui5 bootstrap even starts.

What does it look like

This is what it looks like when the app is loading: Screenshot of the loading screen The text, logo image, and the loader animation are all static content and will show during UI5 bootstrap.

Next Steps

Obviously, this loading screen is only an example to show how you can make it work. The entire design of the loading screen is up to you. Just remember to keep it light and quick: the whole reason to include a loading screen in the first place was to give the user something to look at while ui5 is bootstrapping. If the loading screen itself requires a lot of resources then it defeats its purpose. For this reason, you might consider including any css directly using the <style> element, rather than relying on a network request to load external css with the <link> element.

Finally

Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem. Want more tips? Find other posts with the ui5tips tag!

Monday, February 22, 2021

Year-to-Date on Synapse Analytics 5: Using Window Functions

For one of our Just-BI customers we implemented a Year-to-Date calculation in a Azure Synapse Backend. We encountered a couple of approaches and in this series I'd like to share some sample code, and discuss some of the merits and benefits of each approach.

TL;DR: A Year-to-Date solution based on a SUM() window function is simple to code and maintain as well as efficient to execute. This as compared to a number of alternative implementations, namely a self-JOIN (combined with a GROUP BY), a subquery, and a UNION (also combined with a GROUP BY).

Note: this is the 5th post in a series. (While our use case deals with Azure Synapse, most of the code will be directly compatible with other SQL Engines and RDBMS-es.)

Using window functions


Nowadays, many SQL engines and virtually all major RDBMSes support window functions (sometimes called analytic functions). A window function looks like a classic aggregate function. In some respects it also behaves like one, but at the same time there are essential differences.

Aggregate functions


Consider the following example:
select sum(SalesAmount) as SumOfSalesAmount
,      count(*)         as RowCount
from   SalesYearMonth
The example uses two aggregate functions, SUM() and COUNT(). It returns a result like this:

SumOfSalesAmount RowCount
109,846,381.43 38

Two things are happening here:
  • Even though there are multiple rows in the SalesYearMonth table, the result consists of just one row. In other words, a collection of source rows have been aggregated into fewer (in this case, only one) result row.
  • The functions have caclculated a value based on some aspect of the individual rows in the source collection. In the case of SUM(SalesAmount), the value of the SalesAmount column of each individual row was added to obtain a total. In the case of COUNT(*), each row was counted, adding up to the total number of rows.
Because the previous example uses aggregate functions, we cannot also select any non-aggregated columns. For example, while SalesYear and SalesMonth are present in the individual underlying rows, we cannot simpy select them, because they do not exist in the result row, which is an aggregate.

Window functions


Now, SUM() and COUNT() also exist as window functions. Consider the following query:
select SalesYear
,      SalesMonth
,      SalesAmount
,      sum(SalesAmount) over() as TotalOfSalesAmount
,      count(*)         over() as RowCount
from   SalesYearMonth
You might notice the last two expressions in the SELECT-list look almost identical to the aggregate functions in the previous example. The difference is that in this query, the function call is followed by an OVER()-clause. Syntactically, this is what distinguishes ordinary aggregate functions from window functions.

Here is its result:

SalesYear SalesMonth SalesAmount TotalOfSalesAmount RowCount
2011 5 503,805.92 109,846,381.43 38
2011 6 458,910.82 109,846,381.43 38
...more rows...
2014 6 49,005.84 109,846,381.43 38

Note that we now get all the rows from the underlying SalesYearMonth table: no aggregation has ocurred. But the window functions do return a result that is identical to the one we got when using them as aggregate functions, and that for each row of the SalesYearMonth table.

It's as if for each row of the underlying table, the respective aggregate function was called over all rows in the entire table. Conceptually this is quite like the construct we used in the subquery-solution. The following example illustrates this:
select SalesYear
,      SalesMonth
,      SalesAmount
,      (
           select sum(SalesAmount) 
           from   SalesYearMonth
       ) as TotalOfSalesAmount
,      (
           select count(*)
           from   SalesYearMonth
       ) as RowCount
from   SalesYearMonth

The window and the OVER()-clause


Thinking about window functions as a shorthand for a subquery helps to understand how they work and also explains their name: a window function returns the result of an aggregate function on a particular subset of the rows in the query scope. This subset is called the window and it is defined by the OVER()-clause.

The parenthesis after the OVER-keyword can be used to define which rows will be considered as window. When left empty (like in the example above) all rows are considered.

Controlling the window using the PARTITION BY-clause


If you compare the previous example with our prior subquery-solution, you'll notice that here, we do not have a WHERE-clause to tie the subquery to the current row of the outer query. That's why our result is calculated over the entire table, rather than with respect to the current year and preceding months, as in our prior subquery-solution. This is equivalent to the empty parenthesis following the OVER-keyword in the corresponding window-function example.

In the subquery-solution we wrote a WHERE-clause to specify a condition to tie the rows of the subquery to the current row. For window functions, we can control which rows make up the window window by writing a PARTITION BY-clause inside the parenthesis following the OVER-keyword.

The PARTITION BY-clause does not let you specify an arbitrary condition, like we could in a subquery. Instead, the relationship between the current row and rows in the window must be expressed through one or more attributes for which they share a common value. The following example may illustrate this:
select SalesYear
,      SalesMonth
,      SalesAmount
,      sum(SalesAmount) over(partition by SalesYear) as YearTotalOfSalesAmount
from   SalesYearMonth
In the example above, sum(SalesAmount) over(partition by SalesYear) means: calculate the total of SalesAmount over all rows where the value of SalesYear is equal to the value of the SalesYear in the current row.

The equivalent query using subqueries would be:
select OriginalSales.SalesYear
,      OriginalSales.SalesMonth
,      OriginalSales.SalesAmount
,      (
           select sum(YearSales.SalesAmount) 
           from   SalesYearMonth as YearSales
           where  YearSales.SalesYear = OriginalSales.SalesYear
       ) as YearTotalOfSalesAmount
from   SalesYearMonth as OriginalSales
The result is shown below:

SalesYear SalesMonth SalesAmount YearTotalOfSalesAmount
2011 5 503,805.92 12,641,672.21
2011 6 458,910.82 12,641,672.21
...more rows...
2014 6 49,005.84 20,057,928.81
(Note that 12,641,672.21 is the sum of the SalesAmount for SalesYear 2011; 20,057,928.81 is the total for 2014.)

A partition for the preceding months?


It's great that the PARTITION BY-clause allows us to specify a window for relevant year, but it's still too wide: we want the window to contain only the rows from the current year, but only for this month and its preceding months. In the subquery-solution this was easy, as we could write whatever condition we want in the WHERE-clause. So we wrote:
where  SalesYtd.SalesYear   = SalesOriginal.SalesYear
and    SalesYtd.SalesMonth <= SalesOriginal.SalesMonth
Specifying SalesYear in the window functions' PARTITION BY-clause is equivalent to the first part of the subquery's WHERE-clause condition.

It's less clear what our partition expression should look like to select all months preceding the current month. It's not impossible though. For example, we can write an expression to mark whether the current SalesMonth is equal to or less than a specific month. For example:
-- every month up to and including june is 1, all months beyond june is 0
case
    when SalesMonth <= 6 then 1 
    else 0
end
If we can write such an expression, then of course, we can also use it in a PARTITION BY-clause, like so:
sum(SalesAmount) over (
  partition by 
    SalesYear
  , case
      when SalesMonth <= 6 then 1 
      else 0
    end
) 
Let's try and think what this brings us.

Suppose the value for SalesMonth is 6 (june), or less? The CASE expression would return 1, and the window function would take all rows into account for which this is the case. So january, february, march and so on, up to june would all get the total of those six months - that is, the YTD value for june.

On the other hand, if SalesMonth is larger than 6, the CASE expression evaluates to 0. So all months beyond june (that is: july, august, and so on up to december) form a partition as well, and for those months, whatever is the sum over those months would be returned.

Now, it's not really clear what the outcome means in case the month is beyond june. But it doesn't really matter - what is important, is that we now know how to calculate the correct YTD value for a given month. And, what we did for june, we can do for any other month. So, once we have the YTD expressions for each individual month, we can set up yet another CASE-expression to pick the right one according to the current SalesMonth.

Putting all that together, we get:
select SalesYear
,      SalesMonth
,      SalesAmount
,      case SalesMonth
         -- january
         when 1 then SalesAmount
         -- february
         when 2 then
           sum(SalesAmount) over(
             partition by 
               SalesYear
             , case when SalesMonth <= 2 then 1 else 0 end
           ) 
           
         ...more cases for the other months...
           
         -- december
         when 12 then
           sum(SalesAmount) over(
             partition by 
               SalesYear
             , case when SalesMonth <= 12 then 1 else 0 end
           ) 
       end as YtDOfSalesAmount
from   SalesYearMonth
Like with the UNION-solution, we are taking advantage of our knowledge of the calendar, which allows us to create these static expressions. We would not be able to do this in a general case, or where the number of distinct values is very large. But for 12 months, we can manage.

While it's nice to know that this is possible, there is a much, much nicer way to achieve the same effect - the frame specification.

Frame Specification


The frame specification lets you specify a subset of rows within the partition. The way you can specify the frame feels a bit odd (to me at least), as it is specified in terms of the current row's position in the window. Hopefully the following example will make this more clear:
select SalesYear
,      SalesMonth
,      SalesAmount
,      sum(SalesAmount) over(
         partition by SalesYear
         order by SalesMonth
         rows between unbounded preceding
         and current row
       ) as SalesYtd
from   SalesYearMonth
We already discussed the PARTITION BY-clause, all the clause after that are new.

The ORDER BY-clause sorts the rows within the window, in this case by SalesMonth. We need to rows to be ordered because of how the frame specification works: it lets you pick rows by position, relative to the current row. The position of the rows is undetermined unless we sort them explicitly, so if we want to pick rows reliably we need the ORDER BY-clause to guarantee the order.

The frame specification follows the ORDER BY-clause. There are a number of possible options here, but I will only discuss the one in the example. In this case, it almost explains itself: we want to use the current row, and all rows that precede it. Since we ordered by SalesMonth, this means all the rows that chronologically precede it. As this selection applies to the current partition, we will only encounter months here that are within the current year.

So here we have it: a YTD calculation implemented using a window functions. It's about the same amount of code as compared to the subquery solution, but more delcarative, as we do not need to specify the details of a condition. On the other hand, it is also less flexible than a subquery, but in general one should expect the window functions to perform better than the equivalent subquery.

Generalizing the solutions


So far all our examples were based on the SalesYearMonth table, which provides SalesYear and SalesMonth as separate columns. One might wonder what would it would take to apply these various methods to a realistic use case.

For example, it is likely that in a real dataset, the time would be available as a single column of a DATE or DATETIME data type. A single date column potentially affects the YTD calculation in two ways:
  • Year: As the YTD is calculated over a period of a year and almost all solutions we described used the SalesYear column explicitly to implement that logic.
  • Preceding rows: To calculate the YTD for a specific row, there has to be a clear definition of what rows are in the same year, but which precede it. In our examples we could use the SalesMonth column for that, but this might be a but different in a realistic case.
  • Lowest Granularity: The lowest granularity of the SalesMonthYear table is at the month level, and we collected the YTD values at that level. (If we'd want to be precise we'd have to call that year-to-month).
Apart from the time aspect, the definition of the key affects all solutions that generate "extra" rows and require a GROUP BY to re-aggregate to the original granularity.

The Year


The ON-condition of the JOIN-solution and the WHERE-condition of the subquery-solution both rely on a condition that finds other rows in the same year, and the window function-solution uses the year in its PARTITION BY-clause.

It is usually quite simple to extract the year from a date, date/time or timestamp. In Synapse Analytics or MS SQL one can use the DATEPART or YEAR function to do this.

The UNION-solution has no direct dependency on the year.

The preceding rows


The need to find the preceding rows applies to all solutions that use the year to find the rows to apply the YTD calculation on. In our samples, this could all be solved using the SalesMonth column.

Again, it are the JOIN-solution and subquery-solution that used it in their condition, whereas the window function-solution uses it in its ORDER BY-clause.

In this case, the fix is more straighforward then with the year: instead of the month column, these solutions can simply use the date or date/time column directly. No conversion or datepart extraction is required.

Lowest granularity


The granularity is of special concern to the UNION-solution. The solution relies on an exhaustive and static enumeration of all possible future dates within the year. Already at the month level, this already required a lot of manual code.

Below the month, the next level would be day. While it would in theory be possible to extend the solution to that level, it is already bordering the impractible at the month level.

The Key


The key definition affects both the JOIN-solution and the UNION-solution, as that both require a GROUP BY over the key.

Year-to-Date on Synapse Analytics 4: Using UNION and GROUP BY

For one of our Just-BI customers we implemented a Year-to-Date calculation in a Azure Synapse Backend. We encountered a couple of approaches and in this series I'd like to share some sample code, and discuss some of the merits and benefits of each approach.

TL;DR: A Year-to-Date solution based on a SUM() window function is simple to code and maintain as well as efficient to execute. This as compared to a number of alternative implementations, namely a self-JOIN (combined with a GROUP BY), a subquery, and a UNION (also combined with a GROUP BY).

Note: this is the 4th post in a series. (While our use case deals with Azure Synapse, most of the code will be directly compatible with other SQL Engines and RDBMS-es.)

Using a UNION


We mentioned how the solution with the JOIN relates each row of the main set with a subset of "extra" rows over which the YTD value is calculated by aggregating over the key of the main set using a GROUP BY.

It may not be immediately obvious, but we can also use the SQL UNION (or rather, UNION ALL) operator to generate such a related subset. Just like with the JOIN-solution, this can then be aggregated using GROUP BY. An example will help to explain this:
select      SalesYear
,           SalesMonth
,           sum(SumOfSalesAmount)      as SumOfSalesAmount
,           sum(YtdOfSumOfSalesAmount) as YtdOfSumOfSalesAmount
from (
    select  SalesYear
    ,       SalesMonth
    ,       SumOfSalesAmount
    ,       SumOfSalesAmount           as YtdOfSumOfSalesAmount
    from    SalesYearMonth
    union all
    -- JANUARY
    select  SalesYear
    ,       SalesMonth + 1            -- february
    ,       null
    ,       SumOfSalesAmount
    from    SalesYearMonth
    where   SalesMonth = 1
    union all
    select  SalesYear
    ,       SalesMonth + 2            -- march
    ,       null
    ,       SumOfSalesAmount
    from    SalesYearMonth
    where   SalesMonth = 1
    union all
    
    ... and so on, all for JANUARY ...

    union all
    select  SalesYear
    ,       SalesMonth + 11            -- december
    ,       null
    ,       SumOfSalesAmount
    from    SalesYearMonth
    where   SalesMonth = 1
    union all
    -- FEBRUARY
    select  SalesYear
    ,       SalesMonth + 1            -- march
    ,       null
    ,       SumOfSalesAmount
    from    SalesYearMonth
    where   SalesMonth = 2
    union all
    
    ... and so on, for the rest of FEBRUARY, 
        and then again for MARCH, APRIl, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER...   

    -- NOVEMBER
    select  SalesYear
    ,       SalesMonth + 1            -- december
    ,       null
    ,       SumOfSalesAmount
    from    SalesYearMonth
    where   SalesMonth = 11
) Sales
group by    SalesYear
,           SalesMonth

Duplicating metric-data so it contributes to the following months


In the top of the UNION we simply provide the entire resultset from SalesYearMonth, reporting the SumOfSalesAmount-metric as is, but also copying it to YtdSumOfSalesAmount. The other parts of the UNION are used to selectively duplicate the data for the SumOfSalesAmount-metric into the YtdSumOfSalesAmount, so that its data contributes to the YtdSumOfSalesAmount for all following months.

We start by grabbing january's data by applying the condition that demands that the SalesMonth equals 1:
-- JANUARY
select  SalesYear
,       SalesMonth + 1            -- february
,       null
,       SumOfSalesAmount
from    SalesYearMonth
where   SalesMonth = 1
union all
... repeat to duplicate january's data into february, march, and so on all the way up to december...
. This is done for a total of 11 times, each time adding 1, 2, 3 and so on - all the way up to 11 - to the SalesMonth attribute. This ensures january's data, as captured by the condition in the WHERE clause, is reported also in february (SalesMonth + 1), march (SalesMonth + 2), and so on, all the way up to december (SalesMonth + 11).

After the string of UNIONs for january appear more parts to duplicate the data also for february and all following months: -- FEBRUARY select SalesYear , SalesMonth + 1 -- march , null , SumOfSalesAmount from SalesYearMonth where SalesMonth = 02 union all ... repeat to duplicate february's data into march, april, and so on all the way up to december... . Again, february's data is selected by applying the condition
where   SalesMonth = 2
, and this happens now 10 times, again adding a number to the SalesMonth so it is duplicated to march, april, may, all the way up to december - in other words, all months following february.

What we thus did for january and februry is repeated for march, april, and so on for all months up to november. November is the last month we need to do this for: November's data still needs to be copied to december, but as december is the last month, that data only needs to be counted in december itself.

While it may seem wasteful to duplicate all this data, it really is not that different in that respect from the other solutions we've seen so far. It's just that now it's really in your face, because there is a pretty direct correspondence between the SQL code and the data sets that are being handled. The JOIN and subquery solutions hande similar amounts of data, it's just achieved with way less code, and in a far more implicit manner.

Original metric is retained


Note that the original metric also computes correctly, because the parts of the union only duplicate the data to the YTD column. The union parts that duplicate the data to the subsequent months select a NULL for the original metric. So the data for the original metric is never duplicated, and thus retains its normal value.

Drawbacks to the UNION-solution


The main drawback to the UNION-solution is its maintainability. A lot of code is required, far more than for any of the methods we have seen so far. Despite the indiviual patterns are simple (condition to get one month, adding a number to project that data to a future month), it is suprisingly easy to make a little mistake somewhere

We just argued that this solution is not so much different from the JOIN solution, but that remark only pertains to how the calculation is performed. The JOIN-solution generates the data it operates upon dynamically and declaratively; the UNION solution does this statically and explicitly. This is also why it is impossible to generalize ths approach for any arbitrary JOIN: YTD is a special case, because we know exactly how often we should duplicate the data as this is dictated by the cyclical structure of our calendar.

Next installment: Solution 4 - window functions


In the next installment we will present and discuss a solution based on a window functions.

Year-to-Date on Synapse Analytics 3: Using a Subquery

For one of our Just-BI customers we implemented a Year-to-Date calculation in a Azure Synapse Backend. We encountered a couple of approaches and in this series I'd like to share some sample code, and discuss some of the merits and benefits of each approach.

TL;DR: A Year-to-Date solution based on a SUM() window function is simple to code and maintain as well as efficient to execute. This as compared to a number of alternative implementations, namely a self-JOIN (combined with a GROUP BY), a subquery, and a UNION (also combined with a GROUP BY).

Note: this is the 3rd post in a series. (While our use case deals with Azure Synapse, most of the code will be directly compatible with other SQL Engines and RDBMS-es.)

Using a subquery


We can also think of YTD calculation as a separate query that we perform for each row of the SalesYearMonth table. While this does imply a row-by-row approach, we can still translate this easily to pure SQL by creating an expression in the SELECT-list, which uses a subquery to calculate the YTD value for the current row:
select      SalesOriginal.SalesYear
,           SalesOriginal.SalesMonth
,           SalesOriginal.SalesAmount
,           (
                select sum(SalesYtd.SalesAmount)
                from   SalesYearMonth as SalesYtd
                where  SalesYtd.SalesYear   = SalesOriginal.SalesYear
                and    SalesYtd.SalesMonth <= SalesOriginal.SalesMonth
            ) as SalesYtd
from        SalesYearMonth as SalesOriginal
There's a similarity with the JOIN-solution, in that we use the SalesYearMonth table twice, but in different roles. In the JOIN-solution both appeared on one side of the JOIN keyword and we used the aliases OriginalSales and YtdSales to be able to keep them apart. In the subquery approach, the distinction between these two different instances of the SalesYearMonth table is more explicit: the main instance of the SalesYearMonth table occurs in the FROM-clause, and the one for the YTD calculation occurs in the SELECT-list.

Also similar to the JOIN solution is the condition to tie the set for the YTD calculation to the main query using the SalesYear and SalesMonth columns. Such a subquery is referred to as a correlated subquery.

As for any differences with the JOIN solution: In the condition, the only difference is the left/right placement of SalesOriginal and SalesYtd, which is chosen only by order of appearance in the query but functionally completely equivalent. The most striking difference between the JOIN solution and the subquery is the absence of the GROUP BY-list in the latter.

Drawbacks of the subquery


As we had much to complain about the GROUP BY-list in the JOIN solution, it might seem that the subquery solution is somehow "better". However, a solution with a correlated subquery in general tends to be slower than a JOIN solution. Whether this is actually the case depends on on many variables and you'd really have to check it against your SQL engine and datasets.

Another drawback of the subquery solution becomes clear when we want to calculate the YTD for multiple measures. Our example only has one SalesAmount measure, but in this same context we can easily imagine that we also want to know about price, discount amounts, tax amounts, shipping costs, and so on.

In the JOIN solution, we would simply add any extra measures to the select list, using MAX() (or MIN() or AVG()) to obtain the original value, and SUM() to calculate its respective YTD value: As long as it's over the same set, the JOIN, its condition, and even the GROUP BY-list would remain the same, no matter for how many different measures we would add a YTD calculation.

This is very different in the subquery case. Each measure for which you need a YTD calculation would get its own subquery. Even though the condition would be the same for each such YTD calculation, you would still need to repeat the subquery code - one for each YTD measure.

Next installment: Solution 3 - using a UNION


In the next installment we will present and discuss a solution based on a UNION and a GROUP BY.

Year-to-Date on Synapse Analytics 2: Using a self-JOIN and GROUP BY

For one of our Just-BI customers we implemented a Year-to-Date calculation in a Azure Synapse Backend. We encountered a couple of approaches and in this series I'd like to share some sample code, and discuss some of the merits and benefits of each approach.

TL;DR: A Year-to-Date solution based on a SUM() window function is simple to code and maintain as well as efficient to execute. This as compared to a number of alternative implementations, namely a self-JOIN (combined with a GROUP BY), a subquery, and a UNION (also combined with a GROUP BY).

Note: this is the 2nd post in a series. For sample data and setup, please see the first post in this series. (While our use case deals with Azure Synapse, most of the code will be directly compatible with other SQL Engines and RDBMS-es.)

Using a self-JOIN


The recipe for the set-oriented approach can be directly translated to SQL:
select      SalesOriginal.SalesYear
,           SalesOriginal.SalesMonth
,           max(SalesOriginal.SalesAmount) as SalesAmount
,           sum(SalesYtd.SalesAmount)      as SalesYtd
from        SalesYearMonth as SalesOriginal
inner join  SalesYearMonth as SalesYtd
on          SalesOriginal.SalesYear  = SalesYtd.SalesYear
and         SalesOriginal.SalesMonth >= SalesYtd.SalesMonth
group by    SalesOriginal.SalesYear
,           SalesOriginal.SalesMonth

The self-JOIN


In our discussion of the set-oriented approach we mentioned combining the rows from the table with each other to produce all different combinations. In the code sample about, the JOIN-clause takes care of that aspect.

As you can see, the SalesYearMonth table appears twice: on the left hand and on the right hand of the JOIN-keyword, but using different aliases: SalesOriginal and SalesYtd. It is a so-called self-join.

Even though both aliases refer to an instance of the same SalesYearMonth base table, each has a very different role. We can think of the one with the SalesOriginal alias as really the SalesYearMonth table itself. The SalesYtd alias refers to an instance of the SalesYearMonth table that, for any given row from SalesOriginal, represents a subset of rows that chronologically precedes the row from SalesOriginal.

The ON-clause that follows controls which combinations should be retained: for each particular row of SalesOriginal we only want to consider rows from SalesYtd from the same year, which is why the first predicate in the ON-clause is:
SalesOriginal.SalesYear  = SalesYtd.SalesYear
Within that year, we only want to consider rows that precede it chronologically, and that explains the second predicate:
SalesOriginal.SalesMonth >= SalesYtd.SalesMonth

GROUP BY and SUM()


It is is important to realize the JOIN is only half of the solution.

While the JOIN takes care of gathering and combining all related rows necessary to compute the YTD value, the actual calculation is done by the SUM() function in the SELECT-list, and the GROUP BY defines which rows should be taken together to be summed.

In summary:
  • the JOIN generates new rows by combining rows from its left-hand table with the rows from its right-hand table, bound by the condition in the ON-clause.
  • The GROUP BY partitions the rows into subsets having the same combinations of values for SalesYear and SalesMonth.
  • The SUM() aggregates the rows in each SalesYear, SalesMonth partition, turning its associated set of rows into one single row, while adding the values of the SalesAmount column together.
Note that the columns in the GROUP BY list are qualified by the SalesOriginal alias - and not SalesYtd. Also note that the GROUP BY columns form the key of the original SalesYearMonth table - together they uniquely identify a single row from the SalesYearMonth table. This is not a coincidence: it expresses precisely that SalesOriginal really has the role of being just itself - the SalesYearMonth table.

What about the other columns?


The GROUP BY affects treatment of the non-key columns as well. In this overly simple example, we had only one other column - OriginalSales.SalesAmount.

(Note that this is different from YtdSales.SalesAmount, which we aggregated using SUM() to calculate the YTD value)

Since OriginalSales.SalesAmount comes from the SalesOriginal instance of the SalesYearMonth table, we can reason that after the GROUP BY on the key columns SalesYear and SalesMonth, there must be exactly one SalesAmount value for each distinct combination of SalesYear and SalesMonth. In other words, SalesAmount is functionally dependent on SalesYear and SalesMonth.

Some SQL engines are smart enough to realize this and will let you refer to any expression that is functionally dependent upon the expressions in the GROUP BY-list in the SELECT-list. Unfortunately, Synapse and MS SQL Server are not among these and if we try we will get an Error:
Msg 8120, Level 16, State 1, Line 11
Column 'Sales.SalesAmount' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
The error message suggets we can do two things to solve it:
  • either we aggregate by wrapping the SalesOriginal.SalesAmount-expression into some aggregate function
  • or we expand the GROUP BY-list and add the SalesOriginal.SalesAmount-expression there.
To me, neither feels quite right.

SalesAmount is clearly intended as a measure, and it feels weird to treat them the same as the attributes SalesYear and SalesMonth. So adding it to the GROUP BY-list feels like the wrong choice. Besides, it also makes the code less maintainable, as each such column will now appear twice: once in the SELECT-list, where we need it no matter what, and once again in the GROUP BY-list, just to satisfy the SQL engine.

So, if we don't want to put it in the GROUP BY-list, we are going to need to wrap it in an aggregate function. We just mentioned that SalesAmount is a measure and therefore that does not sound unreasonable. However, we have to be careful which one we choose.

One would normally use SalesAmount as an additive measure and be able to use SUM() for that. But here, in this context, SUM() is definitily the wrong choice!

All we want to do is to "get" back" whatever value we had for SalesAmount, in other words, unaffected by the whole routine of join-and-then-aggregate, which we did only to calculate the YTD value. The "extra" rows generated by the JOIN are only needed to do the YTD calculation and should not affect any of the other measures. Using SUM() would simply add the SalesAmount just as many times as there are preceding rows in the current year, which simply does not have any meaningful application.

What we want instead is to report back the original SalesAmount for any given SalesYear, SalesMonth combination. We just reasoned that there will be just one distinct SalesOriginal.SalesAmount value for any combination of values in SalesOriginal.SalesYear, SalesOriginal.SalesMonth, and it would be great if we had an aggregate function that would simply pick the SalesOriginal.SalesAmount value from any of those rows. To the best of my knowledge, no such aggregate function exists in MS SQL Server or Synapse Analytics.

We can use MAX() or MIN(), or even AVG(). While this would all work and deliver the intended result, it still feels wrong as it seems wasteful to ask the SQL engine to do some calculation on a set of values while it could pick just any value.

Next installment: Solution 2 - using a subquery


In the next installment we will present and discuss a solution based on a subquery.

DuckDb Performance: min, max, and median vs quantile

I was playing with some classical statistics in DuckDB and I ran into something I'd like to share. It's about the measures minimum,...