MongoDB как зеркало мировой СУБД-революции

Данная заметка послужила основой для одноименной главы книги "СУБД для программиста. Базы данных изнутри".

* * *

Несколько эпизодов из жизни NoSQL глазами YesSQL. Тема всплыла в результате исследования технических средств для проекта переработки существующей системы. Пуркуа бы и не па?

Для теста был выбран сценарий позволяющий:

  • оценить пригодность СУБД к интенсивной вставке данных от множества устройств
  • оценить простоту и производительность запросов к полученной таким образом базе данных

Тест вставки, или изгнание из рая

Тест интенсивной вставки данных от множества датчиков я ограничил 10 миллионами записей. Реальные объёмы на порядки выше, но зачем проводить долгое время за процессом, если вдруг и на таком количестве возникнут проблемы.

На сайте MongoDB была загружена последняя стабильная версия 2.0.2 для 64-разрядной Windows. В качестве альтернативы MongoDB выступал MS SQL Server 2008 R2 Developer Edition, тоже 64-разрядный (если у вас такого нет, тест пойдет и на бесплатном SQL Server Express 2005 и выше). Компьютер для обоих тестов использовался слабенький, уровня обычной рабочей станции под Windows 7, двухядерный Intel 2.6 GHz с одним медленным диском (5400 rpm, 300 Gb), но с 6 гигабайтами памяти.

Данные вставляются в коллекцию MongoDB и таблицу SQL Server, соответственно, имеющие одинаковую структуру. Соответствующие скрипты можно загрузить (MongoDB, SQL Server).

Для SQL-скрипта надо сделать пояснение. YesSQL - это мир транзакций. Если вы будете тупо построчно вставлять в таблицу данные, как будто перед вами плоский файл, то скорость будет в разы ниже, чем в случае файла. Поэтому запишите у себя в конспекте: для интенсивной построчной вставки в реляционную СУБД приложение должно сначала накопить массив строк, потом начать транзакцию, вставить эти несколько строк в таблицу и закрыть транзакцию.

В скрипте выбран пакет всего в 10 строк, который увеличивает скорость вставки в 5 раз по сравнению с построчным вбиванием данных.

MongoDB, цитирую does not use traditional locking or complex transactions with rollback, as it is designed to be lightweight and fast and predictable in its performance. В общем, согласен, если просто построчно вставлять записи в таблицу, то не-транзакционность окажется быстрее в любом случае.

Для продвинутых поясню, что пакетная вставка (BULK INSERT) не всегда приемлема по логике приложения. Стандартное использование - массовый импорт данных. Собственно, для BULK-копирования и тестировать особо нечего, оно или есть в СУБД, или нет. В MongoDB и SQL Server эта функциональность присутствует.

По итогам вставки получились два графика и цифры по объему получившейся базы данных.

Коллекция из 10 миллионов документов заняла 3,95 гигабайт, база данных SQL Server - 0,5 Гб (без компрессии), то есть в 8 раз меньше.

Скорость вставки

 

Хотя время вставки документа примерно в 3 раза выше, чем строки в таблицу (пачками по 10), оно может быть вполне приемлемым для логики приложения. Меня больше заинтересовал временный провал в производительности (горб на втором графике) и неуёмное желание MongoDB отожрать всю оперативную память: 4 Гб против 1 Гб SQL Server при выставленном лимите в 3 Гб.

Можно ли ограничить объем используемой оперативной памяти? Ответ MongoDB - нельзя. Запрос на изменения висит больше года на трекере и не встретил у разработчиков никакого понимания. В общем случае решение состоит в создании выделенного виртуального сервера под СУБД. Какие-то неофициальные советы можно найти поиском (например). Словом, остается надеяться на лучшее в будущем.

Поэтому для дальнейших тестов мне приходится делать перезапуск mongod для очистки памяти. В SQL Server для аналогичного эффекта просто очищаем буферы и кэш (DBCC).

О, этот кэш! Во второй части вы увидите, насколько он эффективен.

Запросы, или сошествие во адЪ

Пришло время применить полученную таблицу по назначению, нагрузив СУБД разными запросами.

