优化具有多个属于它的记录的记录的创建

Optimise the creation of a record with many records that belong to it

描述

我正在使用 ReactJSLaravel[=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);
}

我目前的代码算法:

  1. 创建Quiz对象
  2. foreach循环中将Quiz::id分配给Question对象quiz_id外键列并创建
  3. 在内部 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 questionsanswers.
  • 一个Answer 属于一个question和它的一个quiz

所以这样做。

  1. 创建 Quiz.
  2. quiz_id批量插入所有questions包括新字段_question_id.
  3. Select全部Quizquestions.
  • 准备一个包含所有 Answers 的嵌套数组,并在 _question_id 的帮助下将 actual question_id 添加到每个答案中。
  1. 批量插入全部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 倍。)