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 Expression script 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-Object when you want a projection pick/rename/compute, keep the result as a structured object.
  • ForEach-Object when 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

-Last buffers the entire pipeline in memory. If the upstream is huge, sort first and use -First instead.

-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:

  1. Pick a script that creates an intermediate [pscustomobject] in a ForEach-Object then pipes it to Export-Csv. Replace it with a single Select-Object projection. The pipeline gets shorter and the column order becomes deterministic.
  2. Find a Get-ChildItem | Sort-Object Length followed by Select-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 -First doesn't save you. Profile with Measure-Command.
  3. 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).