Execution plans (actual, not estimated) need to be added to the Q for a definitive answer but...
How Can the Same Query in Two Nearly Identical Instances Generate Two
Different Execution Plans?
Because, by your admission, they are not identical. Most likely explanation for the different execution plans is a variance in statistics.
Table rows counts are slightly larger on Server A... Statistics are
updated nightly at the same time on both servers.
Row counts are different and the statistics update was probably a default sample, rather than FULLSCAN
. I've witnessed some comically unfortunate stats histograms as the result of sampling, each of which has been corrected by a FULLSCAN
update.
Your code says you are doing a LEFT OUTER JOIN
, but are you really? Your very first WHERE
clause (as well as each that follow) filters rows for inclusion from the outer table:
WHERE (A.[Operator_Name] LIKE '%'+ @OName +'%')
This turns your LEFT OUTER JOIN
into an INNER JOIN
, whether you meant it or not. Perhaps you meant to move those filters to the ON
clause.
Of course I would probably remove the non-sargeable MONTH()
function from the JOIN
, since this is going to force a complete scan on the remote table, and write it this way instead - also eliminating the join to your local months table:
DECLARE
@OName VARCHAR(50) = 'John',
@Start_Date DATE = NULL,--'20120101', -- use safe date formats
@End_Date DATE = '20121231'; -- use semi-colons
-- deal with NULLs here so the query is simpler:
SELECT @Start_Date = COALESCE(@Start_Date, '20010101'),
@End_Date = COALESCE(@End_Date, CURRENT_TIMESTAMP);
;WITH n(n) AS
(
-- get the # of months you need instead of relying on your Connector table:
SELECT TOP (DATEDIFF(MONTH, @Start_Date, @End_Date)+1) Number
FROM master..spt_values
WHERE [type] = N'P' AND Number >= 0
ORDER BY Number
), m(m) AS
(
-- convert those to months:
SELECT DATEADD(MONTH, n, DATEADD(MONTH, DATEDIFF(MONTH, 0, @Start_Date), 0))
FROM n
)
SELECT
m.m,
MonthName = DATENAME(MONTH, m.m),
[Widget Count] = COUNT(w.Widget_ID)
FROM m
LEFT OUTER JOIN [Server].[Database].dbo.Widget AS w -- meaningful alias
ON
w.Widget_Date >= m.m
AND w.Widget_Date < DATEADD(MONTH, 1, m.m) -- open-ended date range query
AND w.Operator_Name LIKE '%' + @OName + '%'
GROUP BY m.m
ORDER BY m.m;
Just ignore the m
column in the output (you can't order by it without including it in the output, and it doesn't make sense to order by the name).
Another suggestion: don't use 'single quotes'
as alias delimiters. Forms of this are deprecated and it also makes column alias look like string literals. Use [square brackets]
instead.
Best Answer
I'm going to address this part of the question:
Assuming the table definition looks something like this:
Empty table
The following query (not my recommendation, just an example):
Gives this execution plan (with no data in the table):
The numbers correspond to the Node ID property of each operator.
E1
Table Scan (4) returns all rows (in no particular order). For each row:E1.Salary
value as a parameter.E2
Table Scan (8) finds rows matchingE2.Salary > E1.Salary
E2.Salary
.E2.Salary
). The new count column is given the labelExpr1008
.bigint
) tointeger
because the query specifiesCOUNT
rather thanCOUNT_BIG
. The new column is given the labelExpr1004
.Expr1004 = 1
E1.Salary
values.With duplicate Salary values
If we add a few rows to the table, with some duplicate
Salary
values:The query above produces a plan with a couple of new operators (highlighted):
The new Sort orders rows by
E1.Salary
. The ensures that any duplicateSalary
values are presented to the join sequentially.The Lazy Index Spool saves the output from the operators below it as they arrive. These are the Stream Aggregate, Distinct Sort, and
E2
Table Scan (with predicate), which as described before filterE2
(using the current value ofE1.Salary
), remove duplicateE2.Salary
values, and count the resulting rows. The Lazy Index Spool saves recomputing the result in case the sameSalary
value from tableE1
is presented on the next iteration of the Nested Loops join.Because the Sort ordered rows from
E1
byE1.Salary
, any duplicates are presented to the join one after the other, so the saved result in the spool can be reused instead of re-running the Stream Aggregate, Distinct Sort, andE2
Table Scan each time.The index on the spool is keyed by
Salary
, making it quick to find the savedCOUNT(DISTINCT E2.Salary)
result for the currentE1.Salary
value.If you look at the Actual Executions value for each operator in the plan, you will see that the Lazy Index Spool executes 6 times (once per outer row), whereas the Stream Aggregate, Distinct Sort, and
E2
Table Scan execute only 3 times (once per distinct salary value).The optimizer introduces the extra Sort and Spool as a performance optimization, based on automatically collected statistical information about the data in the table.