Оптимизация индекса - все знают Mysql, кто знает меня MongoDB

MongoDB

Посмотреть план выполнения

Оптимизация индекса — это тема, которую нельзя обойти стороной, и MongoDB как NoSQL не является исключением. В Mysql вы можете просмотреть соответствующую информацию об индексе с помощью команды объяснения, как и в MongoDB.

1. db.collection.explain().<method(...)>
    db.products.explain().remove( { category: "apparel" }, { justOne: true })

2. db.collection.<method(...)>.explain({})
    db.products.remove( { category: "apparel" }, { justOne: true }).explain()

Если вы работаете в mongoshell, между первым и вторым нет никакой разницы.Если вы используете его в клиентском инструменте, таком как robot 3T, вы должны вызвать finish() или next() позже.

db.collection.explain().find({}).finish()

Существует три режима объяснения, а именно:

  1. queryPlanner (по умолчанию): в режиме queryPlanner оператор запроса фактически не запрашивается, но план выполнения будет проанализирован для оператора запроса, и будет выбран план-победитель.
  2. executeStats : MongoDB запускает оптимизатор запросов для выбора выигрышного плана, выполняет выигрышный план до его завершения и возвращает статистику, описывающую выполнение выигрышного плана.
  3. allPlansExecution: возвращаются как queryPlanner, так и executeStats. эквивалентноexplain("allPlansExecution") = explain({})

queryPlanner (план запроса)

В таблице журнала хранится журнал операций пользователя. Мы часто запрашиваем журнал операций статьи. Данные следующие:

{
  "_id" : NumberLong(7277744),
  "operatorName" : "autotest_cp",
  "operateTimeUnix" : NumberLong(1586511800890),
  "module" : "ARTICLE",
  "opType" : "CREATE",
  "level" : "GENERAL",
  "recordData" : {
      "articleId" : "6153324",
      "categories" : "100006",
      "title" : "testCase-2 this article is created for cp edior to search",
      "status" : "DRAFT"
  },
  "responseCode" : 10002
}

В коллекции около 7 миллионов данных, для такого запроса

db.getCollection('operateLog').find({"module": "ARTICLE", "recordData.articleId": "6153324"}).sort({_id:-1})

Сначала посмотрите, что возвращает queryPlanner:

"queryPlanner" : {
  "plannerVersion" : 1,
  "namespace" : "smcp.operateLog",
  "indexFilterSet" : false,
  "parsedQuery" : {
    "$and" : [ 
      {
        "module" : {
            "$eq" : "ARTICLE"
        }
      }, 
      {
        "recordData.articleId" : {
            "$eq" : "6153324"
        }
      }
    ]
  },
  "winningPlan" : {
    "stage" : "FETCH",
    "filter" : {
      "$and" : [ 
        {
          "module" : {
              "$eq" : "ARTICLE"
          }
        }, 
        {
          "recordData.articleId" : {
              "$eq" : "6153324"
          }
        }
      ]
    },
    "inputStage" : {
      "stage" : "IXSCAN",
      "keyPattern" : {
          "_id" : 1
      },
      "indexName" : "_id_",
      "isMultiKey" : false,
      "multiKeyPaths" : {
          "_id" : []
      },
      "isUnique" : true,
      "isSparse" : false,
      "isPartial" : false,
      "indexVersion" : 2,
      "direction" : "backward",
      "indexBounds" : {
          "_id" : [ 
              "[MaxKey, MinKey]"
          ]
      }
    }
  },
  "rejectedPlans" : [ 
    {
      "stage" : "SORT",
      "sortPattern" : {
          "_id" : -1.0
      },
      "inputStage" : {
        "stage" : "SORT_KEY_GENERATOR",
        "inputStage" : {
          "stage" : "FETCH",
          "filter" : {
              "recordData.articleId" : {
                  "$eq" : "6153324"
              }
          },
          "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
                "module" : 1.0,
                "opType" : 1.0
            },
            "indexName" : "module_1_opType_1",
            "isMultiKey" : false,
            "multiKeyPaths" : {
                "module" : [],
                "opType" : []
            },
            "isUnique" : false,
            "isSparse" : false,
            "isPartial" : false,
            "indexVersion" : 2,
            "direction" : "forward",
            "indexBounds" : {
                "module" : [ 
                    "[\"ARTICLE\", \"ARTICLE\"]"
                ],
                "opType" : [ 
                    "[MinKey, MaxKey]"
                ]
            }
          }
        }
      }
    }
  ]
}