Общее впечатление от MongoDB: я попал в эпоху даже не FoxPro 2, где встроенный SQL уже был, а куда-то в Clipper 87, увидевший свет летом 1987 года. "Клиппер" во всей красе, с его бесконечными обходами таблиц в циклах, явным выбором текущего индекса для поиска, наложениями фильтров, ручным суммированием и прочими давно забытыми «прелестями».

В MongoDb нет встроенных функций агрегации. Для нахождения сумм или средних величин нужно фактически написать свой аналог стандартных в SQL функций SUM() или AVG() на языке JavaScript или использовать более общий метод MapReduce.

Справедливости ради, надо сказать, что начиная с версии SQL Server 2005 особо одаренные программисты могут писать свои нестандартные функции агрегации на C#. Но я посоветовал бы вначале хорошо подумать о необходимости этого шага.

В случае, если запрос с агрегацией возвращает более 10 тысяч документов, использование MapReduce обязательно, иначе выдается ошибка. Таким образом, встроенным является только подсчет числа элементов в том числе уникальных. Но вот беда - использовать их можно только в применении к целым коллекциям, то есть вне контекста запросов с группировками. Так что, дорогие программисты, и для COUNT() с группировкой пишите рутинный типовой код на JavaScript.

Как отсортировать результаты запроса с группировкой? Ответ - никак, сделайте сортировку на клиенте. Диалог ниже оставляю без цензурных комментариев.
Q: How can I sort on the resultset of the group by operation?
A: Group currently just returns an object, so you should be able to sort easily client side

В результате код запросов с кратким и ясным синтаксисом декларативного языка SQL превращается в длинную кашу из императивных инструкций и параметров-деклараций. "Эволюцию" можно проследить ниже на примере использованных в тесте запросов. Добрые люди даже нарисовали эту картинку трансформации MySQL-запроса в MongoDB.

Запрос Q1

Поиск минимального и максимального значения дат в таблице/коллекции.

SQL Server MongoDB
SELECT MIN(measureDate), MAX(measureDate)
FROM dbo.measuresData
var minDate = new Date(1900,1,1,0,0,0,0);
var maxDate = new Date(2100,1,1,0,0,0,0);
db.measuresData.group(
{
  key: { },
  reduce: function(obj, prev) 
  {
    if (obj.measureDate.getTime() < prev.minMsec) 
      prev.minMsec = obj.measureDate.getTime(); 
    else if (obj.measureDate.getTime() > prev.maxMsec) 
      prev.maxMsec = obj.measureDate.getTime(); 
  },
  initial: { minMsec: maxDate.getTime(), maxMsec: minDate.getTime() }, 
  finalize: function(out)
  { 
    out.minMeasureDate = new Date(out.minMsec);
    out.maxMeasureDate = new Date(out.maxMsec);
  }
})
.forEach(printjson);

Вариант MongoDB заставляет работать с представлением дат в виде числа миллисекунд от 01/01/1970, потому что прямое сравнение дат в функции вызывает ошибку TypeError: this["get" + UTC + "FullYear"] is not a function nofile_b:2

"Клипперный" опыт пригодился в MongoDB. Если построить индекс по элементу "measureDate", то можно найти крайние значения относительно простым способом: отсортировать по индексу в порядке возрастания и взять первый элемент, повторив процедуру в порядке убывания значений.

db.measuresData.ensureIndex({measureDate: 1});
 
db.measuresData.find({}, {measureDate: 1}).sort({measureDate: 1}).limit(1);
 
db.measuresData.find({}, {measureDate: 1}).sort({measureDate: -1}).limit(1);

Запрос Q2

Подсчет сумм целых и вещественных значений по состоянию и группе устройств. Запрос для MongoDB, напомню, не выполняет сортировку.

SQL Server MongoDB (не выполняет сортировку)
SELECT SUM(intVal), SUM(floatVal), stateId, groupId
FROM dbo.measuresData
GROUP BY stateId, groupId
ORDER BY stateId, groupId
db.measuresData.group(
{
  key: { stateId: true, groupId: true },
  reduce: function(obj, prev) 
  {
    prev.sumIntVal += obj.intVal; 
    prev.sumFloatVal += obj.floatVal; 
  },
  initial: { sumIntVal: 0, sumFloatVal: 0.0 }
})
.forEach(printjson);

