THE SQL Server Blog Spot on the Web

Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | |
in Search

Paul White: Page Free Space

A technical SQL Server blog from New Zealand. See also my articles on SQLperformance.com

Iterators, Query Plans, and Why They Run Backwards

Iterators

SQL Server uses an extensible architecture for query optimisation and execution, using ‘iterators’ as basic building-blocks. 

Iterators are probably most familiar in their graphical showplan representation, where each icon represents a single iterator.  They also show up in XML query plan output as RelOp nodes.

Iterators

Each iterator performs a single simple function, such as applying a filtering condition, or performing an aggregation.  It can represent a logical operation, a physical operation, or (most often) both.

For example, ‘Aggregate’ is a logical operation, and Stream Aggregate and Hash Aggregate are physical operations.  Similarly, ‘Inner Join’ is a logical operation; Nested Loops a physical one.  Both graphical plans and XML showplan output show these properties:

Logical_Physical[8]

Logical iterators can be combined in complex ways to satisfy any requirement that can be expressed in SQL.  The physical iterators combine to produce an executable tree used by the execution engine to produce results.

The logical iterators represent the original query translated into relational algebra; the physical iterators represent a physical algebra: the concrete algorithms used by the SQL Server storage engine and query processor to perform a logical task.

Those of you that followed my series of posts on optimiser internals may remember that implementation rules in the query optimiser convert a logical requirement to one of the possible physical implementations.  Each round of exploration rules expand the plan search space by applying transformations, before the implementation rules run to map in the possible physical iterator forms.  Once that’s done, cost-based decisions can be made to select a final plan for execution.

A Common Interface and Design

Iterators are code objects, which share a common interface of methods and properties.  The most frequently used of these are the methods used to process rows, set and retrieve property information, and produce a cost estimate for the optimiser to use.

These common features (and the abstractness of the design) enable easy extensibility as new database and language features are introduced.  It also allows iterators to be connected in a tree structure without any of them needing to know anything about what other iterators it might be connected to.

The executable code that forms the core of the iterator’s implementation is also written to avoid hard-wired dependencies on the parent and child iterators in the plan tree.  This avoids the need to recompile the iterator base code each time it is incorporated in a new plan.  Those of you that have some programming experience will recognise this arrangement as one that uses “function pointers”.

All iterators also require a small fixed amount of memory to store state (think local variables) while they are executing.  This memory forms part of the compiled plan, so a formal memory grant is not required each time a plan is executed.  The cached size of a plan includes this memory allocation.

The common methods most relevant to the present discussion are those used to process a set of rows.  These are the Open, GetNext, and Close methods.  (These names might give you a clue as to why query plan nodes are known as ‘iterators’).

Plan Execution

Let’s look at a simple plan (the query itself isn’t important):

Simple-Plan

 

 

 

 

 

Have you ever wondered exactly how a plan like that is executed?  Where does it start, and how does it know when it has finished?  Judging from the lines and arrows that connect the iterators in a graphical plan, we might assume that execution starts with the data-producing iterators (the scans) and continues until they run out of rows.

The arrow heads on the connecting lines do indicate the direction of data flow; the iterator execution order is quite different, however.  Query execution always begins with the root iterator – the hash join in the diagram above.

The execution engine only ever directly interacts with this root iterator.  It calls its Open method to prepare it to return data, and then repeatedly calls its GetNext method to retrieve one row of query output at a time.  As soon as the GetNext method call fails to return a new row, the engine calls the Close method, and query execution ends.  Simple as.

The Ultimate Cursors

