SQL That Meant Well: Applying Conditions in the Subquery vs the Outer Query

Posted by

This is a new series that has been percolating in my head for a while. When I help clients with SQL, we’re often adding (or removing) indexes to help SQL performance. It has been said (though not by me) that SQL re-write is obsolete because the Db2 Optimizer will take care of that. But there are still some ways you can write a query that will have it using a much more expensive access plan than is needed. This series covers SQL that returned the required results, but does so in a way that could have been written much more efficiently.

Much of what is reviewed here would be true for any RDBMS, but I know it to be true for these queries on Db2 for Linux, Unix, and Windows. Table or column names may be changed to protect the innocent.

This particular SQL was found in the an analysis of the package cache using my favorite SQL for finding problem SQL in the package cache. In several different time frames analyzed, it was found to be consuming more than 10% of the Rows Read by all the queries in the package cache.

SQL Statement or Fragment Found in the Wild

SELECT *
FROM    (SELECT
                     ROW_NUMBER() OVER (PARTITION BY person_id 
                        ORDER BY person_id, environment_start_dt DESC ) seq
                   , person_id
                   , sample1
                   , sample2
                   , sample3
                   , environment_no
                   , environment_start_dt
        FROM
        schema.ENVIRNOMENT
        ) AS recentenv
WHERE
       seq = 1
       and person_id=?
;

Access Plan

Explaining this query showed most of the expense was in the table scan of schema.ENVIRONMENT.

      Rows
     RETURN
     (   1)
      Cost
       I/O
       |
   1.91471e-06
     FILTER
     (   2)
     29995.8
      17868
       |
     573015
     TBSCAN
     (   3)
     29880.2
      17868
       |
     573015
     SORT
     (   4)
     29859.9
      17868
       |
     573015
     TBSCAN
     (   5)
     29505.3
      17868
       |
     573015
 TABLE: SCHEMA
 ENVIRONMENT
       Q1

A Better Way to Get the Same Results

We tried this query instead to reduce the amount of data that actually needed to be processed:

SELECT *
FROM    (SELECT
                     ROW_NUMBER() OVER (PARTITION BY person_id 
                        ORDER BY person_id, environment_start_dt DESC ) seq
                   , person_id
                   , sample1
                   , sample2
                   , sample3
                   , environment_no
                   , environment_start_dt
        FROM
        schema.ENVIRNOMENT
        WHERE person_id=?
        ) AS recentenv
WHERE
       seq = 1
;

Modified Access Plan

Looking at the access plan, this syntax made a world of difference:

        Total Cost:             32.5022
        Query Degree:           1

                Rows
               RETURN
               (   1)
                Cost
                 I/O
                 |
                  1
               FILTER
               (   2)
               32.5022
                  3
                 |
               1.09716
               FETCH
               (   3)
               32.5014
                  3
             /---+----\
         1.09716      573015
         IXSCAN   TABLE: SCHEMA
         (   4)    ENVIRONMENT
         21.6697        Q1
            2
           |
         573015
     INDEX: DB2INST1
    IDX_ENVIRONMENT_01
           Q1

The Difference

That’s a 99.999% improvement in the timeron cost – not only do we need to evaluate fewer rows, but there is also an index that we can use to get those rows.

If we look at what we’re asking Db2 to do with each query, the difference is easy to understand:

Original Query: First, figure out a table that returns 6 columns from the table that represents every row from environment along with a column ranking the rows for each person_id from most recent environment start date (1) to least recent. After you have that, find the rows that match a specific person_id, and out of those, find the one row with a rank of 1.


Modified Query: First, figure out a table that returns 6 columns from the table, along with a column ranking the rows from most recent environment start date (1) to least recent for a specific person_id. Then return the row with a rank of 1.

Because the condition is applied on the inner query, and the cardinality of this column is pretty low (it is fairly unique), the rows that even need to be evaluated and sorted is much lower. The first query had to evaluate and work with over 500,000 rows to return one. The second query on average has to deal with one or two. This requires far fewer I/Os. Even if the table is already entirely in the buffer pool, this requires fewer CPU cycles to execute the query. With the modified query, we’re also getting the data in order from an index, which luckily allows us to also eliminate the sort.

Lead Db2 Database Engineer and Service Delivery Manager , XTIVIA
Ember is always curious and thrives on change. Working in IT provides a lot of that change, but after 17 years developing a top-level expertise on Db2 for mid-range servers and more than 7 years blogging about it, Ember is hungry for new challenges and looks to expand her skill set to the Data Engineering role for Data Science. With in-depth SQL and RDBMS knowledge, Ember shares both posts about her core skill set and her journey into Data Science. Ember lives in Denver and work from home for XTIVIA, leading a team of Db2 DBAs.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.