Over a million developers have joined DZone.

Correlated Subqueries Are Evil and Slow... Or Are They?

Are correlated subqueries in SQL really that evil or slow? Read on as we examine some data concerning this myth and try to reach a conclusion.

· Database Zone

Learn NoSQL for free with hands-on sample code, example queries, tutorials, and more.  Brought to you in partnership with Couchbase.


A common myth in SQL is the idea that correlated subqueries are evil and slow. For example, this query here:

SELECT 
 first_name, last_name,
 (SELECT count(*) 
 FROM film_actor fa 
 WHERE fa.actor_id = a.actor_id)
FROM actor a


It “forces” the database engine to run a nested loop of the form (in pseudo code):

for (Actor a : actor) {
 output(
 a.first_name,
 a.last_name,
 film_actor.where(fa -> fa.actor_id = a.actor_id)
 .size()
}


So, for every actor, collect all the corresponding film_actors and count them. This will produce the number of films each actor has played in.

It appears that it would be much better to run this query in “bulk”, I.e. to run:

SELECT 
 first_name, last_name, count(*)
FROM actor a
JOIN film_actor fa USING (actor_id)
GROUP BY actor_id, first_name, last_name


But is it really faster? And if so, why would you expect that?

Bulk aggregation vs nested loops

Bulk aggregation in this case really just means that we’re collecting all actors and all film_actors and we then merge them in memory for the group by operation. The execution plan (in Oracle) looks like this:

-------------------------------------------------------------------
| Id  | Operation              | Name                    | A-Rows |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                         |    200 |
|   1 |  HASH GROUP BY         |                         |    200 |
|*  2 |   HASH JOIN            |                         |   5462 |
|   3 |    TABLE ACCESS FULL   | ACTOR                   |    200 |
|   4 |    INDEX FAST FULL SCAN| IDX_FK_FILM_ACTOR_ACTOR |   5462 |
-------------------------------------------------------------------


There are 5462 rows in our film_actor table, and for each actor, we join and group and aggregate them all to get to the results. Let’s compare this to the nested loop’s plan:

-----------------------------------------------------------------------
| Id  | Operation         | Name                    | Starts | A-Rows |
-----------------------------------------------------------------------
|   0 | SELECT STATEMENT  |                         |      1 |    200 |
|   1 |  SORT AGGREGATE   |                         |    200 |    200 |
|*  2 |   INDEX RANGE SCAN| IDX_FK_FILM_ACTOR_ACTOR |    200 |   5462 |
|   3 |  TABLE ACCESS FULL| ACTOR                   |      1 |    200 |
-----------------------------------------------------------------------


I’ve now included the “Starts” row to illustrate that after having collected all the 200 actors, we start the subquery 200 times to get the count for each actor. But this time, with a range scan.

From the plan only, can we tell which one is faster? Bulk aggregation will need more memory (to load all the data), but has a lower algorithmic complexity (linear). Nested looping will need less memory (all the required info is available from the index directly) but seems to have a higher algorithmic complexity (quadratic).

Fact is, this isn’t exactly true.

The bulk aggregation is indeed linear, but according to O(M + N) where M = number of actors and N = number of film_actors, whereas nested looping isn’t quadratic, it’s O(M log N). We don’t need to traverse the entire index to get the count.

At some point, the higher complexity is worse, but with this little amount of data, it’s not:

On the x-axis is the size of N and on the y-axis is the “complexity value”, e.g. how much time (or memory) is used by an algorithm.

Effects of algorithmic complexity for large N









Effects of algorithmic complexity for large N


Effects of algorithmic complexity for "small" N









Effects of algorithmic complexity for “small” N


Here’s a disclaimer about the above:

Algorithmic complexity for “small N” = “works on my machine”

There’s nothing better than proving things with measurements. Let’s run the following PL/SQL program:

SET SERVEROUTPUT ON
DECLARE
 v_ts TIMESTAMP;
 v_repeat CONSTANT NUMBER := 1000;
BEGIN
 v_ts := SYSTIMESTAMP;

 FOR i IN 1..v_repeat LOOP
 FOR rec IN (
 SELECT 
 first_name, last_name,
 (SELECT count(*) 
 FROM film_actor fa 
 WHERE fa.actor_id = a.actor_id)
 FROM actor a
 ) LOOP
 NULL;
 END LOOP;
 END LOOP;

 dbms_output.put_line('Nested select: ' || (SYSTIMESTAMP - v_ts));
 v_ts := SYSTIMESTAMP;

 FOR i IN 1..v_repeat LOOP
 FOR rec IN (
 SELECT 
 first_name, last_name, count(*)
 FROM actor a
 JOIN film_actor fa USING (actor_id)
 GROUP BY actor_id, first_name, last_name
 ) LOOP
 NULL;
 END LOOP;
 END LOOP;

 dbms_output.put_line('Group by : ' || (SYSTIMESTAMP - v_ts));
END;
/


After three runs, and against our standard Sakila database (get it here: https://github.com/jOOQ/jOOQ/tree/master/jOOQ-examples/Sakila) with 200 actors and 5462 film_actors, we can see that the nested select consistently outperforms the bulk group by:

Nested select: +000000000 00:00:01.122000000
Group by     : +000000000 00:00:03.191000000

Nested select: +000000000 00:00:01.116000000
Group by     : +000000000 00:00:03.104000000

Nested select: +000000000 00:00:01.122000000
Group by     : +000000000 00:00:03.228000000


Conclusion

We have shown that under some circumstances, correlated subqueries can be better than bulk aggregation. In Oracle. With small-medium sized data sets. In other cases, that’s not true as the size of M and N, our two algorithmic complexity variables increase, O(M log N) will be much worse than O(M + N).

The conclusion here is: Don’t trust any initial judgment. Measure. When you run such a query a lot of times, 3x slower can make a big difference. But don’t go replace all your bulk aggregations either.

The Getting Started with NoSQL Guide will get you hands-on with NoSQL in minutes with no coding needed. Brought to you in partnership with Couchbase.

Topics:
database ,queries ,performance ,sql ,pl/sql ,data

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}