Избранное | Русский | Войти

Запросы и индексы

Каждый запрос к хранилищу данных использует индекс – таблицу, которая содержит результаты запроса, расположенные в необходимом порядке. Приложение App Engine определяет свои индексы в файле конфигурации index.yaml. Веб-сервер разработки автоматически добавляет в этот файл записи, когда сталкивается с запросами, индексы для которых еще не настроены. Индексы можно настраивать вручную, редактируя файл перед загрузкой приложения.

Механизм запросов, основанных на индексах, поддерживает наиболее распространенные типы запросов, но может не поддерживать некоторые типы запросов из других технологий баз данных, которые могут быть вам привычны. Ограничения, применяемые к запросам, вместе с соответствующими пояснениями перечислены ниже.

Запросы: введение

Запрос извлекает из хранилища данных объекты, удовлетворяющие заданному набору условий. В запросе указывается тип объекта, ноль или более условий, описывающих значения свойств объекта и иногда называемых "фильтрами", а также ноль или более описаний порядков сортировки. При выполнении запрос извлекает все объекты указанного типа, удовлетворяющие указанным условиям и отсортированные в заданном порядке.

API хранилища данных Python предоставляет два интерфейса для подготовки и выполнения запросов: интерфейс Query, использующий методы для подготовки запросов, и интерфейс GqlQuery, использующий язык запросов GQL, подобный SQL, для подготовки запросов из строк запросов. Эти интерфейсы подробно описаны в статье Создание, получение и удаление данных: получение объектов с помощью запроса и на соответствующих страницах справочного руководства.

class Person(db.Model):
  first_name = db.StringProperty()
  last_name = db.StringProperty()
  city = db.StringProperty()
  birth_year = db.IntegerProperty()
  height = db.IntegerProperty()

# The Query interface prepares a query using instance methods.
q = Person.all()
q.filter("last_name =", "Smith")
q.filter("height <", 72)
q.order("-height")

# The GqlQuery interface prepares a query using a GQL query string.
q = db.GqlQuery("SELECT * FROM Person " + 
                "WHERE last_name = :1 AND height < :2 " +
                "ORDER BY height DESC",
                "Smith", 72)

# The query is not executed until results are accessed.
results = q.fetch(5)
for p in results:
  print "%s %s, %d inches tall" % (p.first_name, p.last_name, p.height)

Фильтр включает имя свойства, оператор сравнения и значение. Объект соответствует фильтру, если имеет свойство с заданным именем, а его значение совпадает с заданным значением, как указано оператором. Объект является результатом запроса, если соответствует всем фильтрам.

Оператор фильтра может быть одним из следующих:

  • < меньше
  • <= меньше или равно
  • = равно
  • > больше
  • >= больше или равно
  • != не равно
  • IN соответствует любому значению в указанном списке

Оператор != выполняет 2 запроса: в одном запросе все остальные фильтры одинаковые, а фильтр неравенства заменен фильтром "меньше", а во втором запросе фильтр неравенства заменен фильтром "больше". Результаты объединяются по порядку. Как описано ниже в описании фильтров неравенства, запрос может иметь только один фильтр неравенства.

Оператор IN также выполняет несколько запросов, один для всех элементов в указанном значении списка, где все остальные фильтры одинаковые, а фильтр IN заменен фильтром равенства. Результаты объединяются в порядке элементов списка. Если в запросе несколько фильтров IN, запрос выполняется как несколько запросов, каждый для отдельной комбинации значений в фильтрах IN.

Один запрос, содержащий операторы != и IN, ограничен 30 подзапросами.

Индексы: введение

Хранилище данных App Engine поддерживает индекс для каждого запроса, который собирается выполнить приложение. Как только приложение изменяет объекты хранилища данных, хранилище обновляет индексы, корректируя результаты. Когда приложение выполняет запрос, хранилище данных получает результаты напрямую из соответствующего индекса.

У приложения есть индекс для каждой комбинации типа, оператора фильтра, свойства фильтра и порядка сортировки, указанных в запросе. Рассмотрим запрос, написанный на GQL:

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                  ORDER BY height DESC