Значение поля

Значение некоторых важных полей

  • queryPlanner.namespace
    какую таблицу запрашивать

  • queryPlanner.winningPlan
    Подробная информация об оптимальном плане выполнения, возвращенном оптимизатором запросов для этого запроса.

  • queryPlanner.winningPlan.stage
    Этапы выполнения оптимального плана, каждый этап содержит информацию, специфичную для этого этапа. Например, этап IXSCAN будет включать диапазоны индексов, а также другие данные, характерные для сканирования индексов. Если у этапа есть подэтап или подэтапы, то этап будет иметь inputStage или inputStages.

  • queryPlanner.winningPlan.inputStage
    Документ, описывающий подэтап, который предоставляет ключ документа или индекса своему родителю. Это поле существует, если у родительского этапа есть только один дочерний этап.

  • queryPlanner.winningPlan.inputStage.indexName
    Индекс, выбранный планом-победителем, здесь отсортирован по _id, поэтому используется индекс _id

  • queryPlanner.winningPlan.inputStage.isMultiKey
    Будь то Multikey, возврат здесь будет false, если индекс построен на массиве, здесь будет true

  • queryPlanner.winningPlan.inputStage.isUnique
    Является ли используемый индекс уникальным индексом, где _id — уникальный индекс

  • queryPlanner.winningPlan.inputStage.isSparse
    Это разреженный индекс

  • queryPlanner.winningPlan.inputStage.isPartial
    Это частичный индекс

  • queryPlanner.winningPlan.inputStage.direction
    Порядок запроса этого запроса по умолчанию — вперед, потому что sort({_id:-1}) используется для отображения в обратном порядке.

  • queryPlanner.winningPlan.inputStage.indexBounds
    Диапазон индексов сканируется winplan, потому что sort({_id:-1}) используется здесь для сортировки _id в обратном порядке, поэтому диапазон равен [MaxKey, MinKey]. Если это положительная последовательность, это [MinKey, MaxKey]

  • queryPlanner.rejectedPlans
    Подробная информация об отклоненных планах, каждое поле имеет то же значение, что и winPlan.

executeStats (результат выполнения)

Давайте взглянем на результат выполнения executeStats.

"executionStats" : {
  "executionSuccess" : true,
  "nReturned" : 1,
  "executionTimeMillis" : 24387,
  "totalKeysExamined" : 6998084,
  "totalDocsExamined" : 6998084,
  "executionStages" : {
    "stage" : "FETCH",
    "filter" : {
      "$and" : [ 
        {
          "module" : {
              "$eq" : "ARTICLE"
          }
        }, 
        {
          "recordData.articleId" : {
              "$eq" : "6153324"
          }
        }
      ]
    },
    "nReturned" : 1,
    "executionTimeMillisEstimate" : 1684,
    "works" : 6998085,
    "advanced" : 1,
    "needTime" : 6998083,
    "needYield" : 0,
    "saveState" : 71074,
    "restoreState" : 71074,
    "isEOF" : 1,
    "invalidates" : 0,
    "docsExamined" : 6998084,
    "alreadyHasObj" : 0,
    "inputStage" : {
      "stage" : "IXSCAN",
      "nReturned" : 6998084,
      "executionTimeMillisEstimate" : 290,
      "works" : 6998085,
      "advanced" : 6998084,
      "needTime" : 0,
      "needYield" : 0,
      "saveState" : 71074,
      "restoreState" : 71074,
      "isEOF" : 1,
      "invalidates" : 0,
      "keyPattern" : {
          "_id" : 1
      },
      "indexName" : "_id_",
      "isMultiKey" : false,
      "multiKeyPaths" : {
          "_id" : []
      },
      "isUnique" : true,
      "isSparse" : false,
      "isPartial" : false,
      "indexVersion" : 2,
      "direction" : "backward",
      "indexBounds" : {
          "_id" : [ 
              "[MaxKey, MinKey]"
          ]
      },
      "keysExamined" : 6998084,
      "seeks" : 1,
      "dupsTested" : 0,
      "dupsDropped" : 0,
      "seenInvalidated" : 0
    }
  },

  "allPlansExecution" : [
    {...},
    {...}
  ]
}

