Разбивка числовых диапазонов - PullRequest
1 голос
/ 16 мая 2011

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

Учитывая набор диапазонов номеров, то есть 1-1000, 1500-1600, довольно просто создать mysql, где условие для выбора записей, которые находятся между этими значениями.

т. Е. Вы бы просто сделали:

WHERE (lft BETWEEN 1 and 1000) OR (lft BETWEEN 1500-1600). Однако, что если вы захотите включить НЕ МЕЖДУ также.

Например, если вы определяете несколько правил, например ...

  • РАЗРЕШИТЬ МЕЖДУ 1 - 1000
  • РАЗРЕШИТЬ МЕЖДУ 1500 - 1600
  • РАЗРЕШИТЬ МЕЖДУ 1250 - 1300
  • ДЕНЬ МЕЖДУ 25 - 50

Как мне объединить эти правила, чтобы эффективно сгенерировать условие WHERE. Я хотел бы, чтобы ГДЕ рассекал ALLOW BETWEEN 1 - 1000, чтобы создать в нем пробел. Чтобы оно стало 1-24 и 51-1000. Поскольку правило DENY определяется после первого правила, оно «перезаписывает» предыдущие правила.

В качестве другого примера, Скажи, что у тебя есть

  • РАЗРЕШИТЬ МЕЖДУ 5 - 15
  • ДЕНЬ МЕЖДУ 10 - 50
  • РАЗРЕШИТЬ МЕЖДУ 45 - 60

Тогда я бы хотел сгенерировать условие WHERE, которое позволило бы мне выполнить:

WHERE (lft BETWEEN 5 and 9) OR (lft BETWEEN 45 and 60).

Примечания (правки)

  • Кроме того, максимальный диапазон, который когда-либо был бы разрешен, составляет 1 - 5600000. (который будет «Землей»), т.е. Разрешить все на Земле.
  • Числовые диапазоны фактически являются левыми значениями в NESTED SET MODEL. Это не уникальные ключи. Вы можете прочитать, почему я хочу сделать это в этом вопросе, который я задавал ранее. https://stackoverflow.com/questions/6020553/generating-a-mysql-between-where-condition-based-on-an-access-ruleset
  • Возможное важное замечание о моих диапазонах номеров Возможно, мне не следовало использовать пример, который я использовал, но одно важное замечание о природе диапазонов номеров состоит в том, что диапазоны должны фактически всегда быть полностью потреблять или быть поглощенным предыдущим правилом. Например, я использовал приведенный выше пример, 10-50 разрешить, а запретить 45-60. Это на самом деле никогда не произойдет в моем наборе данных. На самом деле это будет allow 10-50, тогда DENY будет либо полностью поглощен этим диапазоном, то есть 34-38. ИЛИ, полностью уничтожить предыдущее правило. 9-51. Это потому, что диапазоны на самом деле представляют значения lft и rgt в модели вложенного набора , и вы не можете иметь перекрытия, как я представил.

Я не думал упоминать об этом при задании вопроса, но, увидев рабочий пример кода ниже, я вижу, что это примечание действительно важно.

(отредактированный пример mysql для включения ИЛИ вместо AND согласно комментарию ниже)

Ответы [ 4 ]

8 голосов
/ 16 мая 2011

Честно, зачем?Пока ключ, к которому вы обращаетесь, индексируется, просто поместите туда несколько запросов:

WHERE (foo BETWEEN 1 AND 1000 
        OR foo BETWEEN 1500 AND 1600
        OR foo BETWEEN 1250 AND 1300
    ) AND (
        foo NOT BETWEEN 25 AND 50
    )

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

Таким образом, вместо того, чтобы тратить время на создание системы, которая сделает это за вас, просто внедрите простое решение (OR объединение разрешений и AND объединение Denys) и переходите к более важным вещам,Тогда, если это станет проблемой позже, посетите это тогда.Но я действительно не думаю, что это когда-либо станет слишком большой проблемой ...

Edit Хорошо, вот очень простой алгоритм для этого.Он использует строки в качестве хранилища данных, поэтому он достаточно эффективен для меньших чисел (менее 1 миллиона):

