de Cristopher Chaverri

El SQL Dinámico Es Lento


La verdadera esencia del mito “el SQL Dinámico es Lento” es bastante simple; SQL dinámico puede ser lento cuando se hace mal.

El problema es que el SQL dinámico es frecuentemente utilizado por las razones equivocadas, a veces sin saberlo. Para aclarar la confusión, voy a utilizar los siguientes términos:

SQL Embebido

Embeber SQL directamente en el código fuente de un programa es bastante común en las bases de datos procedimentales como Oracle PL/SQL o Microsoft Transact-SQL. También es posible embeber SQL en otros lenguajes como C.

El beneficio de SQL embebido es la transición fluida con el respectivo lenguaje de programación. Sin embargo, SQL embebido es compilado en el programa. No puede cambiar en tiempo de ejecución, es estatico.

SQL Dinámico

SQL Dinámico es manejado como una cadena en la aplicación. La aplicación puede cambiar la cadena SQL en tiempo de ejecución antes de pasarla a la capa de base de datos. De hecho, es la manera más común de acceder a las bases de datos.

SQL Estático

Uso este término SQL estático para describir instrucciones SQL que no cambian en tiempo de ejecución, ya sea SQL embebido que no puede cambiar en tiempo de ejecución o SQL dinámico que podría cambiar, pero no lo hace.

La clave de estas definiciones que es una sentencia SQL puede ser dinámica y estatica al mismo tiempo. En otras palabras, existen distintos niveles de SQL dinámico. Considere los ejemplos siguientes:

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

ResultSet rs = con.executeQuery(sql);

¿Esto SQL dinámico? De acuerdo con la definición anterior, lo es. La sentencia SQL es preparada como una cadena y se pasa a la capa de base de datos. Pero, ¿Es también SQL estático? Asumiendo que el valor de la variable employeeId, cambia, entonces no es SQL estático porque la cadena SQL cambia en tiempo de ejecución. Este es un ejemplo de SQL dinámico que afectaría el rendimiento. El problema no es que sea SQL dinámico, sino que no se utiliza variables bind. Por ejemplo, el signo de interrogación ? o :name, son marcadores de posición para valores que cambian durante la ejecución. Eso significa que, el ejemplo puede convertir SQL estático utilizando variables bind en lugar del valor de la variable employeeId.

Importante

No utilizar variables bind es un mal uso de SQL dinámico.

Las variables bind son muy importantes para la seguridad y rendimiento.

Un uso razonable de SQL dinámico consiste en cambiar la estructurade una sentencia en tiempo de ejecución. Esto es algo que no se puede hacer con variables bind. Por ejemplo, una cláusula where condicional:

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

El código construye una sentencia SQL para recuperar los datos de los empleados de acuerdo con tres criterios de filtrado. Aunque resulte engorroso, el SQL generado puede ser ejecutado utlizando el mejor índice disponible. No obstante, este enfoque es problemático debido a la posible vulnerabilidad deinyección SQL y al alto costo de optimización: la base de datos tiene que recrear el plan de ejecución cada vez, porque los términos de búsqueda —que pueden ser diferente en cada ocasión— impiden el uso de la caché. Sentencias con variables explica en detalle este costo de optimización. De nuevo, SQL dinámico no es el problema, sino el hecho de no utilizar las variables bind.

Se omite un ejemplo que construya la cláusula where dinámicamente y utilice variables bind porque resulta aun más engorroso que el ejemplo anterior. Sin embargo, la mayoría de frameworks ORM ofrecen una manera suficientemente práctica para crear SQL dinámico utilizando variables bind. A continuación se muestran algunos ejemplos:

Java

El siguiente ejemplo muestra las clases Criteria de Hibernate:

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()
  );
}

Cuando se provee únicamente LAST_NAME Hibernate genera el siguiente SQL (Oracle):

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

Nótese que se utiliza una variable bind y la función LOWER para implementar la funcionalidad ignoreCase(). Lo mismo ocurre con la restricción ilike. Este es un aspecto muy importante para el uso de índices basados en funciones.

La API de Persistencia De Java ofrece una funcionalidad similar:

Sin embargo, es menos directa y no cuenta con una búsqueda nativa insensible a mayúsculas y minúsculas (lo cual probablemente sea algo bueno):

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]));

Se puede observar que el ejemplo es menos directo en favor de la seguridad del tipado en tiempo de compilación. Otra diferencia es que JPA no soporta nativamente —operadores de mayúsculas y minúsculas— por lo que es necesario realizar una conversión explicíta. Probablemente sea bueno tener consiencia y control sobre ello. Como nota adicional; La API nativa de Hibernate también soporta conversión de mayúscula/minúscula.

Perl

El siguiente ejemplo utiliza el framework DBIx::Class de Perl:

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