Запрос Q3

Подсчет общего числа устройств и количества уникальных устройств по состоянию и их группе.

SQL Server MongoDB (не выполняет сортировку)
SELECT COUNT(deviceId), COUNT(DISTINCT deviceId), stateId, groupId
FROM dbo.measuresData
GROUP BY stateId, groupId
ORDER BY stateId, groupId
db.measuresData.group(
{
  key: { stateId: true, groupId: true },
  reduce: function(obj, prev) 
  { 
    prev.count++;
  },
  initial: { count: 0 },
  finalize: function(out)
  {
    out.distCount = db.measuresData
      .distinct("deviceId", {stateId: out.stateId, groupId: out.groupId})
      .length;
  }
})
.forEach(printjson);

Журнал ядра БД показывает примерно 35 секунд для функции finalize на каждый цикл вычисления distinct из примерно 200 строк. Срочно обрываем запрос.

Для получения ответа в течение минут требуется построить индекс по элементам stateId и groupId, иначе запрос выполняется в течение почти 2 часов. Иными словами без оптимизации (вмешательства администратора БД) простой adhoc-запрос фактически не работает.

db.measuresData.ensureIndex({stateId: 1, groupId: 1})

Индекс строится 141 секунду (аналогичный в SQL Server – за 20 секунд). Размер БД вырастет до 4,2 Гб.

Перезапускаем запрос. Победа разума, с индексом цикл вычисления distinct снижается до 300 миллисекунд (в 100 раз). Третий перезапуск, время цикла выросло до 1,2 секунды, потом начинает снижаться до 300 мсек. Общее время: более 15 минут! Полное впечатление, что вместо осмысленного использования кеша, в память намеренно заносится мусор, только мешающий нормальной работе движка БД.

Запрос Q4

Подсчет сумм целых и вещественных значений по состоянию и группе устройств для заданного диапазона дат. В качестве диапазона выбрана 1 минута примерно в середине интервала между минимальным и максимальным значениями дат. Диапазон включает около 500К строк.

SQL Server MongoDB (не выполняет сортировку)
SELECT SUM(intVal), SUM(floatVal), stateId, groupId
FROM dbo.measuresData
WHERE measureDate BETWEEN '20120208 22:54' AND '20120208 22:55'
GROUP BY stateId, groupId
ORDER BY stateId, groupId
var date1 = new Date(ISODate("2012-02-08T15:20:00.000Z"));
var date2 = new Date(ISODate("2012-02-08T15:21:00.000Z"));
 
db.measuresData.group(
{
  key: { stateId: true, groupId: true },
  cond: {measureDate: { $gte: date1, $lt: date2 } },
  reduce: function(obj, prev)
  {
    prev.sumIntVal += obj.intVal; 
    prev.sumFloatVal += obj.floatVal; 
  },
  initial: { sumIntVal: 0, sumFloatVal: 0.0 }
})
.forEach(printjson)

Запрос Q5

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

SQL Server MongoDB (не выполняет сортировку)
SELECT COUNT(deviceId), COUNT(DISTINCT deviceId), stateId, groupId
FROM dbo.measuresData
WHERE measureDate BETWEEN '20120208 22:54' AND '20120208 22:55'
GROUP BY stateId, groupId
ORDER BY stateId, groupId
var date1 = new Date(ISODate("2012-02-08T15:20:00.000Z"));
var date2 = new Date(ISODate("2012-02-08T15:21:00.000Z"));
 
db.measuresData.group(
{
  key: { stateId: true, groupId: true },
  cond: {measureDate: { $gte: date1, $lt: date2 } },
  reduce: function(obj, prev) 
  { 
    prev.count++;
  },
  initial: { count: 0 },
  finalize: function(out)
  {
    out.distCount = db.measuresData
      .distinct("deviceId", {stateId: out.stateId, groupId: out.groupId})
      .length;
  }
})
.forEach(printjson);

Хронометраж запросов

