Building an admittedly rather simple test bed on SQL Server 2012 (11.0.6020) allows me to recreate a plan with two hash matched queries being concatenated via a UNION ALL
. My test-bed does not display the incorrect estimate you see. Perhaps this is a SQL Server 2014 CE problem.
I get an estimate of 133.785 rows for a query that actually returns 280 rows, however that is to be expected as we'll see further on down:
IF OBJECT_ID('dbo.Union1') IS NOT NULL
DROP TABLE dbo.Union1;
CREATE TABLE dbo.Union1
(
Union1_ID INT NOT NULL
CONSTRAINT PK_Union1
PRIMARY KEY CLUSTERED
IDENTITY(1,1)
, Union1_Text VARCHAR(255) NOT NULL
, Union1_ObjectID INT NOT NULL
);
IF OBJECT_ID('dbo.Union2') IS NOT NULL
DROP TABLE dbo.Union2;
CREATE TABLE dbo.Union2
(
Union2_ID INT NOT NULL
CONSTRAINT PK_Union2
PRIMARY KEY CLUSTERED
IDENTITY(2,2)
, Union2_Text VARCHAR(255) NOT NULL
, Union2_ObjectID INT NOT NULL
);
INSERT INTO dbo.Union1 (Union1_Text, Union1_ObjectID)
SELECT o.name, o.object_id
FROM sys.objects o;
INSERT INTO dbo.Union2 (Union2_Text, Union2_ObjectID)
SELECT o.name, o.object_id
FROM sys.objects o;
GO
SELECT *
FROM dbo.Union1 u1
INNER HASH JOIN sys.objects o ON u1.Union1_ObjectID = o.object_id
UNION ALL
SELECT *
FROM dbo.Union2 u2
INNER HASH JOIN sys.objects o ON u2.Union2_ObjectID = o.object_id;
I think the reason is around the lack of statistics for the two resulting joins that are UNIONed. SQL Server needs to make educated guesses in most cases around the selectivity of columns when faced with the lack of statistics.
Joe Sack has an interesting read on that here.
For a UNION ALL
, it's safe to say we'll see exactly the total number of rows returned by each component of the union, however since SQL Server is using row estimates for the two components of the UNION ALL
, we see it adds the total estimated rows from both queries to come up with the estimate for the concatenation operator.
In my example above, the estimated number of rows for each portion of the UNION ALL
is 66.8927, which when summed equals 133.785, which we see for the estimated number of rows for the concatenation operator.
The actual execution plan for the union query above looks like:

You can see the "estimated" vs "actual" number of rows. In my case, adding the "estimated" number of rows returned by the two hash match operators exactly equals the amount shown by the concatenation operator.
I would try to get output from trace 2363 etc as recommended in Paul White's post you show in your question. Alternately, you might try using OPTION (QUERYTRACEON 9481)
in the query to revert back to the version 70 CE to see if that "fixes" the issue.