Rendimiento pobre update ~10k entidades (Postgres + Spring Boot)

soulsville

Hola, una vez más, recurro a vosotros para que me iluminéis con vuestra sabiduría y conocimiento.

El problema que os presento a continuación es sobre un método encargado de realizar la actualización de una colección de entidades que puede oscilar de media en 8/10k, en concreto una actualización de 5 columnas de una tabla con 600k filas pero que podrá llegar a los millones en el futuro.
Actualmente tarda sobre 5/6 segundos, pero me sigue pareciendo demasiado y el objetivo es bajar a 1sec si es posible sin recurrir a un multithreading porque sospecho que hay otros puntos donde mejorar esto.

Datos:

  • Máquina virtual con 12 CPU (2 socket 6 cores) + 4GB + 80GB HDD
  • Tabla con 1 index de tres columnas y 2 constraints de clave foránea a otras tablas.
  • PostgresSQL 12.0 + Spring Boot 2.1.4
  • La tabla utiliza un Identificador de tipo Sequence con el algoritmo pooled hi-lo.

Pruebas realizadas:

  • He configurado el batchsize en el application.properties con diferentes valores (30,50,100,300,500,1000) con semejante resultado, ejecutando una instrucción flush en función del tamaño del batch en una transacción.
  • Almacenar las entidades en una lista y utilizar el saveAll del JPA Repo.
  • Quitado/puesto los index de la tabla (hay un index con tres columnas).
  • Sustitución del bucle For por un Parallel Stream
  • Consulta SQL Nativa con el Update.
  • Pruebas con diferentes drivers de Postgres

Código:

private void guardarPartida(Partida partidaActual) {
		 		
	for(Jugador j: jugadores) {
		Carton c = cartones.get(j.getCarton().getId());
		c.setNumerosCompletados(j.getNumerosCompletados().stream().sorted().collect(Collectors.toList()).toString());
		c.setPatronCompletado(j.getPatronCompletado().stream().sorted().collect(Collectors.toList()).toString());
		c.setOrdenCompletado("Pendiente completar");
		c.setPremiado(j.getMapaJuegosPremiados().size() > 0);
		c.setPartida(partidaActual);
}
		cartonService.saveAll(cartonesJugados); // zZzZz
	
}

¿Alguna idea de cómo mejorar esto? ¿Qué puede estar pasando?
Gracias.

Ranthas

#1 ¿Por qué ordenas numeros_completados y patron_completado? Ahí puedes rascar un poco si te lo ahorras. También si es un grupo grande de elementos, te recomendaría pasar del stream/sorted/collect y llamar directamente Collections.sort()

Por otra parte, puedes estar perdiendo tiempo en el update si tus relaciones están mapeadas bi-direccionalmente. Viendo que parece una partida de bingo, no creo que sean necesarias relaciones bi-direccionales.

La clave está en los dos puntos que ya has comentado: SQL nativas y jugar con el tamaño del batch. Si con JPA no consigues sacarlo, no sé como de costoso sería implementarlo usando JDBCTemplate, que te permite trabajar más a bajo nivel.

1 2 respuestas
soulsville

#2 Estoy testeando con JDBC Template y he bajado un 50% los tiempos. Seguiré mirando ~~

JuAn4k4

¿Una sola consulta con el update lo puedes hacer? es lo que mejor te va a ir en sync.

¿Puede ser async? (background)

¿Estas seguro que solo el update se lleva tiempo? ¿cuanto tarda tu bucle ?

1 respuesta
Lecherito

6 segundos no tarda un update ni de blas. Lo primero que tienes que hacer es ver donde se va el tiempo, sin una tabla de donde se va el tiempo no puedes hacer nada.

1 respuesta
Ranthas

#5 Mapeos bi-direcciones de las entidades, sin el tunning adecuado, a la hora de hacer hasta un select te comes el jodido n+1

Si #1 ha pasado a JDBC Template y ha bajado los tiempos, es cosa de JPA y del mapeo de las entidades seguro.

2 respuestas
Lecherito

#6 no tengo ni idea de jpa, pero debugear me suena.

Lo que no le veo es el sentido a toda esa mierda de jpa etc, me gusta hacer las cosas a la antigua xD

1 respuesta
soulsville

#4 El tiempo consumido en el bucle es de 0,15seg para 10k, por eso digo que el problema está en el update. Tiene que ser sync.

Con lo que comentó #2 estoy en 2,2seg, pero querría apretar más. Tras los cambios para testear el JDBC y flusheando cada 500 items:

               dataSource.getConnection().setAutoCommit(false);
		entityManager = emf.createEntityManager();
		entityManager.getTransaction().begin();
		int i = 0;
		for(Jugador j: jugadores) {
					
		preparedStatement.setString(1, j.getNumerosCompletados().stream().sorted().collect(Collectors.toList()).toString());
		preparedStatement.setString(2, j.getPatronCompletado().stream().sorted().collect(Collectors.toList()).toString());
		preparedStatement.setString(3, "Pendiente completar");
		preparedStatement.setLong(4, idPartida);
		preparedStatement.setInt(5, j.getCarton().getId());

		preparedStatement.addBatch();
	
		if(i>0 && i%500==0) {
			int[] updatedRecords = preparedStatement.executeBatch();
			entityManager.getTransaction().commit();
			entityManager.getTransaction().begin();
			System.out.println("Actualizados " + updatedRecords.length);
		}
		i++;/

#6 Tengo una bidireccional con otra entidad Compra. ¿Recomendarías que eliminase la owner en Compra

@OneToMany(mappedBy="compra", fetch = FetchType.LAZY, cascade=CascadeType.ALL)
	private Set<Carton> cartones = new HashSet<Carton>();

--- CARTON
private Compra compra;
	@ManyToOne(fetch=FetchType.LAZY)
	@JoinColumn(name="id_compra")		
	public Compra getCompra() { return compra; }
	public void setCompra(Compra compra) { this.compra = compra; }
2 respuestas
JuAn4k4

#8 Al revés, elimina el one to many, y conviértelo en una query cuando lo necesites leer.

1 respuesta
Ranthas

#7 Tiene su aquel, ahorra bastante tiempo, pero a la que tienes un modelo de datos grandito y bien normalizado, empieza a hacerse un poco cuesta arriba manejarlo. Como todo, tiene su uso, depende de darle un uso correcto y no usarlo de silver bullet

#8 No te lo recomiendo, una compra sí o sí debe tener un comprador, la compra no puede existir por sí misma (es mi suposición basandome en el mundo real). Sin embargo, el comprador (Owner) puede o no tener una compra (si tienes cardinalidad minima cero, en lugar de comprador sería mas adecuado usar el termino cliente), depende de ti y de los requisitos de tu app el mapear bidireccionalmente.

Como dice #9 , puedes eliminar la relacion bidireccional cuando necesites saber datos de las compras de un cliente, haces la query directamente.

Ya nos dices si has podido afinar un poco más, espero que lo resuelvas.

1

Usuarios habituales

  • Ranthas
  • JuAn4k4
  • soulsville
  • Lecherito