Sunday, February 2, 2020

EntitySpaces 2020.2.1 Released

In this release of EntitySpaces the "esAlias" class is introduced and the OVER Clause is officially released for Microsoft SQL Server. An instance of the esAlias class can be obtained when aliasing a column via the .As() syntax. We are going to introduce these new features by demonstrating how they come to the rescue in query we need to write.

The Problem


We need to obtain a result set to see the sales amount for each company per year. This part is easy, we can use a simply GroupBy() to obtain the sales amount per each company per year. However, we also need a running cumulative amount per company and the total amount of sales per the the entire company, and for this we need to use the new OVER clause syntax. Notice below how the Cumulative Amount is adding the Amount per each year of the company, while the Total Amount.
                                              Total            Cumulative   Total
CompanyName                          Period   Items  Amount    Amount       Amount 
------------------------------------ -------  ------ --------  -----------  -------
Alfreds Futterkiste                  1997     6      2022.50   2022.50      4273.00
Alfreds Futterkiste                  1998     6      2250.50   4273.00      4273.00
Ana Trujillo Emparedados y helados   1996     2        88.80     88.80      1402.95
Ana Trujillo Emparedados y helados   1997     4       799.75    888.55      1402.95
Ana Trujillo Emparedados y helados   1998     4       514.40   1402.95      1402.95
Antonio Moreno Taquería              1996     1       403.20    403.20      7023.97
Antonio Moreno Taquería              1997     14     5960.77   6363.97      7023.97
Antonio Moreno Taquería              1998     2       660.00   7023.97      7023.97

In fact, we need a nested query to get the entire result set. So we'll attack this query in two steps.

Step 1


Let's create what will become our nested query. We need to obtain the CompanyName, Period, TotalItems, and Amount grouped by CompanyName and the YEAR. We start from the Orders table and join with the Customers table to get the CompanyName and the Order Details table to get the line item information for each order.
OrdersCollection coll = new OrdersQuery("o", out var o)
.InnerJoin<CustomersQuery>("c", out var c).On(c.CustomerID == o.CustomerID)
.InnerJoin<OrderDetailsQuery>("od", out var od).On(od.OrderID == o.OrderID)
.Select
(
    // We're going to grab the aliased columns here for re-use in the outer query later
    c.CompanyName.As("CompanyName"),
    o.OrderDate.DatePart("year").As("Period"),
    o.Count().As("TotalItems"),
    ((1.00M - od.Discount) * od.UnitPrice * od.Quantity).Cast(esCastType.Decimal, 19, 2)
        .Sum().Round(2).As("Amount")
)
.GroupBy(c.CompanyName, o.OrderDate.DatePart("year"))
.OrderBy(c.CompanyName.Ascending, o.OrderDate.DatePart("year").Ascending)
.ToCollection<OrdersCollection>();

The SQL generated by the above query is as follows:
SELECT
   c.[CompanyName] AS 'CompanyName',
   DATEPART(year, o.[OrderDate]) AS 'Period',
   COUNT(*) AS 'TotalItems',
   CAST(SUM(ROUND((((1.00 - od.[Discount]) * od.[UnitPrice]) * od.[Quantity]), 2)) 
      AS decimal(19, 2)) AS 'Amount' 
FROM
   [Orders] o 
   INNER JOIN [Customers] c ON c.[CustomerID] = o.[CustomerID] 
   INNER JOIN [Order Details] od ON od.[OrderID] = o.[OrderID] 
GROUP BY c.[CompanyName], DATEPART(year, o.[OrderDate]) 
ORDER BY c.[CompanyName] ASC, DATEPART(year, o.[OrderDate]) ASC

The result set is as follows:

                                                     Total
CompanyName                              Period      Items       Amount
---------------------------------------- ----------- ----------- -----------
Alfreds Futterkiste                      1997        6           2022.50
Alfreds Futterkiste                      1998        6           2250.50
Ana Trujillo Emparedados y helados       1996        2             88.80
Ana Trujillo Emparedados y helados       1997        4            799.75
Ana Trujillo Emparedados y helados       1998        4            514.40
Antonio Moreno Taquería                  1996        1            403.20
Antonio Moreno Taquería                  1997        14          5960.77
Antonio Moreno Taquería                  1998        2            660.00

Step 2


Okay, so step one is complete. But now we need to obtain a cumulative running total sales amount for the company as each year progresses and we need the total amount of sales for the entire company. These two values are not "per" year so we are going to nest the query above into yet another query and use the OVER clause syntax to get the two additional values. Let's see how that is done.

Don't let this query overwhelm you. If you could collapse the nested query all you would see is the Select() and the OrderBy() on the nested queries columns. Take a close look at how we obtain the "CumulativeAmount" and "TotalAmount" values. Also notice the use of esAlias and that it's knows what alias to use, in this case "sub".

Our full query.
// We grab these aliases in the nested query via "out" parameters
esAlias aliasCompany = null, aliasPeriod = null, aliasAmount = null, aliasItemCount = null;