Индекс для этого запроса представляет собой таблицу ключей объектов типа Person, содержащую столбцы значений свойств height и last_name. Индекс отсортирован в убывающем порядке по свойству height.

Для двух запросов одинаковой формы, но с разными значениями фильтров используется один и тот же индекс. Например, для следующего запроса используется тот же индекс, что и для запроса выше:

SELECT * FROM Person WHERE last_name = "Jones"
                       AND height < 63
                     ORDER BY height DESC

Хранилище данных выполняет запрос следующим образом:

  1. Хранилище данных определяет индекс, который соответствует типу запроса, свойствам и операторам фильтров, а также порядкам сортировки.
  2. Хранилище данных начинает сканировать индекс с первого объекта, который удовлетворяет всем условиям фильтров, используя значения фильтров запроса.
  3. Хранилище данных продолжает сканировать индекс, возвращая все объекты до тех пор, пока не найдет следующий объект, не удовлетворяющий условиям фильтров. Этот процесс продолжается, пока не будет достигнут конец индекса или собрано максимально разрешенное количество результатов.

В таблице индекса содержатся столбцы для каждого свойства, используемого в фильтре или порядке сортировки. Строки отсортированы по следующим критериям в указанном порядке:

  • родители;
  • значения свойств, используемых в фильтрах равенства;
  • значения свойств, используемых в фильтрах неравенства;
  • значения свойств, используемых в порядках сортировки.

Примечание. В индексах фильтры IN обрабатываются как фильтры "=", а фильтры "!=" – как другие фильтры неравенства.

Это позволяет расположить результаты всевозможных запросов, использующих данный индекс, в последовательных строках таблицы.

Совет. Явно задать частичное совпадение со строковым значением в фильтрах запроса нельзя, но можно сымитировать префиксное совпадение с помощью фильтров неравенства:

db.GqlQuery("SELECT * FROM MyModel WHERE prop >= :1 AND prop < :2", "abc", u"abc" + u"\ufffd")

Такой запрос соответствует всем объектам MyModel со строковым свойством prop, которое начинается с символов abc. Строка u"\ufffd" описывает наибольший возможный символ Unicode. Когда значения свойств в индексе отсортированы, в данном диапазоне окажутся значения, которые начинаются с указанного префикса.

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

Запрос не возвращает объекты, у которых нет свойства, включенного в фильтр

В индексе содержатся только объекты, обладающие всеми свойствами, на которые он ссылается. Если у объекта нет свойства, на которое ссылается индекс, этот объект не появится в индексе и не сможет быть результатом запроса, использующего данный индекс.

Обратите внимание, что хранилище данных App Engine различает объекты, которые не обладают свойством, и объекты, которые обладают свойством с нулевым значением (None). Чтобы каждый объект определенного типа мог стать потенциальным результатом выполнения запроса, можно использовать модель данных, которая присваивает свойствам, используемым в фильтрах запросов, значения по умолчанию (например, None).

Текстовые и большие двоичные значения не индексируются

Свойства, имеющие значения типа Text (большие текстовые значения) или Blob (большие двоичные значения), не включаются в индексы и не обнаруживаются запросами.

Вследствие того, что такие значения свойств не включаются в индекс, запрос, содержащий фильтр или порядок сортировки по некоторому свойству, никогда не вернет объект, у которого это свойство имеет значение типа Text или Blob. Свойства с такими значениями обрабатываются так, как если бы их значения не были заданы относительно фильтров и порядков сортировки запроса.

Значения свойств, имеющие смешанный тип, упорядочиваются по типу

Если у двух объектов есть свойства с одинаковыми названиями, но разными типами значений, индекс этого свойства сортирует объекты сначала по типу значений, а затем в соответствующем каждому типу порядке. Например, если у двух объектов есть свойство "age", у одного с целочисленным значением, а у второго – со строковым, при сортировке по свойству "Age" объект с целочисленным значением всегда будет появляться прежде, чем объект со строковым, вне зависимости от самих значений.

Это особенно важно помнить при работе с целыми числами и числами с плавающей точкой, которые хранилище данных считает разными типами. Свойство с целым значением 38 после сортировки окажется раньше по порядку, чем значение с плавающей точкой 37.5, поскольку значения типа integer всегда выводятся раньше значений типа float.

