Get both min/max value for 24 hours of each day between two dates

Viewed 253

I have Two table [FuelTankHistory] and [Product]

CREATE TABLE [dbo].[FuelTankHistory](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [FuelLevel] [decimal](18, 2) NULL,
    [FuelLevelTime] [datetime] NULL,
    [TankId] [bigint] NULL,
    [Volume] [decimal](18, 2) NULL,
    [ProductId] [bigint] NOT NULL
)
GO

CREATE TABLE [dbo].[Product](
    [ProductId] [bigint] IDENTITY(1,1) NOT NULL,
    [ProductName] [varchar](50) NULL,
    [IsActive] [bit] NULL)
GO

click here to view on SQL Fiddle. Now I want to Generate a report Like Below (sale report for petrol)

ReportDate  sale_petrol
2021-08-01  113.000
2021-08-02  348.000
2021-08-03  -134.000
2021-08-04  519.000
2021-08-05  0.000

I my case tank 1 and 2 are petrol tanks, now the tricky thing is I need to get sale of petrol between some date range.

if the date is '2021-08-02'

I want to get

first level between '2021-08-01 13:00:00' and '2021-08-02 13:00:00' (opening_stock of 24 hour)

and

last level between '2021-08-01 13:00:00' and '2021-08-02 13:00:00' (closing_stock of 24 hour)

for every tank, then get total sale done from every tank which is (opening_stock - closing_stock). then get SUM of all tanks (in my case petrol tanks), in my case which is equal to 348.000 !

I have created a query to do this. but its taking too long to generate report for a month because history table have millions of records and I don't think looping is a good way to do this.

If you are facing any problem to understand my problem, please visit SQL Fiddle by clicking the given link

Schema and data

CREATE TABLE [dbo].[FuelTankHistory](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [FuelLevel] [decimal](18, 2) NULL,
    [FuelLevelTime] [datetime] NULL,
    [TankId] [bigint] NULL,
    [Volume] [decimal](18, 2) NULL,
    [ProductId] [bigint] NOT NULL
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Product](
    [ProductId] [bigint] IDENTITY(1,1) NOT NULL,
    [ProductName] [varchar](50) NULL,
    [IsActive] [bit] NULL)
GO
SET IDENTITY_INSERT [dbo].[FuelTankHistory] ON 
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (1, CAST(124.00 AS Decimal(18, 2)), CAST(N'2021-08-01T01:05:12.000' AS DateTime), 1, CAST(120.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (2, CAST(665.00 AS Decimal(18, 2)), CAST(N'2021-08-01T12:05:12.000' AS DateTime), 2, CAST(333.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (3, CAST(879.00 AS Decimal(18, 2)), CAST(N'2021-08-01T07:05:12.000' AS DateTime), 2, CAST(345.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (4, CAST(675.00 AS Decimal(18, 2)), CAST(N'2021-08-01T18:05:12.000' AS DateTime), 2, CAST(567.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (5, CAST(434.00 AS Decimal(18, 2)), CAST(N'2021-08-01T20:05:12.000' AS DateTime), 2, CAST(744.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (6, CAST(552.00 AS Decimal(18, 2)), CAST(N'2021-08-01T05:05:12.000' AS DateTime), 2, CAST(346.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (7, CAST(835.00 AS Decimal(18, 2)), CAST(N'2021-08-02T08:05:12.000' AS DateTime), 1, CAST(643.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (8, CAST(246.00 AS Decimal(18, 2)), CAST(N'2021-08-02T04:05:12.000' AS DateTime), 1, CAST(897.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (9, CAST(477.00 AS Decimal(18, 2)), CAST(N'2021-08-02T13:05:12.000' AS DateTime), 2, CAST(675.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (10, CAST(235.00 AS Decimal(18, 2)), CAST(N'2021-08-03T23:05:12.000' AS DateTime), 1, CAST(67.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (11, CAST(88.00 AS Decimal(18, 2)), CAST(N'2021-08-03T19:05:12.000' AS DateTime), 2, CAST(87.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (12, CAST(345.00 AS Decimal(18, 2)), CAST(N'2021-08-03T02:05:12.000' AS DateTime), 1, CAST(67.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (13, CAST(255.00 AS Decimal(18, 2)), CAST(N'2021-08-03T07:05:12.000' AS DateTime), 2, CAST(65.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (14, CAST(433.00 AS Decimal(18, 2)), CAST(N'2021-08-03T04:05:12.000' AS DateTime), 1, CAST(345.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (15, CAST(567.00 AS Decimal(18, 2)), CAST(N'2021-08-03T17:05:12.000' AS DateTime), 2, CAST(565.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (16, CAST(388.00 AS Decimal(18, 2)), CAST(N'2021-08-04T22:05:12.000' AS DateTime), 1, CAST(45.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (17, CAST(234.00 AS Decimal(18, 2)), CAST(N'2021-08-04T06:05:12.000' AS DateTime), 1, CAST(34.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (18, CAST(976.00 AS Decimal(18, 2)), CAST(N'2021-08-04T09:05:12.000' AS DateTime), 1, CAST(124.00 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (19, CAST(345.00 AS Decimal(18, 2)), CAST(N'2021-08-04T11:05:12.000' AS DateTime), 2, CAST(45.00 AS Decimal(18, 2)), 1)
GO
SET IDENTITY_INSERT [dbo].[FuelTankHistory] OFF
GO
SET IDENTITY_INSERT [dbo].[Product] ON 
GO
INSERT [dbo].[Product] ([ProductId], [ProductName], [IsActive]) VALUES (1, N'Petrol', 1)
GO
INSERT [dbo].[Product] ([ProductId], [ProductName], [IsActive]) VALUES (2, N'Diesel', 1)
GO
INSERT [dbo].[Product] ([ProductId], [ProductName], [IsActive]) VALUES (3, N'CNG', 1)
GO
SET IDENTITY_INSERT [dbo].[Product] OFF
GO

My Query to generate Report

declare @dt1 date='2021-08-01'
declare @dt2 date='2021-08-22'
DECLARE @tbl_temp_max TABLE(max_fuelLevel decimal(18,3),FuelStationTankId bigint,ReportDate date)
DECLARE @tbl_temp_min TABLE(min_fuelLevel decimal(18,3),FuelStationTankId bigint,ReportDate date)
-- is there any way to get both max and min in single query?
-- and do the same thing without using loop
WHILE ( @dt1 <= @dt2)
BEGIN
    -- declare loop variables
    DECLARE @date2 datetime= CAST(CAST(@dt1 AS DATE) AS DATETIME)
    set @date2 = DATEADD(HOUR, 13,@date2)
    DECLARE @date1 datetime= DATEADD(DAY, -1, @date2)
    --------------- Get Opening -------------------------------------------------------------------------------------------------
    insert into @tbl_temp_min
    select tbl.[FuelLevel],tbl.TankId ,@dt1
    from [dbo].[FuelTankHistory] tbl
    inner join (
    select min(t.[FuelLevelTime]) [date],t.TankId
      from [dbo].[FuelTankHistory] t
      where t.[FuelLevelTime] between @date1 and @date2 and t.TankId in (1,2) and t.ProductId=1
      group by t.TankId
    ) temp on tbl.TankId=temp.TankId and temp.[date]=tbl.[FuelLevelTime]
    --------------- Get Closing -------------------------------------------------------------------------------------------------
    insert into @tbl_temp_max
   select tbl.[FuelLevel],tbl.TankId ,@dt1
    from [dbo].[FuelTankHistory] tbl
    inner join (
    select max(t.[FuelLevelTime]) [date],t.TankId
      from [dbo].[FuelTankHistory] t
      where t.[FuelLevelTime] between @date1 and @date2 and t.TankId in (1,2) and t.ProductId=1
      group by t.TankId
    ) temp on tbl.TankId=temp.TankId and temp.[date]=tbl.[FuelLevelTime]
    ----------------------------------------------------------------------------------------------------------------
    SET @dt1= DATEADD(DAY, 1, @dt1)
END

select ReportDate,sum(sale) sale_petrol from
(select [max].*,ISNULL([min].min_fuelLevel, 0 ) [min_fuelLevel],(ISNULL([max].max_fuelLevel, 0 )-ISNULL([min].min_fuelLevel, 0 )) sale from @tbl_temp_max [max]
full join @tbl_temp_min [min] on [max].ReportDate=[min].ReportDate and [max].FuelStationTankId=[min].FuelStationTankId) total
group by ReportDate;

MODIFIED CONTENT

One more thing is I want the opening stock of current date should be equal to closing stock of previous date. I have millions of data which have hundreds of records for a minute. and this query skips one or more record while getting opening stock for current day.For example 2021-08-01 12:59:45 level is 100 & 2021-08-01 13:00:01 level is 90. now previous day closing level is 100 and current day opening level is 90 which is not acceptable. current day opening should be 100 also.

5 Answers

Based on your description, you can get get the first and last volumes on each date. The calculation is a bit complicated, because you need to subtract 13 hours to get the date you really want.

Then aggregate:

select  DATEADD(day,1,FuelLevelDate) AS ReportDate, sum(last_fuel_level - first_fuel_level)
from (select distinct fth.TankId, v.FuelLevelDate,
             first_value(fth.FuelLevel) over (partition by fth.TankId, v.FuelLevelDate order by fth.FuelLevelTime) as first_fuel_level,
             first_value(fth.FuelLevel) over (partition by fth.TankId, v.FuelLevelDate order by fth.FuelLevelTime desc) as last_fuel_level
      from FuelTankHistory fth cross apply
           (values (convert(date, dateadd(hour, -13, fth.FuelLevelTime)))
           ) v(FuelLevelDate)
      ) fth
group by FuelLevelDate;

Here's a working and, I hope, faster code: http://sqlfiddle.com/#!18/b554a/64

Because of the platform (sqlfiddle) constraints, I could not use the input as variables, hence they are embedded in the code.

Also, the day column is off by a day and embeds the time.

Please, if it's a valid solution, after you work on these nuisances, feel free to edit this answer with the final conde.

The complete solution is:

WITH Dates(day) AS 
(
    SELECT CAST('20210731 13:00:00' as DATETIME) as day
    --
    UNION ALL
    --
    SELECT
      DATEADD(day, 1, day) as day
    FROM Dates
    WHERE day <= '20210822'
)
select
  agg.day,
  sum(
    agg.FuelLevelEnd -
    agg.FuelLevelStart
  )
from
  (
    select distinct
        dt.day,
        t.TankId,
        --
        first_value(fth.FuelLevel)
          over(
            partition by dt.day, t.TankId
            order by fth.FuelLevelTime
          ) as FuelLevelStart,
        --
        first_value(fth.FuelLevel)
          over(
            partition by dt.day, t.TankId
            order by fth.FuelLevelTime desc
          ) as FuelLevelEnd
    from
      (
        Dates dt
          cross join(
            select distinct TankId
            from FuelTankHistory
          ) t
      )
        left outer join FuelTankHistory fth
        on(
              fth.TankId = t.TankId
          --
          and fth.FuelLevelTime >= dt.day
          and fth.FuelLevelTIme <  DATEADD(day, 1, dt.day)
        )
  ) agg
  group by agg.day

Which yields the expected result, and, as a bonus, NULL for missing ranges :-)

day result
2021-07-31T13:00:00Z 113
2021-08-01T13:00:00Z 348
2021-08-02T13:00:00Z -134
2021-08-03T13:00:00Z 519
2021-08-04T13:00:00Z 0
2021-08-05T13:00:00Z (null)
2021-08-06T13:00:00Z (null)

A solution can be the following which first obtains a single value of type DATE for the first and second half of the stock period and which is used for groupings through PARTITION BY, and then still correctly distinguish the beginning and the end of the interval due to proper sequencing maintained by the value FuelLevelTime of type DATETIME.

WITH FTH_tfix AS (
  SELECT Id, 
    CASE   
      /* proper sequence ordering maintained by FuelLevelTime */
      WHEN CAST(FuelLevelTime AS Time) >= '13:00:00' THEN 0   /* 1st half (today) */
      WHEN CAST(FuelLevelTime AS Time) <  '13:00:00' THEN -1  /* 2nd half (tomorrow) */
    END AS t_fix
  FROM FuelTankHistory),
FTH_tfixed AS (
  SELECT /* FTH.Id, */ 
    TankId, 
    FuelLevel,
    DATEADD(day, t_fix, CONVERT(DATE, FuelLevelTime)) AS t_fixed_day,
    FuelLevelTime
  FROM FuelTankHistory FTH
    INNER JOIN FTH_tfix ON FTH.Id = FTH_tfix.Id),
FTH_tfixed_start_end AS (
  SELECT 
    TankId, 
    t_fixed_day,
    /* FuelLevelTime is a Datetime and preserves sequence ordering in t_fixed_day ranges */
    FIRST_VALUE(FuelLevel) OVER(PARTITION BY TankId, t_fixed_day ORDER BY FuelLevelTime ASC ) AS OpeningStockAmount, 
    FIRST_VALUE(FuelLevel) OVER(PARTITION BY TankId, t_fixed_day ORDER BY FuelLevelTime DESC) AS ClosingStockAmount
  FROM FTH_tfixed),
FTH_final_helper AS (
  SELECT TankId, t_fixed_day, 
    MIN(OpeningStockAmount) AS OpeningStockAmount, 
    MIN(ClosingStockAmount) AS ClosingStockAmount
  FROM FTH_tfixed_start_end
  GROUP BY TankId, t_fixed_day) 
SELECT t_fixed_day, SUM(OpeningStockAmount - ClosingStockAmount) 
FROM FTH_final_helper
GROUP BY t_fixed_day

http://sqlfiddle.com/#!18/fcdab0/1

The OpeningStock of the Current Date should be equal to the Closing Stock of the previos Date.

This Query gives the Opening Stock for 2021-08-02 and the Closing Stock for 2021-08-01.

SELECT FuelLevel 
FROM [dbo].[FuelTankHistory]
WHERE [TankId] = 1
AND [FuelLevelTime] < '20210802 13:00:00'
ORDER BY FuelLevelTime DESC
OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY;

The following lists OpingStock, ClosingStock and Sale for every Date in Calendar and TankId in Tank.

SELECT date, TankId, OpeningStock, ClosingStock, OpeningStock - ClosingStock sale
FROM [dbo].[Calendar] c
CROSS APPLY (SELECT 1 UNION ALL SELECT 2) Tank(TankId)
CROSS APPLY (SELECT FuelLevel FROM [dbo].[FuelTankHistory]
            WHERE [TankId] = Tank.TankId
            AND   [FuelLevelTime] < DATEADD(day, -1, c.[Date])
            ORDER BY FuelLevelTime DESC
            OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) as t(OpeningStock)
CROSS APPLY (SELECT FuelLevel FROM [dbo].[FuelTankHistory]
            WHERE [TankId] = Tank.TankId
            AND   [FuelLevelTime] <  c.[Date]
            ORDER BY FuelLevelTime DESC
            OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) as t2(ClosingStock))
|                 date | TankId | OpeningStock | ClosingStock | sale |
|----------------------|--------|--------------|--------------|------|
| 2021-08-01T13:00:00Z |      1 |          124 |          835 | -711 |
| 2021-08-01T13:00:00Z |      2 |          665 |          434 |  231 |
| 2021-08-02T13:00:00Z |      1 |          835 |          433 |  402 |
| 2021-08-02T13:00:00Z |      2 |          434 |          255 |  179 |
| 2021-08-03T13:00:00Z |      1 |          433 |          976 | -543 |
| 2021-08-03T13:00:00Z |      2 |          255 |          345 |  -90 |
| 2021-08-04T13:00:00Z |      1 |          976 |          388 |  588 |
| 2021-08-04T13:00:00Z |      2 |          345 |          345 |    0 |
| 2021-08-05T13:00:00Z |      1 |          388 |          388 |    0 |
| 2021-08-05T13:00:00Z |      2 |          345 |          345 |    0 |
| 2021-08-06T13:00:00Z |      1 |          388 |          388 |    0 |
| 2021-08-06T13:00:00Z |      2 |          345 |          345 |    0 |
| 2021-08-07T13:00:00Z |      1 |          388 |          388 |    0 |
| 2021-08-07T13:00:00Z |      2 |          345 |          345 |    0 |
| 2021-08-08T13:00:00Z |      1 |          388 |          388 |    0 |
| 2021-08-08T13:00:00Z |      2 |          345 |          345 |    0 |
| 2021-08-09T13:00:00Z |      1 |          388 |          388 |    0 |
| 2021-08-09T13:00:00Z |      2 |          345 |          345 |    0 |
WITH a as (
  SELECT date, TankId, OpeningStock, ClosingStock, OpeningStock - ClosingStock sale
  FROM [dbo].[Calendar] c
  CROSS APPLY (SELECT 1 UNION ALL SELECT 2) Tank(TankId)
  CROSS APPLY (SELECT FuelLevel FROM [dbo].[FuelTankHistory]
               WHERE [TankId] = Tank.TankId
               AND   [FuelLevelTime] < DATEADD(day, -1, c.[Date])
               ORDER BY FuelLevelTime DESC
               OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) as t(OpeningStock)
  CROSS APPLY (SELECT FuelLevel FROM [dbo].[FuelTankHistory]
               WHERE [TankId] = Tank.TankId
               AND   [FuelLevelTime] <  c.[Date]
               ORDER BY FuelLevelTime DESC
               OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) as t2(ClosingStock))
SELECT date ReportDate, sum(sale) sale_petrol 
FROM a
WHERE date BETWEEN '20210801 00:00:00' AND '20210810 00:00:00'
GROUP BY date;
|           ReportDate | sale_petrol |
|----------------------|-------------|
| 2021-08-01T13:00:00Z |        -789 |
| 2021-08-02T13:00:00Z |        -480 |
| 2021-08-03T13:00:00Z |         581 |
| 2021-08-04T13:00:00Z |        -633 |
| 2021-08-05T13:00:00Z |         588 |
| 2021-08-06T13:00:00Z |           0 |
| 2021-08-07T13:00:00Z |           0 |
| 2021-08-08T13:00:00Z |           0 |
| 2021-08-09T13:00:00Z |           0 |

Changes to the Schema

[dbo].[Calendar] is created with the following Query

WITH CALENDAR(date) AS (
      SELECT CAST('20210630 13:00:00' as DATETIME) as date
      UNION ALL SELECT DATEADD(day, 1, date) as date
      FROM CALENDAR
      WHERE date <= '20210822'
)
SELECT * 
INTO [dbo].[Calendar] 
FROM CALENDAR
OPTION (MAXRECURSION 0);
GO

These rows are a OpeningStocks for any Date after 19010101

INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (1, CAST(0.0 AS Decimal(18, 2)), CAST(N'1901-01-01T00:00:00.000' AS DateTime), 1, CAST(0 AS Decimal(18, 2)), 1)
GO
INSERT [dbo].[FuelTankHistory] ([Id], [FuelLevel], [FuelLevelTime], [TankId], [Volume], [ProductId]) VALUES (1, CAST(0.0 AS Decimal(18, 2)), CAST(N'1901-01-01T00:00:00.000' AS DateTime), 2, CAST(0 AS Decimal(18, 2)), 1)
GO

SqlFiddle: http://sqlfiddle.com/#!18/1334c/7

‍♂️

WITH x AS (
  SELECT FuelLevelTime
       , DateAdd( -- Bit of a cheat to adjust the `FuelLevelTime` to the correct reporting date
            day
          , CASE WHEN DatePart(hour, FuelLevelTime) >= 13 THEN 1 ELSE 0 END  -- Consider 1PM onwards part of the "next" day
          , Cast(FuelLevelTime AS date)
         ) AS fuel_level_date
       , FuelLevel
       , Volume
       , TankId
       , ProductId
  FROM   dbo.FuelTankHistory
  WHERE  EXISTS ( -- Filter to only petrol products.
           SELECT *
           FROM   dbo.Product
           WHERE  Product.ProductName = 'Petrol'
           AND    Product.ProductId = FuelTankHistory.ProductId
         )
)
, y AS (
  -- Add a couple of row numbers in each group.
  -- This allows us to identify the "first" and "last" rows in each group
  SELECT fuel_level_date
       , TankId
       , FuelLevelTime
       , FuelLevel
       , First_Value(FuelLevel) OVER (PARTITION BY TankId, fuel_level_date ORDER BY FuelLevelTime DESC) AS opening_stock
       , First_Value(FuelLevel) OVER (PARTITION BY TankId, fuel_level_date ORDER BY FuelLevelTime  ASC) AS closing_stock
       , Row_Number() OVER (PARTITION BY TankId, fuel_level_date ORDER BY FuelLevelTime ASC) AS row_num
  FROM   x
)
SELECT fuel_level_date
     , Sum(opening_stock - closing_stock) AS sale_petrol
FROM   y
WHERE  row_num = 1
GROUP
    BY fuel_level_date
ORDER
    BY fuel_level_date
;

http://sqlfiddle.com/#!18/b554a/299/0

Forgive the badly named CTEs.

But it does give the output you've requested, with no additional objects or anything particularly funky/complex.

I had considered using First_Value() instead of Row_Number() but to get that to work I threw in a DISTINCT (or another Row_Number()) which just didn't feel right.


EDIT: here's the version using First_Value()

; WITH x AS (
  SELECT Id
       , FuelLevelTime
       , DateAdd( -- Bit of a cheat to adjust the `FuelLevelTime` to the correct reporting date
            day
          , CASE WHEN DatePart(hour, FuelLevelTime) >= 13 THEN 1 ELSE 0 END  -- Consider 1PM onwards part of the "next" day
          , Cast(FuelLevelTime AS date)
         ) AS fuel_level_date
       , FuelLevel
       , Volume
       , TankId
       , ProductId
  FROM   dbo.FuelTankHistory
  WHERE  EXISTS ( -- Filter to only petrol products.
           SELECT *
           FROM   dbo.Product
           WHERE  Product.ProductName = 'Petrol'
           AND    Product.ProductId = FuelTankHistory.ProductId
         )
)
, y AS (
  -- Add a couple of row numbers in each group.
  -- This allows us to identify the "first" and "last" rows in each group
  SELECT Id
       , fuel_level_date
       , TankId
       , FuelLevelTime
       , FuelLevel
       , First_Value(FuelLevel) OVER (PARTITION BY TankId, fuel_level_date ORDER BY FuelLevelTime DESC) AS opening_stock
       , First_Value(FuelLevel) OVER (PARTITION BY TankId, fuel_level_date ORDER BY FuelLevelTime  ASC) AS closing_stock
       , Min(Id) OVER (PARTITION BY TankId, fuel_level_date) AS min_id  -- this will be used as a tie-breaker to reduce the set to a single row per group
  FROM   x
)
SELECT fuel_level_date
     , Sum(opening_stock - closing_stock) AS sale_petrol
FROM   y
WHERE  min_id = Id  -- Filter down to a single row per group
GROUP
    BY fuel_level_date
ORDER
    BY fuel_level_date
;
Related