Почему Yii2 ActiveRecord использует множество одиночных SELECT вместо JOIN?

В соответствии с документами я использую реализацию Yii2 ActiveRecord в (надеюсь) точно так, как она должна использоваться.

проблема

В довольно простой настройке с простыми отношениями между таблицами, получение 10 результатов выполняется быстро, 100 - медленно. 1000 невозможно. База данных чрезвычайно мала и отлично индексируется. Проблема - это, конечно, Yii2 способ запросить данные, а не сам db.

Я использую стандартный ActiveDataProvider, например:

$provider = new ActiveDataProvider([
    'query' => Post::find(),
    'pagination' => false // to get all records
]);

Что я подозреваю

Отладка с помощью панели инструментов Yii2 показала тысячи одиночных SELECT для простого запроса, который должен просто получить 50 строк из таблицы A с помощью некоторых простых "JOINs" в таблицу B в таблицу C. В простом SQL все решат это с помощью одного оператора SQL и двух соединений, Однако Yii2 запускает SELECT для каждого отношения в каждой строке (что имеет смысл сохранить ORM в чистоте). Результат (более или менее) 1 * 50 * 30 = 1500 запросов для получения двух отношений каждой строки.

Вопрос

Почему Yii2 использует так много одиночных SELECT, или это ошибка на моей стороне? Кроме того, кто-нибудь знает, как "исправить" это?

Поскольку это очень важный вопрос для меня, я дам 500 баунти 14 мая.

Ответы

Ответ 1

По умолчанию Yii2 использует ленивую загрузку для лучшей производительности. Эффект от этого заключается в том, что любое отношение выбирается только при его доступе, следовательно, тысячи запросов sql. Вам нужно использовать активную загрузку. Вы можете сделать это с помощью \yii\db\ActiveQuery::with() который:

Определяет отношения, с которыми должен выполняться этот запрос

Скажем, ваше отношение - это comments, решение выглядит следующим образом:

'query' => Post::find()->with('comments'),

Из руководства для отношений, with выполнением дополнительного запроса для получения отношений, то есть:

SELECT * FROM 'post';
SELECT * FROM 'comment' WHERE 'postid' IN (....);

Чтобы использовать правильное соединение, используйте joinWith с параметром eagerLoading установленным в true:

Этот метод позволяет повторно использовать существующие определения отношений для выполнения запросов JOIN. Основываясь на определении указанного отношения (-ов), метод добавит к текущему запросу один или несколько операторов JOIN.

Так

'query' => Post::find()->joinWith('comments', true);

приведет к следующим запросам:

SELECT 'post'.* FROM 'post' LEFT JOIN 'comment' comments ON post.'id' = comments.'post_id';
SELECT * FROM 'comment' WHERE 'postid' IN (....);

Из комментария @laslov и https://github.com/yiisoft/yii2/issues/2379

важно понять, что использование joinWith() не будет использовать запрос JOIN для активной загрузки связанных данных. По разным причинам, даже с помощью JOIN, WHERE postid IN (...) будет выполняться для обработки загружаемой загрузки. Таким образом, вы должны использовать только joinWith() когда вам определенно нужен JOIN, например, для фильтрации или заказа в одном из соответствующих столбцов таблицы

TL;DR:

joinWith= with плюс фактическая РЕГИСТРИРУЙТЕСЬ (и, следовательно, возможность фильтрации/заказ/группу и т.д. с помощью одной из соответствующих столбцов)

Ответ 2

Чтобы использовать реляционное AR, рекомендуется, чтобы ограничения первичного ключа были объявлены для таблиц, которые необходимо объединить. Ограничения помогут сохранить согласованность и целостность реляционных данных.

Поддержка ограничений внешнего ключа различается в разных СУБД. SQLite 3.6.19 или ранее не поддерживает ограничения внешнего ключа, но вы все равно можете объявлять ограничения при создании таблиц. MySQL MyISAM движок не поддерживает внешние ключи вообще.

В AR существует четыре типа отношений:

  • BELONGS_TO: если связь между таблицей A и B является "один ко многим", тогда B принадлежит A (например, "Пост принадлежит пользователю");
  • HAS_MANY: если отношение между таблицами A и B является "один ко многим", то A имеет много B (например, пользователь имеет много сообщений);
  • HAS_ONE: это особый случай HAS_MANY, где A имеет не более одного B (например, пользователь имеет не более одного профиля);
  • MANY_MANY: это соответствует отношениям "многие ко многим" в базе данных. Для разбиения отношения "многие ко многим" на отношения "один ко многим" необходима ассоциативная таблица, так как большинство СУБД не поддерживают отношения "многие ко многим" напрямую. В нашей схеме базы данных примера используется tbl_post_category. В терминологии AR мы можем объяснить MANY_MANY как сочетание BELONGS_TO и HAS_MANY. Например, Post принадлежит многим категориям и категориям, которые имеют много сообщений.

Следующий код показывает, как мы объявляем отношения для классов User и Post.

class Post extends CActiveRecord
{
    ......

    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
            'categories'=>array(self::MANY_MANY, 'Category',
                'tbl_post_category(post_id, category_id)'),
        );
    }
}

class User extends CActiveRecord
{
    ......

    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}

Результат запроса будет сохранен в качестве экземпляра соответствующего класса AR. Это называется ленивым подходом к загрузке, т.е. Реляционный запрос выполняется только тогда, когда к ним обращаются связанные объекты. Пример ниже показывает, как использовать этот подход:

// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post author: a relational query will be performed here
$author=$post->author;

Вы как-то ошибаетесь, пожалуйста, перейдите от документального документа здесь http://www.yiiframework.com/doc/guide/1.1/en/database.arr