Объединение — это операция между двумя таблицами данных, объединяющая результаты путем поиска ключей из одной таблицы во второй таблице. Несмотря на простую концептуальную операцию, есть много способов сделать это, и понимание вариаций важно для понимания поведения базы данных (обсуждение выбора алгоритмов см. В разделе « Планы выполнения SQL в Javascript »).
Очевидно, что это важно для настройки, так как в нем рассказывается, как вы можете повлиять на результат, но также объясняется одна из многих причин, по которой выход строки строки базы данных не гарантируется от запуска к выполнению одного запроса.
Чтобы представить, как это работает, мы сначала настроили простой эквивалент таблицы базы данных в Javascript:
schema = { tables: { people: [ { first_name: 'gary', last_name: 'sieling', company: 1 }, { first_name: 'bob', last_name: 'sieling', company: 1 }, { first_name: 'ella', last_name: 'sieling', company: 2 } ], companies: [ { id: 1, company_name: 'acme corp' }, { id: 2, company_name: 'bubble' } ] } } |
Самый простой тип объединения — это соединение с вложенным циклом, которое выполняет foreach для каждой таблицы, возвращая только совпадающие строки. Если в таблице A есть m строк, а в таблице B — n строк, это mxn операций.
Обратите внимание, что это внутреннее соединение — чтобы выполнить внешнее соединение, на каждом уровне необходимо отслеживать, была ли добавлена строка, и при необходимости вставлять дополнения.
function nested_loop_join(a, b, keys, select) { output = []; second = []; while(row_b = b()) { second[second.length] = row_b; } var idx = 0; while(row_a = a()) { $.each(second, function(i, row_b) { if (row_a[keys[0]] === row_b[keys[1]]) { var new_row = {}; $.each(select, function(k, col) { // cheat here for simplicity - should handle aliasing new_row[col] = (row_a[col] ? row_a[col] : row_b[col]) }) output[idx++] = new_row; } }) } return s({from: output}) } joined = nested_loop_join( s({from:schema.tables.companies}), s({from:schema.tables.people}), ["id", "company"], ["last_name", "company_name"]) |
Второй вариант — декартово соединение. Это объединяет каждую строку в таблице A с каждой строкой в таблице B, не сравнивая, а затем фильтрует результаты до нуля. Это также операции mxn, и, как правило, это плохая идея, хотя, если две таблицы маленькие, их иногда выбирает база данных.
function cartesian_join(a, b, keys, select) { output = []; second = []; while(row_b = b()) { second[second.length] = row_b; } var cols = select; if (cols) { $.each(keys, function(i, c) { if ($.inArray(c, select) === -1) { cols[cols.length] = c; } }); } var idx = 0; while(row_a = a()) { $.each(second, function(i, row_b) { var new_row = {}; $.each(cols, function(k, col) { // cheat here for simplicity - should handle aliasing new_row[col] = (row_a[col] ? row_a[col] : row_b[col]) }); output[idx++] = new_row; }) } return s({from: output, where: function(row) { return row[keys[0]] === row[keys[1]]; }}); } |
Третий вариант — поместить одну из таблиц в хэш-карту, где ключами являются идентификаторы поиска, а значения указывают на строки. Эта операция требует m + n операций — в идеале, большая из двух таблиц должна быть индексированной, так как предварительная сборка превращает ее в n операций.
function hash_join(a, b, keys, select) { var use_first = a.length < b.length; var lookup = {}; var table_to_join = use_first ? a : b; var key_to_join = use_first ? keys[0] : keys[1]; var table_to_hash = use_first ? b : a; var key_to_hash = use_first ? keys[1] : keys[0]; for (var i = 0; i < a.length; i++) lookup[key_to_hash] = table_to_hash[i][key_to_hash]; var idx = 0; output = []; for (var j = 0; j < table_to_join.length; j++) { var new_row = {}; var left_row = table_to_join[j]; var key = left_row[key_to_join]; var right_row = table_to_hash[key]; $.each(select, function(k, col) { new_row[col] = (left_row[col] ? left_row[col] : right_row[col]) }); output[idx++] = new_row; } return output; } hash_join( schema.tables.people, schema.tables.companies, ["company", "id"], ["last_name", "company_name"]) |
Первый вариант этого метода — симметричное хеш-соединение. Обратите внимание, что эта реализация слишком упрощена — я предполагаю, что каждый столбец идентификатора является первичным ключом в своей таблице, а не элементом, который может иметь много различных значений.
Самое замечательное в этом типе объединения состоит в том, что вы можете делать это целиком из индексов, а не из табличных данных, поэтому потенциально гораздо меньше для чтения в память. Если значения повторяются много раз, это будет очень быстро — в этом случае база данных платит за выборку значений для поддержания гистограммы.
function symmetric_hash_join(a, b, keys, select) { debugger; var lookup_a = {}; var lookup_b = {}; for (var i = 0; i < a.length; i++) lookup_a[a[i][keys[0]]] = a[i]; for (var i = 0; i < b.length; i++) lookup_b[b[i][keys[1]]] = b[i]; var output = []; var idx = 0; $.each(lookup_a, function f(key, row_a) { var row_b = lookup_b[key]; if (row_b) { var new_row = {} $.each(select, function(k, col) { if (row_a[col]) new_row[col] = row_a[col] }); $.each(select, function(k, col) { if (row_b[col]) new_row[col] = row_b[col] }); output[idx++] = new_row } }); return output; } |
Это объединение требует, чтобы все хэш-карты помещались в память, часто это неверное предположение, особенно при выборе больших таблиц. Альтернативный подход состоит в том, чтобы хэшировать записи, а затем использовать хеш для разделения данных (например, диапазонов или наиболее значимых цифр) — это позволяет загрузить два блока каждой таблицы, которые гарантированно содержат данные, а затем выполнить объединение. В этом посте я остановился на простых реализациях, но если вы хотите посмотреть, как будет работать элемент управления памятью, посмотрите Масштабирование DataFrame в Javascript .
Последний подход — соединение сортировки-слияния. Это хорошо работает, если вы знаете, что ваши данные отсортированы заранее. Это занимает где-то между операциями max (m, n) и m + n и дает преимущество, заключающееся в том, что вы можете выполнять потоковую передачу записей — в памяти одновременно требуется не более одной строки.
function sort_merge_join(a, b, keys, select) { var sorted_a = a.sort(function f(x, y) { return x[keys[0]] === y[keys[1]] ? 0 : x[keys[0]] > y[keys[1]] ? 1 : -1 }); var sorted_b = b.sort(function f(x, y) { return x[keys[0]] === y[keys[1]] ? 0 : x[keys[0]] > y[keys[1]] ? 1 : -1 }); var a_pos = 0; var b_pos = 0; var a_len = a.length; var b_len = b.length; var output = []; var idx = 0; while (a_pos < a_len && b_pos < b_len) { var row_a = a[a_pos]; var row_b = b[b_pos]; var id_a = row_a[keys[0]]; var id_b = row_b[keys[1]]; if (id_a === id_b) { var new_row = {}; $.each(select, function(k, col) { if (row_a[col]) new_row[col] = row_a[col] }); $.each(select, function(k, col) { if (row_b[col]) new_row[col] = row_b[col] }); output[idx++] = new_row; a_pos++; b_pos++; } else if (id_a < id_b) { a_pos++; } else { b_pos++; } } return output; } |