Для MongoDB результаты без оптимизации ужасающие, а с оптимизацией, где она возможна, от "хуже" до "просто плохие". Со стороны SQL Server вся оптимизация свелась к построению кластерного индекса по полю measureDate (точнее, перестроению вместо имевшегося по первичного ключу measureId). В запросах Q2 и Q3 необходимо полное сканирование данных, поэтому оптимизация стандартными средствами (индексация) для SQL не производилась.

Вторые и третьи запуски хорошо иллюстрируют работу кеша СУБД.

  SQL Server   MongoDB
Запуски / время, сек 1 2 3   1 2 3
Q1 7 1 1   190 185 189
Q2 8 3 3   >7200
Q3 26 20 20   345 341 349
Q4 8 1 1   22 22 22
Q5 15 10 12   141 141 141
После оптимизации
Q1 0 0 0   0 0 0
Q2 8 3 3   322 800 619
Q3 нет       нет    
Q4 1 1 1   12 12 13
Q5 7 7 7   85 84 85

За кадром

За кадром остался интересный вопрос: "Кто исполняет ява-скрипты, ядро монго или же её клиент mongo shell?". Надеюсь на первое, потому что во втором случае речь вообще идет о не СУБД, а о менеджере записей (record manager), вроде приснопамятного BTrieve из локальных Novell-сетей 90-х годов.

Выводы, или вперед в прошлое

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

Нет, ребята, модель здесь вторична, а об универсальности, как и о промышленном решении говорить пока преждевременно. Для использования NoSQL нужно иметь серьёзные основания.

Проще сказать, что не является причиной выбора NoSQL.

  • Отсутствие компетенций в области баз данных и надежда обойтись без в дальнейшем;
  • Протест квазимонополии "Big-3" (Oracle, IBM, Microsoft). Для этого есть PostgreSQL, Firebird и, с оговорками, MySQL;
  • Использование вместо технических аргументов маркетинговые термины "мода", "современное течение", "прогрессивное направление" и т.д. На самом деле, модели данных NoSQL использовались еще в дореляционную эпоху.
Прикрепленный файлРазмер
Plain text icon test_vol_mongodb.js_.txt995 байта
Binary Data test_vol_mssql.sql1.69 KB

Комментарии

Изображение пользователя ipanshin.

написанное из письма

Прочитал и, если все это правильно (а мне нет причин не доверять написанному), то все это ужасно и говорит о том, что Библия права и техническое общество идет по пути регресса также. Либо, как говорили адепты государства: "Повторенье - мат ученья". А также "Мат-природа". Про отца они почему-то молчат. Либо отец просто неизвестен. Я к этому также не имею никакого отношения.

Какой-то феерический туман
Людей стремящихся вокруг
Их фонарей смешной обман
В твой освещенный круг...

В силу своей лени скопипастил написанное из письма (письмо писал я же, но раньше). Думаю, что в пику теме.

Я тут прочитал наконец-то на выходных статью, которая меня заинтересовала
http://queue.acm.org/detail.cfm?id=1961297
в надежде все-таки уяснить себе, что предлагают нового в подходе nosql.

До этого момента
!(a && b) == (!a)||(!b)
!(a||b) == (!a)&&(!b)
все вроде было нормально. Всякие там PK-FK для SQL и LINq для nosql.

Я все-таки кое что помню из логики высказываний и понимаю, что правила деМоргана основываются на том что пара логических функций & and ! или V and ! являются ортогональным базисом, иначе говоря любую логическую функцию можно представить в этих базисах. В первом случе мы получаем коньюнктивную форму, во втором - дизъюнктивную.
Авторы статьи почемуто это все называют duality (Examples of duality abound in computer science.) и говорят, что такого "дуализма" много и в конечном итоге переходят к паре sql - nosql. Это что доказательство или заказная статья?

Опять же из того, что я помню с тех времен, когда SQL движков не было. Да, тогда просто брались и создавались структуры в рамках работы некоторого приложения. Далее оказалось, что этот путь очень трудоемок в плане интеграции данных и был придуман интерпретирующий подход доступа к данным. В районе 70-80 годов был написан интерпретатор языка sql, который брал задачи структурирования информации (insert,delete,update,select) на себя.

