Пробуем JSON в Django и PostgreSQL (и сравниваем с MongoDB)
В Django 1.9 будет добавлено поле JSONField, его можно использовать с базой данных PostgreSQL >= 9.4. Давайте попробуем с ним поработать и оценить, насколько оно удобно.
На данный момент доступна альфа версия django 1.9, финальная запланирована на декабрь 2015. Установить альфа версию можно так:
pip install --pre django
Итак представим, что у нас есть интернет магазин, в котором мы предлагаем товары разных типов. Например, ноутбуки и футболки. Очевидно, что у таких товаров будет разный набор параметров: у футболок будет размер, цвет, а у ноутбуков - размер экрана, частота процессора, объем жесткого диска и прочее. Один из подходов для работы с такими данными в SQL - Entity–attribute–value model (EAV).
Но теперь у нас есть JSON, попробуем организовать данные с помощью этого типа.
Создадим простейшую модель для товаров:
from django.db import models
from django.contrib.postgres.fields import JSONField
class Category(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category)
price = models.IntegerField()
attributes = JSONField()
def __str__(self):
return self.name
Как видим, у нас будет несколько общих полей для всех товаров (name, category, price), а также специфичные для каждого наименования атрибуты (attributes) в виде поля JSON.
Создадим несколько объектов:
tshirt = Category.objects.create(name='tshirts')
notebook = Category.objects.create(name='notebook')
# Tshirts
Product.objects.create(name='Silk tshirt', category=tshirt, price=100, attributes={
'colors': ['red', 'black'],
'sizes': ['S', 'M'],
'model': 'polo',
'material': 'silk',
})
Product.objects.create(name='Bamboo tshirt', category=tshirt, price=120, attributes={
'colors': ['white', 'yellow'],
'sizes': ['M', 'L', 'XL'],
'model': 'poet',
'material': 'bamboo',
})
# Notebooks
Product.objects.create(name='MacBook Pro', category=notebook, price=2000, attributes={
'brand': 'Apple',
'screen': 15.0,
'speed': 2200,
'hd': 256,
})
Product.objects.create(name='ATIV Book 9', category=notebook, price=1200, attributes={
'brand': 'Samsung',
'screen': 12.2,
'speed': 2400,
'hd': 128,
})
Запросы
Посмотрим, какие запросы мы можем делать.
-
Получить все футболки размера ‘M’ и ‘L’ (у товара есть оба размера):
>>> Product.objects.filter(category=tshirt, attributes__contains={'sizes': ['M', 'L']}) [<Product: Bamboo tshirt>]
-
Получить все футболки с размерами ‘M’ и ‘L’, белого и желтого цветов, с надписей (model = poet):
>>> Product.objects.filter(category=tshirt, attributes__contains={'sizes': ['M', 'L'], 'colors': ['white', 'yellow'], 'model': 'poet'}) [<Product: Bamboo tshirt>]
-
Получить все ноутбуки с частотой процессора 2400 и диагональю экрана 12.2
>>> Product.objects.filter(category=notebook, attributes__contains={'speed': 2400, 'screen': 12.2}) [<Product: ATIV Book 9>]
-
Получить все футболки красного цвета, поло, с размерами ‘M’ или ‘L’
>>> Product.objects.filter(category=tshirt, attributes__contains={'colors': ['red'], 'model': 'polo'}, attributes__sizes__has_any_keys=['M', 'L']) [<Product: Silk tshirt>]
-
Получить все ноутбуки с частотой процессора свыше 2000 и диагональю экрана больше 13
>>> Product.objects.filter(category=notebook, attributes__speed__gt=2000, attributes__screen__gt=13) [<Product: MacBook Pro>]
-
Получить все ноутбуки с частотой процессора 2200 или 2400
>>> Product.objects.filter(category=notebook, attributes__speed__in=(2200, 2400)) [<Product: ATIV Book 9>, <Product: MacBook Pro>]
или так:
>>> from django.db.models import Q >>> Product.objects.filter(category=notebook).filter( Q(attributes__contains={'speed': 2200}) | Q(attributes__contains={'speed': 2400}))
Индексы
Посмотрим, насколько эффективны могут быть наши запросы.
В PostgreSQL для JSON полей можно применять разные индексы:
-
GIN
данный индекс в свою очередь может поддерживать различные операторы:
- jsonb_ops (по умолчанию), поддерживает операторы
@>, ?, ?&, ?|
- jsonb_path_ops, поддерживает только оператор
@>
, но работает быстрее и занимает меньше места
- jsonb_ops (по умолчанию), поддерживает операторы
-
btree
может быть полезен только для поиска полного соответствия json документов
-
hash
как и btree, полезен для поиска полного соответствия json документов
Соответсвие некоторых операций в django и postgres операторов
Django Postgres
----------------------
contains @>
contained_by <@
has_key ?
has_any_keys ?|
has_keys ?&
В нашем случае наиболее интересный оператор - @>
, именно в него django транслирует фильтр contains
для json полей.
Если мы просто добавим db_index=True
, будет создан btree индекс:
class Product(models.Model):
...
attributes = JSONField(db_index=True)
Для наших запросов намного полезней будет GIN индекс. Для его создания воспользуемся операцией RunSQL.
Сперва создадим пустую миграцию. В моем случае приложение с товарами называется catalogue_simple
python manage.py makemigrations --empty catalogue_simple
В созданном файле (у меня это 0002_auto_20150928_1610.py) добавим пару импортов, а также команды для создания и отката индекса:
from catalogue_simple.models import Product
from psycopg2.extensions import AsIs
class Migration(migrations.Migration):
# ...
operations = [
migrations.RunSQL(
[("CREATE INDEX catalogue_product_attrs_gin ON %s USING gin"
"(attributes jsonb_path_ops);", [AsIs(Product._meta.db_table)])],
[('DROP INDEX catalogue_product_attrs_gin;', None)],
)
]
Здесь catalogue_product_attrs_gin - это название индекса (выбираем сами), attributes - название JSON поля, Product - модель товара. Создаем индекс jsonb_path_ops, т.к. в основном нам будет интересна только операция contains.
Конструкция AsIs нужна, чтобы соответствующий параметр %s
не оборачивался в одиночные кавычки.
А btree индекс нам не нужен, поэтому не будем добавлять db_index=True
:
class Product(models.Model):
...
attributes = JSONField()
Тестовые данные
Я сгенерил 1 000 000 товаров 4-х разных категорий, по 250 000 в каждой. У каждой категории товаров - свои атрибуты, от 4-х до 7. Некоторые значения - скалярные величины (material у футболок), некоторые - списки (sizes у футболок).
Запросы по индексам
-
Получить все футболки размера ‘M’ и ‘L’ (у товара есть оба размера):
>>> Product.objects.filter(category=tshirt, attributes__contains={'sizes': ['M', 'L']})
Соответсвующий SQL (я заменил перечисление всех полей на
*
для краткости):SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 5 AND "catalogue_simple_product"."attributes" @> '{"sizes": ["M", "L"]}');
Без GIN индекса для поля attributes запрос выполняется 292 ms, EXPLAIN ANALYSE.
Этот же запрос после создания GIN индекса для поля attributes - 250 ms, EXPLAIN ANALYSE.
В данном случае большого выйгрыша мы не получили (было 292ms, стало 250ms), но это потому что результат содержит много строк: 66412. Это называется “low selectivity”. Selectivity - отношение отфильтрованных записей к их общему числу. Если это отношение стремится к 1, говорят low selectivity, если к 0 - high selectivity. С помощью этого показателя можно оценить эффективность индекса. При low selectivity индекс не принесет особой пользы.
-
Получить все футболки с размерами ‘M’ и ‘L’, белого и желтого цветов, с надписей (model = poet):
>>> Product.objects.filter(category=tshirt, attributes__contains={'sizes': ['M', 'L'], 'colors': ['white', 'yellow'], 'model': 'poet'})
Соответствующий SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 5 AND "catalogue_simple_product"."attributes" @> '{ "colors": ["white", "yellow"], "model": "poet", "sizes": ["M", "L"] }');
Без GIN индекса - 240 ms, EXPLAIN ANALYSE.
После создания GIN индекса - 49 ms, EXPLAIN ANALYSE.
Прирост заметен: 240 ms vs 49 ms. Запрос возращает 3737 строк, большая selectivity чем в предыдущем запросе.
-
Получить все ноутбуки с частотой процессора 2400 и диагональю экрана 12.2
>>> Product.objects.filter(category=notebook, attributes__contains={'speed': 2400, 'screen': 12.2})
Соответствующий SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 7 AND "catalogue_simple_product"."attributes" @> '{"screen": 12.2, "speed": 2400}');
Без GIN индекса - 222 ms, EXPLAIN ANALYSE.
После создания GIN индекса - 34 ms, EXPLAIN ANALYSE.
222ms vs 34ms. Запрос возращает 10389 строк.
-
Получить все футболки красного цвета, поло, с размерами ‘M’ или ‘L’
>>> Product.objects.filter(category=tshirt, attributes__contains={'colors': ['red'], 'model': 'polo'}, attributes__sizes__has_any_keys=['M', 'L'])
Соответствующий SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 5 AND "catalogue_simple_product"."attributes" @> '{"colors": ["red"], "model": "polo"}' AND "catalogue_simple_product"."attributes" -> 'sizes' ?| ARRAY['M', 'L']);
Без GIN индекса - 253 ms, EXPLAIN ANALYSE.
После создания GIN индекса - 78 ms, EXPLAIN ANALYSE.
253 ms против 78 ms. Запрос возвращает 18428 строк. В этом запросе операция
has_any_keys
никак не может использовать индекс, т.к. мы объявили индекс вида jsonb_path_ops. Однако, запрос вида"attributes" -> 'sizes' ?| ARRAY['M', 'L']
не будет использовать и jsonb_ops индекс, т.к. мы ищем не по ключам первого уровня, а по значениям списка. Если нам часто нужен такой запрос и у него будет большая selectivity, то можно создать индекс именно на этот ключ в JSON поле. Примерно так:CREATE INDEX gin_sizes ON catalogue_simple_product USING gin ((attributes -> 'sizes'));
Но в нашем случае этого делать не нужно, т.к. фильтр
"attributes" -> 'sizes' ?| ARRAY['M', 'L']
обладет низкой selectivity:>>> Product.objects.filter(category=tshirt, attributes__contains={'colors': ['red'], 'model': 'polo'}, attributes__sizes__has_any_keys=['M', 'L']).count() 18428 >>> Product.objects.filter(category=tshirt, attributes__contains={'colors': ['red'], 'model': 'polo'}).count() 25162
Т.е. по размеру мы отфильтровываем лишь ~25%.
-
Получить все ноутбуки с частотой процессора свыше 2000 и диагональю экрана больше 13
>>> Product.objects.filter(category=notebook, attributes__speed__gt=2000, attributes__screen__gt=13)
Соответствующий SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 7 AND "catalogue_simple_product"."attributes" -> 'screen' > '13' AND "catalogue_simple_product"."attributes" -> 'speed' > '2000')
В данном случае GIN индекс нам не поможет. Если такие запросы используются часто, возможно имеет смысл создать btree индекс по конкретным ключам из JSON поля:
CREATE INDEX attrs_screen_speed ON catalogue_simple_product ((attributes -> 'screen'), (attributes -> 'speed'));
Запрос возвращает 10536 строк.
Без btree индекса attrs_screen_speed - 352 ms, EXPLAIN ANALYSE
После создания btree индекса attrs_screen_speed - 46 ms, EXPLAIN ANALYSE.
-
Получить все ноутбуки с частотой процессора 2200 или 2400
>>> Product.objects.filter(category=notebook, attributes__speed__in=(2200, 2400))
Соответствующий SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 7 AND "catalogue_simple_product"."attributes" -> 'speed' IN ('2200', '2400'))
Этот запрос не покрывается нашим GIN индексом. Время выполнения ~ 389 ms, EXPLAIN ANALYSE.
Попробуем переписать этот же запрос с использованием существующего GIN индекса
>>> from django.db.models import Q >>> Product.objects.filter(category=notebook).filter(Q(attributes__contains={'speed': 2200}) | Q(attributes__contains={'speed': 2400}))
SQL:
SELECT * FROM "catalogue_simple_product" WHERE ("catalogue_simple_product"."category_id" = 7 AND ("catalogue_simple_product"."attributes" @> '{"speed": 2200}' OR "catalogue_simple_product"."attributes" @> '{"speed": 2400}'));
Здесь GIN индекс может быть использован, время выполнения ~ 337 ms EXPLAIN ANALYSE.
Особой разницы нет. Однако, посмотрим на selectivity данного запроса. В результате мы имеем 124 995 строк из 250 000 возможных для данной категории, т.е. имеем очень низкий selectivty.
Создадим 100 ноутбуков с частотой процессора 3200 и 100 ноутбуков с частотой 3500. Других ноутбуков с такими скоростями в БД нет. Посмотрим насколько эффективно будет использовать индекс в этом случае.
Запрос без GIN индекса:
>>> Product.objects.filter(category=notebook, attributes__speed__in=(3200, 3500))
Получаем те же ~ 391 ms EXPLAIN ANALYSE.
Теперь запрос, в котором может быт использован GIN индекс:
>>> Product.objects.filter(category=notebook).filter(Q(attributes__contains={'speed': 3200}) | Q(attributes__contains={'speed': 3500}))
В итоге получаем время выполнения лишь 0.773 ms! EXPLAIN ANALYSE.
Резюме по индексам
Как мы видим, мы можем использовать один индекс GIN (jsonb_path_ops) в запросах по всем атрибутам, а не по одному конкретному ключу! Конечно, это не серебряная пуля, всегда нужно анализировать те данные, с которыми мы работаем и выбирать индексы исходя из нужных запросов.
NoSQL database (MongoDB)
Давайте посмотрим, как мы можем хранить те же данные и выполнять аналогичные запросы в MongoDB (v3.0.6).
Для того, чтобы использовать один индекс при фильтрации по неизвестным заранее полям в MongoDB, нам нужно будет использовать немного другую структуру данных.
Поле attributes у нас будет списком, а значения - вложенные документы с ключами name и value:
> db.catalogue_simple.find().pretty()
{
"_id" : ObjectId("560ab1970a0a88fe77d00f02"),
"category" : "tshirts",
"name" : "Silk tshirt",
"price" : 100,
"attributes" : [
{
"name" : "colors",
"value" : "red"
},
{
"name" : "colors",
"value" : "black"
},
{
"name" : "sizes",
"value" : "S"
},
{
"name" : "sizes",
"value" : "M"
},
{
"name" : "model",
"value" : "polo"
},
{
"name" : "material",
"value" : "silk"
}
]
}
{
"_id" : ObjectId("560ab1dd0a0a88fe77d00f03"),
"category" : "tshirts",
"name" : "Bamboo tshirt",
"price" : 120,
"attributes" : [
{
"name" : "colors",
"value" : "white"
},
{
"name" : "colors",
"value" : "yellow"
},
{
"name" : "sizes",
"value" : "M"
},
{
"name" : "sizes",
"value" : "L"
},
{
"name" : "sizes",
"value" : "XL"
},
{
"name" : "model",
"value" : "poet"
},
{
"name" : "material",
"value" : "bamboo"
}
]
}
{
"_id" : ObjectId("560ab2cb0a0a88fe77d00f04"),
"category" : "notebook",
"name" : "MacBook Pro",
"price" : 2000,
"attributes" : [
{
"name" : "brand",
"value" : "Apple"
},
{
"name" : "screen",
"value" : 15
},
{
"name" : "speed",
"value" : 2200
},
{
"name" : "hd",
"value" : 256
}
]
}
{
"_id" : ObjectId("560ab2ec0a0a88fe77d00f05"),
"category" : "notebook",
"name" : "ATIV Book 9",
"price" : 1200,
"attributes" : [
{
"name" : "brand",
"value" : "Samsung"
},
{
"name" : "screen",
"value" : 12.2
},
{
"name" : "speed",
"value" : 2400
},
{
"name" : "hd",
"value" : 128
}
]
}
Запросы (MongoDB)
-
Получить все футболки размера ‘M’ и ‘L’ (у товара есть оба размера):
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "sizes", "value": "M"}}, {"$elemMatch": {"name": "sizes", "value": "L"}} ]}, category: 'tshirts'}) {"name" : "Bamboo tshirt", /* ... */}
-
Получить все футболки с размерами ‘M’ и ‘L’, белого и желтого цветов, с надписей (model = poet):
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "sizes", "value": "M"}}, {"$elemMatch": {"name": "sizes", "value": "L"}}, {"$elemMatch": {"name": "colors", "value": "white"}}, {"$elemMatch": {"name": "colors", "value": "yellow"}}, {"$elemMatch": {"name": "model", "value": "poet"}} ]}, category: 'tshirts'}) {"name" : "Bamboo tshirt", /* ... */}
-
Получить все ноутбуки с частотой процессора 2400 и диагональю экрана 12.2
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "speed", "value": 2400}}, {"$elemMatch": {"name": "screen", "value": 12.2}} ]}, category: 'notebook'}) {"name" : "ATIV Book 9", /* ... */}
-
Получить все футболки красного цвета, поло, с размерами ‘M’ или ‘L’
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "colors", "value": "red"}}, {"$elemMatch": {"name": "model", "value": "polo"}}, {"$elemMatch": {"name": "sizes", "value": {"$in": ["M", "L"]}}} ]}, category: 'tshirts'}) {"name" : "Silk tshirt", /* ... */}
-
Получить все ноутбуки с частотой процессора свыше 2000 и диагональю экрана больше 13
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "speed", "value": {"$gt": 2000}}}, {"$elemMatch": {"name": "screen", "value": {"$gt": 13}}} ]}, category: 'notebook'}) {"name" : "MacBook Pro", /* ... */}
-
Получить все ноутбуки с частотой процессора 2200 или 2400
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "speed", "value": {"$in": [2200, 2400]}}}, ]}, category: 'notebook'}) {"name" : "MacBook Pro", /* ... */}, {"name" : "ATIV Book 9", /* ... */}
Индексы (MongoDB)
В данной орагнизации данных мы можем создать multikey index:
> db.catalogue_simple.ensureIndex({"attributes.name" : 1, "attributes.value" : 1})
Для честного сравнения создадим индекс по полю category (django автоматически создает его для ForeignKey полей)
> db.catalogue_simple.ensureIndex({"category": 1})
Еще следует учитывать, что mongodb будет использовать индекс только для первого фильтра оператора $all. Хоть в доках MongoDB говорится о index intersesection, однако похоже, что пересечение индексов не работает в данных запросах.
Наглядный пример. Пусть у нас в базе только 1 товар с размером “XXXS”, и очень много товаров с размером “M”.
Мы хотим найти товары, у которых есть оба размера “XXXS” и “M”.
Можем сделать такой запрос:
> db.catalogue_simple.find({attributes: {$all: [
{"$elemMatch": {"name": "sizes", "value": "M"}},
{"$elemMatch": {"name": "sizes", "value": "XXXS"}},
]}, category: 'tshirts'})
В этом случае MongoDB применит индекс к значению “M”. В итоге будет просканированно много сущностей, затрачено много времени, а в результате только 1 документ:
"nReturned" : 1,
"executionTimeMillis" : 1902,
"totalKeysExamined" : 249934,
"totalDocsExamined" : 249934,
А если сделаем запрос так (“XXXS” на первом месте):
> db.catalogue_simple.find({attributes: {$all: [
{"$elemMatch": {"name": "sizes", "value": "XXXS"}},
{"$elemMatch": {"name": "sizes", "value": "M"}},
]}, category: 'tshirts'})
то результат получим сразу:
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
Мораль такая, что на первое место нужно ставить поле с наибольшим selectivity. Если у нас конечно есть такая инфорамция.
Тестовые данные (MongoDB)
Тестовые данные полностью идентичны использованным в PostgreSQL (за исключением структуры): 4 категории, по 250 000 товаров в каждой, всего 1 000 000.
Запросы по индексам (MongoDB)
-
Получить все футболки размера ‘M’ и ‘L’ (у товара есть оба размера):
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "sizes", "value": "M"}}, {"$elemMatch": {"name": "sizes", "value": "L"}} ]}, category: 'tshirts'})
Без индекса по attributes запрос выполняется 706 ms, используется индекс category.
Вывод
.explain('executionStats')
:"winningPlan" : { // ... "indexName" : "category_1", } "executionStats" : { "nReturned" : 66412, "executionTimeMillis" : 706, "totalKeysExamined" : 250001, "totalDocsExamined" : 250001, }
С индексом по attributes ничего не меняется, т.к. optimizer не считает этот индекс лучше чем category (видимо малая selectivity).
-
Получить все футболки с размерами ‘M’ и ‘L’, белого и желтого цветов, с надписей (model = poet):
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "sizes", "value": "M"}}, {"$elemMatch": {"name": "sizes", "value": "L"}}, {"$elemMatch": {"name": "colors", "value": "white"}}, {"$elemMatch": {"name": "colors", "value": "yellow"}}, {"$elemMatch": {"name": "model", "value": "poet"}}, ]}, category: 'tshirts'})
Ситуация аналогична первому примеру.
Она может поменяться, если на первое место мы поставим значение с большой selectivity. В тестовых данных цветов больше, чем размеров. Поэтому, если поставить цвет на первое место, то будет использоваться индекс attributes:
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "colors", "value": "white"}}, {"$elemMatch": {"name": "sizes", "value": "M"}}, {"$elemMatch": {"name": "sizes", "value": "L"}}, {"$elemMatch": {"name": "colors", "value": "yellow"}}, {"$elemMatch": {"name": "model", "value": "poet"}}, ]}, category: 'tshirts'}).explain('executionStats')
Вывод:
"winningPlan" : { // ... "indexName" : "attributes.name_1_attributes.value_1", } "executionStats" : { "nReturned" : 3737, "executionTimeMillis" : 658, "totalKeysExamined" : 124902, "totalDocsExamined" : 124902, }
Время запроса - 658 ms.
-
Получить все ноутбуки с частотой процессора 2400 и диагональю экрана 12.2
Аналогично пунктам 1 и 2.
-
Получить все футболки красного цвета, поло, с размерами ‘M’ или ‘L’
Аналогично пунктам 1 и 2.
-
Получить все ноутбуки с частотой процессора свыше 2000 и диагональю экрана больше 13
> db.catalogue_simple.find({attributes: {$all: [ {"$elemMatch": {"name": "speed", "value": {"$gt": 2000}}}, {"$elemMatch": {"name": "screen", "value": {"$gt": 13}}} ]}, category: 'notebook'}).explain('executionStats')
Как мы помним, в PostgreSQL мы не могли использовать GIN индекс для этого запроса. Однако в случае структуры данных, которую мы используем в MongoDB, существующий индекс будет работать. Вопрос лишь в том, какой процент данных этот фильтр отсекает. Тут selectivity оказалась относительно хорошей:
"winningPlan" : { // ... "indexName" : "attributes.name_1_attributes.value_1", } "executionStats" : { "nReturned" : 10536, "executionTimeMillis" : 160, "totalKeysExamined" : 62472, "totalDocsExamined" : 62472, }
-
Получить все ноутбуки с частотой процессора 2200 или 2400
Аналогично предыдущим пунктам.
Итоги
В PostgreSQL 9.4 появился тип jsonb, который можно эффективно использовать в запросах. Причем создав лишь один индекс мы можем фильтровать по всем ключам из JSON. Не все операции будут доступны (например больше/меньше, для них придется создавать отдельный индекс на каждый JSON ключ), но тем не менее этого достаточно для широкого круга задач. И начиная с Django 1.9 эти функции доступны из коробки.
В MongoDB аналога оператору @>
нет. Можно изменить структуру данных и делать аналогичные запросы по группе полей, используя один индекс. Но это менее эффективно, чем в PostgreSQL. Ведь индекс будет применяться только к первому ключу, а не ко всем.
Зато MongoDB поддерживает гораздо больше операций при фильтрации по одному любому ключу, используя один индекс.
Пока мне очень нравится JSON в PostgreSQL, оно здорово облегчает решение многих задач. При этом мы сохраняем возможности SQL: транзакции и join’ы, которых нет в MongoDB. И теперь это поддерживается Django ORM.