разбор поля

  • executionStats.executionSuccess
    Является ли выполнение успешным

  • executionStats.nReturned
    Количество возвращенных элементов из запроса

  • executionStats.executionTimeMillis
    Общее время (в миллисекундах), необходимое для выбора плана запроса и выполнения запроса.

  • executionStats.totalKeysExamined
    количество сканирований индекса

  • executionStats.totalDocsExamined
    время сканирования документов

  • executionStats.executionStages
    Детальное выполнение плана выигрыша в виде дерева этапов, т. е. этап может иметь один inputStage или несколько inputStage. Как объяснялось выше.

  • executionStats.executionStages.inputStage.keysExamined
    сколько раз индекс сканировался

  • executionStats.executionStages.inputStage.docsExamined
    Сколько раз документ был отсканирован, обычно когда стадия COLLSCAN, там и будет это значение.

  • exlexecutionStats.allPlansExecution
    Подробная информация обо всех планах запросов показана здесь. (winningPlan + rejectPlans), значение поля такое же, как и вwinningPlan, поэтому не буду вдаваться в подробности

15 этапов

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

  1. COLLSCAN: сканирование всей коллекции
  2. IXSCAN: сканирование индекса
  3. FETCH: получение документов на основе результатов, возвращаемых индексом (как в нашем примере выше).
  4. SHARD_MERGE: объединить данные, возвращаемые каждым осколком.
  5. SORT : вызывается метод сортировки.Когда происходит этот этап, вы можете увидеть два поля memUsage и memLimit
  6. SORT_KEY_GENERATOR: отсортировано в памяти
  7. Ограничение: используйте ограничения ограничения количества возвратов
  8. ПРОПУСТИТЬ: используйте пропуск, чтобы пропустить
  9. IDHACK: запрос к _id
  10. SHARDING_FILTER: запрашивать сегментированные данные через mongos.
  11. COUNT: Используйте db.coll.explain().count() для выполнения операций подсчета, пока вызывается метод count, затем executeStats.executionStages.stage = COUNT
  12. COUNT_SCAN: этап возвращается, когда счетчик использует индекс для подсчета
{
  country: "ID",
  name: "jjj",
  status: 0
},
{
  country: "ZH",
  name: "lisi",
  status: 1
}

Мы индексируем поле страны и одновременно выполняем следующую инструкцию

db.getCollection('testData').explain(true).count({country: "ID"})

Затем посмотрите на результаты выполнения, и вы увидите, что executeStats.executionStages.inputStage.stage = COUNT_SCAN, COUNT_SCAN — это подэтап COUNT.

  1. COUNTSCAN: Стадия возвращается, когда count не использует Index для подсчета.
db.getCollection('testData').explain(true).count({status: 0})

На данный момент executeStats.executionStages.inputStage.stage = COUNTSCAN , COUNTSCAN является подэтапом COUNT

  1. SUBPLAN: поэтапный возврат запроса $or, который не использует индекс
db.getCollection('testData').find({$or : [{name : "lisi"}, {status: 0}]}).explain(true);