Дело не в том, что sql подразумевает использование пар PrimaryKey-ForeignKey, а в том, что реализация его - это интерпретатор, что для призводительности - нож, зато для сложности - благо. Разбиение сложной задачи доступа к данным из приложения разбивалась на две более простых части.

В рамках подхода nosql, как я понял, прежде всего выкидывается sql-интерпретатор и используются linq конструкции в самом приложении. Чем собственно говоря я и могу пользоваться в рамках C# проекта. Это работает до момента, пока не оканчивается оперативная память. После этого все начинает либо тормозить, либо разваливается.
А как же тогда решается задача интеграции данных между приложениями? Никаким другим путем кроме как выгрузки DDL and DML информации в обычные текстовые файлы мне в голову не приходит. Именно отсюда и вырос xml. Так? Если это так, то в 70-80 годах - это была отправной точкой прихода к sql-интерпретатору.

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

Вот такие размышления.

Изображение пользователя Serguei_Tarassov.

Мутная статья

Мутная статья. Начиная с названия, целиком стянутого с Коддовской "A Relational Model of Data for Large Shared Data Banks", через нелепость противопоставления объектов таблицам (абсциссу - ординате) при схожем уровне абстракции к попытке притянуть LINQ в качестве метаязыка доступа к данным для SQL и других.

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

SQL не навязывает интерпретатор. Это декларативный язык, который может быть интерпретирован или скомпилирован (см. embedded SQL для C/C++ и других компиляторов). Собственно, подход LINQ существует практически с момента появления SQL под названием "встраиваемый SQL".

Изображение пользователя Serguei_Tarassov.

У товарища по ссылке

У товарища по ссылке типичная проблема: он не понимает транзакции (вы, видимо, тоже, раз ссылаетесь). Вставка записей по 10-100-1000 в одной транзакции проблему решает, не говоря уже о bulk insert. Об этом написано прямым текстом в самом начале.

Запросы за пределами CRUD практически всегда с group by. CRUD тестировать неинтересно, достаточно вставки.

Michael,
YesSQL is the transactional world.
Wrap yours inserts in transaction and you’ll get the great performance in SQL Server.
Or use trasactions in MongoDb for each insert :)
Hope this helps.

а есть где-то

а есть где-то повторение тестов на вставку с отключённым журналом у монгодб?

Изображение пользователя Serguei_Tarassov.

Зачем?

Зачем вам нужен такой тест? Вам тогда сравнивать придется с аналогичной вставкой (BULK INSERT) в SQL Server. А это многие десятки тысяч строк в секунду. На поток. А если распараллелить, то 1 терабайт заливается за 30 минут. Вам это нужно? На худой конец, придется сравнивать с базой MySQL типа MyISAM. Или с какими-нибудь файлами DBF на файловом сервере.

Не надо микроскопом гвозди забивать

Вы (автор) изначально исходили из неверного утверждения, что MongoDB может полностью заменить SQL-бд.

Пытаться выполнять запросы с агрегацией на MongoDB это тоже самое что использовать спорткар в качестве тягача.

Постройте правильные индексы на MongoDB и повторите тест используюя простые select'ы с операциями "=", "<", ">"

MongoDB нужен не для аналитических запросов (в которых как раз и используется агрегация), а для быстрого развёртывания и простого администрирования больших распределенных хранилищ (до 1000 выделенных серверов с распределением данных по ним) с обеспечением онлайн работы БОЛЬШОГО количества пользователей осуществляющих простые селекты и инзёрты.

В реальных проектах MongoDB используется вместе с SQL решениями и никак не является полностью заменой последних.

Изображение пользователя Serguei_Tarassov.

Автор

Автор исходил из обратного утверждения, что РСУБД в руках специалиста способна заменить весь этот зоопарк noSQL. Тест вставки это показывает с одной стороны. Запросы во второй части далеки от аналитических.

Реальность несколько иная

Ну, в реальности вставки в транзакции выполняются редко, так что графики автора статьи не показательны.
Провел свои тесты - http://picview.ru/d1udmgpy
От MongoDB остались двоякие впечатления. Думаю, что не совсем подходит для моих задач - объем файлов огромный, тяжелый диалект, апдейты происходят медленней чем в MsSQL, на одних вставках далеко не уедешь. Да и больше удручает тот факт, что банально перепутав название полей они автоматом добавляются в структуру документов. В файлах дублируются названия полей для каждого документа, для чего?! Работая через консоль порой происходят зависы при запросах. Короче в морг этот NoSQL. Кстати, NoSQL расшифровывается как Not Only SQL.

