Список статей о внутреннем устройстве Oracle:
- Oracle: реализация order by сортировки
- Oracle: реализация btree индекса
- Hash Join
- Bloom filter
- Lru cache
- Hash агрегация и приблизительный DISTINCT
Начну с операции order by.
При выполнении сортировки, данные сначала помечаются в приватную область памяти pga, называемую "sorting work area". Если данные не помещаются целиком, то массив разбивается на части и сохраняются на диск в TEMP пространство процесса, после чего части сортируются по отдельности в памяти, а потом сливаются в один отсортированный набор (внешняя сортировка слиянием).
До 10 версии Oracle "sort area size" была жестко фиксирована из-за этого нельзя было применять сортировки расходующие дополнительную память. Скорей всего это была сортировка вставками, имеющая стоимость O(N*N) (http://www.hellodba.com/reader.php?ID=185&lang=EN). Алгоритм сортировки вставками на java можно посмотреть здесь: https://github.com/pihel/java/blob/master/oracle_sort/OracleSort.java#L238 .
Одним из основных нововведений Oracle 10gR2 - гибкий размер pga и увеличение области сортировки (pga target), что позволило применять сортировки с дополнительным потреблением памяти.
Переключение новая/старая сортировка осуществляется скрытым параметром "_new_sort" - по умолчанию = true
Новая реализация order включает в себя смесь 3 сортировок: быстрая сортировка (qsort) + поразрядная сортировка (radix sort) + сортировка слиянием (merge sort). Что дает сложность от N*K до N*Log2(N), где K - размер ключа, а N - размер массива.
Общий алгоритм представляет из себя:
1. Используя алгоритм из сортировки слиянием разбиваем входящий массив (MergeSort.sort) на части с размером меньше "sort area size" ( OracleSort.isSortArr )
package oracle_sort; import java.util.*; import org.junit.runner.JUnitCore; import org.junit.runner.Result; /* Сортировка слиянием: * Сложность: N*Log2(N) * Доп. память: O(N) * Общий смысл: ** рекурсивно разбиваем массив на 2 части, пока размер элемента не дойдет до N (тут =1 ) ** сливаем массивы в один последовательным проходом (в худшем случае N+M), сравнивая элементы */ class MergeSort { //проверка достаточности разбиения @SuppressWarnings("rawtypes") protected boolean isSortArr(Comparable[] arr) { if(arr.length <= 1) return true; return false; } //разбитие массива на 2 равные части @SuppressWarnings("rawtypes") public void sort(Comparable[] arr) { //если массив достаточно разбит, то прерываем рекурсию if(isSortArr(arr)) return; //левая половина Comparable[] left = new Comparable[arr.length / 2]; System.arraycopy(arr, 0, left, 0, left.length); sort(left); //swop to disk //правая половина Comparable[] right = new Comparable[arr.length - left.length]; System.arraycopy(arr, left.length, right, 0, right.length); sort(right); //слияние merge(left, right, arr); } //sort } //MergeSort /* * Комбинированная сортировка: слиянием + быстрая + поразрядная * * */ public class OracleSort extends MergeSort { //размер памяти под сортировку int sort_area_size = 1; //размер ключа int avg_key_size = 0; final double log10_2 = Math.log(2); OracleSort(int _sort_area_size, int _avg_key_size) { sort_area_size = _sort_area_size; avg_key_size = _avg_key_size; } //OracleSort protected boolean isSortArr(Comparable[] arr) { if(arr.length < 2) return true; //если после разбиения массива размер меньше размера под сортировку if(arr.length <= sort_area_size) { //если Integer и размер ключа меньше log2(Т), то делаем поразрядную сортировку if( arr[0].getClass() == Integer.class && avg_key_size > 0 && avg_key_size < ( Math.log(arr.length) / log10_2 ) ) { Integer[] _arr = new Integer[arr.length]; //как привести Comparable к Integer? System.arraycopy(arr, 0, _arr, 0, arr.length); Sorts.radixSortUInt(_arr); System.arraycopy(_arr, 0, arr, 0, arr.length); _arr = null; } else { //иначе быструю сортировку Sorts.QSort(arr, 0, arr.length-1); } return true; } return false; } //isSortArr }
2. Подмассивы размером меньше "sort area size" сортируются одним из алгоритмов внутри памяти.
В зависимости от размера ключа сортировки (avg_key_size) выбирается или поразрядная сортировка (radix sort) или быстрая (qsort): OracleSort.isSortArr
2.1. Быстрая сортировка имеет среднюю сложность O (N * Log2(N)) и хорошо использует процессорный кэш, но сильно зависит выбора среднего элемента. Для этого используется статистика на столбце таблицы: максимальное и минимальное значение в нем.
/* Быстрая сортировка: * Сложность: N*Log2(N) * Доп. память: нет * Минусы: * - Сильно зависит от выбора средней точки, при неправильно выборе (крайний элемент) сложность = N*N * - На большом наборе данных из-за рекурсии может вызвать переполнение стека * Общий смысл: ** находим элемент в центре массива со средним значением ** движемся с краев к центру, меняя значения местами, если влевой части значение больше середины или вправой больше ** при доходе до середины, запускам рекурсивно для левой и правой части */ public static <T extends Comparable<T>> void QSort(T[] arr, int l, int r) { int i = l; int j = r; //средний элемент = левая граница + (правая граница - левая граница) / 2 T c = arr[l + ( r - l ) / 2]; // идем к центу, пока края не встретятся while(i < j) { //идем с левого края к центру, пока середина больше текущего значения (ищем элемент больше середины) while(arr[i].compareTo(c) < 0) { i++; } //с правого края к центру, пока середина меньше текущего значения (ищем элемент меньше середины) while(arr[j].compareTo(c) > 0) { j--; } //если левый индекс меньше правого индекса, то меняем местами и смещаемся if(i <= j) { T t = arr[j]; arr[j] = arr[i]; arr[i] = t; i++; j--; } } //рекурсивно запускаемся для левой и правой части if(l < j) { QSort(arr, l, j); } if(r > i) { QSort(arr, i, r); } } //QSort
2.2. Если размер ключа сортировки на основе статистики "avg_key_size" (K) меньше Log2 от размера массива N, то поразрядная сортировка более эффективна, т.к. имеет сложность = O (K*N)
Ниже пример реализации для массива Integer:
/* Поразрядная сортировка: * Сложность: N*K ** где K число разрядов в сортиуемом элементе * Доп. память: O(N) * Минусы/Плюсы: * -/+ быстрей, если k < log2(n) и дольше в других случаях * - разный размер хэш массива для разных типов данных * Особенность: * Порядок сортировки зависит от направления: для цифр нужно справа-навлево, для строк: слева-направо * Общий смысл: * Идем по рязрядам числа справа-налево и помещаем элементы в массив по значению текущего разряда ** т.е. элементы сначала упорядычиваются по последнему элементу ** потом переупорядычиваваются по второму и т.д. пока полностью не упорядочим массив */ public static <T extends Integer> void radixSortUInt(T[] arr) { //хэш массив из 10 элементов для 10-ричного числа final int RADIX = 10; List<T>[] bucket = new ArrayList[RADIX]; for (int i = 0; i < bucket.length; i++) { //список чисел, содержащих нужно число в разряде bucket[i] = new ArrayList<T>(); } //признак, что все разряды перебраны boolean maxLength = false; //значение в разряде int rank_val = -1; //номер разряда int placement = 1; //пока не перебраны все разряды while (!maxLength) { maxLength = true; //для каждого элемента массива for (int i = 0; i < arr.length; i++) { //добавляем элемент в массив по значению в разряде rank_val = arr[i] / placement; bucket[rank_val % RADIX].add(arr[i]); //если в разряде не пусто, то ставим флаг повторения цикла if (maxLength && rank_val > 0) { maxLength = false; } } //разворачиваем двухуровневый массив обратно в последовательный int a = 0; for (int b = 0; b < RADIX; b++) { for (int i = 0; i < bucket[b].size(); i++) { arr[a++] = bucket[b].get(i); } bucket[b].clear(); } //переходим к следующему разряду placement *= RADIX; } } //radixSortUInt
3. Отсортированные части объединяются за один проход в отсортированный массив используя алгоритм слияния:
//слияние 2 упорядоченных массивов в один @SuppressWarnings({ "rawtypes", "unchecked" }) private void merge(Comparable[] left, Comparable[] right, Comparable[] arr) { int iFirst = 0; int iSecond = 0; int iMerged = 0; //бежим, пока не дойдем до конца одного из массивов while (iFirst < left.length && iSecond < right.length) { //если элемент в левом массиве больше, чем в правом if (left[iFirst].compareTo(right[iSecond]) < 0) { //то добавляем элемент из левого arr[iMerged] = left[iFirst]; //и двигаемся в левом на 1 iFirst++; } else { //иначе добавляем элемент из правого arr[iMerged] = right[iSecond]; //двигаемся в правом на 1 iSecond++; } //в любом случае увеличиваем результирующий массив iMerged++; } //оставшиеся элементы - больше последнего (максимального) элемента одного из массивов. Докопируем оставшиеся элементы. System.arraycopy(left, iFirst, arr, iMerged, left.length - iFirst); System.arraycopy(right, iSecond, arr, iMerged, right.length - iSecond); } //merge
Полную версию oracle order by можно посмотреть здесь: https://github.com/pihel/java/blob/master/oracle_sort/OracleSort.java#L463 , включая юнит тесты: https://github.com/pihel/java/blob/master/oracle_sort/OracleSortTest.java#L65
Комментариев нет:
Отправить комментарий