На данный момент executeStats.executionStages.stage = SUBPLAN

  1. ТЕКСТ: Этап возвращается при использовании полнотекстового индекса для запроса.
  2. ПРОЕКЦИЯ: возвращение на сцену, когда поле возврата ограничено

Просматривая executeStats.executionStages.stage и значение каждого inputStage (подэтапа) под ним, вы можете определить, какие точки оптимизации существуют.

Запрос должен сканировать как можно меньше документов, чтобы быть быстрее. Очевидно, что мы не хотим видеть этапы COLLSCAN, SORT_KEY_GENERATOR, COUNTSCAN, SUBPLAN и необоснованные SKIP. Когда вы видите эти этапы, вы должны обратить внимание.

Оптимизация запросов

Когда вы смотрите на winPlan или rejectPlan, вы можете узнать, каков порядок выполнения. Например, в нашем rejectPlan мы сначала извлекаем данные «module = ARTICLE» через «module_1_opType_1», а затем передаем «recordData.articleId=6153324». на этапе FETCH Выполните фильтрацию и, наконец, верните данные после сортировки в памяти. Очевидно, что такой план был отвергнут, по крайней мере, он не был выполнен так быстро, как выигрышный план.

Давайте взглянем на данные, возвращаемые executeStats.

nReturned равно 1, то есть только 1 элемент соответствует условию

Значение executeTimeMillis — 24387, а время выполнения — 24 секунды.

Значение totalKeysExamined равно 6998084. Хотя используется индекс, сканируются почти все ключи.

Значение totalDocsExamined равно 6998084, что также сканирует все документы.

Как видно из приведенного выше вывода, хотя мы и используем индекс, скорость все равно очень низкая. Очевидно, текущий индекс нам не подходит.Чтобы исключить помехи, сначала удаляем индекс module_1_opType_1. Так как мы используем здесь два поля для запроса, а поле recordData.articleId существует не в каждом документе (другие типы данных также хранятся в коллекции), поэтому при создании индекса для recordData.articleId необходимо создать частичный индекс

db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1 },
{
  "partialFilterExpression": {
    "recordData.articleId": {
      "$exists": true
    }
  },

  "background": true
}
)

Сначала я съем яблоко, подожду, пока оно построит индекс, а ты можешь есть все, что хочешь. После построения индекса давайте посмотрим на результаты выполнения Stats.

"executionStats" : {
  "executionSuccess" : true,
  "nReturned" : 1,
  "executionTimeMillis" : 3,
  "totalKeysExamined" : 1,
  "totalDocsExamined" : 1,
  "executionStages" : {
    "stage" : "SORT",
    "sortPattern" : {
        "_id" : -1.0
    },
    "memUsage" : 491,
    "memLimit" : 33554432,
    "inputStage" : {
      "stage" : "SORT_KEY_GENERATOR",
      "inputStage" : {
          "stage" : "FETCH",
          "nReturned" : 1,
          "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
                "module" : 1.0,
                "recordData.articleId" : 1.0
            },
            "indexName" : "module_1_recordData.articleId_1",
            "isMultiKey" : false,
            "multiKeyPaths" : {
              "module" : [],
              "recordData.articleId" : []
            },
            "isPartial" : true,
            "indexVersion" : 2,
            "direction" : "forward",
            "indexBounds" : {
              "module" : [ 
                "[\"ARTICLE\", \"ARTICLE\"]"
              ],
              "recordData.articleId" : [ 
                "[\"6153324\", \"6153324\"]"
              ]
          }
        }
      }
    }
  }
}

Я проигнорировал некоторые неважные поля, как видите,Теперь время выполнения составляет 3 миллисекунды (executionTimeMillis=3), сканируется 1 индекс (totalKeysExamined=1) и сканируется 1 документ (totalDocsExamined=1). По сравнению с предыдущими 24387 миллисекундами, я могу сказать, что моя скорость выполнения увеличилась в 8000 раз, и я спрошу, кто еще.Если об этом узнает редактор шокового отдела Калифорнийского университета, это точно будет снова шокирующий заголовок.