OrdersCollection coll = new OrdersQuery("q", out var q)
.From<OrdersQuery>(out var sub, () => // mimic a CTE
{
    // Nested Query from Step 1 above
    return new OrdersQuery("o", out var o)
    .InnerJoin<CustomersQuery>("c", out var c).On(c.CustomerID == o.CustomerID)
    .InnerJoin<OrderDetailsQuery>("od", out var od).On(od.OrderID == o.OrderID)
    .Select
    (
        // We're going to grab the aliased columns here for re-use in the outer query later
        c.CompanyName.As("CompanyName", out aliasCompany),
        o.OrderDate.DatePart("year").As("Period", out aliasPeriod),
        o.Count().As("TotalItems", out aliasItemCount),
        ((1.00M - od.Discount) * od.UnitPrice * od.Quantity).Cast(esCastType.Decimal, 19, 2)
            .Sum().Round(2).As("Amount", out aliasAmount)
    )
    .GroupBy(c.CompanyName, o.OrderDate.DatePart("year"));
}).As("sub")
// Now act on "sub" query columns using the OVER syntax
.Select(
   aliasCompany(), aliasPeriod(), aliasAmount(), aliasItemCount(),  
   q.Over.Sum(aliasAmount()).PartitionBy(aliasCompany()).OrderBy(aliasPeriod().Ascending).Rows
      .UnBoundedPreceding.As("CumulativeAmount"),
   q.Over.Sum(aliasAmount()).PartitionBy(aliasCompany()).As("TotalAmount")
)
.OrderBy(aliasCompany().Ascending, aliasPeriod().Ascending)
.ToCollection<OrdersCollection>();

if(coll.Count > 0)
{
    // we loaded data
}
The SQL generated by the above query is as follows:

SELECT
   sub.[CompanyName],
   sub.[Period],
   sub.[Amount],
   sub.[TotalItems],
   -- Notice the OVER() clause usage below
   SUM([Amount]) OVER( PARTITION BY [CompanyName] ORDER BY sub.[Period] ASC ROWS UNBOUNDED PRECEDING )
       AS 'CumulativeAmount',
   SUM([Amount]) OVER( PARTITION BY [CompanyName] ) AS 'TotalAmount' 
FROM
   (
      SELECT
         c.[CompanyName] AS 'CompanyName',
         DATEPART(year, o.[OrderDate]) AS 'Period',
         COUNT(*) AS 'TotalItems',
         CAST(SUM(ROUND((((1.00 - od.[Discount]) * od.[UnitPrice]) * od.[Quantity]), 2)) AS decimal(19, 2))
             AS 'Amount' 
      FROM [Orders] o 
      INNER JOIN [Customers] c ON c.[CustomerID] = o.[CustomerID] 
      INNER JOIN [Order Details] od ON od.[OrderID] = o.[OrderID] 
      GROUP BY c.[CompanyName], DATEPART(year, o.[OrderDate])
   )
   AS sub 
ORDER BY sub.[CompanyName] ASC, sub.[Period] ASC
Our final result set is as follows:

                                              Total            Cumulative   Total
CompanyName                          Period   Items  Amount    Amount       Amount 
------------------------------------ -------  ------ --------  -----------  -------
Alfreds Futterkiste                  1997     6      2022.50   2022.50      4273.00
Alfreds Futterkiste                  1998     6      2250.50   4273.00      4273.00
Ana Trujillo Emparedados y helados   1996     2        88.80     88.80      1402.95
Ana Trujillo Emparedados y helados   1997     4       799.75    888.55      1402.95
Ana Trujillo Emparedados y helados   1998     4       514.40   1402.95      1402.95
Antonio Moreno Taquería              1996     1       403.20    403.20      7023.97
Antonio Moreno Taquería              1997     14     5960.77   6363.97      7023.97
Antonio Moreno Taquería              1998     2       660.00   7023.97      7023.97


That is some pretty powerful functionality that EntitySpaces is packing. Both the full SQL Over Clause syntax along with the full SQL Window Frame syntax are supported.

3 comments:

  1. Hi Mike
    First of all, I want you and your family to be safe and healthy.

    It was a big surprise to know that if EntitySpaces is reborn, it's great code.
    Years ago, I worked with Mygeneration and earlier commercial versions of ES.
    Old EnSp allowed me to build a great project, and it was very easy to schedule complex SQL queries.
    Furthermore, the feature for serializing objects was a killer for remote interoperability of applications with a centralized database, over the Internet.

    Now, I have installed the new ES studio and I am having some trouble registering the steps of a configuration in a project. The app freeze some times.
    The generation code its ok
    I also have the old version of ES studio installed.

    If you are agree, I'll like to help you in the reborn of ES. In QA and difussion over RRSS. Obiously like a volunteer.

    I live in Viña del mar, Chile, south america
    I think that ES will be very helpful for independet developers.

    Here I let you a video of my YT channel, where I use the old ES in a demo
    https://youtu.be/PkXluo-QMGI

    Well, I say goodby. I hope have news about you.
    Regards and stay safe.
    Paul C.

    ReplyDelete
  2. Very cool... How do you view the generated SQL from the ES query? I can't seem to find any methods to do so.

    ReplyDelete
  3. I love you, Mike
    I love ES
    I love Tiraggo

    Take care,

    Best regards,

    ReplyDelete