28 февраля 2016 в 07:12

Сделай сам: SQL JOIN на Java tutorial

Я часто собеседую разработчиков и часто задаю им простой, как кувалда, вопрос — как внутри работает JOIN в SQL? В ответ я обычно слышу бессвязное мычание про волшебные деревья и индексы, которые быстрее. Когда-то мне казалось, что каждый программист специалист должен знать то, с чем работает. Впоследствии жизнь объяснила мне, что это не так. Но мне все еще не понятно, как можно годами теребить базёнку, даже не догадываясь, а что там у нее «под капотом»?

Давайте проведем ликбез и вместе посмотрим, как же работают эти джойны, и даже сами реализуем парочку алгоритмов.

SQL JOIN

Постановка задачи


Людям, которые утверждают, что много и плотно работали с SQL, я задаю на собеседованиях вот такую задачу.
Есть SQL команда
SELECT
  left.K,
  left.V1,
  right.V2
FROM left
JOIN right
  ON left.K = right.K;

Нужно выполнить то же самое на Java, т.е. реализовать метод
<K, V1, V2> List<Triple<K, V1, V2>> join(List<Pair<K, V1>> left, List<Pair<K, V2>> right);

Я не прошу прям закодить реализацию, но жду хотя бы устного объяснения алгоритма.

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

Давайте сначала разберемся, для чего нам вообще думать об устройстве джойнов?
  1. Знать теорию полезно из чисто познавательных соображений.
  2. Если вы различаете типы джойнов, то план выполнения запроса, который получается командой EXPLAIN, больше не выглядит для вас как набор непонятных английских слов. Вы можете видеть в плане потенциально медленные места и оптимизировать запрос переписыванием или хинтами.
  3. В новомодных аналитических инструментах поверх Hadoop планировщик запросов или малость туповат (см. Cloudera Impala), или его вообще нет (см. Twitter Scalding, Spark RDD). В последнем случае приходится собирать запрос вручную из примитивных операций.
  4. Наконец, есть риск, что однажды вы попадете на собеседование ко мне или к другому зануде. Но на самом деле, статья не про собеседования, а про операцию JOIN.


Nested Loops Join


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

public static  <K, V1, V2> List<Triple<K, V1, V2>> nestedLoopsJoin(List<Pair<K, V1>> left, List<Pair<K, V2>> right) {
    List<Triple<K, V1, V2>> result = new ArrayList<>();
    for (Pair<K, V1> leftPair: left) {
        for (Pair<K, V2> rightPair: right) {
            if (Objects.equals(leftPair.k, rightPair.k)) {
                result.add(new Triple<>(leftPair.k, leftPair.v, rightPair.v));
            }
        }
    }
    return result;
}

Основной плюс этого метода — полное безразличие к входным данным. Алгоритм работает для любых двух таблиц, не требует никаких индексов и перекладываний данных в памяти, а также прост в реализации. На практике это означает, что достаточно просто бежать по диску двумя курсорами и периодически выплёвывать в сокет совпадения.

Однако ж, фатальный минус алгоритма — высокая временная сложность O(N*M) (квадратичная асимптотика, если вы понимаете, о чем я). Например, для джойна пары небольших таблиц в 100к и 500к записей потребуется сделать аж 100.000 * 500.000 = 50.000.000.000 (50 млрд) операций сравнения. Запросы с таким джойном будут выполняться невежливо долго, часто именно они — причина беспощадных тормозов кривеньких самописных CMS’ок.

Современные РСУБД используют nested loops join в самых безнадежных случаях, когда не удается применить никакую оптимизацию.

UPD. zhekappp и potapuff поправляют, что nested loops эффективен для малого числа строк, когда разворачивать какую-либо оптимизацию выйдет дороже, чем просто пробежаться вложенным циклом. Есть класс систем, для которых это актуально.

Hash Join


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

Проверим размер обоих списков. Возьмем меньший из списков, прочтем его полностью и загрузим в память, построив HashMap. Теперь вернемся к большему списку и пойдем по нему курсором с начала. Для каждого ключа проверим, нет ли такого же в хеш-таблице. Если есть — запишем совпадение в результирующую таблицу.

Временная сложность этого алгоритма падает до линейной O(N+M), но требуется дополнительная память.

public static  <K, V1, V2> List<Triple<K, V1, V2>> hashJoin(List<Pair<K, V1>> left, List<Pair<K, V2>> right) {
    Map<K, V2> hash = new HashMap<>(right.size());
    for (Pair<K, V2> rightPair: right) {
        hash.put(rightPair.k, rightPair.v);
    }

    List<Triple<K, V1, V2>> result = new ArrayList<>();
    for (Pair<K, V1> leftPair: left) {
        if (hash.containsKey(leftPair.k)) {
            result.add(new Triple<>(leftPair.k, leftPair.v, hash.get(leftPair.k)));
        }
    }

    return result;
}

Что важно, во времена динозавров считалось, что в память нужно загрузить правую таблицу, а итерироваться по левой. У нормальных РСУБД сейчас есть статистика cardinality, и они сами определяют порядок джойна, но если по какой-то причине статистика недоступна, то в память грузится именно правая таблица. Это важно помнить при работе с молодыми корявыми инструментами типа Cloudera Impala.

Merge Join


А теперь представим, что данные в обоих списках заранее отсортированы, например, по возрастанию. Так бывает, если у нас были индексы по этим таблицам, или же если мы отсортировали данные на предыдущих стадиях запроса. Как вы наверняка помните, два отсортированных списка можно склеить в один отсортированный за линейное время — именно на этом основан алгоритм сортировки слиянием. Здесь нам предстоит похожая задача, но вместо того, чтобы склеивать списки, мы будем искать в них общие элементы.

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

