I'm no expert on the inner workings/query optimisation process of SQL Server, but here's my 2 pence (or cents if you prefer).
I believe it's due to the use of the ROW_NUMBER() value within the WHERE clause. As an example, I created a sample table, populated with 1000 rows from ID 1 to 1000 (ID as a primary key) like you said.
If you take out the ROW_NUMBER(), and do the query based on the ID column like this:
Select * FROM
(
SELECT ID, Name
FROM Log
)
as LogWithRowNumbers
WHERE ID>=1 and ID<=2
Then it correctly shows row count as being 2 - as expected really.
Now, working backwards, add in the ROW_NUMBER to the inner SELECT but leave the WHERE clause as-is like below:
Select * FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY ID DESC) AS RowNo,
ID, Name
FROM Log
)
as LogWithRowNumbers
WHERE ID>=1 AND ID <=2
This still shows the correct row count as 2.
Finally, set the WHERE clause back to use the RowNo as the column being filtered on instead of ID, this is when the estimated row count jumps to 9.
Hence, I believe it's the use of the ROW_NUMBER() function, being filtered on in the WHERE clause that is the cause. So I'd imagine it's because there are obviously better/more accurate statistics available on actual table columns than there are on this function-produced value.
I hope this at least provides a good starting point, hopefully will be useful!