Подробное объяснение оптимизации общего количества записей в компоненте пейджинга.

Java

1. Предпосылки

Исследуйте mybatis-plus (далее MBP) при использовании его функции пейджинга. нашел одинJsqlParserCountOptimizeКласс обработки оптимизации подкачки, официальный не является подробным введением, и онлайн-язык не найден для анализа логики класса.Это не дерзко с ним.

2 принципа

Во-первых, здесь не повторяется принцип пейджингового перехватчика PaginationInterceptor (принцип реализации общей инкапсуляции пейджинга mybatis довольно прост, вот и все), и наконец, реализованный в запросе в основном делится на 2 sql: проверяем общее количество количество записей + проверка Истинные записи пейджинга. И этот вид оптимизации используется для проверки шага счета. Как оптимизировать этот запрос на подсчет? Вот реальный сценарий, который поможет вам понять: если есть две таблицы user, user_address и user_account для записи пользователей, адресов пользователей и учетных записей пользователей, соответственно, у пользователя может быть несколько адресов, то есть отношение «1 ко многим». ; у пользователя может быть только 1 учетная запись — это отношения 1-к-1.

2.1 Оптимизировать заказ по

Сначала посмотрите на следующий sql, поместите его под запрос на подкачку

select * from user order by age desc, update_time desc 

Традиционные компоненты пагинации часто

查count: 
select count(1) from (select * from user order by age desc, update_time desc)
查记录:
select * from user order by age desc, update_time desc limit 0,50

Нашли проблему? Порядок по при проверке кол-ва можно вообще убрать! В случае сложных запросов, больших таблиц, сортировки по неиндексированным полям и т.д., очень медленно проверяется запись, и необходимо пересчитывать счетчик! Таким образом, счетчик поиска, очевидно, хочет быть оптимизирован какselect count(1) from (select * from user).

2.1.1 Ограничения

Но не все сценарии можно оптимизировать, напримерgroup byзапрос

2.1.2 Исходный код

Поэтому исходный код MBP реализован следующим образом: если нет group by и есть оператор order by, удалить порядок by

// 添加包含groupBy 不去除orderBy
if (null == groupBy && CollectionUtils.isNotEmpty(orderBy)) {
        plainSelect.setOrderByElements(null);
        sqlInfo.setOrderBy(false);
}

2.2 Оптимизация сценариев присоединения

В операции соединения также есть возможность оптимизации, см. следующий sql

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid

В настоящее время при проверке подсчета путем пейджинга вы можете удалить левое соединение и проверить пользователя напрямую, потому что отношение между пользователем и user_account составляет 1 к 1, как показано ниже.

查count: 
select count(1) from user u
查记录: 
select u.id,ua.account from user u left join user_account ua on u.id=ua.uid limit 0,50

2.2.1 Ограничения

Есть еще много ограничений, чтобы проверить, можно ли удалить счетчик из соединения и напрямую проверить первую таблицу, а именно:

Количество записей не может быть увеличено после объединения записей таблицы

Как видно из приведенного выше случая, если количество записей после левого объединения больше, чем общее количество записей в первой непосредственно проверенной таблице, эту оптимизацию выполнить невозможно. Например, 3 пользователя записывают по 2 адреса.

select u.id,ua.address from user u left join user_address ua on u.id=ua.uid (6条)
vs
select count(1) from user u (3条)

В настоящее время удаление левого соединения и проверка количества позволит получить меньшее общее количество записей.Остерегайтесь, это может превратиться в яму, MBP не может автоматически определить, будет ли этот запрос на подкачку выполнять увеличение записи, поэтому оптимизация соединения по умолчанию отключена.Если вы хотите открыть пользовательский компонент JsqlParserCountOptimize, вам необходимо объявить пользовательский компонент и установить для параметра optimizeJoin значение true, как показано ниже.

 @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }

На самом деле дизайн исходного кода здесь немного неразумный, потому что после открытия вы должны внимательно просмотреть код подкачки различных левых объединений.Если есть расширение, вы можете только построить объект Page, установить для оптимизацииCountSql значение false (по умолчанию true ), что эквивалентно Если все оптимизации подсчета для этого запроса отключены, не будут выполняться не только соединения, но и такие оптимизации, как упорядочение по. Вместо этого рекомендуется получать optimJoin от Page (или ThreadLocal?), который можно настроить на каждом уровне запроса, по умолчанию он отключен, и на запросе будут автоматически включаться только те, которые могут быть оптимизированы разработчиком. уровень.

только левое соединение

