13

I've got Entity Framework 4.1 with .NET 4.5 running on ASP.NET in Windows 2008R2. I'm using EF code-first to connect to SQL Server 2008R2, and executing a fairly complex LINQ query, but resulting in just a Count().

I've reproduced the problem on two different web servers but only one database (production of course). It recently started happening with no application, database structure, or server changes on the web or database side.

My problem is that executing the query under certain circumstances takes a ridiculous amount of time (close to 4 minutes). I can take the actual query, pulled from SQL Profiler, and execute in SSMS in about 1 second. This is consistent and reproducible for me, but if I change the value of one of the parameters (a "Date after 2015-01-22" parameter) to something earlier, like 2015-01-01, or later like 2015-02-01, it works fine in EF. But I put it back to 2015-01-22 and it's slow again. I can repeat this over and over again.

I can then run a similar but unrelated query in EF, then come back to the original, and it runs fine this time - same exact query as before. But if I open a new browser, the cycle starts over again. That part also makes no sense - we're not doing anything to retain the data context in a user session, so I have no clue whatsoever why that comes into play.

But this all tells me that the data itself is fine.

In Profiler, when the query runs properly, it takes about a second or two, and shows about 2,000,000 in reads and about 2,000 in CPU. When it runs slowly, it takes 3.5 minutes, and the values are 300,000,000 and 200,000 - so reads are about 150 times higher and CPU is 100 times higher. Again, for the identical SQL statement.

Any suggestions on what EF might be doing differently that wouldn't show up in the query text? Is there some kind of hidden connection property which might cause a different execution plan in certain circumstances?

EDIT

The query that EF builds is one of the ones where it builds a giant string with the parameter included in the text, not as a SQL parameter:

exec sp_executesql 
   N'SELECT [GroupBy1].[A1] AS [C1] 
     FROM ( 
          SELECT COUNT(1) AS [A1]
           ...
           AND ([Extent1].[Added_Time] >= convert(datetime2, ''2015-01-22 00:00:00.0000000'', 121)) 
           ...
           ) AS [GroupBy1]'

EDIT

I'm not adding this as an answer since it doesn't actually address the underlying issue, but this did end up getting resolved by rebuilding indexes and recomputing statistics. That hadn't been done in longer than usual, and it seems to have cleared up whatever caused the issue.

I'll keep reading up on some of the links here in case this happens again, but since it's all working now and unreproduceable, I don't know if I'll ever know for sure exactly what it was doing.

Thanks for all the ideas.

