Disección de rangos de números

Estoy tratando de encontrar la forma más eficiente de generar una consulta WHERE. Hice otra pregunta antes, que era similar, pero iré directamente al grano en esta.

Dada una colección de rangos de números, es decir 1-1000, 1500-1600 Es bastante simple crear una condición donde mysql para seleccionar registros que están entre estos valores.

es decir, solo harías:

WHERE (lft BETWEEN 1 and 1000) OR (lft BETWEEN 1500-1600). Sin embargo, ¿qué pasa si también desea incorporar un NOT BETWEEN.

Por ejemplo, si define varias reglas, como ...

  • PERMITIR ENTRE 1 - 1000
  • PERMITIR ENTRE 1500 - 1600
  • PERMITIR ENTRE 1250 - 1300
  • NEGAR ENTRE 25 - 50

¿Cómo puedo fusionar estas reglas para generar eficientemente una condición WHERE? Me gustaría el DONDE para diseccionar el ALLOW BETWEEN 1 - 1000 para crear un hueco en él. Para que se convierta en 1-24 y 51-1000. Debido a que la regla DENY se define después de la primera regla, "sobrescribe" las reglas anteriores.

Como otro ejemplo, digamos que tienes

  • PERMITIR ENTRE 5 - 15
  • NEGAR ENTRE 10 - 50
  • PERMITIR ENTRE 45 - 60

Entonces me gustaría generar una condición WHERE que me permitiría hacer:

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

Notas (ediciones)

  • Además, el rango máximo que alguna vez se permitiría es 1 - 5600000. (Que sería 'Tierra') es decir. Permitir todo en la Tierra.
  • Los rangos de números son en realidad los valores IZQUIERDOS en un MODELO DE CONJUNTO ANIDADO. Estas no son claves únicas. Puede leer por qué quiero hacer esto en esta pregunta que hice anteriormente. https://stackoverflow.com/questions/6020553/generating-a-mysql-between-where-condition-based-on-an-access-ruleset
  • Posible nota importante sobre mis rangos de números Tal vez no debería haber usado el ejemplo de muestra que hice, pero una nota importante sobre la naturaleza de los rangos de números es que, en realidad, los rangos siempre deben consumirse por completo o ser consumidos por una regla anterior. Por ejemplo, utilicé el ejemplo anterior, 10-50 permitir y denegar 45-60. En realidad, esto nunca sucedería en mi conjunto de datos. En realidad sería allow 10-50, entonces el DENY tendría que ser consumido por completo por ese rango, es decir, 34-38. O, consumir por completo la regla anterior. 9-51. Esto se debe a que los rangos en realidad representar valores lft y rgt en un modelo de conjunto anidado y no puede haber superposiciones como las presenté.

No pensé en mencionar eso al hacer la pregunta, pero después de ver el código de muestra de trabajo a continuación, puedo ver que esta nota es realmente importante.

(Ejemplo editado mysql para incluir OR en lugar de AND según el comentario a continuación)

preguntado el 16 de mayo de 11 a las 19:05

Eso es lógicamente imposible, ya que un número no puede estar entre 5 y 9, y también entre 45 y 60. ¿Quizás te refieres a "O"? -

Lo siento. Si, tienes razón. Es lógicamente imposible. No sé lo que quiero pensar. Tenía la intención de decir OR. -

4 Respuestas

Honestamente, ¿por qué molestarse? Siempre que la clave contra la que está consultando esté indexada, simplemente coloque las consultas múltiples allí:

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
    )

Podría obtener un poco de eficiencia construyendo un disector, pero me preguntaría si vale la pena. Todos los elementos de la cláusula WHERE estarían fuera de un índice, por lo que no está evitando que ocurra ninguna operación difícil (lo que significa que no está deteniendo un escaneo de tabla completa al hacerlo).

