Почему 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