优化具有多个属于它的记录的记录的创建
Optimise the creation of a record with many records that belong to it
描述
我正在使用 ReactJS、Laravel 和 [=69 编写全栈 Web 应用程序=],允许用户创建测验。
我的数据库结构:
测验table
create table quizzes (
id bigint unsigned auto_increment primary key,
title varchar(255) not null,
description text null,
duration smallint unsigned not null,
is_open tinyint(1) default 0 not null,
shuffle_questions tinyint(1) default 0 not null,
user_id bigint unsigned not null,
lesson_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint quizzes_lesson_id_foreign foreign key (lesson_id) references lessons (id) on delete cascade,
constraint quizzes_user_id_foreign foreign key (user_id) references users (id) on delete cascade
) collate = utf8mb4_unicode_ci;
问题table
create table questions (
id bigint unsigned auto_increment primary key,
title text not null,
description text null,
image varchar(255) null,
type enum ('radio', 'checkbox', 'text', 'image') not null,
is_required tinyint(1) default 0 not null,
points tinyint unsigned default '0' not null,
quiz_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint questions_quiz_id_foreign foreign key (quiz_id) references webagu_24082021.quizzes (id) on delete cascade
) collate = utf8mb4_unicode_ci;
答案table
create table answers (
id bigint unsigned auto_increment primary key,
value varchar(1024) null,
is_correct tinyint(1) default 0 not null,
quiz_id bigint unsigned not null,
question_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint answers_question_id_foreign foreign key (question_id) references questions (id) on delete cascade,
constraint answers_quiz_id_foreign foreign key (quiz_id) references quizzes (id) on delete cascade
) collate = utf8mb4_unicode_ci;
当用户按下“保存测验”按钮时来自 UI 的数据
//....
axios
.post('/quizzes', { "quiz": QuizData, "questions": QuestionsData, "answers": AnswersData })
.then(res => {
if(201 === res.status) alert('Quiz saved!');
console.log(res.data)
});
//....
测验控制器store
方法
public function store(Request $request): JsonResponse
{
$quizData = $request->input('quiz');
$questions = $request->input('questions');
$answers = $request->input('answers');
$groupedAnswers = Utils::groupBy('_question_id', $answers);
//DB::beginTransaction();
$quizData['user_id'] = \auth('api')->user()->id;
$quiz = Quiz::create($quizData);
$new_questions = [];
$new_answers = [];
foreach ($questions as $question) {
$question['quiz_id'] = $quiz->id;
$new_question = Question::create($question);
$new_questions[] = $new_question;
$qid = $question['_question_id'];
if (isset($groupedAnswers[$qid])) {
$question_answers = $groupedAnswers[$qid];
foreach ($question_answers as $answer) {
$answer['quiz_id'] = $quiz->id;
$answer['question_id'] = $new_question->id;
$new_answer = Answer::create($answer);
$new_answers[] = $new_answer;
}
}
}
//DB::commit();
$resData = ['quiz' => $quiz, 'questions' => $new_questions, 'answer' => $new_answers];
return response()->json($resData, 201);
}
我目前的代码算法:
- 创建
Quiz
对象
- 在
foreach
循环中将Quiz::id
分配给Question
对象quiz_id
外键列并创建
- 在内部
foreach
循环中将 Question::id
分配给 Answer
对象 question_id
外键列并创建
问题
此算法创建 Q(问题数)* A(答案数) SQL 个查询 - 这非常慢。
例如,如果测验包含 50 个问题,每个问题有 4 个答案变体,则查询将包含 50 * 4 = 200 SQL 个查询。
那么,如何改变这个糟糕的解决方案以使其更快地工作?
以下解决方案将导致:
- 一个查询插入
Quiz
。
- 每个
Question
一个查询。
- 每个
Question
一个查询插入它的 answers
如果有的话。
所以最多 1 + (questions_count)*2
个查询。
如果您的答案不依赖于 question_id
,一切都可以在 4 个查询中完成
public function store(Request $request): JsonResponse
{
/* ******************************************* */
// GETTING AND INSERTING QUIZ
/* ******************************************* */
$quizData = $request->input('quiz');
$quizData['user_id'] = \auth('api')->user()->id;
$quiz = Quiz::create($quizData);
/* ******************************************* */
// GETTING QUESTIONS AND THEIR ANSWERS
/* ******************************************* */
$questions = $request->input('questions');
$answers = $request->input('answers');
$answersByQuestion = Utils::groupBy('_question_id', $answers);
// ***********************************************
// ***********************************************
$new_questions = [];
$new_answers = [];
foreach ($questions as $question) {
// $question['quiz_id'] = $quiz->id; $new_question = Question::create($question);
$new_question = $quiz->questions()->create($question);
$new_questions[] = $new_question; // FOR THE RESPONSE
if (isset($answersByQuestion[$question['_question_id']])) {
// PREPARING ANSWERS FOR BULK INSERT
foreach ($answersByQuestion[$question['_question_id']] as $answer) {
$answer['quiz_id'] = $quiz->id;
$answer['question_id'] = $new_question->id;
$new_answers[] = $answer;
}
}
DB::table('answers')->insert($new_answers);
}
$resData = ['quiz' => $quiz, 'questions' => $new_questions, 'answer' => $new_answers];
return response()->json($resData, 201);
}
4个查询的想法(终极优化)
我看到了:
- 你在
Answers
上使用 _question_id
来用他们的 Question
做 link。
- A
Quiz
has-many questions
和 answers
.
- 一个
Answer
属于一个question
和它的一个quiz
。
所以这样做。
- 创建
Quiz
.
- 取
quiz_id
和批量插入所有questions
包括新字段_question_id
.
- Select全部
Quiz
questions
.
- 准备一个包含所有
Answers
的嵌套数组,并在 _question_id
的帮助下将 actual question_id
添加到每个答案中。
- 批量插入全部
answers
.
实际上,如果你有 50 个问题,你有:
1
查询创建 quiz
50
查询创建 questions
200
查询创建 answers
总计:251
个查询。
如果我没有弄错你的编码,你可以像这样优化你的查询(示例 50 个问题,我在评论区解释):
$input_quiz = $request->input('quiz');
$input_questions = $request->input('questions');
$input_answers = $request->input('answers');
$groupedAnswers = Utils::groupBy('_question_id', $input_answers);
/*********************/
// Create a quiz (1 query)
$quiz = Quiz::create($input_quiz);
// Create questions (50 queries)
$questions = $quiz->questions()->createMany($input_questions);
// Prepare answers data
$answers = [];
// Loop $questions
foreach ($questions as $key => $question){
// If I'm not mistaken, the index on the input
// will be equal to $questions (starting at 0)
$qid = $input_questions[$key]['_question_id'];
if(isset($groupedAnswers[$qid])){
$question_answers = $groupedAnswers[$qid];
// Modify answer
foreach ($question_answers as $_answer){
$_answer['quiz_id'] = $quiz->id;
$_answer['question_id'] = $question->id;
$_answer['created_at'] = now(); // Laravel insert not saved created_at column
$_answer['updated_at'] = now(); // Laravel insert not saved updated_at column
$answers[] = $_answer; // Push it
}
}
}
// Then, we will bulk insert using the insert method (1 query)
$answers = Answer::insert($answers);
现在,你有:
1
查询创建 quiz
50
查询创建 questions
1
查询创建 answers
总计:52
个查询。
我在这种情况下所做的是,只使用 3 个查询。但是要考虑很多,比如使用临时列。但是,我认为您不需要走那么远。
一个测验有很多问题。 questions.quiz_id
.
妥善处理了这种 1-to-many 关系
同样,一个问题有很多答案。这种 1-to-many 关系由 Answers.question_id
.
正确处理
但是有一个次要的 no-no -- Answers.quiz_id
是“多余的”,因为它可以通过 Questions
找到。在正确的架构设计中,该列不应存在。对于仅 200 个答案(甚至一百万个答案),您不能提出“性能参数”。
往返和大量语句
如果可行,可以将所有测验插入一个 INSERT
语句中;另一份陈述中的所有问题;和所有的答案在三分之一。 (不,我不知道如何在 Laravel 中做到这一点;但我可以为 MySQL 解释。)
与此同时,每秒一百个,也许一千个查询无需担心。
(作为奖励,“批处理”插入的运行速度大约是原来的 10 倍。)
描述
我正在使用 ReactJS、Laravel 和 [=69 编写全栈 Web 应用程序=],允许用户创建测验。
我的数据库结构:
测验table
create table quizzes (
id bigint unsigned auto_increment primary key,
title varchar(255) not null,
description text null,
duration smallint unsigned not null,
is_open tinyint(1) default 0 not null,
shuffle_questions tinyint(1) default 0 not null,
user_id bigint unsigned not null,
lesson_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint quizzes_lesson_id_foreign foreign key (lesson_id) references lessons (id) on delete cascade,
constraint quizzes_user_id_foreign foreign key (user_id) references users (id) on delete cascade
) collate = utf8mb4_unicode_ci;
问题table
create table questions (
id bigint unsigned auto_increment primary key,
title text not null,
description text null,
image varchar(255) null,
type enum ('radio', 'checkbox', 'text', 'image') not null,
is_required tinyint(1) default 0 not null,
points tinyint unsigned default '0' not null,
quiz_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint questions_quiz_id_foreign foreign key (quiz_id) references webagu_24082021.quizzes (id) on delete cascade
) collate = utf8mb4_unicode_ci;
答案table
create table answers (
id bigint unsigned auto_increment primary key,
value varchar(1024) null,
is_correct tinyint(1) default 0 not null,
quiz_id bigint unsigned not null,
question_id bigint unsigned not null,
created_at timestamp null,
updated_at timestamp null,
constraint answers_question_id_foreign foreign key (question_id) references questions (id) on delete cascade,
constraint answers_quiz_id_foreign foreign key (quiz_id) references quizzes (id) on delete cascade
) collate = utf8mb4_unicode_ci;
当用户按下“保存测验”按钮时来自 UI 的数据
//....
axios
.post('/quizzes', { "quiz": QuizData, "questions": QuestionsData, "answers": AnswersData })
.then(res => {
if(201 === res.status) alert('Quiz saved!');
console.log(res.data)
});
//....
测验控制器store
方法
public function store(Request $request): JsonResponse
{
$quizData = $request->input('quiz');
$questions = $request->input('questions');
$answers = $request->input('answers');
$groupedAnswers = Utils::groupBy('_question_id', $answers);
//DB::beginTransaction();
$quizData['user_id'] = \auth('api')->user()->id;
$quiz = Quiz::create($quizData);
$new_questions = [];
$new_answers = [];
foreach ($questions as $question) {
$question['quiz_id'] = $quiz->id;
$new_question = Question::create($question);
$new_questions[] = $new_question;
$qid = $question['_question_id'];
if (isset($groupedAnswers[$qid])) {
$question_answers = $groupedAnswers[$qid];
foreach ($question_answers as $answer) {
$answer['quiz_id'] = $quiz->id;
$answer['question_id'] = $new_question->id;
$new_answer = Answer::create($answer);
$new_answers[] = $new_answer;
}
}
}
//DB::commit();
$resData = ['quiz' => $quiz, 'questions' => $new_questions, 'answer' => $new_answers];
return response()->json($resData, 201);
}
我目前的代码算法:
- 创建
Quiz
对象 - 在
foreach
循环中将Quiz::id
分配给Question
对象quiz_id
外键列并创建 - 在内部
foreach
循环中将Question::id
分配给Answer
对象question_id
外键列并创建
问题
此算法创建 Q(问题数)* A(答案数) SQL 个查询 - 这非常慢。
例如,如果测验包含 50 个问题,每个问题有 4 个答案变体,则查询将包含 50 * 4 = 200 SQL 个查询。
那么,如何改变这个糟糕的解决方案以使其更快地工作?
以下解决方案将导致:
- 一个查询插入
Quiz
。 - 每个
Question
一个查询。 - 每个
Question
一个查询插入它的answers
如果有的话。
所以最多 1 + (questions_count)*2
个查询。
如果您的答案不依赖于 question_id
,一切都可以在 4 个查询中完成
public function store(Request $request): JsonResponse
{
/* ******************************************* */
// GETTING AND INSERTING QUIZ
/* ******************************************* */
$quizData = $request->input('quiz');
$quizData['user_id'] = \auth('api')->user()->id;
$quiz = Quiz::create($quizData);
/* ******************************************* */
// GETTING QUESTIONS AND THEIR ANSWERS
/* ******************************************* */
$questions = $request->input('questions');
$answers = $request->input('answers');
$answersByQuestion = Utils::groupBy('_question_id', $answers);
// ***********************************************
// ***********************************************
$new_questions = [];
$new_answers = [];
foreach ($questions as $question) {
// $question['quiz_id'] = $quiz->id; $new_question = Question::create($question);
$new_question = $quiz->questions()->create($question);
$new_questions[] = $new_question; // FOR THE RESPONSE
if (isset($answersByQuestion[$question['_question_id']])) {
// PREPARING ANSWERS FOR BULK INSERT
foreach ($answersByQuestion[$question['_question_id']] as $answer) {
$answer['quiz_id'] = $quiz->id;
$answer['question_id'] = $new_question->id;
$new_answers[] = $answer;
}
}
DB::table('answers')->insert($new_answers);
}
$resData = ['quiz' => $quiz, 'questions' => $new_questions, 'answer' => $new_answers];
return response()->json($resData, 201);
}
4个查询的想法(终极优化)
我看到了:
- 你在
Answers
上使用_question_id
来用他们的Question
做 link。 - A
Quiz
has-manyquestions
和answers
. - 一个
Answer
属于一个question
和它的一个quiz
。
所以这样做。
- 创建
Quiz
. - 取
quiz_id
和批量插入所有questions
包括新字段_question_id
. - Select全部
Quiz
questions
.
- 准备一个包含所有
Answers
的嵌套数组,并在_question_id
的帮助下将 actualquestion_id
添加到每个答案中。
- 批量插入全部
answers
.
实际上,如果你有 50 个问题,你有:
1
查询创建quiz
50
查询创建questions
200
查询创建answers
总计:251
个查询。
如果我没有弄错你的编码,你可以像这样优化你的查询(示例 50 个问题,我在评论区解释):
$input_quiz = $request->input('quiz');
$input_questions = $request->input('questions');
$input_answers = $request->input('answers');
$groupedAnswers = Utils::groupBy('_question_id', $input_answers);
/*********************/
// Create a quiz (1 query)
$quiz = Quiz::create($input_quiz);
// Create questions (50 queries)
$questions = $quiz->questions()->createMany($input_questions);
// Prepare answers data
$answers = [];
// Loop $questions
foreach ($questions as $key => $question){
// If I'm not mistaken, the index on the input
// will be equal to $questions (starting at 0)
$qid = $input_questions[$key]['_question_id'];
if(isset($groupedAnswers[$qid])){
$question_answers = $groupedAnswers[$qid];
// Modify answer
foreach ($question_answers as $_answer){
$_answer['quiz_id'] = $quiz->id;
$_answer['question_id'] = $question->id;
$_answer['created_at'] = now(); // Laravel insert not saved created_at column
$_answer['updated_at'] = now(); // Laravel insert not saved updated_at column
$answers[] = $_answer; // Push it
}
}
}
// Then, we will bulk insert using the insert method (1 query)
$answers = Answer::insert($answers);
现在,你有:
1
查询创建quiz
50
查询创建questions
1
查询创建answers
总计:52
个查询。
我在这种情况下所做的是,只使用 3 个查询。但是要考虑很多,比如使用临时列。但是,我认为您不需要走那么远。
一个测验有很多问题。 questions.quiz_id
.
同样,一个问题有很多答案。这种 1-to-many 关系由 Answers.question_id
.
但是有一个次要的 no-no -- Answers.quiz_id
是“多余的”,因为它可以通过 Questions
找到。在正确的架构设计中,该列不应存在。对于仅 200 个答案(甚至一百万个答案),您不能提出“性能参数”。
往返和大量语句
如果可行,可以将所有测验插入一个 INSERT
语句中;另一份陈述中的所有问题;和所有的答案在三分之一。 (不,我不知道如何在 Laravel 中做到这一点;但我可以为 MySQL 解释。)
与此同时,每秒一百个,也许一千个查询无需担心。
(作为奖励,“批处理”插入的运行速度大约是原来的 10 倍。)