This guide assumes Powershell 5.1 or 7+ and basic familiarity with the pipeline.
Select-Object is one of the first cmdlets every beginner meets and one of the last they actually master. The hashtable syntax for calculated properties unlocks most of its power and almost no introductory post explains it.
The Basics
Get-Process | Select-Object Name, Id, CPU
Pick named properties, drop the rest. Equivalent to a SQL SELECT.
Get-Process | Select-Object -First 5
Get-Process | Select-Object -Last 5
Get-Process | Select-Object -Skip 10 -First 5 # rows 11–15
-First short-circuits the pipeline as soon as it has enough useful when the upstream is expensive.
-ExpandProperty Get the Value, Not the Wrapper
The single most useful flag. Without it:
Get-Process | Select-Object Name | Get-Member -MemberType Properties
# returns objects with a Name property
With it:
Get-Process | Select-Object -ExpandProperty Name
# returns plain strings
When the next step in the pipeline expects a primitive (e.g. another cmdlet's -ComputerName, an array operation, a -join), -ExpandProperty is what you want.
Calculated Properties The Real Power
The hashtable form lets you compute new properties on the fly:
Get-Process | Select-Object Name,
@{ Name = 'MemoryMB'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } }
Short forms (Powershell accepts these abbreviations):
Get-Process | Select-Object Name,
@{ N = 'MemoryMB'; E = { [math]::Round($_.WS / 1MB, 1) } },
@{ Label = 'CpuPct'; Expression = { '{0:p1}' -f ($_.CPU / 100) } }
N/Name/Label are interchangeable. E/Expression likewise. Pick one and stay consistent.
The
Expressionscript block runs once per pipeline object with$_bound to the current item. Anything you can write inside{ }works string interpolation, conditionals, method calls, even pipeline operations.
Combine Multiple Source Properties
Get-Process | Select-Object Name,
@{ N = 'Summary'; E = { "$($_.Id) - $([math]::Round($_.WS/1MB,1)) MB" } }
A single calculated property can pull from any number of source properties. This is how you reshape data from one schema into another inline.
Format Hints Width, Alignment, Format String
The hashtable accepts a few extra keys when used with formatters:
Get-Process |
Select-Object Name,
@{ N = 'MB'; E = { $_.WS / 1MB }; FormatString = 'N1'; Width = 8; Alignment = 'Right' } |
Format-Table -AutoSize
FormatString runs through ToString(format) on the value. Width and Alignment are read by Format-Table (ignored elsewhere).
Select-Object -Property vs ForEach-Object
The mistake everyone makes:
# Select - keeps a structured object
$rows = Get-Process | Select-Object Name, @{N='MB';E={$_.WS/1MB}}
$rows[0].MB # works
# ForEach - emits whatever the script block returns
$rows = Get-Process | ForEach-Object { "$($_.Name) $($_.WS/1MB)" }
$rows[0].MB # ERROR - these are strings
Rule of thumb:
Select-Objectwhen you want a projection pick/rename/compute, keep the result as a structured object.ForEach-Objectwhen you want to transform produce new objects, run side effects, fan out to multiple results.
Powershell 7 added the
.ForEach()and.Where()methods on collections, which are 2–5× faster than the cmdlets because they skip the pipeline machinery. Use them when you've already materialized the array:$procs.Where{ $_.WS -gt 100MB }.ForEach{ $_.Name }.
Renaming Without Computing
A calculated property whose Expression is just $_.SourceName is the canonical "rename a property" pattern:
Get-Process | Select-Object @{ N = 'ProcessName'; E = { $_.Name } },
@{ N = 'PidNumber'; E = { $_.Id } }
Useful when feeding a downstream cmdlet that expects different property names than your input has.
Working With Multiple Inputs
$users = Get-ADUser -Filter * -Properties Department, Manager
$users | Select-Object Name, Department,
@{ N = 'ManagerName'; E = {
if ($_.Manager) { (Get-ADUser -Identity $_.Manager).Name } else { $null }
} }
The expression runs per object you can call other cmdlets, read from a hashtable, do whatever you need. Keep it cheap, because it runs N times.
Slicing -First, -Last, -Skip, -Index
$big = Get-EventLog -LogName System -Newest 10000
$big | Select-Object -First 5 # 5 oldest in the pipeline order
$big | Select-Object -Last 5 # 5 newest - buffers everything
$big | Select-Object -Skip 100 # everything after the first 100
$big | Select-Object -Index 0,5,10 # specific positions
-Lastbuffers the entire pipeline in memory. If the upstream is huge, sort first and use-Firstinstead.
-Unique De-duplicate by All Selected Properties
Get-Process | Select-Object -Property Name -Unique
Compares the combination of selected properties. Two processes named chrome collapse to one.
For unique-by-one-property when you want to keep the rest, use
Group-Object:
Get-Process | Group-Object Name | ForEach-Object { $_.Group | Select-Object -First 1 }
A Real-World Composition
Show every disk on every server, sorted by free space, with formatted output:
$servers | ForEach-Object {
Get-CimInstance Win32_LogicalDisk -ComputerName $_ -Filter 'DriveType = 3' |
Select-Object @{ N = 'Server'; E = { $_.PSComputerName } },
@{ N = 'Drive'; E = { $_.DeviceID } },
@{ N = 'SizeGB'; E = { [math]::Round($_.Size / 1GB, 1) } },
@{ N = 'FreeGB'; E = { [math]::Round($_.FreeSpace / 1GB, 1) } },
@{ N = 'PctFree'; E = { '{0:p1}' -f ($_.FreeSpace / $_.Size) } }
} | Sort-Object FreeGB | Format-Table -AutoSize
One pipeline. No intermediate variables. The output is fully structured you can Export-Csv it, ConvertTo-Json it, or pipe into another filter.
What to Do Next
Calculated properties turn Select-Object from a column picker into a tiny query language. Use -ExpandProperty when you need primitives. Reach for Select-Object when you want a projection and ForEach-Object when you want a transformation. Lean on -First to short-circuit huge pipelines, and remember -Last buffers everything.
Three concrete moves to make the syntax muscle memory:
- Pick a script that creates an intermediate
[pscustomobject]in aForEach-Objectthen pipes it toExport-Csv. Replace it with a singleSelect-Objectprojection. The pipeline gets shorter and the column order becomes deterministic. - Find a
Get-ChildItem | Sort-Object Lengthfollowed bySelect-Object -First N. Nothing wrong with it, but verify the sort happens before the take; if the data set is huge, the sort buffers everything and-Firstdoesn't save you. Profile withMeasure-Command. - For your next dashboard or report, write the data shape with calculated properties first, then build the consumer (CSV, JSON, table) on top. The shape stays declarative, the formatter is one cmdlet.
Pairs naturally with the hashtables vs PSCustomObject post (Select-Object emits PSCustomObject whether you ask for it or not, so understanding what PSCustomObject is matters) and the splatting post (calculated properties paired with splatted parameters keep multi-cmdlet pipelines readable).