Изображение пользователя Serguei_Tarassov.

Наоборот

Наоборот, практически всегда используется. См. например, cached updates в клиентских dataset.

Также зависит от интенсивности вставки. Если буфер приложения накапливает десяток-другой строк, а потом их записывает в БД, то используем транзакцию с последующей очисткой буфера. А если тысячи и десятки тысяч, то bulk insert-ом (см. SqlBulkCopy).

Автор хоть и

Автор хоть и пытается казаться умным и убедить нас в чем-то, но выходит не убедительно.
1 Сравнение действительно из разряда Феррари vs Трактор.
2 Обычно при сравнительных тестах принято выкладывать конфиги серверов и железа, формат данных и желательно тестовый набор данных. Феррари как-то не очень едет на солярке, а трактору нормально.
3 Дальше идет оговорка, что вас интересуют транзакционные вставки. MongoDB четко говорит "у нас транзакций нет", есть атомарность изменения одной записи, тут бы и тест закончить, но нет. Феррари на трек, трактор в поле.
4 Ну ладно, транзакции пропустим. Пара цитат "Тест интенсивной вставки данных" и "В скрипте выбран пакет всего в 10 строк, который увеличивает скорость вставки в 5 раз". 500% звучит солидно, это на практике или в теории?. Как-то не вяжется интенсивная вставка и пакет из 10 записей. Это кто быстрей проедет на первой передаче? Лучшим вариантом была бы сравнительная динамика при увеличении пакета 100-500-1000, хотя бы.
5 Дальше идет сетование про оперативную память. MongoDB говорит "я держу данные в памяти и храню на диске". Чем больше данных, тем больше нужно памяти. Вот ведь неожиданность. Память выделяется кусками, точнее создается memory mapped file(размер определяется в конфигах) и забивается нолями на всю длину. У вас может быть всего 1мб данных, но лежать они могут в 1гб файле, если забили один файл выделяется следующий. Провал на графике мог быть вызван созданием файла(выделением памяти) и\или началом записи данных на диск.
6 MongoDB предоставляет schema-less хранение данных, расплатой за это является неявное хранение схемы в каждом документе, в виде названий полей, что приводит к увеличению размера базы. Если название поля measureDate сократить до md, то экономия получится минимум 9 байт * 10М (84мб), только на хранении имени одного поля. Если игнорировать эту особенность, то на каждом запросе можно пробегать по лишним данным, иногда значительного объема.
7 Все работает в один поток? Вы серьезно?
8 Все работает на одном сервере, использование MongoDB хоть для мало-мальски реальной задачи подразумевает наличие хотя бы 2 отдельных серверов. А тут уже и цифры другие и мерять нужно по другому и факторы влияющие на производительность другие.

В общем не разбираясь и не учитывая особенности, сравнивать не эквивалентные вещи, производить какие-то замеры и делать какие-то выводы, это глупое занятие.

Изображение пользователя Serguei_Tarassov.

По пунктам

По пунктам:
1. Голословное утверждение
2. Минимальные сведения приведены. Для сравнения софта требуется, чтобы аппаратура была одинаковой конфигурации, сама же конфигурация не столь важна.
3. "YesSQL - это мир транзакций", иначе сравнивать надо с bulk load
4. Читаем про пакетную загрузку bulk load, включая соответствующие классы из .NET SqlClient.
5. Читаем абзац про неумение MongoDb управлять оперативной памятью и кешем.
6. Цитирование документации для "объяснения" почему требуется хранить больше данных не имеет смысла. Больший объем файла ведет к большему I/O и снижению производительности.
7. При многопоточной загрузке вышеупомянутые проблемы следует умножать на число потоков, как минимум.
8. Обсуждение фразы "использование ХХХ хоть для мало-мальски реальной задачи...", где вместо ХХХ можно подставить любое название по желанию разглагольствующего, выходит за рамки статьи с цифрами и графиками.