Введение в ReQL
ReQL — это язык запросов RethinkDB. Он предлагает очень мощный и удобный способ манипулирования JSON-документами. Этот документ — введение в концепции ReQL. Вам не обязательно его читать, чтобы эффективно работать с RethinkDB, но понимание некоторых основ поможет.
Хотите сразу писать полезные запросы? Прочитайте краткое руководство (10 минут).
ReQL отличается от других языков запросов NoSQL. Он построен на трёх ключевых принципах:
- ReQL интегрируется в ваш язык программирования. Запросы строятся путём вызова функций на языке программирования, с которым вы уже знакомы. Вам не нужно конкатенировать строки или создавать специализированные JSON-объекты для запроса к базе данных.
-
Все запросы ReQL могут быть объединены. Вы начинаете с таблицы и последовательно добавляете преобразователи в конец запроса, используя оператор
.. -
Все запросы выполняются на сервере. Хотя запросы строятся на клиенте на знакомом языке программирования, они полностью выполняются на сервере базы данных после вызова команды
runи передачи активного подключения к базе данных.
Давайте подробнее рассмотрим эти концепции.
Примечание: в следующих примерах используется драйвер Python, но большинство из них также применимы к драйверам RethinkDB для других языков.
ReQL интегрируется в ваш язык программирования
Вы начинаете использовать ReQL в своей программе аналогично тому, как вы используете другие базы данных:
from rethinkdb import RethinkDB # import the RethinkDB package
r = RethinkDB() # create a RethinkDB object
conn = r.connect() # connect to the server on localhost and default port
Но на этом сходство заканчивается. Вместо того, чтобы создавать строки и передавать их на сервер базы данных, вы получаете доступ к ReQL, используя методы из пакета rethinkdb.
r.table_create('users').run(conn) # create a table `users`
r.table('users').run(conn) # get an iterable cursor to the `users` table
Каждый запрос ReQL, от фильтров до обновлений и объединения таблиц, выполняется путём вызова соответствующих методов.
Преимущества этого подхода:
- Вы можете использовать ту же среду программирования и инструменты, к которым уже привыкли.
- Обучение языку ничем не отличается от обучения любому другому API.
- Вероятность возникновения проблем с безопасностью, связанных с атаками типа "внедрение SQL-кода", минимальна.
Все запросы ReQL могут быть объединены
В ReQL вы можете объединять команды в конце других команд с помощью оператора .:
# Get an iterable cursor to the `users` table (we've seen this above)
r.table('users').run(conn)
# Return only the `last_name` field of the documents
r.table('users').pluck('last_name').run(conn)
# Get all the distinct last names (remove duplicates)
r.table('users').pluck('last_name').distinct().run(conn)
# Count the number of distinct last names
r.table('users').pluck('last_name').distinct().count().run(conn)
Практически все операции ReQL можно объединять. Вы можете рассматривать оператор . аналогично тому, как вы рассматриваете конвейер Unix. Вы выбираете данные из таблицы и передаёте их в команду, которая их преобразует. Вы можете продолжать цепочку преобразований до тех пор, пока ваш запрос не будет выполнен. В ReQL данные передаются слева направо.
Даже если у вас есть кластер узлов RethinkDB, вы можете отправлять свои запросы на любой узел, и кластер создаст и выполнит распределённые программы, которые получат данные с соответствующих узлов, выполнят необходимые вычисления и предоставят вам окончательные результаты, не беспокоясь об этом.
Преимущества этого подхода:
- Язык легко изучать, читать и модифицировать.
- Это естественный и удобный способ выражения запросов.
- Вы можете создавать запросы поэтапно, комбинируя преобразования и проверяя промежуточные результаты.
ReQL эффективен
Выполнение на сервере
Хотя запросы строятся на клиенте, они отправляются на сервер только при вызове команды run. Все вычисления происходят на сервере — запросы не выполняются на клиенте и не требуют промежуточных сетевых обменов между клиентом и сервером. Например, вы можете сохранять запросы в переменных и отправлять их на сервер позже:
# Create the query to get distinct last names
distinct_lastnames_query = r.table('users').pluck('last_name').distinct()
# Send it to the server and execute
distinct_lastnames_query.run(conn)
Подробнее о реализации этой технологии можно прочитать на сайте разработчиков.
Ленивость
Запросы ReQL выполняются лениво:
# Get up to five user documents that have the `age` field defined
r.table('users').has_fields('age').limit(5).run(conn)
Для этого запроса RethinkDB выполнит достаточную работу для получения пяти документов и остановится, когда запрос будет удовлетворён. Даже если у вас нет ограничения на количество запросов, но используется курсор, RethinkDB выполнит ровно столько работы, чтобы вы могли прочитать запрашиваемые данные. Это позволяет запросам выполняться быстро, не тратя ресурсы процессора, полосу пропускания сети и ввод/вывод с диска.
Как и большинство систем баз данных, ReQL поддерживает первичные и вторичные индексы для эффективного доступа к данным. Вы также можете создавать составные индексы и индексы на основе произвольных выражений ReQL для ускорения сложных запросов.
Узнайте, как использовать первичные и вторичные индексы в RethinkDB.
Параллелизм
Все запросы ReQL автоматически максимально параллелизуются на сервере RethinkDB. При возможности выполнение запросов распределяется по ядрам процессора, серверам в кластере и даже по нескольким дата-центрам. Если у вас есть сложные запросы, требующие нескольких этапов обработки, RethinkDB автоматически разделит их на этапы, выполнит каждый этап параллельно и объединит данные, чтобы вернуть полный результат.
Оптимизация запросов
Хотя в RethinkDB пока нет полностью реализованной системы оптимизации запросов, ReQL разработан с учётом её потенциального внедрения. Например, сервер имеет достаточную информацию, чтобы переупорядочить цепочку для повышения эффективности или использовать альтернативные планы реализации для улучшения производительности. Эта функция будет добавлена в будущие версии RethinkDB.
Запросы ReQL функциональны
До сих пор мы видели только простые запросы без условий. ReQL поддерживает знакомый синтаксис для построения более сложных запросов:
# Get all users older than 30
r.table('users').filter(lambda user: user['age'] > 30).run(conn)
# If you'd like to avoid writing lambdas, RethinkDB supports an
# alternative syntax:
r.table('users').filter(r.row['age'] > 30).run(conn)
Этот запрос выглядит точно так же, как любой другой код Python, который вы обычно пишете. Обратите внимание, что RethinkDB выполнит этот запрос на сервере и не выполнит собственный код Python.
Драйверы клиента выполняют значительную работу по анализу кода и его преобразованию в эффективный запрос ReQL, который будет выполняться на сервере:
- Всякий раз, когда это возможно, драйверы клиента используют перегрузку операторов для поддержки выражений, таких как
user['age'] > 30. - Выражение
lambdaвыполняется только один раз на клиенте. Внутренне драйвер передаёт специальный объект в функциюlambda, что позволяет построить представление запроса. Это представление затем отправляется на сервер по сети и оценивается в кластере.
Подробнее о реализации этой технологии можно прочитать на сайте разработчиков.
У этой технологии есть ограничения. Хотя большинство операций позволяют писать знакомый код, вы не можете использовать операции родного языка, которые имеют побочные эффекты (например, print ) или управляющие блоки (например, if и for). Вместо этого вы должны использовать альтернативные команды ReQL:
# WRONG: Get all users older than 30 using the `if` statement
r.table('users').filter(lambda user:
True if user['age'] > 30 else False).run(conn)
# RIGHT: Get all users older than 30 using the `r.branch` command
r.table('users').filter(lambda user:
r.branch(user['age'] > 30, True, False)).run(conn)
Преимущества этого подхода:
- Для большинства запросов вы можете писать знакомый, легко понятный код, не изучая специальные команды.
- Запросы эффективно передаются на сервер (через протокол protobuf) и оцениваются в кластере.
- RethinkDB имеет доступ к структуре запроса, что позволяет использовать методы оптимизации, аналогичные тем, которые доступны в SQL. Эта функция будет добавлена в будущие версии RethinkDB.
Ограничения этой технологии:
- Операции родного языка, имеющие побочные эффекты или управляющие блоки, не могут использоваться внутри
lambda. Подробнее о реализации этого подхода читайте на сайте разработчиков.
Запросы ReQL могут быть составлены
Вы можете объединять несколько запросов ReQL для создания более сложных запросов.
Создание простых команд
Начнём с простого примера. RethinkDB поддерживает выполнение JavaScript на сервере с помощью встроенного движка V8 (конечно же, в защищённом пространстве вне внешних процессов):
# Evaluate a JavaScript expression on the server and get the result
r.js('1 + 1').run(conn)
Поскольку ReQL является составным, вы можете комбинировать команду r.js с любым другим запросом. Например, давайте воспользуемся ею в качестве альтернативы, чтобы получить всех пользователей старше 30 лет:
# Get all users older than 30 (we've seen this above)
r.table('users').filter(lambda user: user['age'] > 30).run(conn)
# Get all users older than 30 using server-side JavaScript
r.table('users').filter(r.js('(function (user) { return user.age > 30; })')).run(conn)
RethinkDB беспрепятственно оценит команду js, обратившись к движку V8 во время оценки запроса filter. Вы можете таким образом объединять большинство запросов в всё более сложные.
Вложенные запросы
Предположим, у нас есть ещё одна таблица authors, и мы хотим получить список авторов, чьи фамилии также есть в таблице users (которую мы видели ранее). Мы можем сделать это, объединив два запроса:
# Find all authors whose last names are also in the `users` table
r.table('authors').filter(lambda author:
r.table('users').pluck('last_name').contains(author.pluck('last_name'))).
run(conn)
Здесь мы используем запрос r.table('users').pluck('last_name') как внутренний запрос в filter, объединяя два запроса для создания более сложного. Даже если у вас есть кластер серверов и обе таблицы authors и users фрагментированы, RethinkDB сделает всё правильно, оценив соответствующие части запроса выше на соответствующих фрагментах, объединит фрагменты данных при необходимости и вернёт полный результат.
Несколько замечаний по поводу этого запроса:
- Мы составляем запрос на клиенте и вызываем
runтолько один раз. Помните, что вы должны вызватьrunтолько один раз для сложного запроса, когда вы готовы к его выполнению.- Вы также можете выполнить этот запрос, используя команду inner_join.
Выражения
Составление запросов не ограничивается простыми командами и внутренними запросами. Вы также можете использовать выражения для выполнения сложных операций. Например, предположим, что мы хотим найти всех пользователей, чья зарплата и бонус не превышают 90 000 долларов, и увеличить их зарплату на 10%:
r.table('users').filter(lambda user: user['salary'] + user['bonus'] < 90000)
.update(lambda user: {'salary': user['salary'] + user['salary'] * 0.1})
Богатый набор команд
В дополнение к командам, описанным здесь, ReQL поддерживает ряд сложных команд, которые можно комбинировать аналогично описанным выше командам. Более подробную информацию см. в следующих документах:
- Узнайте, как использовать map-reduce в RethinkDB.
- Узнайте, как использовать соединения таблиц в RethinkDB.
- Просмотрите справочник API для получения дополнительных команд.
Этот дизайн имеет следующие преимущества:
- В отличие от большинства NoSQL языков, вы можете использовать ReQL для построения запросов произвольной сложности.
- Нет новой синтаксической конструкции или новых команд для сложных запросов. После понимания принципа композиции вы можете писать новые запросы, ничего не изучая.
- Подзапросы могут быть абстрагированы в переменных, что позволяет использовать модульное программирование аналогично большинству других современных языков программирования.
И для интереса, ReQL может выполнять математические операции!
Если вам нужен ещё один калькулятор, ReQL также может с этим справиться!
# Add two plus two
(r.expr(2) + r.expr(2)).run(conn)
# You only need to specify `r.expr` once for the driver to work
(r.expr(2) + 2).run(conn)
# More algebra
(r.expr(2) + 2 / 2).run(conn)
# Logic
(r.expr(2) > 3).run(conn)
# Branches
r.branch(r.expr(2) > 3,
1, # if True, return 1
2 # otherwise, return 2
).run(conn)
# Compute the Fibonacci sequence
r.table_create('fib').run(conn)
r.table('fib').insert([{'id': 0, 'value': 0}, {'id': 1, 'value': 1}]).run(conn)
r.expr([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]).for_each(lambda x:
r.table('fib').insert({'id': x,
'value': (r.table('fib').order_by('id').nth(x - 1)['value'] +
r.table('fib').order_by('id').nth(x - 2)['value'])
})).run(conn)
r.table('fib').order_by('id')['value'].run(conn)
Подробнее
Для получения дополнительной информации о ReQL, ознакомьтесь со следующими ресурсами:
© RethinkDB contributors
Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
https://rethinkdb.com/docs/introduction-to-reql/