public static <K extends Comparable<K>, V1, V2> List<Triple<K, V1, V2>> mergeJoin(
        List<Pair<K, V1>> left,
        List<Pair<K, V2>> right
) {
    List<Triple<K, V1, V2>> result = new ArrayList<>();
    Iterator<Pair<K, V1>> leftIter = left.listIterator();
    Iterator<Pair<K, V2>> rightIter = right.listIterator();
    Pair<K, V1> leftPair = leftIter.next();
    Pair<K, V2> rightPair = rightIter.next();

    while (true)  {
        int compare = leftPair.k.compareTo(rightPair.k);
        if (compare < 0) {
            if (leftIter.hasNext()) {
                leftPair = leftIter.next();
            } else {
                break;
            }
        } else if (compare > 0) {
            if (rightIter.hasNext()) {
                rightPair = rightIter.next();
            } else {
                break;
            }
        } else {
            result.add(new Triple<>(leftPair.k, leftPair.v, rightPair.v));
            if (leftIter.hasNext() && rightIter.hasNext()) {
                leftPair = leftIter.next();
                rightPair = rightIter.next();
            } else {
                break;
            }
        }
    }
    return result;
}

Если данные отсортированы, то временная сложность алгоритма линейная O(M+N) и не требуется никакой дополнительной памяти. Если же данные не отсортированы, то нужно сначала их отсортировать. Из-за этого временная сложность возрастает до O(M log M + N log N), плюс появляются дополнительные требования к памяти.

Outer Joins


Вы могли заметить, что написанный выше код имитирует только INNER JOIN, причем рассчитывает, что все ключи в обоих списках уникальные, т.е. встречаются не более, чем по одному разу. Я сделал так специально по двум причинам. Во-первых, так нагляднее — в коде содержится только логика самих джойнов и ничего лишнего. А во-вторых, мне очень хотелось спать. Но тем не менее, давайте хотя бы обсудим, что нужно изменить в коде, чтобы поддержать различные типы джойнов и неуникальные значения ключей.

Первая проблема — неуникальные, т.е. повторяющиеся ключи. Для повторяющихся ключей нужно порождать декартово произведение всех соответствющих им значений.
В Nested Loops Join почему-то это работает сразу.
В Hash Join придется заменить HashMap на MultiHashMap.
Для Merge Join ситуация гораздо более печальная — придется помнить, сколько элементов с одинаковым ключом мы видели.

Работа с неуникальными ключами увеличивает асимптотику до O(N*m+M*n), где n и m — среднее записей на ключ в таблицах. В вырожденном случае, когда n=N и m=M, операция превращается в CROSS JOIN.

Вторая проблема — надо следить за ключами, для которых не нашлось пары.
Для Merge Join ключ без пары видно сразу для всех направлений JOIN’а.
Для Hash Join сразу можно увидеть нехватку соответствующих ключей при джойне слева. Для того, чтобы фиксировать непарные ключи справа, придется завести отдельный флаг “есть пара!” для каждого элемента хеш-таблицы. После завершения основного джойна надо будет пройти по всей хеш-таблице и добавить в результат ключи без флага пары.

Для Nested Loops Join ситуация аналогичная, причем все настолько просто, что я даже осилил это закодить:

public static  <K, V1, V2> List<Triple<K, V1, V2>> nestedLoopsJoin(
        List<Pair<K, V1>> left,
        List<Pair<K, V2>> right,
        JoinType joinType
) {
    // Массив для обозначения ключей из правого списка, которым не нашлось пары в левом
    BitSet rightMatches = new BitSet(right.size());
    
    List<Triple<K, V1, V2>> result = new ArrayList<>();
    
    for (Pair<K, V1> leftPair: left) {
        // Флаг для обозначения ключей в левом списке, которым не нашлось пары в правом
        boolean match = false;
        for (ListIterator<Pair<K, V2>> iterator = right.listIterator(); iterator.hasNext(); ) {
            Pair<K, V2> rightPair = iterator.next();
            if (Objects.equals(leftPair.k, rightPair.k)) {
                result.add(new Triple<>(leftPair.k, leftPair.v, rightPair.v));
                
                // Отмечаем пары
                match = true;
                rightMatches.set(iterator.previousIndex(), true);
            }
        }
        
        // Заполняем несоответствия в левом списке
        if ((joinType == JoinType.LEFT || joinType == JoinType.FULL) && !match) {
            result.add(new Triple<>(leftPair.k, leftPair.v, null));
        }
    }
    
    // Заполняем несоответствия в правом списке
    if (joinType == JoinType.RIGHT || joinType == JoinType.FULL) {
        for (int i = 0; i < right.size(); ++i) {
            if (!rightMatches.get(i)) {
                Pair<K, V2> rightPair = right.get(i);
                result.add(new Triple<>(rightPair.k, null, rightPair.v));
            }
        }
    }
    return result;
}


Морализаторское послесловие


Если вы дочитали досюда, значит, статья показалась вам интересной. А значит, вы простите мне небольшую порцию нравоучений.

Я действительно считаю, что знания РСУБД на уровне SQL абсолютно не достаточно, чтобы считать себя профессиональным разработчиком ПО. Профессионал должен знать не только свой код, но и примерное устройство соседей по стеку, т.е. 3rd-party систем, которые он использует — баз данных, фреймворков, сетевых протоколов, файловых систем. Без этого разработчик вырождается до кодера или оператора ЭВМ, и в по настоящему сложных масштабных задачах становится бесполезен.

UPD. Несмотря на это послесловие, статья, на самом деле, про JOIN'ы.

Дополнительные материалы




Disclaimer: вообще-то, я обещал еще одну статью про Scalding, но предыдущая не вызвала большого интереса у публики. Из-за этого тему решено было сменить.
Федор Лаврентьев @fediq
карма
32,0
рейтинг 22,8
Data Engineering
Похожие публикации
Самое читаемое Разработка

