ZetCode

F# query expressions

last modified May 17, 2025

In this article, we delve into query expressions in F#—a powerful feature that enables writing queries in a declarative and intuitive manner.

F# query expressions offer an elegant way to retrieve and transform data from various sources, including collections, databases, and structured datasets. They provide a SQL-like syntax that integrates seamlessly with F#'s functional programming capabilities, allowing developers to build efficient and expressive queries. By leveraging query expressions, F# enables concise data manipulation while maintaining readability and composability in code.

Basic query expressions

The simplest query expressions select data from a source collection. F# provides several operators like select, where, and sortBy to filter and transform data.

basic_query.fsx
open System

let vals = [| 1; 2; 3; 4; 5; 6|]

let lst = query {
    for e in vals do
    last
}

Console.WriteLine(lst)

let fst = query {
    for e in vals do
    head
}

Console.WriteLine(fst)

let n = query {
    for e in vals do
    nth 3
}

Console.WriteLine(n)

This example demonstrates basic query operations. The last operator gets the last element, head gets the first element, and nth gets the element at a specific index. Query expressions are enclosed in query { } blocks.

λ dotnet fsi basic_query.fsx
6
1
4

Filtering with where

The where operator filters elements based on a condition. It's similar to the WHERE clause in SQL or the filter operation in functional programming.

where_operator.fsx
open System

let vals = [| 1; 2; 3; 4; 5; 6|]
 
let res = query {
   for v in vals do
   where (v <> 3)
   select v
}

for e in res do
    Console.WriteLine(e)

Console.WriteLine(vals.GetType())

This code filters out the value 3 from the array. The where clause contains the condition, and select specifies what to return. The result is an IEnumerable that we can iterate over.

λ dotnet fsi where_operator.fsx
1
2
4
5
6
Microsoft.FSharp.Core.FSharpOption`1[System.Int32[]]

Counting and selecting elements

Query expressions can count elements and perform more complex operations on custom types. The count operator returns the number of elements that match the query.

count_operator.fsx
open System

type User = {
    Name: string
    Occupation: string
}

let users = [
    { Name = "John Doe"; Occupation = "gardener" }
    { Name = "Roger Roe"; Occupation = "driver" }
    { Name = "Thomas Monroe"; Occupation = "trader" }
    { Name = "Gregory Smith"; Occupation = "teacher" }
    { Name = "Lucia Bellington"; Occupation = "teacher" }
]

let n = query {
    for user in users do
    select user
    count
}

Console.WriteLine(n)

let last = query {
    for user in users do
    last
}

Console.WriteLine(last)

Console.WriteLine("teachers:")

let teachers = query {
    for user in users do
    where (user.Occupation = "teacher")
    select user
}

teachers |> Seq.iter Console.WriteLine

This example works with a list of User records. We count all users, get the last user, and filter users by occupation. The where clause filters for teachers, and select returns the matching users.

λ dotnet fsi count_operator.fsx
5
{ Name = "Lucia Bellington"; Occupation = "teacher" }
teachers:
{ Name = "Gregory Smith"; Occupation = "teacher" }
{ Name = "Lucia Bellington"; Occupation = "teacher" }

Sorting data

The sortBy and thenBy operators allow sorting data by one or more fields. sortBy performs the primary sort, while thenBy adds secondary sorting criteria.

sorting.fsx
open System

type User = {
    FirstName: string
    LastName: string
    Salary: int
}

let users = [
    { FirstName = "Robert"; LastName = "Novak"; Salary = 1770 }
    { FirstName = "John"; LastName = "Doe"; Salary = 1230 }
    { FirstName = "Lucy"; LastName = "Novak"; Salary = 670 }
    { FirstName = "Ben"; LastName = "Walter"; Salary = 2050 }
    { FirstName = "Robin"; LastName = "Brown"; Salary = 2300 }
    { FirstName = "Amy"; LastName = "Doe"; Salary = 1250 }
    { FirstName = "Joe"; LastName = "Draker"; Salary = 1190 }
    { FirstName = "Janet"; LastName = "Doe"; Salary = 980 }
    { FirstName = "Peter"; LastName = "Novak"; Salary = 990 }
    { FirstName = "Albert"; LastName = "Novak"; Salary = 1930 }
]

let sorted = query {
    for user in users do
    sortBy user.LastName
    thenBy user.Salary
    select user
}

sorted |> Seq.iter Console.WriteLine

This code sorts users first by last name, then by salary. The result is a sequence of users ordered alphabetically by last name, with users having the same last name ordered by salary.

λ dotnet fsi sorting.fsx
{ FirstName = "Robin"; LastName = "Brown"; Salary = 2300 }
{ FirstName = "John"; LastName = "Doe"; Salary = 1230 }
{ FirstName = "Amy"; LastName = "Doe"; Salary = 1250 }
{ FirstName = "Janet"; LastName = "Doe"; Salary = 980 }
{ FirstName = "Joe"; LastName = "Draker"; Salary = 1190 }
{ FirstName = "Lucy"; LastName = "Novak"; Salary = 670 }
{ FirstName = "Peter"; LastName = "Novak"; Salary = 990 }
{ FirstName = "Robert"; LastName = "Novak"; Salary = 1770 }
{ FirstName = "Albert"; LastName = "Novak"; Salary = 1930 }
{ FirstName = "Ben"; LastName = "Walter"; Salary = 2050 }

Grouping and aggregation

Query expressions support grouping data and performing aggregations like sum, average, count, etc. The groupBy operator groups elements by a key, and aggregate functions can be applied to each group.

grouping.fsx
open System.Linq

type Revenue =
    { Id: int
      Quarter: string
      Amount: int }

let revenues = [
    { Id = 1; Quarter = "Q1"; Amount = 2340 };
    { Id = 2; Quarter = "Q1"; Amount = 1200 };
    { Id = 3; Quarter = "Q1"; Amount = 980 };
    { Id = 4; Quarter = "Q2"; Amount = 340 };
    { Id = 5; Quarter = "Q2"; Amount = 780 };
    { Id = 6; Quarter = "Q3"; Amount = 2010 };
    { Id = 7; Quarter = "Q3"; Amount = 3370 };
    { Id = 8; Quarter = "Q4"; Amount = 540 }
]

query {
    for revenue in revenues do
        groupBy revenue.Quarter into g
        where (g.Count() = 2)
        select {| Quarter = g.Key
               Total = g.Sum(fun c -> c.Amount) |}
}
|> Seq.iter (fun e -> printfn "%A" e)

This example groups revenues by quarter, filters for quarters with exactly 2 entries, and calculates the total amount for each qualifying quarter. The result is an anonymous record with the quarter and total amount.

λ dotnet fsi grouping.fsx
{ Quarter = "Q1"; Total = 4520 }
{ Quarter = "Q2"; Total = 1120 }
{ Quarter = "Q3"; Total = 5380 }

F# query expressions provide a powerful, declarative way to work with data. They offer SQL-like syntax for filtering, sorting, grouping, and transforming data while maintaining F#'s type safety and functional programming benefits. Whether working with in-memory collections or external data sources, query expressions can make your data processing code more readable and maintainable.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.