El siguiente ejemplo utiliza el framework Doctrine de PHP:

$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 genera el siguiente SQL para una búsqueda por apellido (MySQL):

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

Sugerencia

Descarga el código de ejemplo completoy pruebalo.

Utilizar SQL dinámico con variables bind permite que el optimizador elija el mejor plan de ejecución para la combinación de las sentencias where. Esto proporcionará un mejor rendimiento que las construcciones descritas en Lógica “inteligente”:

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)

La razón por la que SQL dinámico es lento, suele ser, en muchos casos, que no se utilizan variables bind; es decir, utilizar SQL dinámico por la razón equivocada.

Sin embargo, hay algunos casos —diría que raros— en los cuales SQL dinámico puede ser más lento que un "smart logic", como el descrito anteriormente. Esto ocurre cuando se ejecutan sentencias SQL muy baratas (rápidas) que se ejecutan en una frecuencia muy alta. Pero, antes que nada, existen dos términos más por explicar:

Análisis Sintáctico Completo

El análisis sintáctico completo consiste en construir un plan de ejecución a partir de la sentencia SQL. Se trata de un proceso que requiere un esfuerzo mayor; inspeccionar todas las instrucciones SQL; considerando todas las ordenes de unión, etc. El análisis sintáctico completo consume muchos recursos.

Análisis Sintáctico Ligero

El análisis sintáctico ligero consiste en buscar, y utilizar un plan de ejecución almacenado en caché. Se realizan algunas comprobaciones menores, como los derechos de acceso, pero el plan de ejecución se puede utilizar tal cual. Se trata de una operación rápida.

La clave de la caché es básicamente la cadena SQL literal —normalmente un hash de la misma. Si no hay una coincidencia exacta, se activa un análisis completo. Es por eso que los literales en línea —A diferencia de los parámetros bind— activan un análisis completo, a menos que se vuelvan a utilizar los mismos términos. Pero incluso en ese caso, existe una alta posibilidad que la ejecución del plan de ejecución anterior ya haya expirado de la caché, ya que constantemente se están generando nuevos.

Sin embargo, existe una manera de ejecutar una sentencia sin tener que realizar ningún tipo de análisis sintáctico —ni siquiera uno ligero. El truco consiste en mantener abierta la sentencia analizada, por ejemplo, como en el siguiente pseudocódigo Java:

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();

Nótese que el PreparedStatement se abre y se cierra solo una vez —aunque puede ejecutarse muchas veces. Eso significa que solo un hay una operación de análisis sintáctico —durante la preparación— pero ninguna dentro del bucle.

El inconveniente es que al convertir la instrucción en SQL dinámico, la llamada a prepareStatement pasa a realizarse dentro del bucle —causando un análisis ligero— en cada ejecución. La sobrecarga que supone este análisis, que también pueden incluir latencias en la red, pueden superar el ahorro de un mejor plan de ejecución cuando la instrucción ejecutada con frecuencia y de todos modos es rápida. Esto es especialmente cierto si el plan de ejecución actual no varía entre las distintas cláusulas where. por ejemplo, porque siempre está presente una cláusula WHERE bien indexada.

Aunque el truco de "preparar antes del bucle" se utiliza de forma explícita, es muy habitual en procedimientos almacenados, pero de manera implícita. Lenguajes como PL/SQL —con SQL estático real— preparan el SQL cuando el procedimiento es compilado o, como mucho, una vez por ejecución. Cambiar eso por SQL dinámico pueden afectar seriamente el rendimiento.

Previous pageNext page

You can’t learn everything in one day. Subscribe the newsletter via E-Mail, Bluesky or RSS to gradually catch up. Have a look at modern-⁠sql.com as well.

Acerca del autor

Foto de Markus Winand

Markus Winand es defensor del resurgimiento del SQL. Su misión es la de presentar a los desarrolladores la evolución de SQL en el siglo XXI. Es posible contratar a Markus según disponibilidad o como orador o consultor en winand.at.

Adquiere tu libro

Portada de “Rendimiento SQL explicado”: Ardilla corriendo en la hierba

La esencia del tuning de SQL en 200 páginas

Compra ahora
(libro de bolsillo y/o PDF)

Contratar a Markus

La manera más rápida y fácil de beneficiarse de su extenso conocimiento y experiencia.
Aprende más »

Entrar en contacto con Markus

Suscríbete a listas de correoRSS FeedMarkus Winand en LinkedInMarkus Winand en XINGMarkus Winand en MastodonMarkus Winand en Bluesky
Copyright 2017-2026 Martin LE TARNEC, Markus Winand. All righs reserved.
Aspectos legales | Contacto | SIN GARANTÍA | Marcas | Privacidad y RGPD