Комментарии (128)

  • +5
    Профессионал должен знать не только свой код, но и примерное устройство соседей по стеку

    А насколько примерно?
    • +4
      Настолько, чтобы использовать его эффективно и без очевидных ошибок.

      Например, избегать nested loops join в запросах.
      Не создавать каждый раз новый ObjectMapper в Jackson и не гонять по кругу сериализацию.
      Буферизовать IO и не злоупотреблять seek'ами в RandomAccessFile.
      • +1
        nested loops join достаточно эффективный способ, если строк мало. Иначе затраты на сортировку могут быть значительно больше затрат на само соединение.
        • +1
          Да, есть случаи, когда nested loops будет быстрее. Добавил в статью. Спасибо!
  • –4
    Невольно вспомнился Лао Дзы

    Keep sharpening your knife and it will blunt.

    Хотя тренд беспорно правильный любое знание рентабельно. В конце концов можно кого-то порадовать на интервью смекалкой и неплохим опытом.
    • +25
      Он же вроде был китайским философом, а цитата почему-то на английском. Не логичнее ли было бы или по-русски её написать или по-китайски в крайнем случае?
      • +15
        По английски умнее выглядит :)
      • –1
        Потому что эта цитата существует исключительно в английском языке.
      • +2
        Один из вариантов перевода на русский язык:

        Стремясь заточить лезвие до предела, портишь его

        Хотя в оригинале имелось введу про умеренность — не стоит выжимать из вещей их предел. Соблюдай баланс. Дао Дэ Цзин полон таких мыслей.
        • +1
          Тот вариант который ближе по контексту к статье я услышал лет тридцать пять назад от представителя новосибирской научной школы. Полагаю, что перевод был сделан довольно давно и сказавший ссылался на перевод на Лорошфуко "Максимы" (Сборник афоризмов). Есть ли это в Максимах у меня не было времени проверить. Интересно, что англофонов давняя нелюбовь к франкофонам так, что фраза прижилась в английском языке как афоризм Лао Дзы.

          Но тем не менее в русском языке это звучало в тот момент так, что на мой взгляд довольно ближе к англискому оригиналу с передачей ироничной коннотации присущей Лао Дзы

          Если острый нож долго оттачивать он станет тупым.
  • НЛО прилетело и опубликовало эту надпись здесь
  • +36
    А для того, чтобы вскипятить чайник, надо в подробностях знать термодинамику. А для того, чтобы пользоваться компьютером, надо знать электротехнику и электронику. Сколько программистов может вспомнить без гугла законы Кирхгофа?

    А для того, чтобы программировать, надо знать как работает защищенный режим и виртуальная память. А заодно загрузка с жесткого диска с чтением нулевого сектора и планировщик задач. А уж структуру IP-пакета точно должен знать каждый. А также уметь рассказать обо всем этом на собеседовании, и реализовать программно без IDE в Google Docs.
    • НЛО прилетело и опубликовало эту надпись здесь
      • +18
        В чем именно ложность? Вы не используете ОС или сеть?

        Наука и техника развиваются много лет и многими людьми. Невозможно знать все в подробностях.

        Профессионал должен знать не только свой код, но и примерное устройство соседей по стеку

        Вся разница в понимании слова "примерное". Например, в моем понимании "волшебные деревья и индексы" — это и есть примерное устройство (даже с учетом того, что на собеседовании код для nested loops я бы пожалуй написал). Потому что у меня есть конкретное выражение "LEFT JOIN" в синтаксисе, и влиять на его работу я не могу. Когда я пишу запрос с джойнами, я мыслю немного другими категориями, чем вложенные циклы и выражения вида "Triple<K, V1, V2>" — например, множество, тип связи, направление (с какой стороны "1", с какой "много").
        • +7
          Кстати, вопрос к автору. Что именно вы ожидаете услышать в устном ответе на вопрос "как внутри работает JOIN в SQL"? Приведите конкретный пример ответа, пожалуйста. Просто чтобы знать, что ожидает услышать интервьюер.
          • +1
            То есть понятно, что вся статья про это, но в статье в основном код и пояснения к нему.
          • 0
            Для начала, меня устроит устное описание любого из вышеприведенных алгоритмов. Например, нормальный ответ:
            "Берет все записи из левой таблицы и для каждой из них находит соответствующую запись в правой таблице."
            Уже понятно, что человек не мыслит категориями волшебных коробок.

            Далее я попрошу оценить асимптотику этого решения и предложить улучшения.
            • +1
              Если вы ищете программиста, который будет работать с внутренностями БД — тогда есть смысл оценить понимание на уровне написания код для JOIN.
              Если вы ищете программиста, который будет работать с БД снаружи, то навык написания кода для реализации внутренних алгоритмов БД уже не является необходимым.
              Хотя, несомненно, при таком уровне погружения, цена программиста может вырасти ощутимо.

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

              Если же при этом отсеивать тех, кто не может быстро на коленке написать код для реализации алгоритмов JOIN во всех его ипостасях — тогда, на мой взгляд, подавляющее большинство проектов так и не добралось бы до релиза.

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

              Т.е. такой человек пройдёт собеседование? Или всё же без умения написать соответствующий код у него нет шансов?
              • 0
                А, так вот, что всех испугало! Дописал пояснение, спасибо.

                Я не надеюсь, что кто-нибудь мне с ходу напишет код сложнее, чем nested loops. Во-первых, на собеседовании программисты-интроверты находятся в состоянии сильного стресса и "тупят". А во-вторых, на реализацию более сложного алгоритма банально не хватит времени — я писал реализацию merge join для статьи почти полчаса, хотя точно знал, что делать.

                На собеседовании такой роскоши не будет, поэтому мы просто поговорим.

                Что касается того, кого я ищу, то на самом деле, мне плевать на внутренности РСУБД. Я просто пытаюсь понять, насколько глубоко человек изучил те технологии, с которыми он работал. Т.е. если у вас в проекте было много low-level NIO, а с базой вы почти не работали — я не буду ожидать от вас знания Merge Join, но захочу услышать про селекторы и грин-треды.
                • +8
                  Глубокая мудрость реляционной алгебры как раз несёт идею, что "мы определяем, что мы хотим получить, никак не фиксируя непосредственно алгоритм получения". Реальные РСУБД конечно далеки от этого идеала (уже индексы — это шаг в сторону), но всё-таки стремятся к нему.

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

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

                  Вы всё ещё имеете шансы найти горе-программистов, которые ответят вам на вопрос, а потом и реализуют это всё в виде хранимой процедуры, потому, что они не осилили способность самой СУБД соединять таблицы и всегда делают соединения процедурами. А когда их спросят, что это за хрень они тут понаписали, они сошлются на вас — мол, вон, даже на собеседовании это было и надо было делать так!

                  А так делать не надо.
                • +11
                  Я должен отметить, что в общем статья была бы неплоха, если бы из неё убрать весь бред c собеседованием. 100% ваших тупящих просто не поняли, чего от них хотят.

                  Вот просто обзор конкретных алгоритмов, которые там под капотом, с подтекстом "ну конечно всё это СУБД делает за тебя, но неплохо бы знать, как именно она это там делает" — это очень хорошо и важно.
                • +1
                  Тогда лучше спрашивать не "как реализована операция соединения в СУБД", а "как бы вы реализовали операцию соединения". Это переключает мозг с паники по поводу недостатка эрудиции на творческий поиск.
              • –3
                Без представления или умения на ходу прикинуть что же там внутри — программист не сможет говорить о сложности алгоритма. Без разговора о сложности, сугубо на мой взгляд, программист является профнепригодным.
        • –1
          Потому что у меня есть конкретное выражение «LEFT JOIN» в синтаксисе, и влиять на его работу я не могу.
          Можете, конечно. Хинты, индексы, переписать запрос — вот эти способы.
          • +6
            Для этого надо знать особенности хинтов, индексов и анализатора запросов конкретной СУБД. Уметь реализовать join в программном коде для этого не нужно.
            • 0
              Надо знать чем отличается один join от другого. Статья, вроде, как раз об этом.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                Что мне мешает запустить EXPLAIN, увидеть там nested loops, пойти в документацию и прочитать там, что это значит, какие могут быть варианты, и как на это влиять через синтаксис и настройки?
                • НЛО прилетело и опубликовало эту надпись здесь
                  • +7
                    Вы, по-моему, потеряли нить разговора. Вы и автор говорите о том, что для работы с базой данных нужно обязательно знать типы джойнов и уметь реализовать их вручную, иначе разработчик это не разработчик, а оператор ЭВМ. А я говорю, что для работы с базой данных надо знать ее интерфейс, а не реализацию, и что этого достаточно во многих случаях. И что всю необходимую информацию можно узнать по ходу дела, когда это действительно понадобится. И что если человек встретился с вами на собеседовании раньше, чем со сложным запросом, для оптимизации которого надо знать типы реализации джойнов, то это не значит, что он не умеет ими пользоваться, и можно называть его оператором ЭВМ, а не разработчиком.

                    И да, перестаньте, пожалуйста, разговаривать с собеседником в подобном тоне. Я вас не оскорблял.
                    • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Некоторые аналогии достаточно ложные, потому как вы скачете через несколько уровней абстракции. Когда вы в своем приложение работаете с БД и вам важно выстроить правильную архитектуру приложения и модели данных для вашей нагрузки, имеет смысл инвестировать в понимание того как эта БД работает внутри. При этом не так важно знать протоколы SATA-3 по которому данные пойдут между памятью и жестким диском. Тут здравый смысл вполне помощник что нужно изучать, а что нет.

          Абстракции создают комфорт работы, но забивать на их устройство можно если не предполагается никаких проблем с производительностью и ресурсами.
    • +4
      Если продолжить аналогии, для того что бы быть в сознании, нужно понимать как оно работает :)
  • +28
    Уважаемый Фёдор, а вы сами-то пройдёте собеседование с подобным экспертом?

    Сейчас кризис, всё может быть. Например развалится ваша контора, пойдёте в другую, а там захотят от вас услышать чёткий ответ на простой вопрос: "Зачем использовать cross join в SQLite?". Ну ка, быстро, без гугла.

    Абсолютное большинство "специалистов" проводит собеседование сходным образом — вы даже не в состоянии заранее выслать список тем о которых идёт разговор. И ожидаете ответов которые в точности совпадают с вашим мнением.

    Ну и подойдёт вам только кандидат который уже проработал у вас на этой же должности два года, т.е. собственный работник который недавно уволился т.к. з/п маленькая.
    • +2
      Я иногда спрашиваю и то, чего не знаю, особенно если вижу, что кандидат крут. Перед этим сразу говорю «я не знаю правильного ответа, мне интересно ваше мнение».
    • –4
      Я не требую досконального знания каких-либо деталей. Но если кандидат заявляет, что он три года оптимизировал запросы в Oracle, он должен понимать вывод EXPLAIN'а, а значит, должен различать типы JOIN'ов. Если это не так, он либо дилетант, либо лжец.

      При этом я готов простить человеку даже непонимание разницы между float и double, если в остальном он молодец.
      Дилетанты нам не нужны, а недостающие знания мы всегда подкачаем.
      • +8
        вау. Ну, к собеседованиям вас точно нельзя подпускать.
        • +1
          А что именно вам показалось неправильным? По моему все логично. Насчет подпускать или нет — практика критерий истинности. Если автор с помощью своего метода ведения собеседований набрал людей которые его устраивают, то у него нет причин не самого себя не подпускать.
          • +1
            а его начальство те, кого он набрал, устраивают?
            • –1
              Этого я не могу знать, как впрочем и своего предположения. Все что я хотел сказать, это то что оценить мастерство ведения собеседования можно в конечном итоге по тому, как работают люди принятые на работу, ответствуют ли они ожиданиям или нет.
              • +4
                не согласен.

                Автор предлагает оценивать степень владения азами оптимизации БД по умению рисовать какие-то циклы на Java, причём делает это в безапелляционной форме. Он ошибается в техническом плане сам, кроме того, тех кто не согласен называет дилетантами и лжецами.

                Двойное попадание, тут уж никаких сомнений быть не может.
                • 0
                  Где я ошибаюсь?

                  Про дилетантов и лжецов был вполне конкретный контекст. Про согласен/не согласен я ничего не говорил.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Вы что-то странное написали. (+) — это местный (нестандартный) синтаксис для LEFT/RIGHT JOIN.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Что вы хотели сказать-то это ссылкой?
    • +3
      Это еруйня. С верии 10 Oracle планировщик (Cost-based optimizer) не различает вообще как вы соединяете таблицы: JOIN-ом, WHERE, используете IN или SELECT FROM subquery. Также нет разницы между порядком указания таблиц, даже если их объем сильно различается. Планировщик все приводит к стандартному виду и оптимизирует на основе статистики. Если Вы старую версию Oracle, либо указываете в запросе явно использовать Rule-based optimizer, тогда да — запросы выполняются на основе списка правил. В редких случаях это имеет смысл.
      • +1
        Причем в 11ом rule-based уже и нет совсем, если правильно помню.
        • 0
          Он там все еще есть, но отключается автоматически, если в запросе используется какая-то несовместимая с ним фича.

          CBO вообще не такой умный как кажется, поэтому нам пришлось для некоторых запросов ставить /*+Rule*/, чтобы выполнение было всегда оптимальным и предсказуемым. CBO почему-то для некоторых запросов периодически выбирал неоптимальный план, а для нескольких всегда следовал неоптимальному плану, даже при собранной статистике.
  • +32
    Был Петя, который пытался изучить изнутри все технологии, которые использовал в проекте. Его конкурент Бил особо никогда не задумывался над начинкой инструментов используемых, поэтому выпустил свой продукт раньше. Да, забагованный, да медленный. Но со временем он улучшил продукт, а Петя так и не закончил свой проект.

    Но мне все еще не понятно, как можно годами теребить базёнку, даже не догадываясь, а что там у нее «под капотом»?
    Я такое часто слышу от людей, которые никогда не участвовали в разработке больших проектов. Чтобы работать с базой мне надо знать, как с ней работать, мне нет надобности залезать внутрь, так как банально нет времени. Да, мне надо знать, какие запросы медленные, какие быстрее и т.п. Особой надобности глубоко копать, чтоб понять почему так, нет. Естественно, я это сделаю, когда (и если) у меня будет время.

    Можете кидаться тапками, но эйфория и желание познать начинку используемых инструментов проходит в первые годы после окончания университета и после начала работать над огромным проектом, особенно если вы является сооснователем. Любопытство никуда не пропадает, но как у соавтора у вас есть ответственность и, банально, сроки. Клиент не будет ждать, пока вы тешите своё любопытство.
    • –7
      Подобный подход прокатывает на ранних стадиях. На проектах посложнее однажды появляется архитектор или ведущий разработчик, который компенсирует нехватку компетенции основателя.
      • +7
        Ведущий разработчик во всех областях? Чтобы не быть голословным, у нас в проекте, к примеру, используется как минимум 4 базы разных: Riak, Rethinkdb, Redis, MySQL. Хотел бы я посмотреть на разработчика, который бы был в курсе начинки всех этих баз. Это не говоря уже про весь остальной стек, который мы используем.
        • –1
          Я могу на собеседовании вести достаточно детальный разговор про устройство трех баз из вашего списка.
          Базы данных — достаточно узкая область, чтобы ее можно было изучить за вменяемое время.
          • +7
            Базы данных — достаточно узкая область, чтобы ее можно было изучить за вменяемое время.

            Ну если вы ничего больше не изучаете, то может быть. Да и то — неоднозначно, слишком уж понятие "базы данных" широкое.
            • 0
              Основная характеристика базы данных — "как хранятся данные", "как оптимизируется поиск" (индекс). Зная хотя бы примерно эти два параметра — можно говорить о том насколько сложным будет выполнение запроса и как он будет отжирать память\проц, во что упрётся и что можно сделать когда данных станет слишком много.
              • 0
                Особенности реализации конкурентной работы Вы отнести к "как оптимизируется поиск" или забыли?
                • 0
                  Скорее к "как хранятся данные". Если включить сюда и доступ к данным. А если база разрастается, то там ещё вылезает cap теорема с вершинами в консистентности, скорости и надёжности.
                  • +3
                    … и вы серьезно считаете, что знать "как хранятся данные" для всех (окей, хотя бы всех распространенных) БД, включая конкурентность и распределенность — это маленький объем?

                    (а CAP-треугольник вообще хреново людьми осознается)
          • +2
            Базы данных — достаточно узкая область, чтобы ее можно было изучить за вменяемое время.
            смеха ради, с чего вы такое вообще взяли? Поверьте, вы в своём посте далеко не все варианты даже перечислили… Интересно где вы вообще бигдату экспертите?
    • –5
      Был Петя, который пытался изучить изнутри все технологии, которые использовал в проекте. Его конкурент Бил особо никогда не задумывался над начинкой инструментов используемых, поэтому выпустил свой продукт раньше.

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

      По моему это абсолютно нормально знать как устроенно то с чем ты непосредственно работаешь как профессионал. Я также спрашиваю на собеседование сложность вставки элемента в середину списка, массива, хэш-таблицы.
  • +3
    Парадокс в том — что Join-ы вообще нельзя использовать на продакшене с большим числом хитов.
    Да и это скорее всего просто невозможно по причине сложного шардинга данных и архитектуры не
    позволяющей такой связанности.
    К этому сам приходишь — да и послушав/почитав об архитектуре нагруженных проектов (таких как соцсети)
    • +1
      Согласен.

      Меня Join'ы интересуют больше в разрезе офлайн аналитики, различных SQL поверх Hadoop. Там криво написанное условие превращается в лишние каскады map-reduce'ов и множественные OOM'ы.
    • +4
      И как это у вас получается? Дизайн реляционной базы отличается от NoSql тем, что все сущности нормализованы и распеханы по разным таблицам, тогда как NoSQL может разные сущности иерархически в одной коллекции. И чтобы получить мало-мальски релевантную информацию, всегда приходится запрашивать данные сразу из нескольких таблиц. Чтобы получить список заказов, придется запрашивать также таблицу клиентов, т.к. нам хочется показать его имя, а не FK ID. Без JOIN-ов это можно сделать лишь для каждого заказа запрашивать клиента из таблицы (проблема N+1 query), либо объединить необходимых клиентов в один запрос: SELECT… FROM Clients WHERE id IN(?,?,...). Но в любом случае JOIN будет быстрее на порядки.
      Так что если Вы пользуетесь реляционной базой, глупо не пользоваться стандартным функционалом в угоду предрассудкам. Другое дело, если реляционная модель уже не подходит решаемой задаче.
      • 0
        Да просто — делается денормализация там где нужно.
      • 0
        Для каждого заказа и запрашивать. Только не из базы, а из кеша в оперативной памяти. Из этого же кеша смогут запрашивать и другие списки или отчеты. Уж что-что, а ФИО клиента не меняется практически никогда.
        • 0
          Размер таблицы клиентов может быть сравним с заказами и в общем случае не помещаться в память. Любой кеш сильно усложняет архитектуру, особенно когда хотите кешировать запросы. Кстати, многие RDBMS умеют делать materialized views для медленных запросов, который по сути и есть кеш. Для любого решения, которое вы сможете придумать, JOIN-ы будут работать проще и быстрее, пока не встанет проблема горизонтального масштабирования…
          • –2
            Ну вам же не всю таблицу надо держать, а только ФИО в данном случае. Допустим, у нас миллион клиентов. Длина полного имени примерно 30-40 символов. То есть такой кеш будет занимать примерно 60-80 Мб для двухбайтовой кодировки, плюс 4 Мб на интовый ключ. После запроса делаем пост-обработку, где идем по строкам, дергаем кеш и добавляем к результату поле full_name. Никакой сложной архитектуры, один дополнительный цикл, зато минус 1 джойн из всех запросов с ФИО и связанные с ним чтения с диска и передача через сеть.
  • +3
    Как-то тихо пропустили случай когда по нужному полю имеется индекс. А ведь при наличии индекса Nested Loops Join превращается из самого медленного в едва ли не самый быстрый способ! Ведь он — единственный алгоритм, который умеет не делать полную выборку записей из таблицы (т.е. работать за сублинейное относительно размера БД время).
    • +2
      автор дал ссылку на прекрасную статью Как работает реляционная БД, собственный же его текст это очень упрощённый взгляд.

      Непонятно зачем это вообще такое постить если приводится ссылка на, практически, образец точного и доступного описания работы SQL-сервера с данными.
    • +1
      По-моему, алгоритм для данного случая называется Index Join. Да, я про него забыл, моя оплошность.
      • +1
        MS SQL Server называет обе операции "Nested Loops". Кстати, соединение индекса и той таблицы, по которой он построен — тоже называется "Nested Loops", то есть для соединения двух таблиц по индексу в плане выполнения окажется две операции Nested Loops...
        • 0
          То, что MS SQL хочеть запутать следы, называя две вещи одним термином — скорее можно отнести к недостаткам(не критичным) MS SQL.
          P.S. Про сам MS SQL ничего плохого сказать не могу, по отзывам база довольна приятная в использовании, сам не имел дело.
          • +1
            Напротив, они называют вещи своими именами. Вложенный цикл — это и есть вложенный цикл, чтобы там ни было внутри.
            • 0
              В данном случае важна не схема реализации, а тип и сложность операции.
              • 0
                Тип операции виден в другом месте. Без индекса там стоит Clustered Index Scan или Table Scan. С индексом — Nonclustered Index Seek + Clustered Index Seek (как оно выглядит без кластерного индекса — не помню). А снаружи в обоих случаях — Nested Loops, и это правильно.
                • 0
                  Значит просто более детально расписываются вложенные операции. Возможно это и хорошо.
  • +2
    Я считаю, что изучать то, как работает JOIN совсем не обязательно
    С другой стороны, для человека, знакомого с алгоритмами и структурами данных, и который запускал EXPLAIN пару раз и видел надписи FULL SCAN, MERGED JOIN и HASH JOIN, все эти подходы и так очевидны
    • +1
      Согласен. Фундаментальные познания в алгоритмике снимают большую часть проблем.
  • 0
    Спасибо за статью, освежил знания в голове. Согласен с вашими выводами: если работаешь с SQL, всё, что описано в этой статье, знать очень желательно.
  • 0
    Современные РСУБД используют nested loops join в самых безнадежных случаях

    Я бы всже же уточнил, что речь идет о чем-то DataWareHouse-подобном.
    В OLTP системах все в точноти до наоборот.
    • +1
      Да, есть случаи, когда nested loops будет быстрее. Добавил в статью. Спасибо!
  • +11
    Базы данных — это конечно хорошо, знать как они устроены, как они обрабатывают данные, и так далее. Но ваша мысль о проведении собеседований таким образом изначально неверна. Вы ищете себе в команду fullstack-ниндзей, которые знают и умеют всё? Во-первых, таких людей совсем немного в мире. Во-вторых, даже самый крутой подобный ниндзя всё равно проиграет узкому специалисту, долгое время работающему лишь в отдельной области. Или вам просто нравится выпендриться своим объёмом знаний в какой-то области ПО? В любом случае, мотивы некорректные.

    В современном мире IT столько разных областей знаний, что вы не сможете знать всё сразу. У вас просто сил и времени на это не хватит. Поэтому правильно разделять людей на специалистов по фронтенду, бэкенду, базам данных, системному администрированию, дизайну, вёрстке, мобильным приложениям, и так далее. Вполне нормально, если человек прекрасно знает область фронтенда, но при этом не понимает, каким образом ему приходят данные с серверного API. Ему это и не надо понимать, в его задачу это не входит.

    Я сам являюсь fullstack-ниндзей, прекрасно знаю и вёрстку, и JS использую как на фронтенде, так и на бекенде с нодой, и с базами данных постоянно работаю, и сервера настраиваю и поддерживаю, и скрипты на баше пилю, и ботов пишу для мессенджеров — но всё равно я прекрасно знаю, что отдельный профессиональный верстальщик в вёрстке будет шарить намного лучше, чем я. Человек, который много работал с базами данных, будет знать их работу лучше, чем я. И нет в этом ничего удивительного, и я не считаю себя от этого плохим разработчиком, как и никогда не буду считать плохим разработчиком кого-то другого из-за этого.

    Тем более вопрос оптимизации каких-то отдельных частей приложения всегда можно решить уже тогда, когда проблема эта действительно возникнет. Зачем решать несуществующие проблемы?
    • 0
      Есть еще другая манифестация фулстек-ниндзи — не размазанная по языкам и инструментам, а сосредоточенная в одном языке или окружении (Smalltalk например). И вот в таком окружении, которое по сложности как игровая реальность, вполно можно неплохо ориентироваться на полном фулстеке — от виртуальной машины до валидаций форм ввода.
    • –1
      В статье нет ничего про подход к собеседованиям. Эта статья — про операцию JOIN и веру в магию.
      Как я писал в комментах выше:
      Что касается того, кого я ищу, то на самом деле, мне плевать на внутренности РСУБД. Я просто пытаюсь понять, насколько глубоко человек изучил те технологии, с которыми он работал. Т.е. если у вас в проекте было много low-level NIO, а с базой вы почти не работали — я не буду ожидать от вас знания Merge Join, но захочу услышать про селекторы и грин-треды.

  • +5
    Не надо спрашивать "как работает?".

    Надо спрашивать "как бы вы написали?".

    Ибо первое — ссылка на память и знание неких правильных способов, а второе — на умение придумать решение на месте. И разработчику в 90% случаев нужно второе.
    • +1
      В 99% случаев есть доступ к достижениям всего человечества в этой области и в реальности придумывать в рабочем проекте свою версию скорее плохо, чем хорошо.
      Если уж спрашивать, то спрашивать про какие алгоритмы человек слышал(быстрее найдет подходящий, если понадобится). Возможно про особенности этих алгоритмов(умение выбирать походящий). Можно попросить выбрать алгоритм под задачу с полным доступом к интернету — посмотреть как будет искать решение и что получится в итоге.
      И можно попросить написать реализацию тривиального алгоритма — посмотреть как и что человек напишет (можно даже при этом дать полное чёткое описание, как этот алгоритм работает). (умение собственно реализовывать алгоритм в коде)
  • +9
    Компания совершила ошибку, доверив вам собеседования.
    • –2
      Эта статья не про собеседования, а про операцию JOIN.

      Про собеседования мысль вот:
      Что касается того, кого я ищу, то на самом деле, мне плевать на внутренности РСУБД. Я просто пытаюсь понять, насколько глубоко человек изучил те технологии, с которыми он работал. Т.е. если у вас в проекте было много low-level NIO, а с базой вы почти не работали — я не буду ожидать от вас знания Merge Join, но захочу услышать про селекторы и грин-треды.

      • +8
        Материал подан под видом "кто не знает, тот дурак". Сквозь статью фонит высокомерие. Отсюда апелляция к половым органам ("теребить базенку"). Кстати, насколько подробно вы изучили устройство мочеполовой системы? Ведь согласно вашей теории, прежде чем что-то теребить, нужно глубоко изучить внутреннее устройство =)
        • НЛО прилетело и опубликовало эту надпись здесь
    • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
  • +8
    джойны (по-русски правильнее 'объединение', но я намеренно использую жаргон)

    Нет, объединение — это UNION.
    JOIN же в реляционной алгебре по-русски называется "соединение".
    • 0
      С усталости перепутал. Исправил, спасибо.
  • +3
    Я конечно недавно на хабре и может чего не вкуриваю, но ИМХО рассуждать о том, надо ли или не надо разработчику знать алгоритмы работы джойна может только тот, кто никогда не сталкивался с оптимизацией запросов в БД.
  • +9
    Это извечный вопрос — должен ли разработчик цифровых схем детально знать, как работает транзистор?
    И следствие из него — существуют ли хорошие разработчики, не имеющие детального понятия о работе кирпичиков, из которых они строят системы?

    Теперь посмотрим, зачем вообще человеку абстракции? В силу того, что в сложных областях/системах качественно и детально держать всю вертикаль очень сложно — либо немасштабируемо по HR-размерности (вундеркинд может, инженер — нет), либо ненадежно по временной оси (пока проект идет, все более-менее охвачено — прошел год, два, пять — тонкости утрачены, все грабли — опять наши)

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

    Но в любом случае имхо лучше (хотя и хлопотнее/сложнее в оценке) — дать задания, соответствующие основной области — если это бакендщик — пусть спроектирует модель данных, покажет эффективные выборки, разрулит вопросы эффективных изменени (втч конкурентных), правильно организует бизнес-транзакции итд. В конце концов, какой именно способ иннер джойна применить, решает движок БД, и практика показывает, что он ошибается гораздо реже, чем человек на том конце провода, который пытается его обхитрить )

    А насчет типов джойнов — в самих их названиях есть подсказка, имея конечно более-менее внятный IT-бэкграунд, можно догадаться, даже увидя execution plan первый раз в жизни.
  • +1
    Судя по плюсам-минусам, аудитория разделилась на два лагеря — кто знает, что внутри у JOIN (и считает это важным знанием), и кто не знает (и не считает это важным знанием, но боится провалить следующее собеседование).
    • +2
      Куда делись адепты оставшихся двух квадрантов? Потонули в статистическом шуме? ;)
      • –1
        Двум оставшимся квадрантам (если они не пустые) нет особого смысла участвовать в бурном обсуждении.
    • +4
      код многих sql-серверов открыт, можно скачать и посмтреть. В том числе и на Java, тот же Derby из JDK.

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

      Аудитория делится на тех кто это понимает и на тех кто этого не понимает.
      • НЛО прилетело и опубликовало эту надпись здесь
  • +4
    К всем трем вашим Join на самом деле есть нарекания.

    Например, вы говорите, что Nested Loop Join — это для каждой строки в левой таблице надо пройтись по всем строкам правой таблицы. Это не более чем частный случай Nested Loop Join. Другой частный случай Nested Loop Join — это для каждой строки в левой таблице найти равные ей строки в правой таблице используя индекс, и по ним пройтись используя nested loop. И это уже достаточно эффективный способ делать Join, потому что его сложность для индексов на деревьях O(N log M + размер ответа), и константа памяти. А если ключик еще и в виде хештаблицы а не дерева, то логарифм из сложности тоже улетает, оставляя O(N + размер ответа), и константу памяти, что никакой другой джоин превзойти не может.

    Ваш Hash Join делает неоговоренное заранее предположение, что ключи для Join уникальны, причем только с одной стороны. Это, вообще, очень странно поведение, потому что уникальные значения в столбце обычно форсируются индексом, а если есть индекс, то зачем HashJoin, можно использовать Nested Loop Join, описанный выше (потеряем логарифм времени в худшем случае, сэкономим линию памяти).

    Ваш Merge Join просто неправильный. Если первая пара совпадает, она добавится дважды, потому что зачем-то в начале есть специальный случай. Я бы, собеседуя человека, предпочел чтобы он мог два списка слить без ошибок, чем чтобы он знал как joins работают :)

    upd: заметил что внизу статьи упоминается про уникальные ключики, поэтому эта претензия отзывается
    • 0
      Merge Join исправил, спасибо.
      Про Index Join мне уже напомнили в комментах выше, но добавлять его в статью уже не хочется. Тем более, его код будет выглядеть почти как у HashJoin, только без построения хеша.

      Так же я не написал про джойн по не-равенству, алгоритм sort join, anti join и еще много всяких редких штук.
  • –1
    Линейная ассимптотическая сложность поиска в массиве это прорыв.
    На самом деле там O(NlogM) где M — меньший список. Это про hash-join
    • +2
      Откуда берется логарифм в хеш-таблицах?
      • –4
        А как определяется сложность алгоритма? Напишите машину Тьюринга, которая это сделает быстрее ?
        • +1
          Машину Тьюринга я писать не буду. А вот реальные хеш-таблицы сотню раз писал.
          • –3
            Ну тогда да, извините, куда мне спорить с такими суровыми аргументами.
            • +2
              Хм. Я еще аргументов не приводил, а вы уже спорить отказываетесь :)

              PS https://habrahabr.ru/post/128457/ — неплохой обзор структур данных. Как видно, никаких логарифмов у хеш-таблицы нет.
        • +2
          А как определяется сложность алгоритма?

          Как-как, анализом. Bottom line (Wikipedia): O(1) в среднем, O(n) в худшем случае.

          Зависит от качества хэширующей функции и от соотношения числа ячеек (k) к количеству элементов (n). Но логарифму там взяться неоткуда: при равномерном распределении у вас в каждой ячейке будет n/k элементов. Стоимость доступа к ячейке, очевидно, константна, стоимость дальнейшего перебора линейно зависит от n/k. Если n > k (а это условие эффективной применимости хэш-таблиц), то вы получите константный общий доступ.
          • 0
            ...n < k, конечно же, при форматировании затупил.
          • 0
            Есть кукушкино хеширование с константным худшим случаем.
            • 0
              Хм. Спасибо за наводку, не видел эту статью. Теперь признаю O(1).
            • 0
              У него вставка более тяжелая: чтобы получить среднее O(1), надо иметь n < k/2.
              • +1
                Какое среднее O(1)? Константа там в худшем случае, а не в среднем.
                • 0
                  Это на чтение константа в худшем случае. А на вставку — вероятностное O(1): "The expected time used per insertion is
                  O(1), so the total expected time for trying to insert all keys is O(n)."
                  • 0
                    Мда, правда.
                  • 0
                    Если вероятностное O(1) на вставку не устраивает, существует деамортизированный вариант: "In this work we follow Kirsch and Mitzenmacher and present a de-amortization of cuckoo hashing that provably guarantees constant worst case operations."
                    • 0
                      Specifically, the dictionary utilizes 2(1 + ε)n + nε words [ε > 0]

                      Я не к тому, что алгоритм плохой, я к тому, что почти всегда неизбежно какой-то компромис.
              • 0
                Асимптотически это то же самое, что и для хеш-таблицы с цепочками в среднем случае, просто load factor нельзя делать больше 0,5. Но "кукушечный" алгоритм гарантирует O(1) в худшем случае, что круто.
                • 0
                  Это правда, но — ценой > 2n памяти.
            • 0
              Очень дерзкий алгоритм, спасибо за наводку.
          • –4
            удивительная "средняя асимптотическая сложность". При идеальной реализации хещ-функции (сложность вычисления которой тоже не ноль) в среднем, для поиска элемента потребуется константное число операций.
            Однако, асимптотическая сложность O(logN) потому как я МОГУ поддерживать список сортированным и для этого достаточно логарифма.
            • 0
              Какая бы ни была сложная функция хеширования, сложность вычисления хеша каждого конкретного элемента никак не зависит от числа этих элементов. Константа. Какая бы она большая ни была. Поэтому в O-нотации никак не проявляется.
              • 0
                Спасибо, кэп.
            • +3
              Еще раз: асимптотическая сложность обращения к хэш-таблице — O(1). Спорить с этим достаточно бессмысленно, это подтверждается и анализом и практикой. Разница между константой и логарифмом (на самом деле, конечно, NlogN, не забывайте, что вам надо сортировку сформировать и поддерживать) достаточно велика, чтобы использование хэш-таблиц было выгодным даже при большой константе.
            • +2
              Отличный аргумент!

              Ваша хеш-таблицы работают за константу, а моя будет работать медленее, потому что я МОГУ!

              Вспоминаю свои достижения в седьмом классе — быструю сортировку односвязных списков за N2logN :)
              • –5
                Ну теперь понятно что за специалисты собрались, че.

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