EF Core - SELECT Queries Involving Multiple Tables

By FoxLearn 2/6/2025 7:48:45 AM   3
When you design a database with related tables, it’s common to need data from more than one table or filter records based on values from another table.

In this article, I’ll demonstrate how to execute queries involving multiple tables, using both LINQ and raw SQL.

While most queries can be done using LINQ, there are some advanced scenarios where generating the query you want can be tricky. In those cases, raw SQL can provide a cleaner solution.

Using Include() to Populate Linked Entities

Let’s consider a scenario where a series has multiple seasons. The Series model has a List<Season> property. By default, querying a series won’t load its seasons.

To include the seasons in the query, you can use Include(), like so:

using (var context = new MediaContext(connectionString))
{
    var series = await context.Series
        .Include(nameof(Series.Seasons))
        .AsNoTracking()
        .ToListAsync();

    foreach (var s in series)
    {
        Console.WriteLine($"{s.Title} has {s.Seasons.Count()} seasons");
    }
}

SQL Generated:

This code will execute a query with a LEFT JOIN to populate the Seasons collection:

SELECT [s].[Id], [s].[Title], [s].[YearStarted], [s].[YearEnded], [se].[Id], [se].[SeasonNumber], [se].[SeriesId]
FROM [Series] AS [s]
LEFT JOIN [Seasons] AS [se] ON [s].[Id] = [se].[SeriesId]
ORDER BY [s].[Id], [se].[Id]

Here, each row for a series is repeated for every season, but EF Core will map it correctly to a single Series object and populate the Seasons list.

INNER JOIN - Two Tables

Imagine a Movie has many Actors. If you want to list all actors for the movie The Matrix, here’s how you could do it using both LINQ and raw SQL.

LINQ

using (var context = new MediaContext(connectionString))
{
    var actors = await (from actor in context.Actors
                        join movie in context.Movies on actor.MovieId equals movie.Id
                        where movie.Title == "The Matrix"
                        select actor)
                       .AsNoTracking()
                       .ToListAsync();

    foreach (var actor in actors)
    {
        Console.WriteLine($"Actor: {actor.Name}");
    }
}

SQL Generated:

SELECT [a].[Id], [a].[Name], [a].[MovieId]
FROM [Actors] AS [a]
INNER JOIN [Movies] AS [m] ON [a].[MovieId] = [m].[Id]
WHERE [m].[Title] = N'The Matrix'

Console Output:

Actor: Keanu Reeves
Actor: Laurence Fishburne
Actor: Carrie-Anne Moss

Raw SQL

Alternatively, you can execute this query using raw SQL:

using (var context = new MediaContext(connectionString))
{
    var movieTitle = "The Matrix";
    var actors = await context.Actors.FromSqlInterpolated(
        $@"SELECT a.* FROM Actors a
           INNER JOIN Movies m ON a.MovieId = m.Id
           WHERE m.Title = {movieTitle}")
        .AsNoTracking()
        .ToListAsync();

    foreach (var actor in actors)
    {
        Console.WriteLine($"Actor: {actor.Name}");
    }
}

SQL Generated:

exec sp_executesql N'SELECT a.* 
                    FROM Actors a
                    INNER JOIN Movies m ON a.MovieId = m.Id
                    WHERE m.Title = @p0',N'@p0 nvarchar(4000)',@p0=N'The Matrix'

Console Output:

Actor: Keanu Reeves
Actor: Laurence Fishburne
Actor: Carrie-Anne Moss

Subquery – WHERE EXISTS

Now, let's consider a many-to-many relationship between the Movies and Directors tables, which is linked through a table called DirectorMovie. If you want to list all movies directed by "Christopher Nolan," you can use a subquery with the WHERE EXISTS clause.

using (var context = new MediaContext(connectionString))
{
    var moviesByNolan = await context.Movies
        .Where(m => m.Directors.Any(d => d.Name == "Christopher Nolan"))
        .AsNoTracking()
        .ToListAsync();

    foreach (var movie in moviesByNolan)
    {
        Console.WriteLine($"Christopher Nolan directed {movie.Title}");
    }
}

SQL Generated:

SELECT [m].[Id], [m].[Title], [m].[ReleaseYear]
FROM [Movies] AS [m]
WHERE EXISTS (
    SELECT 1
    FROM [DirectorMovie] AS [dm]
    INNER JOIN [Directors] AS [d] ON [dm].[DirectorId] = [d].[Id]
    WHERE [m].[Id] = [dm].[MovieId] AND [d].[Name] = N'Christopher Nolan')

Console Output:

Christopher Nolan directed Inception
Christopher Nolan directed Interstellar

When working with multiple tables in EF Core, you can use Include() for simple relationships, LINQ for straightforward queries, or raw SQL for complex join scenarios.