Spring Boor Performance

Logging

  • hibernate.show_sql (niet in prod!)
  • hibernate.format_sql
  • hibernate.use_sql_comments

params: - org.hibernate.type.descriptor.sql level trace

P6Spy

pom.xml

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version> <!-- You can use the latest version -->
</dependency>

application.properties

spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password

databaseDialectDateFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ
customLogMessageFormat=%{currentTime}|%(executionTime)ms|%(category)|connection %(connectionId)|\n%(sqlSingleLine)

modulelist=com.p6spy.engine.outage.P6OutageFactory,com.p6spy.engine.logging.P6LogFactory
logfile=spy.log
logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat
appender=com.p6spy.engine.spy.appender.Slf4JLogger
databaseDialectDateFormat=yyyy-MM-dd HH:mm:ss.SSS
•   modulelist: Specifies which P6Spy modules to enable.
•   logfile: Defines where the log file will be created.
•   appender: Determines where logs should be sent, such as a file or SLF4J for logging through a standard logging framework.

Datasource-proxy

@Bean
public DataSource dataSource() {
    SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener();
    loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator());

    return ProxyDataSourceBuilder.create(actualDataSource())
        .name(DATA_SOURCE_PROXY_NAME)
        .listener(loggingListener)
        .build();
}
  • can have own cutom statement execution listeners

FetchSize

statement.setFetchSize(fetchSize) 10 Oracle 120 SQL PostreSQL MySQL whole resultset in single roundtrip

JPA2.2 getResultStream dan wel zetten voor PostgresQL en MySQL

Streams

MySQL streaming

One Record

statement.setFetchSize(Integer.MIN_VALUE)

Multiple Records

statement.setFetchSize(fetchSize)

Oracle

spring.jpa.properties.hibernate.jdbc.fetch_size=50

MySQL Postgres

@Query("""
    select p
    from Post p
    where date(p.createdOn) >= :sinceDate
""")
@QueryHints(
    @QueryHint(name = AvailableHints.HINT_FETCH_SIZE, value = "25")
)
Stream<Post> streamByCreatedOnSince(
    @Param("sinceDate") LocalDate sinceDate
);

Pagination

Data grows per page

  • FETCH FIRST N ROWS ONLY
  • FETCH NEXT N ROWS ONLY
  • OFFSET M ROWS Oracle 12c SQL2012 PostgresQL 8.4

let op ORDER BY

Top-N

SELECT title
FROM post
ORDER BY created_on DESC, id DESC
FETCH FIRST 5 ROWS ONLY

Next-N

SELECT title
FROM post
ORDER BY created_on DESC, id DESC
OFFSET 5 ROWS
FETCH NEXT 5 ROWS ONLY

PostgreSQL MySQL TOP-N

SELECT title
FROM post
ORDER BY created_on DESC, id DESC
LIMIT 5

PostgreSQL MySQL NEXT-N

SELECT title
FROM post
ORDER BY created_on DESC, id DESC
LIMIT 5
OFFSET 5

NB: OFFSET komt NA LIMIT!!!

JPQL Querying -Pagination

Page<Post> findAllByTitle(
    @Param("titlePattern") String titlePattern,
    Pageable pageRequest
);

@Query("""
    select p
    from Post p
    where p.title like :titlePattern
""")
Page<Post> findAllByTitle(
    @Param("titlePattern") String titlePattern,
    Pageable pageRequest
);

JPQL QUERY Pagination Top-N

Page<Post> posts = postRepository.findAllByTitle(
    "High-Performance Java Persistence %",
    PageRequest.of(0, 25, Sort.by("createdOn"))
);
SELECT p.id, p.created_on, p.title
FROM post p
WHERE p.title LIKE 'High-Performance Java Persistence %' ESCAPE ''
ORDER BY p.created_on ASC
OFFSET 0 ROWS
FETCH FIRST 25 ROWS ONLY
@Query(value = """
    SELECT p.id, p.title, p.created_on
    FROM post p
    WHERE p.title ilike :titlePattern
    ORDER BY p.created_on
""",
nativeQuery = true)
Page<Post> findAllByTitleLike(
    @Param("titlePattern") String titlePattern,
    Pageable pageRequest
);

Top-N

Page<Post> posts = postRepository.findAllByTitle(
    "High-Performance Java Persistence %",
    PageRequest.of(0, 25)
);
SELECT p.id, p.title, p.created_on
FROM post p
WHERE p.title ilike 'High-Performance Java Persistence %'
ORDER BY p.created_on
FETCH FIRST 25 ROWS ONLY

Offset pagination index scanning performance

CREATE INDEX idx_post_created_on ON post (created_on DESC, id DESC);

SELECT id
FROM post
ORDER BY created_on DESC
LIMIT 50;
Limit  (cost=0.28..2.51 rows=50 width=16)
  (actual time=0.013..0.022 rows=50 loops=1)
  -> Index Scan using idx_post_created_on on post p
     (cost=0.28..223.28 rows=5000 width=16)
     (actual time=0.013..0.019 rows=50 loops=1)
Planning time: 0.113 ms
Execution time: 0.055 ms

2e en latere scan

SELECT id
FROM post
ORDER BY created_on DESC
LIMIT 50
OFFSET 50;
Limit  (cost=2.51..4.74 rows=50 width=16)
  (actual time=0.032..0.044 rows=50 loops=1)
  -> Index Scan using idx_post_created_on on post p
     (cost=0.28..223.28 rows=5000 width=16)
     (actual time=0.022..0.040 rows=100 loops=1)
Planning time: 0.198 ms
Execution time: 0.071 ms

Nu 100rows!!! Op de laattste pagina wordt alles gescanned... 1.190ms..... OFFSET doesnt seek/traverse