Но с этим планом выполнения еще есть проблемы, есть проблемы, есть проблемы, а важные вещи проговариваются по три раза. ВыполнениеStages.stage = sort, что доказывает, что он сортируется в памяти. Когда объем данных большой, он потребляет много производительности, поэтому его нельзя игнорировать. Нам нужно улучшить этот момент.

На основании nReturned = totalDocsExamined пусть сортировка тоже идет по индексу. Итак, мы сначала удаляем предыдущий индекс, а затем заново создаем индекс, здесь мы также добавляем поле _id в индекс, и три поля образуют объединенный индекс

db.getCollection('operateLog').createIndex(
{'module': 1, 'recordData.articleId': 1, '_id': -1},
{
  "partialFilterExpression": {
    "recordData.articleId": {
      "$exists": true
    }
  },

  "background": true
}
)

Точно так же давайте посмотрим на наши результаты выполнения:

"executionStats" : {
  "executionSuccess" : true,
  "nReturned" : 1,
  "executionTimeMillis" : 0,
  "totalKeysExamined" : 1,
  "totalDocsExamined" : 1,
  "executionStages" : {
    "stage" : "FETCH",
    "nReturned" : 1,
    "executionTimeMillisEstimate" : 0,
    "docsExamined" : 1,
    "inputStage" : {
      "stage" : "IXSCAN",
      "nReturned" : 1,
      "keyPattern" : {
          "module" : 1.0,
          "recordData.articleId" : 1.0,
          "_id" : -1.0
      },
      "indexName" : "module_1_recordData.articleId_1__id_-1",
      "multiKeyPaths" : {
          "module" : [],
          "recordData.articleId" : [],
          "_id" : []
      },
      "isPartial" : true,
      "direction" : "forward",
      "indexBounds" : {
          "module" : [ 
              "[\"ARTICLE\", \"ARTICLE\"]"
          ],
          "recordData.articleId" : [ 
              "[\"6153324\", \"6153324\"]"
          ],
          "_id" : [ 
              "[MaxKey, MinKey]"
          ]
      }
    }
  }
}

Вы можете видеть, что наш этап на этот раз FETCH+IXSCAN, и nReturned = totalKeysExamined = totalDocsExamined = 1, и мы используем сортировку по индексу вместо сортировки в памяти. Так же по выполнениюTimeMillis=0 видно, что производительность тоже улучшилась по сравнению с предыдущими 3 миллисекундами, так что этот показатель нам и нужен.

Первые результаты и процесс оптимизации говорят нам о том, что ваш запрос все еще может быть медленным при использовании индекса, и нам нужно больше сосредоточиться на количестве отсканированных документов или строк.

Рекомендации по оптимизации индекса

  1. Создание индексов в соответствии с принципами ESR
    Поле точного (равно) совпадения размещается вверху, условие сортировки (Sort) — в середине, а поле совпадения диапазона (Range) — в конце, то же самое относится к ES, ER.

  2. Каждый запрос должен иметь соответствующий индекс

  3. Попробуйте использовать закрытые индексы (чтобы не читать файлы данных)
    Условия, которые необходимо запросить, и возвращаемое значение находятся в индексе.

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

  5. Старайтесь не считать общее количество, особенно когда объем данных велик и запрос не может попасть в индекс.

  6. Избегайте использования пропуска/ограничения подкачки, особенно при большом объеме данных.

    Альтернатива: использовать условие запроса + уникальное условие сортировки
    Первая страница:db.posts.find({}).sort({_id: 1}).limit(20)
    Вторая страница:db.posts.find({_id: {$gt: <第一页最后一个_id>}}).sort({_id: 1}).limit(20)
    страница третья:db.posts.find({_id: {$gt: <第二页最后一个_id>}}).sort({_id: 1}).limit(20)