Entonces, en lugar de perder tiempo construyendo un sistema para que lo haga por usted, simplemente implemente una solución fácil (ORjuntando los Allows, y ANDjuntos a los Denys) y pasar a cosas más importantes. Luego, si se convierte en un problema más adelante, vuelva a visitarlo. Pero realmente no creo que esto se convierta en un problema demasiado grande ...

Editar Ok, aquí hay un algoritmo muy simple para hacer esto. Utiliza cadenas como almacén de datos, por lo que es razonablemente eficiente para números más pequeños (por debajo de 1 millón):

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);
    }
}

Uso:

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

Genera:

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"

contestado el 17 de mayo de 11 a las 01:05

Sin embargo, es una gran idea, no se trata de la situación de las condiciones que se anulan entre sí según el orden de esas condiciones (según su segundo ejemplo). - Brian Fisher

@Brian: Claro que sí. Es por eso que tu AND juntos los bits de Denegar. WHERE (foo BETWEEN 5 AND 15 OR foo BETWEEN 45 AND 60) AND (foo NOT BETWEEN 10 AND 50) coincidiría correctamente 9 y no coincidir correctamente 12... Los OR requiere que uno o mas de los Allows partido, y el AND requiere que ninguno de los Denys coincida (bueno, técnicamente todos coinciden, pero como lo estamos negando con un NOT se vuelve positivo) ... - ircmaxell

Pero qué pasa con 46. La cláusula where que describe no coincidiría con 46, pero Laykes quería que se incluyera porque la última regla permite 45-60. - Brian Fisher

@Brian: No coincidiría con 46 porque se niega explícitamente. Creo que su último ejemplo es defectuoso (el bit sql), ya que el primero y la primera mitad del segundo siguen el patrón Deny-first, pero la segunda mitad del último no. Por lo tanto, supongo que está usando denegar primero y que 46 no debería coincidir. - ircmaxell

Ese podría ser el caso y Laykes no parece estar interviniendo en nuestra conversación, por lo que es difícil saberlo. Había asumido un orden de precedencia estricto a partir de su oración, "Debido a que la regla DENY se define después de la primera regla," sobrescribe "las reglas anteriores". - Brian Fisher

Pasé un poco de tiempo tratando de resolver esto (es un buen problema) y se me ocurrió esto. No es óptimo, ni garantizo que sea perfecto, pero podría ayudarlo a comenzar:

<?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);

La última var_dump salidas:

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)
  }
}

Editado para usar el primer ejemplo en lugar del segundo.

contestado el 17 de mayo de 11 a las 00:05

(Voto a favor del esfuerzo) Intentaré ver dónde está yendo mal. Funciona en superposiciones no complejas, pero cuando empiezas a hacer cosas locas, se cae. Por ejemplo, en esta muestra, no quisiera que se permitiera NADA. Porque la regla final en realidad no permite todo. codepad.viper-7.com/hElHp9 - Layke

Sí, también lo pensaré un poco más. Intenté actualizarlo para cubrir su caso de prueba y rápidamente rompí mi primer caso de prueba, así que, sí. :) Editaré / comentaré aquí si averiguo algo. - Jimmy Sawczuk

Procesaría las instrucciones una a la vez creando una lista de números que deberían incluirse. Luego, finalmente traduciendo esa lista en un conjunto de rangos para la cláusula where. Aquí hay un pseudocódigo:

$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);

contestado el 16 de mayo de 11 a las 23:05

Sí, Brian, pensé en eso. El problema es que potencialmente podría tener un alcance de alrededor de 5.6 millones. Por ejemplo, el nodo ROOT en mi modelo de conjunto anidado es "Tierra". Lo que significa que me gustaría incluir todos los recursos para este usuario dentro de la Tierra. Realmente no sé cómo puedo hacer esto. :) - Layke

Pregunté en IRC y recibí dos respuestas. Voy a publicar los dos para que otras personas se beneficien (y para no perderlos, ya que en breve los veré en detalle a ambos).

Ejemplo 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))));

Ejemplo 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);

contestado el 17 de mayo de 11 a las 01:05

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.