Joe Enos
  • 39,478
  • 11
  • 80
  • 136
  • Ideally you want to try and reproduce in SSMS so that you can look at the execution plan. Usually such bad results are due to an incorrect plan being built. – Guvante Mar 24 '15 at 20:44
  • How does the query scale? Does an increase in records directly lead to a huge determent in performance? – Serguei Fedorov Mar 24 '15 at 20:44
  • 3
    That sounds like bad parameter sniffing to me. Check out this article about the topic. http://sqlinthewild.co.za/index.php/2007/11/27/parameter-sniffing/ (make sure you read all three segments) – Sean Lange Mar 24 '15 at 20:45
  • Ooooo.... I remember reading a question like this. Not EF, but it was an ad-hoc that exploded in execution time after a certain threshold was met. Don't remember much else, unfortunately. –  Mar 24 '15 at 20:45
  • @user2864740 and Sean Lange, Thanks, that does sound like it could be it. But since the query is built by EF, not by me, I don't really have any control over it and can't change it (but I'd love to rewrite the whole thing as a proc someday...) Any ideas on how to mess with the execution plan without changing the query text? – Joe Enos Mar 24 '15 at 20:56
  • 1
    @Guvante I'd love to reproduce in SSMS, but it always runs fine from there no matter what I do. It makes it that much more difficult for the DBAs to help, too, since they can't reproduce it. – Joe Enos Mar 24 '15 at 20:57
  • 1
    @SergueiFedorov Seems to scale just fine in general. In SSMS, and when the app behaves, the query runs just fine. We've got a few hundred thousand records in the main table, and even when executing this query for an unlimited date range, it'll come back in just a few seconds. – Joe Enos Mar 24 '15 at 20:59
  • Sorry I wasn't clear on the parameter - question is edited to show that the parameter I pass into the LINQ query actually ends up in the text of the SQL query, rather than as a parameter. I don't know why EF decides to do this instead of parameterizing the query. – Joe Enos Mar 24 '15 at 21:29
  • Can you get the plan from the cache with `SELECT qp.query_plan FROM sys.dm_exec_cached_plans CROSS APPLY sys.dm_exec_sql_text(plan_handle) st CROSS APPLY sys.dm_exec_query_plan(plan_handle) qp WHERE st.text LIKE '%Extent1].[Added_Time] >= convert(datetime2%' AND st.text NOT LIKE '%this_query%' ` – Martin Smith Mar 24 '15 at 21:55
  • 2
    Probably useful: [Slow in the Application, Fast in SSMS? Understanding Performance Mysteries](http://www.sommarskog.se/query-plan-mysteries.html). SSMS uses different query options than ADO.NET does, so it may not use the same cached query plan, which explains why you can't repro the slowdown in SSMS. Have you tried updating statistics? (The linked article has other troubleshooting suggestions as well.) – Joe White Mar 24 '15 at 21:59
  • (above where condition would actually need to be `LIKE 'Extent1].[[]Added_Time] >= convert(datetime2'` to escape the square bracket. – Martin Smith Mar 24 '15 at 22:01
  • @MartinSmith Thanks, I'll see if we can find this and get it to our DBAs. I still don't understand how to read query plans, but if I'm reading your query right, this will at least show me the plan for my query, so maybe they'll understand how to read it. – Joe Enos Mar 24 '15 at 22:18
  • @JoeWhite That's a bit of a read there...Thanks for the link - I'll check it out. Turns out that our weekly maintenance plan, which includes rebuilding indexes and statistics, hasn't been running for awhile. We're going to get that done tonight and see if that helps. – Joe Enos Mar 24 '15 at 22:20
  • @JoeEnos get the plan from the cache first. Those actions will cause the affected plans to be dropped. – Martin Smith Mar 24 '15 at 22:27
  • check for locking issues. you say 'just a couple of seconds' but really anything over 10ms is too long – Ewan Mar 24 '15 at 22:42
  • 1
    @Ewan you aren't going to get 2 million reads in 10ms. Check for missing indexes may be more to the point... – Martin Smith Mar 24 '15 at 22:47
  • The article Joe White linked is definitely worth a read; we had similar behaviour to that which you're describing (although in our case it was a badly cached execution plan with some duff parameter sniffing). It was a nightmare to track down, as the execution plan was cached for a particular user, so it was incredibly hard to replicate. – Chris Mar 25 '15 at 18:09

5 Answers5

5

I recently had a very similar scenario, a query would run very fast executing it directly in the database, but had terrible performance using EF (version 5, in my case). It was not a network issue, the difference was from 4ms to 10 minutes.

The problem ended up being a mapping problem. I had a column mapped to NVARCHAR, while it was VARCHAR in the database. Seems inoffensive, but that resulted in an implicit conversion in the database, which totally ruined the performance.

I'm not entirely sure on why this happens, but from the tests I made, this resulted in the database doing an Index Scan instead of an Index Seek, and apparently they are very different performance-wise.

comparison index scan and index seek

I blogged about this here (disclaimer: it is in Portuguese), but later I found that Jimmy Bogard described this exact problem in a post from 2012, I suggest you check it out.

Since you do have a convert in your query, I would say start from there. Double check all your column mappings and check for differences between your table's column and your entity's property. Avoid having implicit conversions in your query.
If you can, check your execution plan to find any inconsistencies, be aware of the yellow warning triangle that may indicate problems like this one about doing implicit conversion:

query issue implicit conversion warning

I hope this helps you somehow, it was a really difficult problem for us to find out, but made sense in the end.

Mahmoud Ali
  • 1,289
  • 1
  • 16
  • 31
4

Just to put this out there since it has not been addressed as a possibility:

Given that you are using Entity Framework (EF), if you are using Lazy Loading of entities, then EF requires Multiple Active Result Sets (MARS) to be enabled via the connection string. While it might seem entirely unrelated, MARS does sometimes produce this exact behavior of something running quickly in SSMS but horribly slow (seconds become several minutes) via EF.

One way to test this is to turn off Lazy Loading and either remove MultipleActiveResultSets=True; (the default is "false") or at least change it to be MultipleActiveResultSets=False;.

As far as I know, there is unfortunately no work-around or fix (currently) for this behavior.

Here is an instance of this issue: Same query with the same query plan takes ~10x longer when executed from ADO.NET vs. SMSS

Community
  • 1
  • 1
Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • Interesting, thanks - not a factor here since it was down to a single one-value query, so MARS wasn't coming into play. But it does give me another reason to be wary of Entity Framework. – Joe Enos Mar 27 '15 at 14:54
0

There is an excellent article about Entity Framework performance consideration here.

I would like to draw your attention to the section on Cold vs. Warm Query Execution:

The very first time any query is made against a given model, the Entity Framework does a lot of work behind the scenes to load and validate the model. We frequently refer to this first query as a "cold" query. Further queries against an already loaded model are known as "warm" queries, and are much faster.

During LINQ query execution, the step "Metadata loading" has a high impact on performance for Cold query execution. However, once loaded metadata will be cached and future queries will run much faster. The metadata are cached outside of the DbContext and will be re-usable as long as the application pool lives.

In order to improve performance, consider the following actions:

  • use pre-generated views
  • use query plan caching
  • use no tracking queries (only if accessing for read-only)
  • create a native image of Entity Framework (only relevant if using EF 6 or later)

All those points are well documented in the link provided above. In addition, you can find additional information about creating a native image of Entity Framework here.

Alex Sanséau
  • 8,250
  • 5
  • 20
  • 25
  • Doesn't seem to fit the behaviour described (where it works fine for other dates on the same model and repeatedly badly for the same date eliminating startup cost). Sounds like a bad cached execution plan to me. – Martin Smith Mar 24 '15 at 22:03
  • None of this takes 4 minutes of time. – usr Mar 24 '15 at 22:22
  • 3
    Thanks for the info - if it was just a few seconds I'd agree it could be related to loading up the model, but since it's several minutes, and happens over and over, and the SQL Profiler results are so ludicrously different, I'm leaning toward some kind of an issue executing the actual query rather than internal EF loading. – Joe Enos Mar 24 '15 at 22:23
  • 1
    I missed the reads information. Definitely an execution plan issue then. – Martin Smith Mar 24 '15 at 22:24
0

I don't have an specific answer as to WHY this is happening, but it certainly looks to be related with how the query is handled more than the query itself. If you say that you don't have any issues running the same generated query from SSMS, then it isn't the problem.

A workaround you can try: A stored procedure. EF can handle them very well, and it is the ideal way to deal with potentially complicated or expensive queries.

  • I'd love to switch to procs or regular SQL, but as I mentioned, this is a massive EF LINQ query (with lots of dynamic clauses tacked on depending on what you're searching by), so that's a huge undertaking that I'm not ready to do yet. – Joe Enos Mar 25 '15 at 15:10
-3

Realizing you are using Entity Framework 4.1, I would suggest you upgrade to Entity Framework 6.

There has been a lot of performance improvement and EF 6 is much faster than EF 4.1.

The MSDN article about Entity Framework performance consideration mentioned in my other response has also a comparison between EF 4.1 and EF 6.

There might be a bit of refactoring needed as a result, but the improvement in performance should be worth it (and that would reduce the technical debt at the same time).

Alex Sanséau
  • 8,250
  • 5
  • 20
  • 25
  • 2
    This is unspecific to the question. It could be posted as an answer to about 1000 questions on Stack Overflow. – usr Mar 24 '15 at 22:22
  • I may consider upgrading, or preferably dumping EF altogether, but like you said, that's going to take a significant amount of refactoring and testing. – Joe Enos Mar 24 '15 at 22:24
  • @usr: it might not end up being the accepted answer, but I find it a bit harsh to downvote an answer which suggests to upgrade to the latest version of EF when one has performance issues. That would be the first thing I'd do. – Alex Sanséau Mar 24 '15 at 22:39
  • 3
    @AlexSanséau: That may be, but it's not very relevant to the question. It's a comment at best. – recursive Mar 24 '15 at 23:01
  • This is not a good answer. In fact, there's no good reason for you to post two answers to this question. – John Saunders Mar 24 '15 at 23:34