Query lenta no JPA/Hibernate, e executando rápido no SQL?

Se sua query é rapida ao executar o SQL diretamente e é lenta através do JDBC ou JPA/Hibernate, isto pode ser somente configuração.

Opa, fala jovem, tudo certo ?

Hoje eu resolvi trazer uma dica de um problema que já presenciei algumas vezes em minha carreira, onde executava uma query diretamente e apresentava uma boa performance e quando era executada através do JPA/Hibernate apresentava uma lentidão.

A possível causa pode ser o suporte a unicode, irei abordar o tema para alguns banco de dados:

SQL Server

Antes de falarmos do problema em si, é importante primeiro entendermos algumas coisas antes. No SQL Server temos os tipos VARCHAR, CHAR, NVARCHAR e NCHAR.

NCHAR e NVARCHAR podem armazenar caracteres Unicode.
CHAR e VARCHAR não conseguem armazenar caracteres Unicode.

Devido esta característica é possível termos colunas em uma mesma tabela com suporte a unicode e outras sem este suporte. O driver JDBC do SQL Server por default tem habilitado que toda String enviada como um parâmetro está em unicode, sendo assim ao realizar um where de uma coluna varchar é realizada a conversão de cada uma das linhas da tabela para um tipo N.

Para que fique mais fácil de entender vamos ao exemplo:

-- Criação da tabela
CREATE TABLE Livros {
  id BIGINT NOT NULL,
  titulo VARCHAR(255),
  CONSTRAINT pk_livros PRIMARY KEY(id)
};

-- Criação de indice
CREATE INDEX IDX_Livros_Titulo ON Livros(titulo);

-- Popular a tabela com 200 registros diferentes
DECLARE @count INT;
SET @count = 1;
    
WHILE @count<= 200
BEGIN
   INSERT INTO Livros VALUES('Livro-'+CAST(@count as varchar))
   SET @count = @count + 1;
END;

O código acima é auto-explicativo, basicamente é criado uma tabela chamado livros e um índice para a coluna titulo, em seguida essa tabela é preenchida com 200 registros diferentes na coluna titulo.

Com base nisso o seguinte código será executado:

try {
  // Ativa as estatiticas do SQL Server
  var statement = conn.prepareStatement("SET STATISTICS IO, TIME, PROFILE ON");
  statement.execute();

  //Realiza o select na tabela buscando  por Livro-50
  statement = conn.prepareStatement("SELECT id, titulo FROM Livros WHERE titulo = ? ");
  statement.setString(1, "Livro-50");

  ResultSet resultSet = statement.executeQuery();

  while(resultSet.next()) {
    String executionPlanLines = resultSet.getString(1);
    LOGGER.info("Plano de execução: {}{}",
      System.lineSeparator(),
      executionPlanLines
    );
  }
} catch (SQLException e ) {
  LOGGER.error(e.getMessage(), e);
}

Se olharmos o resultado do plano de execução podemos ver que é realizado um Index Scan ao invés de um Index Seek, ou seja, o índice foi completamente ignorado.

O índice não foi utilizado porque como a coluna de titulo é do tipo varchar e como falado anteriormente toda String é por default é enviada como unicode ele acaba por realizar uma conversão de varchar para nvarchar.

A solução para isto é bem simples, basta adicionarmos o parâmetro sendStringParametersAsUnicode=false na URL de conexão ficando assim:

jdbc:sqlserver://localhost:1433;databaseName=teste;sendStringParametersAsUnicode=false;

Com isto ao executar código acima novamente é utilizado o Index Seek. A instrução acima diz que os parâmetros em String não devem ser interpretados como unicode.

Certo Danilo, isso resolve meu problema em praticamente todas as buscas que faço no sistema, mas e se eu tiver uma coluna que deve ser do tipo NVARCHAR para armazenar um emoji por exemplo, como eu faço ?

Bom neste caso é simples caso usemos JDBC diretamente temos dois métodos auxiliares setNString e getNString para manipular dados unicode, e no caso de usar JPA/Hibernate, basta adicionar a anotação @Nationalized

@Entity(name = "Livros")
public class Livros {
 
    @Id
    @Column(name = "id")
    private Long id;
 
    @Column(name = "titulo")
    @Nationalized
    private String titulo;
     
}

Oracle

No caso do banco de dados Oracle, também temos situação parecida com o do SQL Server, é possível definir colunas em uma mesma tabela que podem ser unicode(NCHAR, NVARCHAR2, NCLOB).

Ao contrário do SQL Server, por default todo parâmetro é enviado como uma String comum, então é bem raro termos este tipo de problema no Oracle.

No caso do Oracle não podemos utilizar o setNString devemos passar um parâmetro da seguinte forma:

pstmt.setFormOfUse(1, OraclePreparedStatement.FORM_CHAR); pstmt.setString(1, "Livro-50");

Se você utilizar JPA/Hibernate basta adicionar a anotação @Nationalized como mostrado acima.

Mas se suas colunas forem todas do tipo N, podemos ter o mesmo problema de lentidão, pois será necessário realizar o cast para o tipo correto.

No caso do Oracle podemos passar a seguinte variável oracle.jdbc.defaultNChar=true ao executar uma aplicação java que usa JDBC diretamente

java -Doracle.jdbc.defaultNChar=true myApplication

Se você utiliza hibernate, basta adicionar a seguinte propriedade para indicar que todo parâmetro é do tipo unicode:

hibernate.connection.defaultNChar=true

PostgreSQL e MySQL

No caso destes dois bancos de dados não temos colunas com suporte a unicode e sem suporte em uma mesma tabela, somente a nível de tabela ou banco de dados, então não temos problema de conversão ao realizar um WHERE em uma tabela. Estes dois banco de dados utilizam a codificação de UTF-8 para fornecer suporte ao unicode.

Com isto basta definir na criação da tabela ou banco de dados para utilizar UTF-8.

-- Exemplos em MySQL
CREATE DATABASE 'teste' CHARACTER SET 'utf8';

CREATE TABLE  `Livros` (
    [...]
) DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

Conclusão

Neste artigo abordei os alguns banco de dados quando se trata de como os parâmetros são interpretados, é importante sempre nos atentarmos e olharmos a documentação destes bancos para entender como o driver jdbc funciona para cada um deles.

É isso pessoal, espero que este conteúdo seja útil para você, um abraço e até a próxima.

Comentários