Covering Index
A covering index refers to an index that contains all the data necessary to satisfy a query.
An index is a method for efficiently searching data. When querying using an index, there's no need to access the actual data. In other words, an index improves query performance.
Satisfying a query -> When all columns used in SELECT, WHERE, ORDER BY, LIMIT, GROUP BY, etc., are included within the index columns.
Example #
SELECT * FROM user WHERE id = 1;

The above is the execution plan for the query.
selected_type: SIMPLE indicates a simple SELECT statement without UNIONs or subqueries.type: When arefjoin maps to a key that is not a PK or Unique Key. Here, since it's a single table query, it refers to searching with an equality condition.key: The index used in the query. Here, it appears as PRIMARY because the PK was used.ref: Indicates which value was used as aconstcomparison condition. Here, you can seeconstbecause the constant value 1 was used.extra: If empty, it's a general query.
Now, let's change the query again. This time, instead of fetching everything, I'll fetch only the id.
select id from user
where id = 1;

You can see that the value of Extra has changed to Using index. This query uses a covering index because the query can be satisfied solely by the id column, which is included in the index.
In other words, a covering index is applied because all columns appearing in the query are part of the index.
GROUP BY #
In the example, we only checked WHERE. How can we apply a covering index to GROUP BY as well?
Assuming the index columns are a, b, c, the GROUP BY index conditions must satisfy the following:
- The order of columns specified in
GROUP BYmust be the same. - The preceding columns must always be specified, while subsequent columns can be omitted.
- Columns not in the index must not be specified.
- When
WHERE+GROUP BYare used together, columns inWHEREdo not need to be inGROUP BY.
GROUP BY a, c, b # Index not applied
GROUP BY a, b, c # Index applied
GROUP BY a # Index applied
GROUP BY a,b # Index applied
GROUP BY a, b, c, d # Index not applied
WHERE a = 1
GROUP BY b, c # Index applied
WHERE a = 1 and b = '홍길동'
GROUP BY c # Index applied
Pagination + Covering Index #
// Before applying covering index
SELECT *
FROM boards
WHERE condition
ORDER BY id DESC
OFFSET page_number
LIMIT page_size
// After applying covering index
SELECT *
FROM boards as b
JOIN (SELECT id
FROM boards
WHERE condition
ORDER BY id DESC
OFFSET page_number
LIMIT page_size) as cover
on cover.id = b.id
Inside the JOIN, the select, where, orderby, and limit clauses consist only of index columns, meaning the index itself contains all the data necessary to satisfy the query.
Therefore, by using the id retrieved via the covering index, items to be queried from the actual data block can be retrieved quickly.
Typically, a query's performance degrades when order by and limit operations access data blocks.

If a covering index is applied, for a query, WHERE, ORDER BY, and LIMIT index searches are processed first, and then only specific rows access the data block, thereby improving query speed.
At this point, the clustered index (PK) id is automatically included in all indexes, so paging operations are quickly handled by the covering index, and only the necessary columns are fetched at the end.

Example Code QueryDSL
public Page<Match> findList(Pageable pageable, MatchSearchRequest searchRequest) {
// Retrieve PK id using covering index
List<Long> ids = queryFactory
.select(match.id)
.from(match)
.where(eqStartAt(searchRequest.getMatchDay()),
eqGender(searchRequest.getGender()),
eqStatus(searchRequest.getMatchStatus()),
eqPersonnel(searchRequest.getPersonnel()),
eqStadiumName(searchRequest.getStadiumName()))
.orderBy(match.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// Return empty page if no target
if (CollectionUtils.isEmpty(ids)) {
return Page.empty();
}
// Retrieve select clause using the fetched PK id
List<Match> matchList = queryFactory
.selectFrom(match)
.where(match.id.in(ids))
.orderBy(match.startAt.asc())
.fetch();
// Retrieve page count using the fetched PK id
JPAQuery<Long> countQuery = queryFactory
.select(match.count())
.from(match)
.where(match.id.in(ids));
return PageableExecutionUtils.getPage(matchList, pageable, countQuery::fetchOne);
}