by Markus Winand.

Dynamic SQL is Slow


The true essence of the “Dynamic SQL is Slow” myth is rather simple; dynamic SQL can be slow—when done wrong.

The problem is that dynamic SQL is often used for the wrong reason, sometimes even without knowing. To clarify the confusion, I will use the following terms as explained:

Embedded SQL

Embedding SQL directly into program source code is very common in procedural database languages such as Oracle PL/SQL or Microsoft Transact-SQL. It is also possible to embed SQL into other languages such as C.

The benefit of embedded SQL is the smooth integration with the respective programming language. However, embedded SQL is compiled into the program. It can not change a runtime—it’s static.

Dynamic SQL

Dynamic SQL is handled as string within the application. The application may change the SQL string at runtime before passing it to the database layer. It is actually the most common way to access databases.

Static SQL

I use the term static SQL to describe SQL statements that do not change at runtime. No matter if it is embedded SQL that cannot change at runtime, or dynamic SQL that could change, but doesn’t.

The crux of these definitions is that a statement can be dynamic and static SQL at the same time. In other words, there are different levels of dynamic SQL. Consider the following example:

String sql = "SELECT first_name, last_name"
           + "  FROM employees"
           + " WHERE employee_id = " + employeeId;

ResultSet rs = con.executeQuery(sql);

Is that dynamic SQL? According to the definition above, it is. The SQL statement is prepared as string and passed to the database layer. But is it static SQL as well? Assuming that value of the employeeId variable changes, it’s not static SQL because the SQL string changes at runtime. It is an example for dynamic SQL that will actually hurt performance. The problem is not that it is dynamic SQL but that it doesn’t use bind variables. SQL bind variables—e.g., the question mark ? or :name—are placeholders for values that change during execution. That means, that the example can be turned into static SQL by using a bind variable instead of the actual value of the employeeId variable.

Important

Not using bind parameters is misused dynamic SQL.

Bind parameters are very important for security and performance.

A reasonable use of dynamic SQL is to change the structure of the statement at runtime. That’s something that cannot be done with bind parameters. E.g., a conditional where clause:

String where = "";
if (subsidiaryId != null) {
   where += (where == "") ? " WHERE " : " AND " 
         +  "subsidiary_id = " + subsidiaryId;
}
if (employeeId != null) {
   where += (where == "") ? " WHERE " : " AND " 
         +  "employee_id = " + employeeId;
}
if (lastName != null) {
   where += (where == "") ? " WHERE " : " AND " 
         +  "UPPER(last_name) = '"+lastName.toUpperCase()+"'";
}
String SQL = "SELECT employee_id, first_name, last_name "
           + "  FROM employees" 
           +   where;
// execute SQL

The code builds an SQL statement to fetch employees based on any combination of three filter criteria. Although it is rather awkward, the constructed SQL can be executed using the best available index. Nevertheless, this approach is troublesome because of the possible SQL injection vulnerability and the high optimization overhead: that the database has to recreate the execution plan every time, because the search terms—that can be different every time—prevent caching. Parameterized Queries explains the optimization overhead in detail. Again, dynamic SQL is again not the problem but not using bind parameters is.

On my Own Behalf

I offer training, tuning and consulting. Buying my book “SQL Performance Explained” (from €9.95) also supports my work on this website.

An example that builds the where clause dynamically and uses bind parameters is omitted because it is even more awkward as the example above. However, most ORM frameworks offer a sufficiently convenient way to dynamically create SQL using bind parameters. The following overview shows some samples:

Java

The following sample demonstrates Hibernate’s Criteria classes:

Criteria criteria = session.createCriteria(Employees.class);

if (subsidiaryId != null) {
  criteria.add(Restrictions.eq("subsidiaryId", subsidiaryId));
}
if (employeeId != null) {
  criteria.add(Restrictions.eq("employeeId", employeeId));
}
if (lastName != null) {
  criteria.add(
    Restrictions.eq("lastName", lastName).ignoreCase()
  );
}

When providing LAST_NAME only, the following SQL is generated by Hibernate (Oracle):

select this_.subsidiary_id as subsidiary1_0_0_,
       [... other columns ...]
  from employees this_
 where lower(this_.last_name)=?

Please note that a bind parameter is used and the LOWER function to implement the ignoreCase() functionality. The same is true for the ilike restriction. That’s a very important fact for function-based indexing.

The Java Persistence API (JPA) has a similar functionality:

However, it’s less straight and doesn’t support a native case-insensitive search that’s probably good):

List<Predicate> predicates = new ArrayList<Predicate>();

if (lastName != null) {
   predicates.add(queryBuilder.equal(
         queryBuilder.upper(r.get(Employees_.lastName))
       , lastName.toUpperCase())
   );
}
if (employeeId != null) {
   predicates.add(queryBuilder.equal(
         r.get(Employees_.employeeId)
       , employeeId)
   );
}
if (subsidiaryId != null) {
   predicates.add(queryBuilder.equal(
         r.get(Employees_.subsidiaryId)
       , subsidiaryId)
   );
}

query.where(predicates.toArray(new Predicate[0]));

You see that the example is less straight in favor of compile time type safety. Another difference is that JPA doesn’t support native case-insensitive operators—explicit case conversion is needed. That’s probably good to have awareness and control over it. Just as a side note; the native Hibernate API supports explicit case conversion as well.

Perl

