1

I have a table with more than a billion rows, with one of the column definitions being:

row_id int NOT NULL

And an index on it

 CREATE NONCLUSTERED INDEX row_id_IX ON dbo.test_table (  row_id ASC  )  

I want to find the minimum value of row_id in the table. So I write

select min(row_id) from test_table 

Given this index is a B+ tree in SQL Server, and the values are NOT-nullable, I expect the query plan to recursively seek towards left child of the root node of B+ tree, until it finds the minimum value. But instead, the planner decides to do an Index scan and ends up reader a billion values followed by an aggregation to find the min value.

Why is that?

enter image description here

row_id_IX is a partitioned index.

5
  • 2
    This is classic problem with partitioned indexes, which you conveniently don't mention are at play here Commented Jan 26 at 20:11
  • 1
    Please use brentozar.com/pastetheplan for execution plans
    – Dale K
    Commented Jan 26 at 20:13
  • So this is just something microsoft could optimize but did not do yet ?
    – Ram
    Commented Jan 26 at 20:20
  • We need you to share a link to the plan via PasteThePlan, the screenshot isn't very useful. And CREATE NONCLUSTERED INDEX row_id_IX ON dbo.test_table ( row_id ASC ) is clearly not the full definition, as the table is partitioned. Commented Jan 27 at 15:03
  • That could be the CREATE that was run as it will default to partition aligning if the underlying table is partitioned but full DDL including name of partition function would be useful Commented Jan 27 at 16:38

1 Answer 1

6

A solution to this is to get the MIN per partition (which should just require reading the single row at the end of the index from each) and then get the MIN of those.

One query approach for this (covered by Itzik Ben Gan here) is

SELECT MIN(m.row_id) AS row_id
FROM   sys.partitions AS P
       CROSS APPLY (SELECT MIN(row_id) AS row_id
                    FROM  dbo.test_table
                    WHERE  $PARTITION.YourPartitionFunction(YourPartitionColumn)  = P.partition_number) AS m
WHERE  P.object_id = OBJECT_ID('dbo.test_table')
       AND P.index_id <= 1; 

You haven't told us the name of your partition function or partitioning column so replace YourPartitionColumn and YourPartitionFunction as appropriate above (and remember to replace both references to test_table when applying it against a different table name).

The execution plan for this should have the partition numbers returned from sysrowsets on the outside of a nested loops and on the inside of this an ordered index scan with a seek predicate to seek into just the single partition of interest. This should be under a TOP 1 operator to stop reading from the scan after the first row is read.

If you don't see the above plan (e.g. as the column you are aggregating is nullable) a more verbose alternative is

SELECT MIN(m.row_id) AS row_id
FROM   sys.partitions AS P
       CROSS APPLY (SELECT TOP 1 row_id
                    FROM  dbo.test_table
                    WHERE  $PARTITION.YourPartitionFunction(YourPartitionColumn)  = P.partition_number
                       AND row_id IS NOT NULL
                       ORDER BY row_id
                    ) AS m
WHERE  P.object_id = OBJECT_ID('dbo.test_table')
       AND P.index_id <= 1; 

The more verbose alternative does mean that you will potentially miss out on the message:

Warning: Null value is eliminated by an aggregate or other SET operation.

but I doubt anyone cares about seeing that.

2
  • Perhaps a small optimization would be to do AND p.rows > 0? Or does it mess things up Commented Jan 26 at 20:54
  • It shouldn't mess things up w.r.t. the execution plan but rows isn't necessarily going to be transactionally consistent - e.g. if running at RCSI and so they should see the state at the start of statement execution and mid execution another transaction deletes all rows from a partition Commented Jan 26 at 20:59

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.