class Dissector {
    protected $range = '';
    public function allow($low, $high) {
        $this->replaceWith($low, $high, '1');
    }
    public function deny($low, $high) {
        $this->replaceWith($low, $high, '0');
    }
    public function findRanges() {
        $matches = array();
        preg_match_all(
            '/(?<!1)1+(?!1)/', 
            $this->range, 
            $matches, 
            PREG_OFFSET_CAPTURE
        );
        return $this->decodeRanges($matches[0]);
    }
    public function generateSql($field) {
        $ranges = $this->findRanges();
        $where = array();
        foreach ($ranges as $range) {
            $where[] = sprintf(
                '%s BETWEEN %d AND %d', 
                $field, 
                $range['from'], 
                $range['to']
            );
        }
        return implode(' OR ', $where);
    }
    protected function decodeRanges(array $matches) {
        $range = array();
        foreach ($matches as $match) {
            $range[] = array(
                'from' => $match[1] + 1, 
                'to' => ($match[1] + strlen($match[0]))
            );
        }
        return $range;
    }
    protected function normalizeLengthTo($size) {
        if (strlen($this->range) < $size) {
            $this->range = str_pad($this->range, $size, '0');
        }
    }
    protected function replaceWith($low, $high, $character) {
        $this->normalizeLengthTo($high);
        $length = $high - $low + 1;
        $stub = str_repeat($character, $length);
        $this->range = substr_replace($this->range, $stub, $low - 1, $length);
    }
}

Использование:

$d = new Dissector();
$d->allow(1, 10);
$d->deny(5, 15);
$d->allow(10, 20);
var_dump($d->findRanges());
var_dump($d->generateSql('foo'));

Генерирует:

array(2) {
  [0]=>
  array(2) {
    ["from"]=>
    int(1)
    ["to"]=>
    int(4)
  }
  [1]=>
  array(2) {
    ["from"]=>
    int(10)
    ["to"]=>
    int(20)
  }
}
string(44) "foo BETWEEN 1 AND 4 OR foo BETWEEN 10 AND 20"
1 голос
/ 17 мая 2011

Я потратил немного времени, пытаясь решить эту проблему (это аккуратная проблема), и придумал это. Это не оптимально, и я не гарантирую, что оно идеально, но оно может помочь вам начать:

<?php

/*$cond = array(
    array('a', 5, 15),
    array('d', 9, 50),
    array('a', 45, 60)
);*/

$cond = array(
    array('a', 1, 1000),
    array('a', 1500, 1600),
    array('a', 1250, 1300),
    array('d', 25, 50)
);

$allow = array();

function merge_and_sort(&$allow)
{
    usort($allow, function($arr1, $arr2)
    {
        if ($arr1[0] > $arr2[0])
        {
            return 1;
        }
        else
        {
            return -1;
        }
    });

    $prev = false;

    for ($i = 0; $i < count($allow); $i++)
    {
        $c = $allow[$i];
        if ($i > 0 && $allow[$i][0] < $allow[$i - 1][1])
        {
            if ($allow[$i][1] <= $allow[$i - 1][1])
            {
                unset($allow[$i]);
            }
            else
            {
                $allow[$i - 1][1] = $allow[$i][1];
                unset($allow[$i]);
            }
        }
    }

    usort($allow, function($arr1, $arr2)
    {
        if ($arr1[0] > $arr2[0])
        {
            return 1;
        }
        else
        {
            return -1;
        }
    });
}

function remove_cond(&$allow, $start, $end)
{
    for ($i = 0; $i < count($allow); $i++)
    {
        if ($start > $allow[$i][0])
        {
            if ($end <= $allow[$i][1])
            {
                $temp = $allow[$i][1];
                $allow[$i][1] = $start;
                $allow []= array($end, $temp);
            }
            else
            {
                $found = false;
                for ($j = $i + 1; $j < count($allow); $j++)
                {
                    if ($end >= $allow[$j][0] && $end < $allow[$j][1])
                    {
                        $found = true;
                        $allow[$j][0] = $end;
                    }
                    else
                    {
                        unset($allow[$j]);
                    }
                }

                if (!$found)
                {
                    $allow[$i][1] = $start;
                }
            }
        }
    }
}

foreach ($cond as $c)
{
    if ($c[0] == "a")
    {
        $allow []= array($c[1], $c[2]);

        merge_and_sort($allow);
    }
    else
    {
        remove_cond($allow, $c[1], $c[2]);
        merge_and_sort($allow);
    }
}

var_dump($allow);

Последние var_dump выходы:

array(4) {
  [0]=>
  array(2) {
    [0]=>
    int(1)
    [1]=>
    int(25)
  }
  [1]=>
  array(2) {
    [0]=>
    int(50)
    [1]=>
    int(1000)
  }
  [2]=>
  array(2) {
    [0]=>
    int(1250)
    [1]=>
    int(1300)
  }
  [3]=>
  array(2) {
    [0]=>
    int(1500)
    [1]=>
    int(1600)
  }
}

Отредактировано для использования первого примера вместо второго.

0 голосов
/ 17 мая 2011

Я спросил на IRC и получил два ответа.Я собираюсь опубликовать их оба, чтобы другие люди могли извлечь выгоду (и чтобы я не потерял их, так как я кратко рассмотрю их обоих в ближайшее время).