The following sample demonstrates Perl’s DBIx::Class framework:

my @search = ();

if (defined $employee_id) {
   push @search, {employee_id => $employee_id};
}
if (defined $subsidiary_id) {
   push @search, {subsidiary_id => $subsidiary_id};
}
if (defined $last_name) {
   push @search, {'UPPER(last_name)' => uc($last_name)};
}

my @employees = $schema->resultset('Employees')
              ->search({-and => \@search});
SELECT me.employee_id, me.subsidiary_id,
       me.last_name,   me.first_name,
       me.date_of_birth
  FROM employees me
 WHERE ( UPPER(last_name) = :p1 )
PHP

The following sample demonstrates PHP’s Doctrine framework:

$filter = $qb->expr()->andx();

if (isset($employee_id)) {
   $filter->add(
       $qb->expr()->eq('e.employee_id', ':employee_id'));
   $qb->setParameter('employee_id', $employee_id);
}
if (isset($subsidiary_id)) {
   $filter->add(
       $qb->expr()->eq('e.subsidiary_id', ':subsidiary_id'));
   $qb->setParameter('subsidiary_id', $subsidiary_id);
}
if (isset($last_name)) {
   $filter->add($qb->expr()->eq(
       $qb->expr()->upper('e.last_name'), ':last_name'));
   $qb->setParameter('last_name', strtoupper($last_name));
}

if ($filter->count() > 0) {
   $qb->where($filter);
}

Doctrine generates the following SQL for a search by last name (MySQL):

SELECT e0_.employee_id AS employee_id0, 
       [... other columns ...]
  FROM employees e0_
 WHERE UPPER(e0_.last_name) = ?

Tip

Download the complete sample code and try yourself.

Using dynamic SQL with bind parameters allows the optimizer to choose the best execution plan for the particular combination of where clauses. That will yield better performance than constructions like described in Smart Logic:

SELECT first_name, last_name
  FROM employees
 WHERE (     employee_id = ? OR ? IS NULL)
   AND (   subsidiary_id = ? OR ? IS NULL)
   AND (UPPER(last_name) = ? OR ? IS NULL)

The reason for the observation that dynamic SQL is slow is very often not using bind parameters—that is, using dynamic SQL for the wrong reason.

However, there are some—I’d say rare—cases when dynamic SQL can be slower than “smart logic” like above. That’s when very cheap (fast) SQL statements are executed at a very high frequency. But, first of all, there are two more terms to explain:

Hard Parsing

Hard parsing is constructing an execution plan based on the SQL statement. That’s a major effort; inspecting all parts of the SQL; considering all indexes; considering all join orders and so on. Hard parsing is very resource intensive.

Soft Parsing

Soft parsing is searching, finding and using a cached execution plan. There are some minor checks done, e.g., access rights, but the execution plan can be re-used as is. That is a rather fast operation.

The key to the cache is basically the literal SQL string—usually a hash of it. If there is no exact match, a hard parse is triggered. That’s why inlined literals—as opposed to bind parameters—trigger a hard parse unless the very same search terms are used again. But even in that case there are good chances that the previous execution plan was already expired from the cache because new ones are coming over and over again.

However, there is a way to execute a statement without any parsing at all—not even soft parsing. The trick is to keep the parsed statement open, e.g., like in the following Java pseudo-code:

PreparedStatement pSQL = con.prepareStatement("select ...");
for (String last_name:last_names) {
    pSQL.setString(1, last_name.toUpperCase());
    ResultSet rs = pSQL.executeQuery();
    // process result
}
pSQL.close();

Note that the PreparedStatement is opened and closed once only—yet it can be executed many times. That means that there is only one parsing operation—during prepare—but non inside the loop.

The pitfall is that converting the statement to dynamic SQL moves the prepareStatement call into the loop—causing a soft-parse for each execution. The parsing overhead, that might also include network latencies, can exceed the savings of a better execution plan when the statement is executed often and runs fast anyway. That’s especially true if the actual execution plan doesn’t vary for the different where clauses—e.g., because one well indexed where clause is always present.

Even though the “prepare before loop” trick is seldom used explicitly, it is very common in stored procedures—but implicit. Languages such as PL/SQL—with real static SQL—prepare the SQL when the procedure is compiled or, at most, once per execution. Changing that to dynamic SQL can easily kill performance.

About the Author

Photo of Markus Winand

Markus Winand provides insights into SQL and shows how different systems support it at modern-sql.com. Previously he made use-the-index-luke.com, which is still actively maintained. Markus can be hired as trainer, speaker and consultant via winand.at.

Buy his Book

Cover of “SQL Performance Explained”: Squirrel running on grass

The essence of SQL tuning in 200 pages

Buy now!
(paperback and/or PDF)

Paperback also available at Amazon.com.

Hire Markus

Markus offers SQL training and consulting for developers working at companies of any size.
Learn more »

Do not use offset for pagination

Learn why

Visit my sibling!A lot changed since SQL-92!

The Use The Index, Luke! mug

Stickers, coasters, books and coffee mugs. All you need for learning.

Shop now

Connect with Markus Winand

Markus Winand on LinkedInMarkus Winand on XINGMarkus Winand on Twitter
“Use The Index, Luke!” by Markus Winand is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Legal | Contact | NO WARRANTY | Trademarks | Privacy and GDPR | CC-BY-NC-ND 3.0 license