timeout

Fix Your EF Core Query Timeouts Using This Simple SQL Trick

If you ever had a problem with your query timing out due to filtering by too many ids, you’ve come to the right place. You can solve it by using openjson filtering in your EF Core query.

This article presents an alternate way for standard filtering with Where LINQ extension. The result is query has similar performance (perhaps slightly better). But the most important, it doesn’t time out due too many filter parameters.

Why the query times out with a large number of filter parameters?

T-SQL query (generated by EF) can be very fast when you use it with a small number of parameters, but then timeout when you use it with larger parameters set.

Why?

The problem is Parameter Sniffing.

When the T-SQL query runs for the first time on SQL Server, it looks at passed parameter values and creates an execution plan based on these parameters. Then it stores the plan in the query plan cache. The query plan can be used for the same query but with different parameters set (the larger one, in our case). This mechanism is used in SQL Server to save some time for compiling & recompiling queries.

The problem arises when the first time the query is executed, the set of parameters generates an acceptable plan for that set of parameters but very bad for other more common sets of parameters.

What is openjson

In T-SQL there’s a mechanism called openjson.

OpenJson is a table-valued function that parses JSON text and returns objects and properties from the JSON input as rows and columns. It provides a rowset view over a JSON document.

OpenJson returns a set of rows. Due to this fact, you can use it in FROM clause of Transact SQL statement.

Note that OpenJson function is only available for databases that have compatibility level 130 or higher.

Example of a query used to filter collection using openjson


    DECLARE @employeeIds NVARCHAR(4000) = N'[1,2,3,4]'

    SELECT * FROM employee
    INNER JOIN OPENJSON(@employeeIds) selectedEmployee
        ON employee.Id = selectedEmployee.value

This example provides a list of identifiers as a JSON array of numbers. The query converts the JSON array to a table of identifiers and filters all employees with ID specified within the list.

This query is an equivalent to

    SELECT * FROM employee
    WHERE employee.Id IN (1,2,3,4)

The difference is that in the second query the parameters are embedded within the query.

This is an important difference which we’ll use later on in EF Core

Implementation of openjson in EF Core

In our example let’s assume that the collection will be filtered by ids. In the sample codes, I used EF Core 3.1 .

First, we’ll create FilterItem class which will be used in EF Context and in the query. This is a keyless entity type in terms of EF Core. The purpose is to serve as a return type for raw SQL queries.

    public class FilterItem
    {
        public int Value { get; set; }
    }

Second, we have to create a property for openjson filtering in Context.

    public virtual IQueryable<FilterItem> OpenJsonFilter(string ids) =>
        Set<FilterItem>().FromSqlInterpolated($"select value from openjson( {ids} )");

In the model builder, we have to specify that FilterItem has no key. This helps EF Core interpreter to use this property correctly.

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<FilterItem>().HasNoKey();
    }

That is all the setup that we need.

Usage of OpenJsonFilter in query

OpenJson filtering is slightly different than regular filtering. As shown in T-SQL example, the filtering will not be just using Where() extension. We’ll have to use Join() instead.

For the purpose of the example, let’s say we have to find all orders for selected customers.

    public class OrderQuery
    {
        public IEnumerable<int> CustomerIds { get; set; }
        // Other properties for querying against customers
    }

    public class Order
    {
        public int CustomerId { get; set; }
        public int Id { get; set; }
        public IEnumerable<int> ItemIds { get; set; }
        // other order properties
    }

    public class OrderProvider
    {
        private Context _context; // injected via ctor

        public Order GetByCustomerIds(OrderQuery query)
        {
            var ids = JsonSerializer.Serialize(query.CustomerIds);

            var result = _context.Orders
                .Join(
                    _context.OpenJsonFilter(ids),
                    order => order.CustomerId,
                    jsonFilter => jsonFilter.Value,
                    (order, jsonFilter) => order
                );

            return result;
        }
    }

So in this query, we’ll get all Orders matching given customer Ids. Customer Ids are passed in the OrderQuery as a parameter of GetByCustomerIds method. Given Ids are serialized to string, as JSON array, and passed into OpenJsonFilter.

Then, the ids are used to filter out the expected rowset by inner join.

This operation tells EF Core to use given ids as local variables in the query, instead of inserting variables directly into it. Which solves the problem of parameters sniffing and the query gets compiled with every execution, instead of being read from the cache.

Summary

This technique is great when filtering by a large array of ids is required and the query is timing out from time to time, due to this fact.

The problem that it solves is that the cached execution plan might be optimal for a small amount of parameters, but not for the same query with a larger set of parameters. This is called parameters sniffing.

This solution handles it by forcing EF to generate a query that uses local variables. This triggers SQL server to compile the query plan, instead of reading it from the cache.