Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
AI & KI NachrichtenAI Restrictions at NYCPS and LAUSD Reverberate Nationwide(24.09.2026 um 01:16 Uhr)
AI & KI NachrichtenMeta Connect 2026: The biggest news and announcements(24.09.2026 um 00:45 Uhr)
AI & KI NachrichtenMeta ditches the camera on its newest smart glasses(24.09.2026 um 01:37 Uhr)
AI & KI NachrichtenMuse is coming to Meta smart glasses(24.09.2026 um 01:40 Uhr)
AI & KI NachrichtenAI Restrictions at NYCPS and LAUSD Reverberate Nationwide(24.09.2026 um 01:16 Uhr)
AI & KI NachrichtenMeta Connect 2026: The biggest news and announcements(24.09.2026 um 00:45 Uhr)
AI & KI NachrichtenMeta ditches the camera on its newest smart glasses(24.09.2026 um 01:37 Uhr)
AI & KI NachrichtenMuse is coming to Meta smart glasses(24.09.2026 um 01:40 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Retrieving the latest row per group from PostgreSQL

In this post I'll show you several approaches you can use if you need to solve tasks along the lines of "retrieve the earliest/oldest/last etc row per group" from your database. I rank them by how performant I found them to be on a made-up…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

In this post I'll show you several approaches you can use if you need to solve tasks along the lines of "retrieve the earliest/oldest/last etc row per group" from your database. I rank them by how performant I found them to be on a made-up dataset I made for this purpose.



I explain what determines the different timings with the help of visualisations showing how the database processes the query internally. As you will see, we will go from hundreds of ms to about 5, which is quite a significant difference! It is not guaranteed that you will have the same results on your production environment as the data distribution might be different. However if it's reasonably close to my dataset below, you'll be pleasantly surprised.



To support the story in the next sections we'll use a scenario involving meters that regularly record readings. We'll retrieve the latest reading per every meter.






Setup



Let's create a small schema for demonstration purposes. I'm using Postgres version 16.2.



First, we create the table of meters.




create table meters
(
id bigserial,
... other columns
primary key (id)
);






Then, we create the table of readings.




create table readings
(
id bigserial,
meter_id bigint,
date date, <--- or timestampz, etc, but I keep it simple
reading double precision,
primary key (id),

constraint fk__readings_meters foreign key (meter_id) references meters (id)
);






At this point we have our tables in place. For integrity, they are linked them together with the help of a foreign key constraint. Let's now populate our tables with some rows. Let's add 500 meters, with one reading every day, for a year:




insert into meters select * from generate_series(1, 500) seq;

insert into readings(meter_id, date, reading)
select m.id, seq, random() from generate_series('2024-02-01'::date, '2025-02-01'::date, '1 day'::interval) seq, meters m;






Our schema is ready to be queried. Let's start somewhere. This approach might not be the first that comes to your mind, I start with it because I found it to be slowest. This is by no means a reflection of the feature utility as a whole, also not on its performance for other scenarios.






1. Window functions: ~250ms






explain(analyse, buffers)
with readings_with_rownums as (
select
meters.id as meter_id,
readings.reading as reading,
row_number() over (partition by meters.id order by readings.date desc) as rownum
from
readings
join
meters on meters.id = readings.meter_id
)
select
meter_id,
reading
from
readings_with_rownums
where
readings_with_rownums.rownum = 1;






This runs in about 250ms. Let's have a look at the explain plan to understand it better. To visualise it, I am using the excellent visualisation tool from Dalibo.



explain-plan



I've opened the relevant nodes. In the upper part of the picture above, we can notice that it's relatively slow because the query efficiency is low. It discards ~99.73% of rows that it read. In the Sort node arrive 183k rows from the nodes below, but then, only 500 are returned. While not fast, we can also conclude that this approach is not scalable, because it is very sensitive to increases in the dataset.






2. DISTINCT ON: ~250ms



Let's try another approach, this time using the DISTINCT ON clause. For details about how it works, you can read about it in the Postgres docs for SELECT, section DISTINCT clause. Here's how it looks like when it is used to retrieve our latest reading per meter:




explain (analyse, buffers)
select
distinct on (meters.id)
meters.id as meter_id,
readings.reading as reading
from
meters
join
readings on meters.id = readings.meter_id
order by
meters.id,
readings.date desc;






I find this approach very elegant, but I didn't get a noticeable difference with regards to how fast it runs compared with the previous approach. The explain plan looks quite similar to the one we have seen before.



explain-plan



We can observe that it doesn't have the WindowAgg node, but this didn't get us far. It's is still inefficient, reading a lot of rows and discarding the majority before returning the results. But one thing I noticed is the following line in the explain plan:




Sort Method: external merge  Disk: 3968kB






This is slowing the sort down because it uses the external disk. This happens when the work_mem setting is too low given the size of the dataset, and the sort can't be done fully in memory, so it spills to disk. Indeed, the default setting for work_mem in Postgres is 4MB.



Let's increase it to make sure it's sufficient.




set work_mem='16MB';






I don't have to restart Postgres for this to take effect. For others we have to.



We retry the query now, we confirm that the sort happens in memory now; the proof is that we can see the following detail in the explain plan:




Sort Method: quicksort  Memory: 14951kB






Cool! Did this make a difference though? Not really, we're still at ~250ms. Remember that this is on my laptop, and there is no network between the disk and the CPU. For example in Amazon RDS, the storage part is handled by EBS, which is network-attached so the difference would be quite noticeable in that scenario. The data has to travel more during the sort procedure then.






3. MAX(DATE): ~115ms



No problem, we still have options. Let's try something else. Let's look at another alternative. How about this?




explain(analyse, buffers)
with latest_reads_per_meter as (
select
readings.meter_id as meter_id,
max(date) as reading_date
from
readings
group by
readings.meter_id
)
select
readings.meter_id,
readings.reading
from
meters
join
readings on meters.id = readings.meter_id
join
latest_reads_per_meter lrpm on lrpm.meter_id = meters.id
and readings.date = lrpm.reading_date;






As usual, the explain plan:



explain-plan



Hmm, this looks a bit different than what we have seen before. In a good way! It's now doing the sort much earlier so it's discarding the irrelevant rows earlier. It doesn't carry it over all the way to the end of the retrieval process. This will consume less memory because the intermediate results are smaller. However, does it speed up our query?



Indeed it does!




Execution Time: 114.942 ms






Finally some solid progress. We cut the runtime in half. This is already quite good. But can we do better? You bet, even reduce it by one order of magnitude.






4. Loose index scans: ~14ms



Let's have a look at how the loose index scan works.



First, let's create the required index. The columns have to be defined exactly in this order, with the "grouping" element first and then the other column which will be used for determining "latest" within a group.




create index idx on readings(meter_id, date);









explain (analyse, buffers)
select
meters.id as meter_id,
readings.reading as reading
from
meters
cross join lateral (
select
*
from
readings
where
readings.meter_id = meters.id
order by
date desc
limit 1
) readings;









Execution Time: 13.814 ms






explain-plan



Whoa! What's the reason this is much more performant? For starters - you don't see the 183k rows anywhere at all, not even in the lower nodes that happen in the beginning. We are not sorting anything anymore, because the index keeps our data sorted, at the expense of insertion overhead.



Let's push the envelope even more. Let's open the IO & Buffers tab of the index scan node in the above explain plan and have a look in there. Here it is:



explain-plans



What happens is that first the index is traversed to determine which rows that have to be retrieved to satisfy the query, however after this step, Postgres has to actually go ahead and retrieve the rows from a different place, namely the table (or heap). You might wonder, can we avoid this extra step to read from the table? You bet!






5. Loose index-only scans: ~5ms



We can implement an index-only scan. We use the include option when creating the index to achieve this, like so:




create index idx_with_including on readings(meter_id, date) include (reading);






Let's retry the query and look at the relevant tabs again.



explain-plans



explain-plans



Two important things to observe here. First, notice the Heap Fetches: 0, which indicates that it does not go to the heap to get the rows because they are in the index already. Secondly, the total number of blocks is now ~1500, which means it's 500 less than before. This confirms again that it doesn't go to the heap.



Let's look at the final result. How fast did we get it?




Execution Time: 5.448 ms






This is very nice. But let's understand it a bit better what's happening under the hood.



The following is a visualisation of what Postgres is doing in order to retrieve our 500 rows as part of the the index-only scan node.



b-tree-vis



For each row returned in the final result, it will do 3 page read operations - first for the root page, then an intermediate/internal B-tree page, and lastly it will read the leaf page from where it will collect the reading (this is because we used the including clause when creating the index). I have marked these steps with 1, 2, 3 next to the arrows that represent descending down the tree.






Conclusion



After quite the journey, we've finally arrived at the fastest solution; meaning end of the line for this post. Note though that as they say, there is no free lunch - every additional index gives the database more work to do at every insert, so you will have to decide if it's worth it. Also, the results do depend on the data distribution. It's so fast because we in our dataset we have many readings for every meter, but things might be different in your setup.






References



The first time I heard about loose index scanning was from this SO answer by E. Brandstetter, next to this I had a look at other blogs tackling the subject.





There is also a Postgres wiki entry on the topic.





Thanks for reading!

IR-PLAYBOOK-VULN-REMEDIATION
MEDIUM
SOC Incident Playbook: Vulnerability Remediation & Verification
1-Click Detection Engineering: Sigma & YARA Rules
SOC Ready
title: Detect Exploitation - Retrieving the latest row per group from PostgreSQL
id: e69f748a-69c1-45ce-be9c-dd6c0950b962
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-24
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-24"
        description = "YARA Signature for "
    strings:
        $str = "Retrieving the latest row per " ascii wide
    condition:
        any of them
}
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Retrieving the latest row per group from PostgreSQL

Thematisch verwandte Begriffe: Retrieving, latest, group, from · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96676 | A vulnerability was identified in Fast FAC1900R 20190827_2.0.2. The impa…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick