Skip to content
Advertisement

Find all rows in between a set in PostgreSQL

I have a table named tc_fuel that is receiving all the fuel related data from GPS vehicles, and I want to get the last “Tank Usage” to calculate MPG for a whole tank, but the readings when the tank is full (100) some times repeat after 2 or 3 or more rows, so I am left with 2 values of a 100 next to each other, I want to be able to get the last “fillup” starting and ending ids.

What I have so far:

SELECT
    "tc_fuel".deviceid,
    "tc_fuel"."id",
    "tc_fuel".fuel,
    "tc_fuel"."fuelUsed",
    "tc_fuel"."fuelUsed"
FROM "tc_fuel"
WHERE fuel=100
    AND deviceid=19
ORDER BY ID
DESC LIMIT 2

Then I go into PHP to check if the id difference its over 100 records to check that the fuel values are not next to each other, but this is doing some more work than it should I was wondering if is there a better way.

For example this vehicle started on a full tank and then drop to 6% tank and did a full tank fill, I want to be able to grab all the data of the last tank.

id    | deviceId  | fuel
------+-----------+-------
1     | 19        | 100  <-- This should be starting point 
2     | 19        | 97  
3     | 19        | 100  
4     | 19        | 96
5     | 19        | 94
6     | 19        | .... (keeps dropping)
7     | 19        | 33
8     | 19        | 31
9     | 19        | 30
10    | 19        | ....
11    | 19        | 6
12    | 19        | 5
13    | 19        | 6    <-- This should be end point (will flag this id as processed)
14    | 19        | 100  <-- Starts all over again in next iteration of the php script
15    | 19        | 99
16    | 19        | 98
17    | 19        | 100
18    | 19        | 99
19    | 19        | 97
20    | 19        | 96
21    | 19        | ....

Advertisement

Answer

The definition of “fillup” is a bit vague. I’ll assume it’s a fillup when the fuel value rises by more than 50. Replace with a number of your choosing. And it seems like a new tank must start with fuel = 100 (though that’s an odd condition). I added that as comment – uncomment to activate:

SELECT *
FROM  (
   SELECT *, count(*) FILTER (WHERE fillup) OVER (PARTITION BY device_id ORDER BY id) AS tank
   FROM  (
      SELECT *
           , fuel - lag(fuel, 1, 0) OVER (PARTITION BY device_id ORDER BY id) > 50
             -- AND fuel = 100  -- additional condition(s)?
             AS fillup
      FROM   tbl
      ) sub1
   ) sub2
WHERE  device_id = 19
AND    tank = 1;

db<>fiddle here

In the subquery sub1, compute the difference between the preceding fuel entry per device and the current one – using the window function lag(). Notably, I use the variant with 3 parameters, providing 0 as default for missing rows to cover the first row per partition. An increase of more than 50 indicates a new fillup.

In subquery sub2, count the number of fillups over time with another window function, thereby assigning a “tank” number to every row.

In the outer SELECT, pick your device and the number of the “tank” filling. Voilá.

If you move the condition WHERE device_id = 19 to the innermost suqbquery, you can drop the PARTITION clauses. Faster, less versatile.

About the FILTER clause:

Only get last tank for given device

According to your comment, defined by “last time tank was filled from 20 or below to 100”.

I assume that later points in time correspond to higher id values. (Be aware that there can be corner case complications with serial columns under concurrent write load.)

The simplest way: just reverse the order and count from the bottom:

SELECT *
FROM  (
   SELECT *, count(*) FILTER (WHERE fillup) OVER (ORDER BY id DESC) AS tank
  FROM  (
      SELECT *, lag(fuel, 1, 0) OVER (ORDER BY id DESC) = 100
                AND fuel <= 20 AS fillup
      FROM   tbl
      WHERE  device_id = 19
      ) sub1
   ) sub2
WHERE  tank = 0
-- ORDER  BY id  -- optional to get result in ascending order

db<>fiddle here

For this, it’s probably faster to walk through rows procedurally as that only needs a single pass and can stop immediately after the first tank is found.
Support it with an index on tbl(device_id, id DESC).

Example code:

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement