Skip to main content
PowerShell · lesson 2 of 12
PowerShell beginner Lesson 2 of 12

Sorting, Grouping, and Measuring Objects

Sort-Object, Group-Object, and Measure-Object turn a list of live objects into an answer — until one of them sorts numbers as text, which is the same trap grep-and-sort scripts fall into.

By SkillByExample · Published

powershell pipeline sort-object group-object measure-object

Three cmdlets turn a list of objects into an answer: Sort-Object, Group-Object, and Measure-Object. All three trust the type of the value they’re given — which is exactly where the first bug in this lesson comes from.

The bug: sorting numbers as text

$numbers = "10","2","33","4"
$numbers | Sort-Object
10
2
33
4

Barely sorted at all. Every value here is a string, and Sort-Object compares strings character by character — "1" sorts before "2" regardless of what comes after it. This is the same failure mode as forgetting -n on sort in bash.

The fix: sort the actual numbers

$numbers = "10","2","33","4"
$numbers | ForEach-Object { [int]$_ } | Sort-Object
2
4
10
33

Casting each value to [int] before it reaches Sort-Object fixes it — now it’s comparing integers, not characters. This never comes up with Get-Process’s own numeric properties, because they’re typed as numbers from the moment the cmdlet creates them; it only bites values that started life as text, like $numbers here, or anything read from a CSV file.

Sorting real typed data

Get-Process | Sort-Object -Property WorkingSet64 -Descending |
    Select-Object -First 5 -Property ProcessName, @{Name="MemoryMB";Expression={[math]::Round($_.WorkingSet64/1MB,1)}}
ProcessName MemoryMB
----------- --------
node         1129.10
node         1126.10
node         1083.40
Code          508.50
mc-fw-host    384.20

WorkingSet64 is already an int64 on the object Get-Process returns, so this sorts correctly with no casting needed — the calculated MemoryMB column exists purely to make the output readable; the sort itself ran on the raw byte count.

Group-Object: counting without pre-sorting

Get-Process | Group-Object -Property ProcessName | Sort-Object -Property Count -Descending |
    Select-Object -First 5 -Property Count, Name
Count Name
----- ----
   99 svchost
   36 Code
   20 conhost
   15 chrome
   14 msedgewebview2

This is the pipeline equivalent of sort | uniq -c | sort -rn — except Group-Object scans the whole input itself and doesn’t need it pre-sorted first. uniq -c only counts consecutive matching lines; feeding it unsorted input silently produces wrong counts, a bug Group-Object structurally can’t have.

Measure-Object: aggregating a property

Get-Process | Measure-Object -Property WorkingSet64 -Sum -Average |
    Select-Object -Property Count, @{Name="TotalMB";Expression={[math]::Round($_.Sum/1MB,0)}}, @{Name="AverageMB";Expression={[math]::Round($_.Average/1MB,1)}}
Count  TotalMB AverageMB
-----  ------- ---------
  427 13608.00     31.90

-Sum and -Average both aggregate the same -Property in one pass — this is the pipeline’s answer to awk '{sum+=$1} END {print sum}', minus the manual accumulator variable.

Practice

1. Find the single heaviest process by memory, using Measure-Object -Maximum.
Get-Process | Measure-Object -Property WorkingSet64 -Maximum | Select-Object -Property Maximum
      Maximum
      -------
1183973376.00

-Maximum returns the raw largest value, in bytes — Select-Object here doesn’t reach into which process it belonged to, since Measure-Object’s output is a summary object, not the original list.

2. Sort processes by CPU time, descending, and show the top three.
Get-Process | Sort-Object CPU -Descending | Select-Object -First 3 -Property ProcessName, CPU
ProcessName         CPU
-----------         ---
BixbySystray   12331.78
Zoom            4686.47
Docker Desktop  4253.41

Same pattern as the memory sort — CPU is already numeric on the object, so descending sort needs no casting, unlike the $numbers example at the top of this lesson.

3. Group a list of file extensions and find the most common one, without sorting first.
$files = "report.csv","data.json","notes.txt","summary.csv","log.txt","export.csv"
$files | ForEach-Object { [System.IO.Path]::GetExtension($_) } | Group-Object | Sort-Object Count -Descending | Select-Object -First 1
Count Name                      Group
----- ----                      -----
    3 .csv                      {.csv, .csv, .csv}

Group-Object’s Group property keeps every original value in the group, not just the count — useful when the next step needs to go back and act on the actual matching items.

Next: variables, arrays, and hashtables — and the quirks in how PowerShell compares them.

Frequently Asked Questions

Why did Sort-Object put "10" before "2"?
Because both were strings, and Sort-Object sorted them alphabetically — character by character, '1' comes before '2' regardless of what follows. This is exactly the same trap as `sort` (without `-n`) in bash; the fix in PowerShell is to make sure the values are actually numbers (cast with [int] or pull them from a property that's already typed as one) before they reach Sort-Object.
How does Sort-Object know a property like WorkingSet64 is numeric instead of text?
It doesn't guess — WorkingSet64 comes from Get-Process typed as an int64 to begin with, so Sort-Object compares it numerically because that's the type it actually holds. The bug in the first example happened only because the strings in $numbers were text from the start, with nothing to tell Sort-Object otherwise.
What's the difference between Select-Object and Where-Object?
Select-Object picks which properties (columns) to keep, or how many objects (rows), and can add calculated ones — it doesn't remove any objects based on their values. Where-Object filters which objects pass through at all, based on a condition. They're often used together: filter first with Where-Object, then shape what's left with Select-Object.
Can Measure-Object do more than count things?
Yes — pass -Sum, -Average, -Maximum, or -Minimum with -Property to aggregate a numeric property across every object in the pipeline, similar to a single column aggregate in SQL. Without any of those switches it just counts, which is still useful as the pipeline equivalent of `wc -l`.
Does Group-Object require the data to be pre-sorted, like bash's uniq -c does?
No — Group-Object builds its groups by scanning the whole input regardless of order, unlike uniq -c, which only counts consecutive matching lines and silently gives wrong counts on unsorted input. This is a common bash bug (piping into uniq -c without sort first) that Group-Object doesn't have a version of.