Введение в ReQL
ReQL — это язык запросов RethinkDB. Он предлагает очень мощный и удобный способ обработки JSON-документов. Этот документ — введение в концепции ReQL. Вам не обязательно его читать, чтобы продуктивно работать с RethinkDB, но понимание основных принципов поможет.
Хотите сразу писать полезные запросы? Обратитесь к десятиминутной инструкции.
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, от фильтров до обновлений и объединений таблиц, выполняется путём вызова соответствующих методов.
Преимущества этого подхода:
- Вы можете использовать ту же среду программирования и инструменты, к которым вы уже привыкли.
- Изучение языка ничем не отличается от изучения любой другой библиотеки.
- Вероятность возникновения проблем безопасности, связанных с инъекциями строк, минимальна.
Все запросы 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/