Jugando con PreparedStatement

 Hace tiempo que he oido que los PreparedStatement de java son más eficientes que los Statement, así que me he puesto a jugar con el tema y verlo por mi mismo.

Mi primera prueba un pequeño fracaso. Cogí una base de datos MySQL e hice 1000 inserciones seguidas con Statement, componiendo la SQL y luego otras 1000 con PreparedStatement. Por igualdad de condiciones, en ambos casos partía de la base de datos vacía. Pues bien, no solo no había diferencia apreciable, sino que además en ocasiones las Statement tardaban menos.

Me puse a investigar y descubro que hay dos requisitos indispensables para que los PreparedStatement sean más efectivos:

  1. El servidor de base de datos debe soportar los Prepared Statement, que realmente no son cosa de java, sino del servidor de base de datos.
  2. El conector/driver que se use también debe soportarlos.

Así que a ver si mi versión de driver y de servidor MySQL lo soportan … pues sí, lo soportan. Sigo investigando y descubro que al conector de MySQL hay que decirle en la configuración que use PreparedStatement del lado del servidor, cosa que por defecto no hace. Esto se consigue poniendo en la cadena de conexión algo como 

conexion = DriverManager.getConnection(
   "jdbc:mysql://localhost/basedatos?useServerPrepStmts=true", "usuario", "password");

Pues nada, pruebo otra vez y esta vez sí que es ligeramente más rápido el PreparedStatement, pero no tanto. El tiempo que tarda cada vez el programa en correr es distinto y aunque el PreparedStatement suele tardar algo menos que el Statement, a veces no ocurre así. Estamos hablando de entre 15 y 18 segundos, en el que el PreparedStatement apenas le saca un segundo al Statement y no siempre.

No puede ser que eso sea así. Algo debo estar haciendo mal. Sigo leyendo y encuentro un sitio (no recuerdo cual) que dice que algunas bases de datos no mantienen los PreparedStatement después del commit. No veo nada en MySQL que indique esto, pero me decido a hacer la prueba. Hago que la conexión no sea auto-commit y pongo el commit fuera del bucle de inserción. Las mil inserciones en cada caso quedan "en el aire" hasta que se hace un commit al finalizar cada bucle.

Esta vez el PreparedStatement sigue sacando su aproximadamente un segundo sobre el Statement … pero el tiempo total se reduce drásticamente, tanto para Statement como para PreparedStatement, a unos 3 ó 4 segundos. El hecho de no hacer commit hasta el final, independientemente del tipo de Statement usado, hace que el tiempo total baje de 15 segundos a 3. Y el segundo a favor de PreparedStatement se convierte ahora sí, en algo apreciable. Seguramente, todo lo que estaba midiendo antes eran los accesos reales a disco duro para realizar realmente las inserciones de una en una con el auto-commit. En el segundo caso, la medida ya sí debe ser mejor comparativa enrtre PreparedStatement y Statement.

Pero … ¿y en una aplicación real?. ¿Podemos permitirnos el lujo de no hacer commit hasta que hayamos hecho un conjunto más o menos grandes de inserciones?. Yo creo que normalmente no, así que ¿realmente merece la pena el uso de PreparedStatement por temas de eficiencia?. Pues supongo que depende entonces de los casos.

Por ejemplo, se me ocurre que si una sola inserción es más compleja que la mia e involucra varias tablas, podemos dejar el commit para cuando se haya hecho la inserción en todas las tablas implicadas. Esto tiene cierta lógica, la inserción se completa totalmente o no se completa en absoluto y el commit confirma varias inserciones, una en cada tabla. La diferencia de velocidad entre PreparedStatement y Statement será mayor cuantas más tablas haya implicadas.

También se me ocurre, aunque sólo sea una tabla, que si tenemos garantía de que van a ir insertándose muchos registros con mucha frecuencia, como para comprometer la velocidad con la que somos capaces de insertarlos en base de datos, podemos usar PreparedStatement y un pequeño Timer que haga commit cada cierto tiempo. Una idea rebuscada, pero sí se de casos donde podemos aplicarla. A veces tenemos equipos hardware "escupiendo" información a toda velocidad, información que necesitamos almacenar en base de datos.

En resumen, PreparedStatement posiblemente sí es más eficiente, pero para sacarle realmente rendimiento, no basta con usarlo sin más. Hay que tener en cuenta más cosas, como si tenemos el auto-commit a true o con qué frecuencia necesitamos hacer los commit. Si se investiga un poco, también ser verá que hay otras variables a tener en cuenta, como temas de caché en la configuración del servidor de base de datos o del conector, pero soy demasiado vago para investigar eso sin necesidad real.

Aparte de todo esto, a favor de los PreparedStatement, también está el tema de seguridad. Con PreparedStatement evitamos tener que chequear y "escapar" los caracteres conflictivos en las cadenas de texto que vamos a insertar en base de datos. Si vamos a insertar un nombre de usuario que pedimos al usuario y a este se le ocurre poner una comilla simple, por ejemplo "O’Donnell", podemos meterla tal cual en un PreparedStatement, pero necesitamos "escapar" la comilla antes de usarla en un Statement.