Определение индексов в файле конфигурации

App Engine создает индексы для ряда простых запросов по умолчанию. Индексы, необходимые для других запросов, приложение должно указывать в файле конфигурации index.yaml. Если приложение, работающее под управлением App Engine, пытается выполнить запрос, для которого отсутствует соответствующий индекс (заданный по умолчанию или указанный в файле index.yaml), то этот запрос не будет выполнен.

App Engine автоматически создает индексы для следующих форм запросов:

  • запросы, использующие только фильтры равенства и родителей;
  • запросы, использующие только фильтры неравенства (которые можно задать только для одного свойства);
  • запросы с единственным порядком сортировки по свойству (по возрастанию либо по убыванию).

Для других форм запросов индексы необходимо указывать в файле index.yaml. К ним относятся:

  • запросы с несколькими порядками сортировки;
  • запросы с сортировкой по ключам в порядке убывания;
  • запросы с одним или несколькими фильтрами неравенства для некоторого свойства и одним или несколькими фильтрами равенства для других свойств;
  • запросы с фильтрами неравенства и родителей.

Веб-сервер разработки облегчает управление конфигурацией индексов: вместо отказа выполнить запрос, для которого требуется отсутствующий индекс, он может создать конфигурацию индекса, которая позволит успешно выполнить запрос. Если во время локального тестирования приложения вызываются все запросы, которые может выполнять приложение (все комбинации типа, родителя, фильтра и порядка сортировки), созданные записи будут представлять собой полный набор индексов. Если во время тестирования были выполнены не все возможные формы запросов, перед загрузкой приложения просмотрите и настройте конфигурацию индексов.

По умолчанию App Engine создает индексы для ряда простых запросов. Для других запросов приложение должно указывать необходимые индексы в файле конфигурации index.yaml. Если приложение, работающее в App Engine, пытается выполнить запрос, для которого не указан соответствующий индекс (заданный по умолчанию или указанный в index.yaml), то этот запрос не будет выполнен.

index.yaml описывает все таблицы индексов, в том числе тип, свойства, необходимые для фильтров запросов и порядков сортировки, указание того, использует ли запрос материнский оператор (оператор Query.ancestor() или GQL ANCESTOR IS). Свойства отсортированы в указанном порядке: сначала свойства, используемые в фильтрах равенства или IN, затем свойства, используемые в фильтрах неравенства, затем порядки сортировки результатов запросов и их направление.

Рассмотрите еще раз следующий пример запроса:

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                     ORDER BY height DESC

Если приложение выполняет только этот запрос (и возможно другие запросы, подобные этому с другими значениями "Smith" и 72), файл index.yaml будет выглядеть следующим образом:

indexes:
- kind: Person
  properties:
  - name: last_name
  - name: height
    direction: desc

Когда объект создается или изменяется, обновляются и соответствующие индексы. Количество индексов, соответствующих объекту, влияет на время, необходимое для создания или изменения объекта.

Дополнительную информацию по синтаксису index.yaml можно найти в Настройке индексов.

Запросы по ключам

Ключи объектов могут быть объектом фильтра запроса или порядке сортировки, используя специальное название __key__ вместо имени свойства. Хранилище данных получает полное значение ключа для таких запросов, включая путь родителя объекта, тип и назначенную приложением строку имени ключа или назначенный системой числовой идентификатор.

Поскольку ключ объекта уникален среди всех объектов в системе, запросы __key__ упрощают получение объектов определенного типа пакетами, например, пакетный дамп содержания хранилища данных. В отличие от смещения, это эффективно работает для любого количества объектов. Например:

class MainHandler(webapp.RequestHandler):
  def get(self):
    query = Entity.gql('ORDER BY __key__')

    # Use a query parameter to keep track of the last key of the last
    # batch, to know where to start the next batch.
    last_key_str = self.request.get('last')
    if last_key_str:
      last_key = db.Key(last_key_str)
      query = Entity.gql('WHERE __key__ > :1 ORDER BY __key__', last_key)

    # For batches of 20, fetch 21, then use result #20 as the "last"
    # if there is a 21st.
    entities = query.fetch(21)
    new_last_key_str = None
    if len(entities) == 21:
      new_last_key_str = str(entities[19].key())

    # Return the data and new_last_key_str.  Client would use
    # http://...?last=new_last_key_str to fetch the next batch.
    # ...

