6

My query is fairly complex, but I have simplified it to figure out this problem and now it is a simple JOIN that I'm running on a SQL Server 2014 database. The query is:

SELECT * FROM SportsCars as sc INNER JOIN Cars AS c ON c.CarID = sc.CarID WHERE c.Type = 1

When I run this query from SMSS and watch it in SQL Profiler, it takes around 350ms to execute. When I run the same query inside my application using Entity Framework or ADO.NET (I've tried both). It takes 4500ms to execute.

ADO.NET Code:

using (var connection = new SqlConnection(connectionString))
{
    connection.Open();
    var cmdA = new SqlCommand("SET ARITHABORT ON", connection);
    cmdA.ExecuteNonQuery();

    var query = "SELECT * FROM SportsCars as sc INNER JOIN Cars AS c ON c.CarID = sc.CarID WHERE c.Type = 1";
    var cmd = new SqlCommand(query, connection);
    cmd.ExecuteNonQuery()
}

I've done an extensive Google search and found this awesome article and several StackOverflow questions (here and here). In order to make the session parameters identical for both queries I call SET ARITHABORT ON in ADO.NET and it makes no difference. This is a straight SQL query, so there is not a parameter sniffing problem. I've simplified the query and the indexes down to their most basic form for this test. There is nothing else running on the server and there is nothing else accessing the database during the test. There are no computed columns in the Cars or SportsCars table, just INTs and VARCHARs.

The SportsCars table has about 170k records and 4 columns, and the Cars table has about 1.2M records and 7 columns. The resulting data set (SportsCars of Type=1) has about 2600 records and 11 columns. I have a single non-clustered index on the Cars table, on the [Type] column that includes all the columns of the cars table. And both tables have a clustered index on the CarID column. No other indexes exist on either table. I'm running as the same database user in both cases.

When I view the data in SQL Profiler, I see that both queries are using the exact same, very simple query plan. In SQL Profiler, I'm using the Performance Event Class and the ShowPlan XML Statistics Profile, which I believe to be the proper event to monitor and capture the actual execution plan. The # of reads is the same for both queries (2596).

How can two exact same queries with the exact same query plan take 10x longer in ADO.NET vs. SMSS?

Community
  • 1
  • 1
  • Just to confirm, you are running SSMS and the ado.net query on the same server? – Chris Larabell Dec 08 '14 at 04:02
  • Yes, same server, same database, same tables within seconds of each other. – Dave Weisberg Dec 08 '14 at 04:11
  • How many rows are there? Maybe the DataTable spends a lot of CPU time. – usr Dec 08 '14 at 04:54
  • You should run that sql in one command, like this: `"SET ARITHABORT ON;SELECT * FROM SportsCars as sc INNER JOIN Cars AS c ON c.CarID = sc.CarID WHERE c.Type = 1"` – Joel Coehoorn Dec 08 '14 at 16:56
  • Thanks @JoelCoehoorn, I tried that and it doesn't make any difference. – Dave Weisberg Dec 08 '14 at 18:46
  • @DaveWeisberg, I'm trying to use ADO.net to get a query plan in c#. We have reason to believe RapidSQL gives one plan and ADO.net is using another, but I won't know how to find the plan in c#. HOw did you go about figuring out the show plan? Just Sql Profiler? – R. Romero Mar 12 '20 at 15:46

2 Answers2

7

Figured it out:

Because I'm using Entity Framework, the connection string in my application has MultipleActiveResultSets=True. When I remove this from the connection string, the queries have the same performance in ADO.NET and SSMS.

Apparently there is an issue with this setting causing queries to respond slowly when connected to SQL Server via WAN. I found this link and this comment:

MARS uses "firehose mode" to retrieve data. Firehose mode means that the server will produce data as fast as possible. This also means that your client application must receive inbound data at the same speed as it comes in. If it doesn't the data storage buffers on the server will fill up and the processing will stop until those buffers empty.

So what? You may ask... But as long as the processing is stopped the resources on the SQL server are in use and are tied up. This includes the worker thread, schema and data locks, memory, etc. So it is crucial that your client application consumes the inbound results as quickly as they arrive.

I have to use this setting with Entity Framework otherwise lazy loading will generate exceptions. So I'm going to have to figure out some other workaround. But at least I understand the issue now.

  • This was the solution to my issues aswell. Thanks. Regarding the need to use the setting for entity framework, i guess you could load the connectionstring manually and add the setting in-code before passing connectionstring to ef context. – Moulde Feb 09 '16 at 07:49
0

How can two exact same queries with the exact same query plan take 10x longer in ADO.NET vs. SMSS?

First we need to be clear about what is considered "same" with regards to queries and query plans. Assuming that the query at the very top of the question is a copy-and-paste, then it is not the same query as the one being submitted via ADO.NET. For two queries to be the same, they need to be byte-by-byte the same, which includes all white-space, capitalization, punctuation, comments, etc.

The two queries shown are definitely very similar. And they might even share the same execution plan. But how was "same"ness determined for those? Was the XML the same in both cases? Or just what was shown graphically in SSMS when viewing the plans? If they were determined to be the same based on their graphical representation then that is sometimes misleading. The XML itself needs to be checked. Even if two query plans have the same query hash, there are still (sometimes) parts of a query plan that are variable and changes do not change the plan hash. One example is the evaluation of expressions. Sometimes they are calculated and their result is embedded into the plan as a constant. Sometimes they are calculated at the start of each execution and stored and reused within that particular execution, but not for any subsequent executions.

One difference between SSMS and ADO.NET is the default session properties for each. I thought I had seen a chart years ago showing the defaults for ADO / OLEDB / SQLNCLI but can't find it out. Either way, it doesn't need to be guess work as it can be discovered using the SESSIONPROPERTY function. Just run this query in the C# code instead of your current SELECT, and inspect the results in debug or print them out or whatever. Either way, run something like this:

SELECT SESSIONPROPERTY('ANSI_NULLS') AS [AnsiNulls],
       SESSIONPROPERTY('ANSI_PADDING') AS [AnsiPadding],
       SESSIONPROPERTY('CONCAT_NULL_YIELDS_NULL') AS [ConcatNullYieldsNull],
       ...;

Make sure to get all of the setting noted in the linked MSDN page. Now, in SSMS, go to the "Query" menu, select "Query Options...", and go to "Execution" | "ANSI". The settings coming back from the C# code need to match the ones showing in SSMS. Anything set different requires adding something like this to the beginning of your ADO.NET query string:

SET ANSI_NULLS ON;
{rest of query}

Now, if you want to eliminate the DataTable loading from being a possible suspect, just replace that line, just replace:

var cars = new DataTable();
cars.Load(reader);

with:

while(reader.Read());

And lastly, why not just put the query into a Stored Procedure? The session settings (i.e. ANSI_NULLS, etc) that typically matter the most are stored with the proc definition so they should work the same whether you EXEC from SSMS or from ADO.NET (again, we aren't dealing with any parameters here).

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • I changed the queries so that they are identical in both cases (same characters, same white-space). I checked the Session Properties and they are identical except for **ARITHABORT** which is OFF for ADO.NET and ON for SMSS. So I put a SET ARITHABORT ON in my ADO.NET code. Still no luck. I took out the DataTable load in the ADO.NET code and still no luck. I updated the question to reflect these changes. I will try a stored proc and let you know, but using a stored proc is not ideal for my application for many reasons. – Dave Weisberg Dec 08 '14 at 16:39
  • @DaveWeisberg Just to rule out a possible red herring, try `SET ARITHABORT OFF` in SSMS and run the query. And what options do you specify in your connection string? Also, in SQL Profiler, if looking at the "SQL: Batch Completed" event, does it show the full 4500 ms for the query? – Solomon Rutzky Dec 08 '14 at 18:28