Entradas relacionadas:

Esta entrada ha sido publicada en java y etiquetada como , , , . Guarda el enlace permanente.

6 respuestas a Jugando con PreparedStatement

  1. Sacarás más provecho del PreparedStatement cuando realices consultas.

    Cuando utilizamos el PreparedStatement, este lo que hace es que la base de datos guarde en caché el plan de ejecución de dicha consulta.

    Es decir, a que tablas ir, en que orden acceder a las tablas, qué indice utilizar,…

    Para verlo puedes activar las trazas de los drivers de la base de datos y verás que las consultas de la primera vez y siguientes es distinta y con ahorro de tiempo.

    Además con los PreparedStatement es bueno utilizar los comodines y no ir componiendo el PreparedStatment a mano (concatenando). Ya que en el segundo caso se pierde muchísima efectividad.

    Salu2.
    Línea de Código.

  2. Chuidiang dijo:

    Gracias línea de código, tendré que hacer unas pruebas. Supongo que para poder comparar, el select no debe ser trivial, sino involucrar varias tablas, tanto en los campos a seleccionar, como en el where.

    Y supongo, entonces, que eso es general. Un insert trivial como el del ejemplo que he hecho no es posiblemente la mejor forma de conseguir ver la diferencia de rendimiento. Sería mejor un insert en el que algunos de los campos a insertar se consigan «automáticamente» a través de otras tablas.

    ¿La conclusión es entonces que si no tienes sql complejas, que involucren cada una a varias tablas simultaneamente, quizás no merezca la pena usar el PreparedStatement?

    Se bueno.

  3. Sergi dijo:

    Buenas,
    la ventaja de rendimiento de los PreparedStatement es que siempre tienes el mismo sql, independientemente de los valores que insertes o uses en los filtros. Como el sql se mantiene estable las diferentes capas de la aplicación lo pueden cachear para mejorar el rendimiento.

    La pool de conexiones puede cachearlos, el driver puede cachearlos y el SGBD igual, aunque estos cada vez son mas listos y convertirán un churro sql con valores al equivalente con variables para poder cachearlo.

    Sabiendo esto puedes preparar un test donde los valores varíen en cada iteración. Si lo ejecutas varias veces seguidas debería haber una gran diferencia entre usar PreparedStatement a partir del segundo pase a usar solo Statement, que no mejorará tanto.

    Se supone que en una aplicación web funcionando con todos sus usuarios tendremos las caches llenas de los sql mas usados, mejorando el rendimiento general.

    ¿Si es tan dificil demostrar que son mas rápidos, merece la pena usar PreparedStatement? Yo creo que si, sobretodo por la seguridad que dan contra «sql injection», y después por la mejora de rendimiento.

  4. Sergi dijo:

    Por cierto, en unas pruebas similares que hice hace ya meses con Tomcat, DBCP y Oracle 10 apenas había diferencia de velocidad hasta que no activé la cache de PreparedStatements en DBCP.

  5. David dijo:

    También puedes usarlos con la opción de addBatch que aún hará que mejore más. Con esto los valores se van añadiendo y después de tenerlos todos añadidos, haces el executeBatch que enviará al servidor todo los valores.
    Ejemplo:
    http://java.sun.com/j2se/1.3/docs/guide/jdbc/spec2/jdbc2.1.frame6.html

  6. atreyu dijo:

    De acuerdo con @Sergi en que lo mejor del PreparedStatement es una barrera mas contra el sql injection (si es la unica mal vamos)
    Normalmente los micromilisegundos que ganas seran una gota en el oceano del retardo en la respuesta del cliente, digo normalmente porque hay setencias en las que igual si se nota.
    Yo tambien he utilizado el PreparedStatement para hacerme unas funciones para generar dinamicamente sql. O sea le paso un map con pares de nombre-parametro del PreparedStatement y sus valores y me monto el Statement de la forma. Es un poco primitivo pero bueno era practico-DRY teniendo en cuenta el codigo con el que tengo que lidiar:

    public PreparedStatement queryFiltrada(Connection conn,String schema, String tabla,
    			String[] columnasTabla,String[][] filtro) throws XXXXException{
    		if (columnasTabla==null||columnasTabla.length==0) columnasTabla=new String[] {"*"};
    		
    		String sql ="SELECT "+ vectorToString(columnasTabla);
    		sql+=" FROM " +schema+"."+tabla+" ";
    		PreparedStatement stmt = null;
    		try {
    			if (filtro!=null){
    				String [] columnasFiltro=getColumna (filtro,0);
    				sql=construirWhere (sql,columnasFiltro);
    			}
    			stmt=conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE, 
    							ResultSet.CONCUR_READ_ONLY);
    			if (filtro!=null) setParametros (stmt,getColumna(filtro,1),1);
    		} catch (SQLException e){
    			throw new XXXXException(this.getClass().getPackage().getName(), this.getClass().toString(), 
    					XXXUtilidades.getCurrentMethodName(), e.getMessage(), e);
    		}  
    		return stmt;
    		
    	}
    

    Ya os imaginareis lo que hace el construirWhere y el setParametros

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.