Индексы являются важной частью правильного моделирования данных для всех баз данных, и DynamoDB не является исключением. Вторичные индексы DynamoDB — это мощный инструмент для реализации новых шаблонов доступа к вашим данным.
В этом посте мы рассмотрим вторичные индексы DynamoDB . Во-первых, мы начнем с некоторых концептуальных моментов о том, как следует относиться к DynamoDB и проблемам, которые решают вторичные индексы. Затем мы рассмотрим несколько практических советов по эффективному использованию вторичных индексов. Наконец, мы остановимся на некоторых соображениях о том, когда следует использовать вторичные индексы, а когда следует искать другие решения.
Давайте начнем.
Прежде чем мы перейдем к вариантам использования и лучшим практикам использования вторичных индексов, нам следует сначала понять, что такое вторичные индексы DynamoDB . И для этого нам нужно немного понимать, как работает DynamoDB.
Это предполагает некоторое базовое понимание DynamoDB. Мы рассмотрим основные моменты, которые вам необходимо знать, чтобы понять вторичные индексы, но если вы новичок в DynamoDB, возможно, вам захочется начать с более простого введения.
DynamoDB — уникальная база данных. Он предназначен для рабочих нагрузок OLTP, а это означает, что он отлично подходит для обработки большого объема небольших операций — например, добавление товара в корзину, лайк видео или добавление комментария на Reddit. Таким образом, он может обрабатывать те же приложения, что и другие базы данных, которые вы могли использовать, например MySQL, PostgreSQL, MongoDB или Cassandra.
Ключевое обещание DynamoDB — гарантия стабильной производительности в любом масштабе . Независимо от того, содержит ли ваша таблица 1 мегабайт данных или 1 петабайт данных, DynamoDB хочет иметь одинаковую задержку для ваших OLTP-подобных запросов. Это очень важно: производительность многих баз данных снижается по мере увеличения объема данных или количества одновременных запросов. Однако предоставление этих гарантий требует некоторых компромиссов, и DynamoDB обладает некоторыми уникальными характеристиками, которые необходимо понимать, чтобы эффективно использовать их.
Во-первых, DynamoDB горизонтально масштабирует ваши базы данных, распределяя данные по нескольким скрытым разделам. Эти разделы не видны вам как пользователю, но они лежат в основе работы DynamoDB. Вы укажете первичный ключ для своей таблицы (либо отдельный элемент, называемый «ключом раздела», либо комбинацию ключа раздела и ключа сортировки), и DynamoDB будет использовать этот первичный ключ, чтобы определить, в каком разделе находятся ваши данные. . Любой ваш запрос будет проходить через маршрутизатор запросов, который определит, какой раздел должен обрабатывать запрос. Эти разделы небольшие — обычно 10 ГБ или меньше — поэтому их можно перемещать, разделять, реплицировать и иным образом управлять ими независимо.
Горизонтальная масштабируемость посредством сегментирования интересна, но ни в коем случае не является уникальной для DynamoDB. Многие другие базы данных — как реляционные, так и нереляционные — используют сегментирование для горизонтального масштабирования. Однако уникальностью DynamoDB является то, что он заставляет вас использовать первичный ключ для доступа к вашим данным. Вместо использования планировщика запросов, который преобразует ваши запросы в серию запросов, DynamoDB заставляет вас использовать первичный ключ для доступа к вашим данным. По сути, вы получаете индекс с прямой адресацией для ваших данных.
API для DynamoDB отражает это. Существует ряд операций над отдельными элементами ( GetItem
, PutItem
, UpdateItem
, DeleteItem
), которые позволяют вам читать, записывать и удалять отдельные элементы. Кроме того, существует операция Query
, которая позволяет получить несколько элементов с одним и тем же ключом раздела. Если у вас есть таблица с составным первичным ключом, элементы с одинаковым ключом раздела будут сгруппированы в одном разделе. Они будут упорядочены в соответствии с ключом сортировки, что позволит вам обрабатывать такие шаблоны, как «Получить последние заказы для пользователя» или «Получить последние 10 показаний датчиков для устройства IoT».
Например, давайте представим SaaS-приложение, имеющее таблицу пользователей. Все Пользователи принадлежат одной Организации. У нас может быть таблица, которая выглядит следующим образом:
Мы используем составной первичный ключ с ключом раздела «Организация» и ключом сортировки «Имя пользователя». Это позволяет нам выполнять операции по выборке или обновлению отдельного пользователя, предоставляя его организацию и имя пользователя. Мы также можем получить всех пользователей для одной организации, указав только организацию для операции Query
.
Имея в виду некоторые основы, давайте теперь посмотрим на вторичные индексы. Лучший способ понять необходимость вторичных индексов — это понять проблему, которую они решают. Мы видели, как DynamoDB разделяет ваши данные в соответствии с вашим первичным ключом и как это заставляет вас использовать первичный ключ для доступа к вашим данным. Это все хорошо для некоторых шаблонов доступа, но что, если вам нужно получить доступ к данным другим способом?
В нашем примере выше у нас была таблица пользователей, к которой мы обращались по их организации и имени пользователя. Однако нам также может потребоваться получить данные одного пользователя по его адресу электронной почты. Этот шаблон не соответствует шаблону доступа по первичному ключу, к которому нас подталкивает DynamoDB. Поскольку наша таблица разделена по разным атрибутам, не существует четкого способа доступа к нашим данным нужным нам способом. Мы могли бы выполнить полное сканирование таблицы, но это медленно и неэффективно. Мы могли бы дублировать наши данные в отдельную таблицу с другим первичным ключом, но это добавляет сложности.
Здесь на помощь приходят вторичные индексы. Вторичный индекс — это, по сути, полностью управляемая копия ваших данных с другим первичным ключом. Вы укажете вторичный индекс в своей таблице, объявив первичный ключ для индекса. Когда в вашу таблицу поступают записи, DynamoDB автоматически реплицирует данные в ваш вторичный индекс.
Примечание *: Все в этом разделе относится к глобальным вторичным индексам. DynamoDB также предоставляет локальные вторичные индексы, которые немного отличаются. Почти во всех случаях вам понадобится глобальный вторичный индекс. Более подробную информацию о различиях можно найти в этой статье о выборе глобального или локального вторичного индекса .*
В этом случае мы добавим в нашу таблицу вторичный индекс с ключом раздела «Электронная почта». Вторичный индекс будет выглядеть следующим образом:
Обратите внимание, что это те же данные, только что они были реорганизованы с использованием другого первичного ключа. Теперь мы можем эффективно искать пользователя по его адресу электронной почты.
В некотором смысле это очень похоже на индекс в других базах данных. Оба предоставляют структуру данных, оптимизированную для поиска по определенному атрибуту. Но вторичные индексы DynamoDB отличаются по нескольким ключевым моментам.
Во-первых, и это самое главное, индексы DynamoDB находятся в совершенно других разделах, чем ваша основная таблица. DynamoDB хочет, чтобы каждый поиск был эффективным и предсказуемым, и хочет обеспечить линейное горизонтальное масштабирование. Для этого ему необходимо повторно распределить ваши данные по атрибутам, которые вы будете использовать для запроса.
В других распределенных базах данных они обычно не перераспределяют ваши данные для вторичного индекса. Обычно они просто поддерживают вторичный индекс для всех данных в сегменте. Однако, если ваши индексы не используют сегментный ключ, вы теряете некоторые преимущества горизонтального масштабирования ваших данных, поскольку запрос без сегментного ключа должен будет выполнить операцию сбора данных по всем сегментам, чтобы найти нужные вам данные. ищите.
Второе отличие вторичных индексов DynamoDB заключается в том, что они (часто) копируют весь элемент во вторичный индекс. Для индексов реляционной базы данных индекс часто содержит указатель на первичный ключ индексируемого элемента. После обнаружения соответствующей записи в индексе базе данных необходимо будет получить полный элемент. Поскольку вторичные индексы DynamoDB находятся на разных узлах, чем основная таблица, они хотят избежать обратного сетевого перехода к исходному элементу. Вместо этого вы скопируете столько данных, сколько вам нужно, во вторичный индекс для обработки чтения.
Вторичные индексы в DynamoDB — это мощный инструмент, но у них есть некоторые ограничения. Во-первых, они доступны только для чтения — вы не можете писать напрямую во вторичный индекс. Вместо этого вы будете записывать данные в свою основную таблицу, а DynamoDB будет выполнять репликацию в ваш вторичный индекс. Во-вторых, с вас взимается плата за операции записи в вторичные индексы. Таким образом, добавление вторичного индекса в вашу таблицу часто удваивает общие затраты на запись для вашей таблицы.
Теперь, когда мы понимаем, что такое вторичные индексы и как они работают, давайте поговорим о том, как их эффективно использовать. Вторичные индексы — мощный инструмент, но ими можно злоупотреблять. Вот несколько советов по эффективному использованию вторичных индексов.
Первый совет кажется очевидным: вторичные индексы можно использовать только для чтения, поэтому вам следует стремиться к тому, чтобы в ваших вторичных индексах были шаблоны, доступные только для чтения! И тем не менее, я постоянно вижу эту ошибку. Разработчики сначала прочитают данные из вторичного индекса, а затем запишут в основную таблицу. Это приводит к дополнительным затратам и дополнительным задержкам, и часто этого можно избежать, если заранее спланировать ситуацию.
Если вы читали что-нибудь о моделировании данных DynamoDB, вы, вероятно, знаете, что сначала вам следует подумать о шаблонах доступа. Это не похоже на реляционную базу данных, где вы сначала проектируете нормализованные таблицы, а затем пишете запросы для их объединения. В DynamoDB вам следует подумать о действиях, которые будет выполнять ваше приложение, а затем спроектировать таблицы и индексы для поддержки этих действий.
При разработке таблицы я предпочитаю начинать с шаблонов доступа на основе записи. В своих записях я часто придерживаюсь определенного типа ограничений — уникальности имени пользователя или максимального количества участников в группе. Я хочу спроектировать свою таблицу таким образом, чтобы сделать это простым, в идеале без использования транзакций DynamoDB или шаблона чтения-изменения-записи, который может зависеть от условий гонки.
Работая над ними, вы, как правило, обнаружите, что существует «основной» способ идентификации вашего предмета, который соответствует вашим шаблонам письма. В конечном итоге это будет ваш первичный ключ. Затем с помощью вторичных индексов можно легко добавить дополнительные вторичные шаблоны чтения.
В приведенном выше примере с пользователями каждый запрос пользователя, скорее всего, будет включать в себя организацию и имя пользователя. Это позволит мне искать отдельные записи Пользователя, а также разрешать определенные действия Пользователя. Поиск адреса электронной почты может использоваться для менее заметных шаблонов доступа, таких как поток «забыли пароль» или поток «поиск пользователя». Это шаблоны, доступные только для чтения, и они хорошо сочетаются со вторичным индексом.
Второй совет по использованию вторичных индексов — использовать их для изменяемых значений в шаблонах доступа. Давайте сначала поймем причину этого, а затем посмотрим на ситуации, когда это применимо.
DynamoDB позволяет обновлять существующий элемент с помощью операции UpdateItem
. Однако вы не можете изменить первичный ключ элемента в обновлении . Первичный ключ — это уникальный идентификатор элемента, а изменение первичного ключа по сути означает создание нового элемента. Если вы хотите изменить первичный ключ существующего элемента, вам необходимо удалить старый элемент и создать новый. Этот двухэтапный процесс более медленный и дорогостоящий. Часто вам нужно сначала прочитать исходный элемент, а затем использовать транзакцию для удаления исходного элемента и создания нового в том же запросе.
С другой стороны, если у вас есть это изменяемое значение в первичном ключе вторичного индекса, DynamoDB выполнит за вас этот процесс удаления + создания во время репликации. Вы можете отправить простой запрос UpdateItem
чтобы изменить значение, а DynamoDB позаботится обо всем остальном.
Я вижу, что эта закономерность возникает в двух основных ситуациях. Первый и наиболее распространенный вариант — когда у вас есть изменяемый атрибут, по которому вы хотите выполнить сортировку. Каноническими примерами здесь являются таблица лидеров для игры, в которой люди постоянно набирают очки, или для постоянно обновляемого списка элементов, в котором вы хотите отображать в первую очередь самые последние обновленные элементы. Подумайте о чем-то вроде Google Диска, где вы можете сортировать файлы по «последнему изменению».
Второй пример, когда это возникает, — это когда у вас есть изменяемый атрибут, по которому вы хотите фильтровать. Здесь вы можете представить себе интернет-магазин с историей заказов пользователя. Возможно, вы захотите разрешить пользователю фильтровать свои заказы по статусу — показывать мне все мои заказы, которые «отправлены» или «доставлены». Вы можете встроить его в ключ раздела или в начало ключа сортировки, чтобы обеспечить фильтрацию точного соответствия. Когда элемент меняет статус, вы можете обновить атрибут статуса и воспользоваться DynamoDB, чтобы правильно сгруппировать элементы в вторичном индексе.
В обеих этих ситуациях перемещение этого изменяемого атрибута в вторичный индекс сэкономит вам время и деньги. Вы сэкономите время, избежав шаблона чтения-изменения-записи, и сэкономите деньги, избежав дополнительных затрат на запись транзакции.
Кроме того, обратите внимание, что этот узор хорошо сочетается с предыдущим советом. Маловероятно, что вы сможете идентифицировать элемент для записи на основе изменяемого атрибута, такого как его предыдущий балл, предыдущий статус или время последнего обновления. Вместо этого вы обновите более постоянное значение, например идентификатор пользователя, идентификатор заказа или идентификатор файла. Затем вы будете использовать вторичный индекс для сортировки и фильтрации на основе изменяемого атрибута.
Выше мы видели, что DynamoDB делит ваши данные на разделы на основе первичного ключа. DynamoDB стремится сохранять эти разделы небольшими — 10 ГБ или меньше — и вам следует стремиться распределять запросы по разделам, чтобы получить преимущества масштабируемости DynamoDB.
Обычно это означает, что вам следует использовать значение высокой мощности в ключе раздела. Подумайте о чем-то вроде имени пользователя, идентификатора заказа или идентификатора датчика. Для этих атрибутов имеется большое количество значений, и DynamoDB может распределять трафик по вашим разделам.
Часто я вижу, как люди понимают этот принцип в своей основной таблице, но затем совершенно забывают об этом в своих вторичных индексах. Часто им нужно заказать определенный тип товара по всей таблице. Если они хотят получить пользователей в алфавитном порядке, они будут использовать вторичный индекс, в котором все пользователи имеют USERS
в качестве ключа раздела и имя пользователя в качестве ключа сортировки. Или, если они хотят упорядочить самые последние заказы в интернет-магазине, они будут использовать вторичный индекс, в котором все заказы имеют ORDERS
в качестве ключа раздела и метку времени в качестве ключа сортировки.
Этот шаблон может работать для приложений с небольшим трафиком, где вы не сможете приблизиться к ограничениям пропускной способности раздела DynamoDB , но это опасный шаблон для приложений с высоким трафиком. Весь ваш трафик может быть направлен в один физический раздел, и вы можете быстро достичь предела пропускной способности записи для этого раздела.
Кроме того, что наиболее опасно, это может вызвать проблемы для вашей основной таблицы. Если ваш вторичный индекс регулируется записью во время репликации, очередь репликации будет резервной копией. Если эта очередь резервирует слишком много данных, DynamoDB начнет отклонять записи в вашу основную таблицу.
Это сделано, чтобы помочь вам: DynamoDB хочет ограничить устаревание вашего вторичного индекса, поэтому он не позволит вам использовать вторичный индекс с большой задержкой. Однако может возникнуть неожиданная ситуация, которая может возникнуть тогда, когда вы меньше всего этого ожидаете.
Люди часто думают о вторичных индексах как о способе репликации всех своих данных с помощью нового первичного ключа. Однако вам не обязательно, чтобы все ваши данные попали во вторичный индекс. Если у вас есть элемент, который не соответствует схеме ключей индекса, он не будет реплицирован в индекс.
Это может быть очень полезно для предоставления глобального фильтра ваших данных. Канонический пример, который я использую для этого, — это почтовый ящик для сообщений. В вашей основной таблице вы можете хранить все сообщения для конкретного пользователя, упорядоченные по времени их создания.
Но если вы похожи на меня, у вас в почтовом ящике много сообщений. Кроме того, вы можете рассматривать непрочитанные сообщения как список дел, как небольшие напоминания о необходимости вернуться к кому-то. Соответственно, я обычно хочу видеть в своем почтовом ящике только непрочитанные сообщения.
Вы можете использовать свой вторичный индекс для предоставления этого глобального фильтра, где unread == true
. Возможно, ваш ключ раздела вторичного индекса — это что-то вроде ${userId}#UNREAD
, а ключ сортировки — это временная метка сообщения. При первоначальном создании сообщения оно будет включать значение ключа раздела вторичного индекса и, таким образом, будет реплицировано во вторичный индекс непрочитанных сообщений. Позже, когда пользователь прочитает сообщение, вы можете изменить status
на READ
и удалить значение ключа раздела вторичного индекса. DynamoDB затем удалит его из вторичного индекса.
Я использую этот трюк постоянно, и он удивительно эффективен. Кроме того, разреженный индекс сэкономит вам деньги. Любые обновления для чтения сообщений не будут реплицироваться во вторичный индекс, и вы сэкономите на затратах на запись.
В качестве последнего совета давайте разовьем предыдущий пункт немного дальше. Мы только что увидели, что DynamoDB не включит элемент в ваш вторичный индекс, если у этого элемента нет элементов первичного ключа для индекса. Этот трюк можно использовать не только для первичных ключевых элементов, но и для неключевых атрибутов данных!
При создании вторичного индекса вы можете указать, какие атрибуты из основной таблицы вы хотите включить во вторичный индекс. Это называется проекцией индекса. Вы можете включить все атрибуты из основной таблицы, только атрибуты первичного ключа или подмножество атрибутов.
Хотя заманчиво включить все атрибуты во вторичный индекс, это может оказаться дорогостоящей ошибкой. Помните, что каждая запись в вашу основную таблицу, которая изменяет значение прогнозируемого атрибута, будет реплицирована в ваш вторичный индекс. Единственный вторичный индекс с полной проекцией эффективно удваивает затраты на запись для вашей таблицы. Каждый дополнительный вторичный индекс увеличивает затраты на запись на 1/N + 1
, где N
— количество вторичных индексов перед новым.
Кроме того, ваши затраты на запись рассчитываются в зависимости от размера вашего предмета. Каждый 1 КБ данных, записываемых в вашу таблицу, использует WCU. Если вы копируете элемент размером 4 КБ в свой вторичный индекс, вы будете платить полные 4 WCU как за основную таблицу, так и за вторичный индекс.
Таким образом, есть два способа сэкономить деньги, сузив прогнозы вторичного индекса. Во-первых, вы можете вообще избежать определенных операций записи. Если у вас есть операция обновления, которая не затрагивает какие-либо атрибуты в проекции вторичного индекса, DynamoDB пропустит запись в ваш вторичный индекс. Во-вторых, для тех операций записи, которые реплицируются в ваш вторичный индекс, вы можете сэкономить деньги, уменьшив размер реплицируемого элемента.
Это может быть непростой баланс, чтобы найти правильный баланс. Прогнозы вторичного индекса не подлежат изменению после создания индекса. Если вы обнаружите, что вам нужны дополнительные атрибуты во вторичном индексе, вам нужно будет создать новый индекс с новой проекцией, а затем удалить старый индекс.
Теперь, когда мы изучили некоторые практические советы по вторичным индексам, давайте сделаем шаг назад и зададим более фундаментальный вопрос: стоит ли вообще использовать вторичный индекс?
Как мы видели, вторичные индексы помогают вам получить доступ к вашим данным другим способом. Однако это происходит за счет дополнительных операций записи. Таким образом, мое практическое правило для вторичных индексов таково:
Используйте вторичные индексы, когда снижение затрат на чтение перевешивает увеличение затрат на запись.
Когда вы это говорите, это кажется очевидным, но при моделировании это может показаться нелогичным. Кажется, так легко сказать: «Добавьте это во вторичный индекс», не думая о других подходах.
Чтобы лучше понять это, давайте рассмотрим две ситуации, когда вторичные индексы могут не иметь смысла.
При использовании DynamoDB вы обычно хотите, чтобы первичные ключи выполняли фильтрацию за вас. Меня немного раздражает, когда я использую запрос в DynamoDB, а затем выполняю собственную фильтрацию в своем приложении — почему я не могу просто встроить ее в первичный ключ?
Несмотря на мою интуитивную реакцию, в некоторых ситуациях вам может потребоваться перечитать данные, а затем отфильтровать их в приложении.
Чаще всего это происходит, когда вы хотите предоставить пользователям множество различных фильтров к вашим данным, но соответствующий набор данных ограничен.
Подумайте о трекере тренировок. Возможно, вы захотите разрешить пользователям фильтровать множество атрибутов, таких как тип тренировки, интенсивность, продолжительность, дата и т. д. Тем не менее, количество тренировок у пользователя будет управляемым — даже опытному пользователю потребуется некоторое время, чтобы превысить 1000 тренировок. Вместо того, чтобы индексировать все эти атрибуты, вы можете просто получить все тренировки пользователя, а затем отфильтровать их в своем приложении.
Здесь я рекомендую заняться математикой . DynamoDB позволяет легко рассчитать эти два варианта и понять, какой из них лучше подойдет для вашего приложения.
Давайте немного изменим нашу ситуацию: что, если наша коллекция предметов большая? Что, если мы создаем трекер тренировок для тренажерного зала и хотим, чтобы владелец тренажерного зала фильтровал все упомянутые выше атрибуты для всех пользователей в тренажерном зале ?
Это меняет ситуацию. Сейчас мы говорим о сотнях или даже тысячах пользователей, у каждого из которых сотни или тысячи тренировок. Нет смысла перечитывать всю коллекцию элементов и выполнять апостериорную фильтрацию результатов.
Но вторичные индексы и здесь не имеют смысла. Вторичные индексы хороши для известных шаблонов доступа, где вы можете рассчитывать на наличие соответствующих фильтров. Если мы хотим, чтобы наш владелец тренажерного зала мог фильтровать различные атрибуты, все из которых являются необязательными, нам нужно будет создать большое количество индексов, чтобы это работало.
Ранее мы говорили о возможных недостатках планировщиков запросов, но у планировщиков запросов есть и положительные стороны. Помимо обеспечения более гибких запросов, они также могут выполнять такие действия, как пересечение индексов, чтобы просматривать частичные результаты из нескольких индексов при составлении этих запросов. Вы можете сделать то же самое с DynamoDB, но это приведет к большому количеству перемещений по вашему приложению, а также к некоторой сложной логике приложения, чтобы разобраться в этом.
Когда у меня возникают проблемы такого типа, я обычно ищу инструмент, лучше подходящий для этого случая использования. Rockset и Elasticsearch — мои основные рекомендации для обеспечения гибкой фильтрации по принципу вторичного индекса в вашем наборе данных.
В этом посте мы узнали о вторичных индексах DynamoDB. Сначала мы рассмотрели некоторые концептуальные моменты, чтобы понять, как работает DynamoDB и почему необходимы вторичные индексы. Затем мы рассмотрели несколько практических советов, которые помогут понять, как эффективно использовать вторичные индексы, и изучить их особенности. Наконец, мы рассмотрели, как следует относиться к вторичным индексам, чтобы понять, когда следует использовать другие подходы.
Вторичные индексы — мощный инструмент в вашем наборе инструментов DynamoDB, но они не панацея. Как и при любом моделировании данных DynamoDB, прежде чем приступить к работе, обязательно тщательно продумайте свои схемы доступа и подсчитайте затраты.
Узнайте больше о том, как можно использовать Rockset для фильтрации по вторичному индексу, в блоге Алекса ДеБри «Фильтрация и агрегирование запросов DynamoDB с использованием SQL в Rockset» .