Упорядочивание ключей происходит в следующей очередности: по пути к родителю, по типу, а затем по идентификатору или названию ключа. Типы и названия ключей являются строками и упорядочиваются по байтовому значению. Идентификаторы являются целыми числами и упорядочиваются численно. Если объекты с одинаковым родителем и типом используют как строковые названия ключей, так и числовые идентификаторы, считается, что объекты с числовыми идентификаторами меньше, чем объекты со строковыми названиями ключей. Аналогично сравниваются и элементы пути к родителю: по типу (строка), затем по названию ключа (строка) или идентификатору (число).

Запросы, содержащие ключи, используют индексы так же, как запросы, содержащие свойства, с одним небольшим отличием: запрос с фильтром равенства по ключу, в котором есть и другие фильтры, должен использовать пользовательский индекс, определяемый в файле конфигурации индекса приложения. Как и для других запросов, при проверке запроса, которому требуется пользовательский индекс, веб-сервер разработки делает в этом файле соответствующие записи о конфигурации.

Ограничения, применяемые к запросам

Природа механизма запросов с использованием индексов налагает на действия запросов ряд ограничений.

Для фильтрации или сортировки по свойству требуется существование этого свойства

Условие фильтра запроса или порядок сортировки по свойству также требует, чтобы для данного свойства объекта было задано значение.

Объект хранилища данных необязательно должен иметь значение для свойства, для которого есть значения у других объектов того же типа. Фильтр по свойству применяется только к тем объектам, для которых значение этого свойства задано. Объекты, для которых значение свойства, используемого в фильтре или порядке сортировки, не задано, исключаются из индекса, создаваемого для запроса.

Невозможно отфильтровать объекты, у которых нет свойства

Выполнить запрос для объектов, у которых отсутствует заданное свойство, невозможно. Вариантом решения является создание фиксированного (смоделированного) свойства со значением по умолчанию None с последующей фильтрацией объектов со значением свойства None.

Можно использовать фильтры неравенства только по одному свойству

Запрос может использовать фильтры неравенства (<, <=, >=,>, !=) только по одному свойству среди всех своих фильтров.

Например, следующий запрос допустим:

SELECT * FROM Person WHERE birth_year >= :min
                       AND birth_year <= :max

Однако следующий запрос недопустим, так как он использует фильтры неравенства по двум различным свойствам одновременно:

SELECT * FROM Person WHERE birth_year >= :min_year
                       AND height >= :min_height     # ERROR

Фильтры могут сочетать операторы равенства (=) для различных свойств в одном запросе, в том числе в запросах с одним или несколькими условиями неравенства для свойства. Вот пример допустимого запроса:

SELECT * FROM Person WHERE last_name = :last_name
                       AND city = :city
                       AND birth_year >= :min_year

Механизм запросов предполагает, что все результаты запроса расположены в таблице индекса подряд, во избежание необходимости сканировать всю таблицу результатов. Одна таблица индекса не может содержать несколько фильтров неравенства по разным свойствам, одновременно обеспечивая расположение результатов подряд.

Свойства в фильтрах неравенства должны быть отсортированы перед применением других порядков сортировки

Если запрос содержит фильтр неравенства и один или несколько порядков сортировки, в нем также должна выполняться сортировка по свойству, используемому в неравенстве, причем перед сортировкой по другим свойствам.

Следующий запрос недопустим, так как он использует фильтр неравенства и не выполняет сортировку по используемому в этом фильтре свойству:

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name              # ERROR

Недопустим и следующий запрос, так как сортировка по используемому в фильтре свойству не выполняется перед сортировкой по другим свойствам:

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY last_name, birth_year  # ERROR

Следующий запрос допустим:

SELECT * FROM Person WHERE birth_year >= :min_year
                     ORDER BY birth_year, last_name

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

Порядки сортировки и свойства с несколькими значениям