Seek or keyset pagination

SELECT id, created_on
FROM post
ORDER BY created_on DESC, id DESC
LIMIT 50

created_on EN id moeten ERIN

Next-N

SELECT id, created_on
FROM post
WHERE (created_on, id) < ('2024-10-02 21:00:00.0', 4951)
ORDER BY created_on DESC, id DESC
LIMIT 50;
•   The row value expression (a, b) < (c, d) is PostgreSQL and MySQL.
•   It’s equivalent to a < c | (a = c & b < d).

Nu wel max 50 results in set

Spring Data JPA - WindowIterator

Oplossing!

WindowIterator<PostComment> commentWindowIterator = WindowIterator.of(
    position -> postCommentRepository.findByPost(
        post,
        PageRequest.of(
            0, pageSize,
            Sort.by(
                Sort.Order.desc(PostComment_.CREATED_ON),
                Sort.Order.desc(PostComment_.ID)
            )
        ),
        position
    )
).startingAt(ScrollPosition.keyset());

commentWindowIterator.forEachRemaining(this::processPostComment);

met Blaze Persistance Top-N

// Blaze Persistence – Keyset pagination
PagedList<Post> firstPage = cbf
    .create(entityManager, Post.class)
    .orderByAsc(Post_.CREATED_ON).orderByAsc(Post_.ID)
    .page(0, pageSize)
    .withKeysetExtraction(true)
    .getResultList();
SELECT p.id, p.created_on, p.title,
       (SELECT count(*) FROM post)
FROM post p
ORDER BY p.created_on, p.id
OFFSET 0
ROWS FETCH FIRST 25 ROWS ONLY;

Next N

// Blaze Persistence – Keyset pagination
PagedList<Post> nextPage = cbf
    .create(entityManager, Post.class)
    .orderByAsc(Post_.CREATED_ON).orderByAsc(Post_.ID)
    .page(postPage.getKeysetPage(),
          postPage.getPage() * postPage.getMaxResults(),
          postPage.getMaxResults())
    .getResultList();
SELECT p.id, p.created_on, p.title,
       (SELECT count(*) FROM post)
FROM post p
WHERE ('2024-09-09 12:10:00.0', 10) < (p.created_on, p.id)
ORDER BY p.created_on, p.id
OFFSET 0
ROWS FETCH FIRST 25 ROWS ONLY;

Projections

Fetching too many columns

Instead of fetching all columns:

SELECT *  
FROM post_comment pc  
LEFT JOIN post p ON p.id = pc.post_id  
LEFT JOIN post_details pd ON p.id = pd.id  


Fetch a custom SQL projection:


SELECT pc.id, pc.review FROM post_comment pc LEFT JOIN post p ON p.id = pc.post_id LEFT JOIN post_details pd ON p.id = pd.id

(Joins zijn niet nodig... => dus tweede is dan sneller)

Tuple projection

not type save

• The JPA Tuple wraps the default Object[] projection and allows you to retrieve the column values via their aliases.

List<Tuple> commentTuples = postRepository.findAllCommentTuplesByPostTitle(titleToken);
Tuple commentTuple = commentTuples.get(0);

long id = commentTuple.get("id", Number.class).longValue();
String title = commentTuple.get("title", String.class);

Interface-based projection

• If you want a type-safe projection, Spring Data JPA provides the option of wrapping the result in a Proxy based on a given interface.

The Interface-based projection can be used like this:

@Query("""
select
p.id as id,
p.title as title,
c.review as review
from PostComment c
join c.post p
where p.title like :postTitle
order by c.id
""")
List<PostCommentSummary> findAllCommentSummariesByPostTitle(
@Param("postTitle") String postTitle
);

// nu type safe!       
Long id = commentSummary.getId();
String title = commentSummary.getTitle();

Met ingebakken (!) DTOs en Records

DTOs

properties.put(
    "hibernate.integrator_provider",  // Hibernate property for custom integrator
    (IntegratorProvider) () -> Collections.singletonList( // Lambda to provide integrator
        new ClassImportIntegrator( // Hypersistence Utils integrator
            List.of( // List of classes to register
                PostCommentDTO.class,  // First DTO class
                PostCommentRecord.class // Second DTO class
            )
        )
    )
);

String jpql = "SELECT new PostCommentDTO(pc.id, pc.comment) FROM PostComment pc WHERE pc.post.id = :postId";


Records

public record PostCommentDTO(Long id, String comment) {}
public record PostCommentRecord(Long id, String content) {}

properties.put(
    "hibernate.integrator_provider",
    (IntegratorProvider) () -> Collections.singletonList(
        new ClassImportIntegrator(
            List.of(
                PostCommentDTO.class,   // Register the record as a DTO
                PostCommentRecord.class // Register additional records as needed
            )
        )
    )
);

@Query(
    value = "SELECT pc.id, pc.comment FROM post_comment pc WHERE pc.post_id = :postId",
    nativeQuery = true
)
List<PostCommentDTO> findCommentsByPostId(@Param("postId") Long postId);

Long id = commentRecord.id();
String title = commentRecord.title();

TupleTransformer and ResultListTransformer

The SQL projection can contain data from multiple tables, like the post and post_comment, which form a one-to-many table relationship.

SELECT p.id AS p_id,
p.title AS p_title,
pc.id AS pc_id,
pc.review AS pc_review
FROM post p
JOIN post_comment pc ON p.id = pc.post_id
ORDER BY pc.id

komt uit:

p_id p_title pc_id pc_review
1 High-Performance Java Persistence 1 Best book on JPA and Hibernate!
1 High-Performance Java Persistence 2 A must-read for every Java developer!
2 Hypersistence Optimizer 3 It's like pair programming with Vlad!