Новый aggregation framework в MongoDB 2.1

    В релизе 2.1 было заявлена реализация такой функциональности, как новый фреймворк агрегирования данных. Хотелось бы рассказать о первых впечатлениях от этой весьма интересной штуки. Данный функционал должен позволить в некоторых местах отказаться от Map/Reduce и написания кода на JavaScript в пользу достаточно простых конструкций, предназначенных для группировки полей почти как в SQL.



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

    Итак, самая главная сложность в выборке данных из MongoDB это работа с массивами и данными, содержащимися внутри каких-то отдельных элементов. Да, мы можем их выбрать как и в SQL, но не можем агрегировать по ним непосредственно при выборке. Новый фреймвок представляет собой декларативный способ работы с такими данными, основываясь на цепочке специальных операторов (их всего 7 штук). Данные выборки передаются из выхода одного оператора на вход другого, совсем как в unix. Отчасти при помощи новых операторов можно повторить уже существующие. Пусть коллекция test — это коллекция для хранения данных о людях. Стандартная выборка:

    db.test.find({name: "Ivan"});

    будет аналогична

    db.test.aggregate({$match: {name: "Ivan"}});

    Но все немного интереснее, потому что во втором примере мы можем строить цепочку обработки данных, перечисляя операторы через запятую. Для сортировки предназначен оператор $sort, например:

    db.test.aggregate({$match: {name: "Ivan"}}, {$sort: {age: 1}});

    Так мы веберем всех людей с именем «Ivan» и отсортируем выборку по возрасту. А для того, что бы выбрать самого старшего Ивана нам надо отсечь выборку одним элементом:

    db.test.aggregate({$match: {name: "Ivan"}}, {$sort: {age: -1}}, {$limit: 1});

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

    Оператор $project


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

    {$project: {name: 1, age: 1}}

    На вход следующего оператора попадут все документы только с двумя полями, других полей в потоке не будет (за исключением поля _id, что бы его исключить надо специально указать _id: 0). Цифра 1 включает, цифра 0 исключает передачу поля. Кроме того этот оператор позволяет переименовывать поля, «доставать» поля из вложенного объекта какого-либо поля или же добавлять новые поля на основе каких-либо вычислений.

    Оператор $unwind


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

    db.test.insert({name: "Ivan", likes: ["Maria", "Anna"]});
    db.test.insert({name: "Serge", likes: ["Anna"]});

    Пусть поле likes означает какие девочки нравятся какому мальчику. Применим оператор $unwind:

    db.test.aggregate({$unwind: "$likes"});

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f598de76a8f8bc74573e9fd"),
                            "name" : "Ivan",
                            "likes" : "Maria"
                    },
                    {
                            "_id" : ObjectId("4f598de76a8f8bc74573e9fd"),
                            "name" : "Ivan",
                            "likes" : "Anna"
                    },
                    {
                            "_id" : ObjectId("4f598e086a8f8bc74573e9fe"),
                            "name" : "Serge",
                            "likes" : "Anna"
                    }
            ],
            "ok" : 1
    }

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

    Оператор $group


    Для удобства дополним выборку еще одним полем заполненным цифрой 1 (так проще будет суммировать):

    db.test.aggregate({$unwind: "$likes"}, {$project: {name:1, likes:1, count: {$add: [1]}}});

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f598de76a8f8bc74573e9fd"),
                            "name" : "Ivan",
                            "likes" : "Maria",
                            "count" : 1
                    },
                    {
                            "_id" : ObjectId("4f598de76a8f8bc74573e9fd"),
                            "name" : "Ivan",
                            "likes" : "Anna",
                            "count" : 1
                    },
                    {
                            "_id" : ObjectId("4f598e086a8f8bc74573e9fe"),
                            "name" : "Serge",
                            "likes" : "Anna",
                            "count" : 1
                    }
            ],
            "ok" : 1
    }

    Это позволит нам использовать оператор агрегирования $sum. То есть теперь мы просто добавляем в поле number значение поля count каждый раз и группируем всю выборку по полю likes, содержающую имя девочки.

    db.test.aggregate({$unwind: "$likes"}, {$project: {name:1, likes:1, count: {$add: [1]}}}, {$group: {_id: "$likes", number: {$sum: "$count"}}});

    {
            "result" : [
                    {
                            "_id" : "Anna",
                            "number" : 2
                    },
                    {
                            "_id" : "Maria",
                            "number" : 1
                    }
            ],
            "ok" : 1
    }

    Осталось отсортировать и ограничить вывод только одним документом:

    db.test.aggregate({$unwind: "$likes"}, {$project: {name:1, likes:1, count: {$add: [1]}}}, {$group: {_id: "$likes", number: {$sum: "$count"}}}, {$sort: {number: -1}}, {$limit: 1});


    { "result" : [ { "_id" : "Anna", "number" : 2 } ], "ok" : 1 }


    Наша самая популярная девочка — это Анна.

    А теперь конкретный пример.



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

    db.zoo.insert({name: "Lion", ration: [{meat: 20}, {fish: 1}, {water: 30}], holidays: [1,4], staff: {like:  ["Petrovich", "Mihalich"], dislike: "Maria"}});
    db.zoo.insert({name: "Tiger", ration: [{meat: 15}, {water: 25}], holidays: [6], staff: {like:  ["Petrovich", "Maria"]}});
    db.zoo.insert({name: "Monkey", ration: [{banana: 15}, {water: 10}, {nuts: 1}], holidays: [2], staff: {like:  ["Anna"], dislike: "Petrovich"}});
    db.zoo.insert({name: "Panda", ration: [{bamboo: 15}, {dumplings: 50}, {water: 3}], staff: {like:  ["Petrovich", "Mihalich", "Maria", "Anna"]}});

    Поле name хранит имя, поле ration это массив объектов хранящих сколько и какой еды требуется зверю ежедневно, holidays это дни в которые зверь отдыхает и не показывается посетителям, staff.like — смотрители, которые ему нравятся (панды, очаровашки, любят вапще всех-всех), staff.dislike — не нравятся.

    Начнем с простой выборки — только названия животных, что бы директор зоопарка не забывал кого как зовут. Тут все просто:

    db.zoo.aggregate({$project: {name: 1}});

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f58b7f627f86b11258dc70c"),
                            "name" : "Lion"
                    },
                    {
                            "_id" : ObjectId("4f58b86027f86b11258dc70d"),
                            "name" : "Tiger"
                    },
                    {
                            "_id" : ObjectId("4f58b90c27f86b11258dc70e"),
                            "name" : "Monkey"
                    },
                    {
                            "_id" : ObjectId("4f58b98727f86b11258dc70f"),
                            "name" : "Panda"
                    }
            ],
            "ok" : 1
    }


    Каких зверей надо бояцца?


    Бояться надо хищников. А хищник это тот, у кого в рационе есть мясо. Давайте их найдем. Для начала отфильтруем поток и выделим только два поля в документах — имя и рацион.

    db.zoo.aggregate({$project: {name: 1, _id: 0, ration: 1}});

    {
            "result" : [
                    {
                            "name" : "Lion",
                            "ration" : [
                                    {
                                            "meat" : 20
                                    },
                                    {
                                            "fish" : 1
                                    },
                                    {
                                            "water" : 30
                                    }
                            ]
                    },
                    {
                            "name" : "Tiger",
                            "ration" : [
                                    {
                                            "meat" : 15
                                    },
                                    {
                                            "water" : 25
                                    }
                            ]
                    },
                    {
                            "name" : "Monkey",
                            "ration" : [
                                    {
                                            "banana" : 15
                                    },
                                    {
                                            "water" : 10
                                    },
                                    {
                                            "nuts" : 1
                                    }
                            ]
                    },
                    {
                            "name" : "Panda",
                            "ration" : [
                                    {
                                            "bamboo" : 15
                                    },
                                    {
                                            "dumplings" : 50
                                    },
                                    {
                                            "water" : 3
                                    }
                            ]
                    }
            ],
            "ok" : 1
    }


    Затем развернем массив рациона на элементы основного массива:

    db.zoo.aggregate({$project: {name: 1, _id: 0, ration: 1}}, {$unwind: "$ration"});

    {
            "result" : [
                    {
                            "name" : "Lion",
                            "ration" : {
                                    "meat" : 20
                            }
                    },
                    {
                            "name" : "Lion",
                            "ration" : {
                                    "fish" : 1
                            }
                    },
                    {
                            "name" : "Lion",
                            "ration" : {
                                    "water" : 30
                            }
                    },
                    {
                            "name" : "Tiger",
                            "ration" : {
                                    "meat" : 15
                            }
                    },
                    {
                            "name" : "Tiger",
                            "ration" : {
                                    "water" : 25
                            }
                    },
                    {
                            "name" : "Monkey",
                            "ration" : {
                                    "banana" : 15
                            }
                    },
                    {
                            "name" : "Monkey",
                            "ration" : {
                                    "water" : 10
                            }
                    },
                    {
                            "name" : "Monkey",
                            "ration" : {
                                    "nuts" : 1
                            }
                    },
                    {
                            "name" : "Panda",
                            "ration" : {
                                    "bamboo" : 15
                            }
                    },
                    {
                            "name" : "Panda",
                            "ration" : {
                                    "dumplings" : 50
                            }
                    },
                    {
                            "name" : "Panda",
                            "ration" : {
                                    "water" : 3
                            }
                    }
            ],
            "ok" : 1
    }


    Далее отфильтруем выборку только по тем полям, где есть поле ration.meat

    db.zoo.aggregate({$project: {name: 1, _id: 0, ration: 1}}, {$unwind: "$ration"}, {$match: {"ration.meat": {$exists: true}}});

    {
            "result" : [
                    {
                            "name" : "Lion",
                            "ration" : {
                                    "meat" : 20
                            }
                    },
                    {
                            "name" : "Tiger",
                            "ration" : {
                                    "meat" : 15
                            }
                    }
            ],
            "ok" : 1
    }


    И окончательный вывод только имени хищника

    db.zoo.aggregate({$project: {name: 1, _id: 0, ration: 1}}, {$unwind: "$ration"}, {$match: {"ration.meat": {$exists: true}}}, {$project: {name: 1, _id: 0}});

    {
            "result" : [
                    {
                            "name" : "Lion"
                    },
                    {
                            "name" : "Tiger"
                    }
            ],
            "ok" : 1
    }


    В какие дни отдыхает хотя бы один зверь?


    Для этого «расслоим» массив holidays на весь массив зверей (панда как обычно доступна всем и всегда).

    db.zoo.aggregate({$project: {name: 1, holidays: 1}}, {$unwind: "$holidays"});

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f58b7f627f86b11258dc70c"),
                            "name" : "Lion",
                            "holidays" : 1
                    },
                    {
                            "_id" : ObjectId("4f58b7f627f86b11258dc70c"),
                            "name" : "Lion",
                            "holidays" : 4
                    },
                    {
                            "_id" : ObjectId("4f58b86027f86b11258dc70d"),
                            "name" : "Tiger",
                            "holidays" : 6
                    },
                    {
                            "_id" : ObjectId("4f58b90c27f86b11258dc70e"),
                            "name" : "Monkey",
                            "holidays" : 2
                    },
                    {
                            "_id" : ObjectId("4f58b98727f86b11258dc70f"),
                            "name" : "Panda"
                    }
            ],
            "ok" : 1
    }


    И отфильтруем только те, где поле holidays это число большее -1 (ну или 0, кому как удобнее)

    db.zoo.aggregate({$project: {name: 1, holidays: 1}}, {$unwind: "$holidays"},{$match: {holidays : {$gt: -1}}});

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f58b7f627f86b11258dc70c"),
                            "name" : "Lion",
                            "holidays" : 1
                    },
                    {
                            "_id" : ObjectId("4f58b7f627f86b11258dc70c"),
                            "name" : "Lion",
                            "holidays" : 4
                    },
                    {
                            "_id" : ObjectId("4f58b86027f86b11258dc70d"),
                            "name" : "Tiger",
                            "holidays" : 6
                    },
                    {
                            "_id" : ObjectId("4f58b90c27f86b11258dc70e"),
                            "name" : "Monkey",
                            "holidays" : 2
                    }
            ],
            "ok" : 1
    }


    Уберем все лишнее.

    db.zoo.aggregate({$project: {name: 1, holidays: 1}}, {$unwind: "$holidays"},{$match: {holidays : {$gt: -1}}}, {$project: {holidays: 1, _id: 0}});

    {
            "result" : [
                    {
                            "holidays" : 1
                    },
                    {
                            "holidays" : 4
                    },
                    {
                            "holidays" : 6
                    },
                    {
                            "holidays" : 2
                    }
            ],
            "ok" : 1
    }


    Сколько продуктов в день необходимо закупать.


    Самая интересная, на мой взгляд, задача. Для ее реализации вспомним, что $project умеет создавать поля и создадим поле meat со значением свойства meat.

    db.zoo.aggregate({$project: {ration: 1, _id: 0}}, {$unwind: "$ration"}, {$project: {ration: 1, meat: "$ration.meat", _id: 0}});
    


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

    {
            "result" : [
                    {
                            "ration" : {
                                    "meat" : 20
                            },
                            "meat" : 20
                    },
                    {
                            "ration" : {
                                    "fish" : 1
                            }
                    },
                    {
                            "ration" : {
                                    "water" : 30
                            }
                    },
    ...
    }


    Поступим таким образом для всех типов еды и уберем вывод самого объекта ration:

    db.zoo.aggregate({$project: {ration: 1}}, {$unwind: "$ration"}, {$project: {ration: 0, _id: 0, meat: "$ration.meat", fish: "$ration.fish", water: "$ration.water", banana: "$ration.banana", bamboo: "$ration.bamboo", nuts: "$ration.nuts", dumplings: "$ration.dumplings", _id: 0}});


    в результате получим

    {
            "result" : [
                    {
                            "_id" : ObjectId("4f58e58227f86b11258dc713"),
                            "meat" : 20
                    },
                    {
                            "_id" : ObjectId("4f58e58227f86b11258dc713"),
                            "fish" : 1
                    },
                    {
                            "_id" : ObjectId("4f58e58227f86b11258dc713"),
                            "water" : 30
                    },
                    {
                            "_id" : ObjectId("4f58e5e127f86b11258dc714"),
                            "meat" : 15
                    },
                    {
                            "_id" : ObjectId("4f58e5e127f86b11258dc714"),
                            "water" : 25
                    },
                    {
                            "_id" : ObjectId("4f58e60027f86b11258dc715"),
                            "banana" : 15
                    },
                    {
                            "_id" : ObjectId("4f58e60027f86b11258dc715"),
                            "water" : 10
                    },
                    {
                            "_id" : ObjectId("4f58e60027f86b11258dc715"),
                            "nuts" : 1
                    },
                    {
                            "_id" : ObjectId("4f58e64a27f86b11258dc716"),
                            "bamboo" : 15
                    },
                    {
                            "_id" : ObjectId("4f58e64a27f86b11258dc716"),
                            "dumplings" : 50
                    },
                    {
                            "_id" : ObjectId("4f58e64a27f86b11258dc716"),
                            "water" : 3
                    }
            ],
            "ok" : 1
    }


    Осталось лишь сложить/сгруппировать все это дело при помощи функции $group. Указание поля _id в группировке здесь обязательно, но нам оно в принципе не нужно, поэтому пусть это будет какая-нибудь ерунда. Для каждого типа еды создаем соответствующее поле для суммирования отдельных рационов каждого животного:

    db.zoo.aggregate({$project: {ration: 1}}, {$unwind: "$ration"}, {$project: {ration: 0, _id: 0, meat: "$ration.meat", fish: "$ration.fish", water: "$ration.water", banana: "$ration.banana", bamboo: "$ration.bamboo", nuts: "$ration.nuts", dumplings: "$ration.dumplings"}}, {$group: {_id: "s", sum_meat: {$sum: "$meat"}, sum_fish: {$sum: "$fish"}, sum_water: {$sum: "$water"}, sum_banana: {$sum: "$banana"}, sum_nuts: {$sum: "$nuts"}, sum_bamboo: {$sum: "$bamboo"}, sum_dumplings: {$sum: "$dumplings"}}});

    {
            "result" : [
                    {
                            "_id" : "s",
                            "sum_meat" : 35,
                            "sum_fish" : 1,
                            "sum_water" : 68,
                            "sum_banana" : 15,
                            "sum_nuts" : 1,
                            "sum_bamboo" : 15,
                            "sum_dumplings" : 50
                    }
            ],
            "ok" : 1
    }


    Самый любимый смотритель


    Фильтруем по полям и разматываем массив staff.like:

    db.zoo.aggregate({$project: {name: 1, _id: 0, "staff.like": 1}}, {$unwind: "$staff.like"});

    Вспоминаем, что $project умеет поднимать поле на уровень вверх:

    db.zoo.aggregate({$project: {name: 1, _id: 0, "staff.like": 1}}, {$unwind: "$staff.like"}, {$project: {_id: 0, name: "$staff.like"}});

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

    {
            "result" : [
                    {
                            "name" : "Petrovich"
                    },
                    {
                            "name" : "Mihalich"
                    },
                    {
                            "name" : "Petrovich"
                    },
                    {
                            "name" : "Maria"
                    },
                    {
                            "name" : "Anna"
                    },
                    {
                            "name" : "Petrovich"
                    },
                    {
                            "name" : "Mihalich"
                    },
                    {
                            "name" : "Maria"
                    },
                    {
                            "name" : "Anna"
                    }
            ],
            "ok" : 1
    }


    Теперь необходимо просуммировать эти поля. Но так просто это не сделать, так как у нас нет поля для суммирования, поэтому создаем это поле при уже известной фишки.

    db.zoo.aggregate({$project: {name: 1, _id: 0, "staff.like": 1}}, {$unwind: "$staff.like"}, {$project: {_id: 0, name: "$staff.like", count: {$add: [1]}}});

    В результате к каждому объекту добавится еще одно поле count со значением 1. Группируем и суммируем:

    db.zoo.aggregate({$project: {name: 1, _id: 0, "staff.like": 1}}, {$unwind: "$staff.like"}, {$project: {_id: 0, name: "$staff.like", count: {$add: [1]}}}, {$group: {_id: "$name", num: {$sum: "$count"}}});

    Сортируем и ограничиваем вывод самым первым элементом

    db.zoo.aggregate({$project: {name: 1, _id: 0, "staff.like": 1}}, {$unwind: "$staff.like"}, {$project: {_id: 0, name: "$staff.like", count: {$add: [1]}}}, {$group: {_id: "$name", num: {$sum: "$count"}}}, {$sort: {num: -1}}, {$limit: 1});

    И получим следующее:

    { "result" : [ { "_id" : "Petrovich", "num" : 3 } ], "ok" : 1 }


    Вот собственно и все. Для интересующихся есть два простеньких доклада на английском по этой теме: раз и два.

    Если честно то MongoDB мне очень нравится, хотя мы использовали его только на части проекта для хранения разрозненных данных. Те же Map/Reduce для меня всегда были чем-то страшным и непонятным, но новая штука агрегирования данных позволяет частично исключить JavaScript, потому что так или иначе он язык интерпретируемый, а потому медленный и заменить его уже готовыми, а значит быстрыми, элементами языка.

    P.S. Стоит отметить что версия 2.1 пока что достаточно сырая. Я постоянно получал всякие исключения по assertion failed. Но я думаю, что в 2.2 это наконец-то будет стабильно и клево.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 16
    • +1
      Осталось узнать какова производительность этого, гм, фреймворка. Пока чем-то напоминает как раз тот самый reduce.
      Думаю вы зря так про js с Map-Reduce, правда я его использую в CouchDB и там не ad-hoc.
      • 0
        В кауче божественный map-reduce!
        • +6
          Производительность относительно M/R выше. Пока еще достаточно много ограничений, но по моим тестам на моих данных быстрее от 2 до 10 раз. Хотя заметно, что кое-где пока что не использует индексы. Впрочем, от distinct мне тоже не удалось добиться консистентной работы с индексами.
          • 0
            c текущим map/reduce еще ведь/ проблема что одновременно выполняется только 1 map reduce, а если новые функции не используют javascript то такого ограничения там не будет.
          • 0
            А если в процессе агрегации какой-то элемент изменяет свои данные, как она себя ведёт?
            • 0
              Думаю, агрегация вешает write lock.
            • +2
              Главное использовать подходящие инструменты в любой ситуации, иначе получается вот это:

              Must. Use. Mongo. DB. It. Is. Web. Scale. www.youtube.com/watch?v=b2F-DItXtZs
            • +5
              Я вот думаю: а не проще ли было реализовать SQL синтаксис. Он многим знаком, привычен.

              У меня есть сомнения насчет читабельности и сопровождаемости вот такого:
              db.zoo.aggregate({$project: {name: 1, holidays: 1}}, {$unwind: "$holidays"},{$match: {holidays : {$gt: -1}}}, {$project: {holidays: 1, _id: 0}});
              В реальных проектах это будет в 10 раз сложнее.
              • +2
                Как раз наоборот, каждый шаг прост и понятен. А SQL обычно превращается в гигантский «blob-запрос», который нельзя трогать руками иначе всё сломается. Ну и здесь еще соль в том, что всё поддаётся концепции map/reduce (по сути, этот язык — надстройка над типичными операциями map/reduce), а потому все данные проходят через некоторые пайпы, а значит всё прозрачно с точки зрения памяти, необходимой для обработки данных.
                • +4
                  Читаемость sql, вы шутите?
                  Не самый удачный пример sql запроса, но такое тоже бывает:

                  select left_id, right_id from
                  (
                  select max(W.COUNTS.TIME_START) maxtime, W.COUNTS.left_id, W.COUNTS.right_id  from
                  (
                  select correct.* from
                  (
                  select sub1.* from
                  (
                  select W.COUNTS.left_id, W.COUNTS.right_id from
                  W.COUNTS, W.ORDER, W.CLIENT
                  where W.CLIENT.NET = 2
                  and W.ORDER.ID = W.CLIENT.ID
                  and W.COUNTS.ID = W.ORDER.ID
                  group by W.COUNTS.left_id, W.COUNTS.right_id
                  ) sub1, W.CHANGES
                  where W.CHANGES.left_id = sub1.left_id
                  and W.CHANGES.right_id = sub1.right_id
                  and (W.CHANGES.TYPE = 1 or W.CHANGES.TYPE = 2 or W.CHANGES.TYPE = 0)
                  and [CHTIME] > '2010-1-1' and [CHTIME] < '2010-9-1'
                  )correct, W.EMPLOYEES
                  where W.EMPLOYEES.left_id = correct.left_id
                  and W.EMPLOYEES.right_id = correct.right_id
                  and W.EMPLOYEES.STATE <= 3
                  )sub2, W.COUNTS
                  where W.COUNTS.left_id = sub2.left_id
                  and W.COUNTS.right_id = sub2.right_id
                  group by W.COUNTS.left_id, W.COUNTS.right_id
                  )wtime
                  where maxtime < '2012-2-27'
                  
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • НЛО прилетело и опубликовало эту надпись здесь
                  • +2
                    Такие структуры кстати проще будет генерировать, чем SQL.
                  • 0
                    Спасибо за статью.
                    +1 в карму ;)
                    • 0
                      спасибо автору, очень полезная статья

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