При индексации свойств, имеющих несколько значений, используется необычный порядок сортировки:

  • Если объекты сортируются по свойству с несколькими значениями в возрастающем порядке, для сортировки используется наименьшее из значений.
  • Если объекты сортируются по свойству с несколькими значениями в убывающем порядке, для сортировки используется наибольшее из значений.
  • Величина и количество остальных значений на сортировку не влияют.
  • При наличии связи ключ объекта используется для ее разрыва.

Такой порядок сортировки имеет необычное следствие: значения [1, 9] будут располагаться перед значениями [4, 5, 6, 7] и при сортировке по возрастанию, и по убыванию.

Следует сделать важное предупреждение относительно запросов, содержащих как фильтр равенства, так и порядок сортировки по свойству с несколькими значениями. В таких запросах порядок сортировки не учитывается. Для свойств с одним значением это обеспечивает простую оптимизацию. У всех результатов значение свойства будет одинаковым, поэтому необходимости дополнительно их сортировать нет.

Однако при наличии у свойства дополнительных значений могут возникать трудности. Так как указанная сортировка не применяется, результаты запроса могут возвращаться в другом порядке, чем необходимо. (Восстановление проигнорированного порядка сортировки требует лишних затрат и дополнительных индексов. Описанный случай возникает редко, поэтому планировщик запросов не рассматривает его.)

Большие объекты и разрастающиеся индексы

Как описано выше, каждое свойство (значение которого имеет тип, отличный от Text или Blob) каждого объекта добавляется по крайней мере в одну таблицу индекса, в том числе в простой индекс, задаваемый по умолчанию, и любые другие индексы, описанные в файле приложения index.yaml и ссылающиеся на данное свойство. Для объекта, каждое свойство которого имеет только одно значение, App Engine сохраняет значение свойства в своем простом индексе единожды, а в пользовательском индексе столько раз, сколько существует обращений к данному свойству. Каждую из таких записей в индексах необходимо обновлять после каждого изменения значения свойства, поэтому чем больше индексов ссылаются на свойство, тем больше потребуется времени на его обновление.

Чтобы избежать длительного обновления объекта, хранилище данных ограничивает количество записей индекса, относящихся к одному объекту. Это ограничение достаточно велико, поэтому на работу большинства приложений оно не повлияет. Однако при некоторых обстоятельствах вы можете испытать действие данного ограничения. Например, ограничение на количество записей в индексе может превысить объект, имеющий очень много свойств с одним значением.

Каждое значение свойств, имеющих несколько значений, сохраняется в отдельной записи индекса. Объект, имеющий единственное свойство с очень большим количеством значений может превысить ограничение на количество записей в индексе.

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

Например, следующий индекс (использован синтаксис index.yaml) содержит свойства x и y объектов типа MyModel:

indexes:
- kind: MyModel
  properties:
  - name: x
  - name: y

В следующем фрагменте кода создается объект с двумя значениями свойства x и двумя значениями свойства y:

class MyModel(db.Expando):
  pass

e2 = MyModel()
e2.x = ['red', 'blue']
e2.y = [1, 2]
e2.put()

Чтобы точно представить эти значения, нужно сохранить в индексе 12 значений свойств: два для встроенных индексов по свойствам x и y и по два для каждой из четырех комбинаций свойств x и y в пользовательском индексе. Если свойство имеет много значений, это значит, что в индексе необходимо хранить слишком много записей для одного объекта. Индекс, который ссылается на несколько свойств с несколькими значениями, можно назвать "разрастающимся", так как он может стать очень большим при добавлении всего нескольких значений.

Если вызов метода put() приведет к превышению ограничения на количество записей, возникнет ошибка и будет вызвано исключение. Если при построении нового индекса количество записей в нем превысит заданное ограничение для какого-либо объекта, запросы по данному индексу не будут выполняться, а сам индекс появится в консоли администрирования с состоянием "Error".

Чтобы обработать индексы в состоянии "Error", удалите их из файла index.yaml и выполните команду appcfg.py vacuum_indexes. Затем измените определение индекса и соответствующие запросы либо удалите объекты, приводящие к разрастанию индекса. Наконец, добавьте индекс обратно в файл index.yaml и выполните команду appcfg.py update_indexes.

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