Если это внутреннее или правое соединение, количество записей будет часто увеличиваться, поэтому оптимизация MBP автоматически определит, что если в нескольких соединениях есть какие-либо нелевые соединения, эта оптимизация не будет выполняться, напримерfrom a left join b .... right join c... left join dВ настоящее время он не будет оптимизирован напрямую

Оператор on имеет условия запроса

Например

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid and ua.account > ?

где оператор содержит условия для объединения таблиц

Например

select u.id,ua.account from user u left join user_account ua on u.id=ua.uid where ua.account > ?

2.2.2 Исходный код

Исходный код оптимизации соединения MBP выглядит примерно следующим образом, что соответствует приведенным выше оптимизациям и ограничениям.

List<Join> joins = plainSelect.getJoins();
// 是否全局开启了optimizeJoin(这里建议还可以从Page中按每次查询设置)
if (optimizeJoin && CollectionUtils.isNotEmpty(joins)) {
    boolean canRemoveJoin = true;
    String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY);
    for (Join join : joins) {
            // 仅限left join
            if (!join.isLeft()) {
                    canRemoveJoin = false;
                    break;
            }
            Table table = (Table) join.getRightItem();
            String str = Optional.ofNullable(table.getAlias()).map(Alias::getName).orElse(table.getName()) + StringPool.DOT;
            String onExpressionS = join.getOnExpression().toString();
            /* 如果 join 里包含 ?(代表on语句有查询条件) 
            或者 
            where语句包含连接表的条件
            就不移除 join */
            if (onExpressionS.contains(StringPool.QUESTION_MARK) || whereS.contains(str)) {
                    canRemoveJoin = false;
                    break;
            }
    }
    if (canRemoveJoin) {
            plainSelect.setJoins(null);
    }
}

2.3 Оптимизация позиции select count(1)

В традиционном пейджинге select count(1) часто устанавливается на внешнем уровне исходного SQL-запроса, например

select count(1) from (select * from user)

Настоящая цель count — получить количество записей, что совершенно не нужно в исходном запросе.select *Это требует дополнительного времени, поэтому его можно оптимизировать до следующего утверждения

select count(1) from user

2.3.1 Ограничения

Точно так же существуют некоторые сценарии, в которых невозможно выполнить оптимизацию позиции подсчета.

Поле выбора содержит параметры

Если выборка содержит параметры #{}, ${}, ожидающие замены, эта оптимизация не может быть выполнена, поскольку последующий этап реального значения замены плейсхолдера вызовет ошибку из-за уменьшения количества плейсхолдеров, например

select count(1) from (select power(#{aSelectParam},2) from user_account where uid=#{uidParam}) ua
vs
select count(1) from user_account where uid=#{uidParam} ua 

Официальный выпуск MBP # 95 зарегистрировал этот выпуск

содержит различные

Select содержит отдельные операторы дедупликации. Если их удалить, количество записей count может увеличиться, поэтому эту оптимизацию выполнить невозможно. Например

select count(1) from (select distinct(uid) from user_address) ua
vs
select count(1) from user_address ua  #记录数可能增大

содержит группу по

В операторе, содержащем group by, поскольку в select часто есть агрегатная функция, встроенная семантика count(1) становится агрегатной функцией, и эта оптимизация не может быть выполнена. Например

select count(1) from (select uid,count(1) from user_address group by uid) ua #返回单行单列总记录数
vs
select count(1) from user_address group by uid #返回多行单列聚合count数

2.3.2 Исходный код

Соответствующий исходный код в MBP выглядит следующим образом

//select的字段里包含参数不优化
for (SelectItem item : plainSelect.getSelectItems()) {
        if (item.toString().contains(StringPool.QUESTION_MARK)) {
                return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
        }
}
// 包含 distinct、groupBy不优化
if (distinct != null || null != groupBy) {
        return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
}
...
// 优化 SQL,COUNT_SELECT_ITEM其实就是select count(1)语句
plainSelect.setSelectItems(COUNT_SELECT_ITEM);

3 Резюме

Эта статья на самом деле нацелена на некоторые идеи оптимизации шага проверки количества записей счетчика в общем компоненте пейджинга.Давайте рассмотрим:

  • Оптимизировать заказ по
  • Оптимизация оператора соединения
  • Оптимизировать позицию select count(1)
  • Обратите внимание на ограничения, соответствующие приведенным выше оптимизациям, иначе это может привести к бизнес-ошибкам (особенно оптимизация соединений, которая относительно скрыта).

На самом деле, это не ограничивается MBP, вы также можете попробовать использовать пользовательские блокировщики страниц, оптимизация страниц все еще эффективна. Используется для записи эволюции жизни, повторения истории. Надеюсь стать платформой, которая приносит помощь и размышления всем