Пример 1 TML

<pre><?php

$cond = array(
    array('a', 5, 15),
    array('a', 5, 15),
    array('d', 9, 50),
    array('a', 45, 60),
    array('a', 2, 70),
    array('d', 1, 150),
);



function buildAcl($set) {
    $allow = array();
    foreach($set as $acl) {
        $range = range($acl[1], $acl[2]);
        switch($acl[0]) {
            case 'a':
                $allow = array_unique(array_merge(array_values($allow), $range));
                break;
            case 'd':
                foreach($range as $entry) {
                    unset($allow[array_search($entry, $allow)]);
                }
        }
    }
    return $allow;
}

var_dump(buildAcl($cond));
var_dump(buildAcl(array(array('a', 5, 15), array('d', 10, 50), array('a', 45, 60))));

Пример 2 (matslin)

<?php
$conds = array(
    array('a', 5, 15),
    array('a', 5, 15),
    array('d', 9, 50),
    array('a', 45, 60),
    array('a', 2, 70),
    array('d', 1, 150),
);

$segments = array();

foreach($conds as $cond)
{
    print($cond[0] . ': ' . $cond[1] . ' - ' . $cond[2] . "\n");
    if ($cond[0] == 'a')
    {
        $new_segments = array();
        $inserted = false;
        $prev_segment = false;

        foreach($segments as $segment)
        {
            if ($segment['begin'] > $cond[2])
            {
                $new_segments[] = array('begin' => $cond[1], 'end' => $cond[2]);
                $new_segments[] = $segment;
                $inserted = true;
                print("begun\n");
                continue;
            }

            if ($segment['end'] < $cond[1])
            {
                print("end\n");
                $new_segments[] = $segment;
                continue;
            }

            if ($cond[1] < $segment['begin'])
            {
                $segment['begin'] = $cond[1];
            }

            if ($cond[2] > $segment['end'])
            {
                $segment['end'] = $cond[2];
            }

            $inserted = true;

            if (
                $prev_segment &&
                ($prev_segment['begin'] <= $segment['begin']) &&
                ($prev_segment['end'] >= $segment['end'])
            )
            {
                print("ignore identical\n");
                continue;
            }

            print("default\n");
            $prev_segment = $segment;
            $new_segments[] = $segment;
        }

        if (!$inserted)
        {
            print("inserted at end\n");
            $new_segments[] = array('begin' => $cond[1], 'end' => $cond[2]);
        }

        $segments = $new_segments;
        print("---\n");
    }

    if ($cond[0] == 'd')
    {
        $new_segments = array();

        foreach($segments as $segment)
        {
            # not contained in segment
            if ($segment['begin'] > $cond[2])
            {
                print("delete segment is in front\n");
                $new_segments[] = $segment;
                continue;
            }

            if ($segment['end'] < $cond[1])
            {
                print("delete segment is behind\n");
                $new_segments[] = $segment;
                continue;
            }

            # delete whole segment
            if (
                ($segment['begin'] >= $cond[1]) &&
                ($segment['end'] <= $cond[2])
            )
            {
                print("delete whole segment\n");
                continue;
            }

            # delete starts at boundary
            if ($cond[1] <= $segment['begin'])
            {
                print("delete at boundary start\n");
                $segment['begin'] = $cond[2];
                $new_segments[] = $segment;
                continue;
            }
            # delete ends at boundary
            if ($cond[2] >= $segment['end'])
            {
                print("delete at boundary end\n");
                $segment['end'] = $cond[1];
                $new_segments[] = $segment;
                continue;
            }

            # split into two segments
            print("split into two\n");
            $segment_pre = array('begin' => $segment['begin'], 'end' => $cond[1]);
            $segment_post = array('begin' => $cond[2], 'end' => $segment['end']);

            $new_segments[] = $segment_pre;
            $new_segments[] = $segment_post;
        }

        print("--\n");
        $segments = $new_segments;
    }

    print("----\n");
    var_dump($segments);
    print("----\n");
}

var_dump($segments);
0 голосов
/ 16 мая 2011

Я обработал бы инструкции по одному, создав список чисел, которые должны быть включены. Затем, наконец, перевести этот список в набор диапазонов для предложения where. Вот некоторый псевдокод:

$numbers = array();
foreach (conditions as $condition) {
    if ($condition is include) {
        for ($i = $condition.start; $i <= $condition.end; $i++) {
            $numbers[$i] = true;
        }
    } else {
        for ($i = $condition.start; $i <= $condition.end; $i++) {
            unset($numbers[$i]);
        }
    }
}
ksort($numbers);
...