Ok, so there’s a bit more to it than that.  Let’s see what happens when the Open method is called on the hash join iterator:

  1. The hash join calls the Open method on its build input.
  2. The Open method of the Scan iterator prepares the storage engine to start producing rows from the Product table.  Control then returns to the hash join (which is still running its Open method, remember).
  3. The hash join repeatedly calls the GetRow method on the Scan iterator.
  4. Each GetRow call returns a single row from the Scan to the hash join.  These rows are used to build the join hash table.
  5. As soon as a call to GetRow on the Product table Scan reports that there are no more rows, the hash join calls the Close method on the Scan.
  6. The Scan iterator performs its clean-up tasks, and control again returns to the hash join.
  7. The hash join now calls Open on its inner input (the probe input).
  8. The Inventory table Scan iterator prepares its connection to the storage engine, and returns control to the hash join.
  9. The hash join returns from its Open method call (finally!) and control returns to the execution engine.

The execution engine now repeatedly calls GetRow on the hash join, to return the query results:

  1. The hash join calls GetRow on its probe input.
  2. The Scan’s GetRow method returns a single row from the Inventory table.
  3. The hash join performs the join using its hash table.
  4. If the row does not join, go back to step 1
  5. If the row does join, return it to the engine

This process continues until the probe input Scan reports that no more rows are available.  The hash join then also returns from its GetRow method without producing a row.  When that happens, the execution engine knows that the query is complete, and calls the Close method on the hash join, to allow it to clean up:

  1. The hash join calls Close on its probe input.
  2. The Scan performs its clean-up tasks and returns control to the hash join.
  3. The hash join deallocates its hash table, and returns from the Close method.

This row-by-row processing (calling the GetNext method repeatedly) is why query plan nodes are referred to as iterators: they iterate over a set of input rows, returning one row at a time to its parent.

Execution of a query plan starts at the root, and flows down the plan via a series of nested method calls.  Data is pulled up the plan in the reverse direction: from the leaves toward the root, a row at a time.  Query plans do run backward :)

Inside an Iterator

Exactly what is done in the Open, GetNext, and Close methods depends on the type of iterator, and the mode in which it is running.  The following table gives a broad outline of the work performed by different iterators when these methods are called:

Basic-Iterator-Methods

There is a lot more to say about query plan execution and iterators, so I’ll have to return to this topic in a future post.

© Paul White
email: SQLkiwi@gmail.com
twitter: @SQL_Kiwi

Published Thursday, August 05, 2010 8:10 AM by Paul White

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Dylan said:

Paul,

You've been posting some really interesting articles recently!  Thanks for you hard work!

August 5, 2010 11:30 AM
 

Page Free Space: Paul White said:

Background One of the core assumptions made by the SQL Server query optimiser’s model is that clients

August 18, 2010 4:29 AM
 

Simon said:

Hi Paul,

If I understand correctly, that mean for Hash Match, all Product records needs to be selected first before any rows are obtained from ProductInventory? So SQL cant build its input and probe table concurrently?

November 2, 2011 9:16 PM
 

Paul White said:

Hi Simon,

That's right.  For hash matching you have to populate the hash table with all entries from the build input first, before probing for matches.  If you didn't, you might miss a match!

There's an explanation of hash matching in Books Online:

http://msdn.microsoft.com/en-us/library/ms189313.aspx

Paul

November 2, 2011 9:29 PM
 

Simon said:

Thanks for answering my question. Its an excellent article.

November 2, 2011 10:09 PM
 

Paul White: Page Free Space said:

There was an interesting question asked by Mark Storey-Smith on dba.stackexchange.com back in October

May 2, 2012 1:57 PM
 

Paul White: Page Free Space said:

This is a post for T-SQL Tuesday #43 hosted by my good friend Rob Farley . The topic this month is Plan

June 11, 2013 5:00 AM
 

Andre Ranieri said:

Paul:

This is a great article.  Thanks for posting this, it's helped further my understanding of how SQL optimizer internals work.

Thanks,

Andre Ranieri

July 15, 2013 1:26 PM
 

Paul White: Page Free Space said:

Nested loops join query plans can be a lot more interesting (and complicated!) than is commonly realized.

August 30, 2013 9:24 PM

Leave a Comment

(required) 
(required) 
Submit
Powered by Community Server (Commercial Edition), by Telligent Systems
  Privacy Statement