¿Cómo generar una serie 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, ... en SQL estándar o T-SQL?


11

Dado dos números ny m, quiero generar una serie de la forma

1, 2, ..., (n-1), n, n, (n-1), ... 2, 1

y repítelo mveces.

Por ejemplo, para n = 3y m = 4, quiero una secuencia de los siguientes 24 números:

1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1
----------------  ----------------  ----------------  ----------------

Sé cómo lograr este resultado en PostgreSQL por cualquiera de los dos métodos:

Usando la siguiente consulta, que usa la generate_seriesfunción, y algunos trucos para garantizar que el orden sea el correcto:

WITH parameters (n, m) AS
(
    VALUES (3, 5)
)
SELECT 
    xi
FROM
(
    SELECT
        i, i AS xi
    FROM
        parameters, generate_series(1, parameters.n) AS x(i)
    UNION ALL
    SELECT
        i + parameters.n, parameters.n + 1 - i AS xi
    FROM
        parameters, generate_series(1, parameters.n) AS x(i)
) AS s0 
CROSS JOIN 
    generate_series (1, (SELECT m FROM parameters)) AS x(j)
ORDER BY
    j, i ;

... o use una función para el mismo propósito, con bucles adjuntos y anidados:

CREATE FUNCTION generate_up_down_series(
    _elements    /* n */ integer,
    _repetitions /* m */ integer)
RETURNS SETOF integer AS
$BODY$
declare
    j INTEGER ;
    i INTEGER ;
begin
    for j in 1 .. _repetitions loop
        for i in         1 .. _elements loop
              return next i ;
        end loop ;
        for i in reverse _elements .. 1 loop
              return next i ;
        end loop ;
    end loop ;
end ;
$BODY$
LANGUAGE plpgsql IMMUTABLE STRICT ;

¿Cómo podría hacer el equivalente en SQL estándar o en Transact-SQL / SQL Server?

Respuestas:


4

En Postgres, es fácil usar la generate_series()función:

WITH 
  parameters (n, m) AS
  ( VALUES (3, 5) )
SELECT 
    CASE WHEN g2.i = 1 THEN gn.i ELSE p.n + 1 - gn.i END AS xi
FROM
    parameters AS p, 
    generate_series(1, p.n) AS gn (i),
    generate_series(1, 2)   AS g2 (i),
    generate_series(1, p.m) AS gm (i)
ORDER BY
    gm.i, g2.i, gn.i ;

En SQL estándar, y suponiendo que hay un límite razonable en el tamaño de los parámetros n, m, es decir, menos de un millón, puede usar una Numberstabla:

CREATE TABLE numbers 
( n int not null primary key ) ;

llénelo con el método preferido de su DBMS:

INSERT INTO numbers (n)
VALUES (1), (2), .., (1000000) ;  -- some mildly complex SQL here
                                  -- no need to type a million numbers

y luego úsalo, en lugar de generate_series():

WITH 
  parameters (n, m) AS
  ( VALUES (3, 5) )
SELECT 
    CASE WHEN g2.i = 1 THEN gn.i ELSE p.n + 1 - gn.i END AS xi
FROM
    parameters AS p
  JOIN numbers AS gn (i) ON gn.i <= p.n
  JOIN numbers AS g2 (i) ON g2.i <= 2
  JOIN numbers AS gm (i) ON gm.i <= p.m 
ORDER BY
    gm.i, g2.i, gn.i ;

En la práctica, no espero que esos números sean mayores que 100; pero en teoría podrían ser cualquier cosa.
joanolo

10

Postgres

Puede hacer que funcione con una matemática única generate_series() y básica (ver funciones matemáticas ).

Envuelto en una simple función SQL:

CREATE OR REPLACE FUNCTION generate_up_down_series(n int, m int)
  RETURNS SETOF int AS
$func$
SELECT CASE WHEN n2 < n THEN n2 + 1 ELSE n*2 - n2 END
FROM  (
   SELECT n2m, n2m % (n*2) AS n2
   FROM   generate_series(0, n*2*m - 1) n2m
   ) sub
ORDER  BY n2m
$func$  LANGUAGE sql IMMUTABLE;

Llamada:

SELECT * FROM generate_up_down_series(3, 4);

Genera el resultado deseado. n y m puede ser cualquier número entero, donde n * 2 * m no se desborde int4.

¿Cómo?

En la subconsulta:

  • Genere el número total deseado de filas ( n * 2 * m ), con un número ascendente simple. Lo nombro n2m. 0 a N-1 (no 1 a N ) para simplificar la siguiente operación de módulo .

  • Tómelo % n * 2 ( %es el operador del módulo) para obtener una serie de n números ascendentes, m veces. Lo nombro n2.

En la consulta externa:

  • Agregue 1 a la mitad inferior ( n2 <n ).

  • Para la mitad superior ( n2> = n ) espejo de la mitad inferior con n * 2 - n2 .

  • Agregué ORDER BYpara garantizar el pedido solicitado. Con las versiones actuales o Postgres también funciona sin ORDER BYla consulta simple, ¡pero no necesariamente en consultas más complejas! Ese es un detalle de implementación (y no va a cambiar) pero no está garantizado por el estándar SQL.

Desafortunadamente, generate_series()es Postgres específico y no SQL estándar, como se ha comentado. Pero podemos reutilizar la misma lógica:

SQL estándar

Puede generar los números de serie con un CTE recursivo en lugar de generate_series(), o, más eficientemente para uso repetido, crear una tabla con números enteros de serie una vez. ¡Cualquiera puede leer, nadie puede escribirle!

CREATE TABLE int_seq (i integer);

WITH RECURSIVE cte(i) AS (
   SELECT 0
   UNION ALL
   SELECT i+1 FROM cte
   WHERE  i < 20000  -- or as many you might need!
   )
INSERT INTO int_seq
SELECT i FROM cte;

Entonces, lo anterior se SELECTvuelve aún más simple:

SELECT CASE WHEN n2 < n THEN n2 + 1 ELSE n*2 - n2 END AS x
FROM  (
   SELECT i, i % (n*2) AS n2
   FROM   int_seq
   WHERE  i < n*2*m  -- remember: 0 to N-1
   ) sub
ORDER  BY i;

5

Si necesita SQL simple. Teóricamente debería funcionar en la mayoría de los DBMS (probado en PostgreSQL y SQLite):

with recursive 
  s(i,n,z) as (
    select * from (values(1,1,1),(3*2,1,2)) as v  -- Here 3 is n
    union all
    select
      case z when 1 then i+1 when 2 then i-1 end, 
      n+1,
      z 
    from s 
    where n < 3), -- And here 3 is n
  m(m) as (select 1 union all select m+1 from m where m < 2) -- Here 2 is m

select n from s, m order by m, i;

Explicación

  1. Generar series 1..n

    Asumiendo que n=3

    with recursive s(n) as (
      select 1
      union all
      select n+1 from s where n<3
    )
    select * from s;

    Es bastante simple y se puede encontrar en casi cualquier documento sobre CTE recursivos. Sin embargo, necesitamos dos instancias de cada valor.

  2. Generar series 1,1, .., n, n

    with recursive s(n) as (
      select * from (values(1),(1)) as v
      union all
      select n+1 from s where n<3
    )
    select * from s;

    Aquí simplemente duplicamos el valor inicial, que tiene dos filas, pero el segundo grupo que necesitamos en el orden inverso, por lo que introduciremos el orden en un momento.

  3. Antes de presentar el orden, observe que esto también es una cosa. Podemos tener dos filas en la condición inicial con tres columnas cada una, nuestra n<3sigue siendo una sola columna condicional. Y, todavía estamos aumentando el valor de n.

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(1,1,1)) as v
      union all
      select
        i,
        n+1,
        z 
      from s where n<3
    )
    select * from s;
  4. Del mismo modo, podemos mezclarlos un poco, ver cómo cambia nuestra condición de inicio aquí : aquí tenemos un (6,2),(1,1)

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(6,1,2)) as v
      union all
      select
        i,
        n+1,
        z 
      from s where n<3
    )
    select * from s;
  5. Generar series 1..n, n..1

    El truco aquí es generar la serie, (1..n) dos veces, y luego simplemente cambiar el orden en el segundo conjunto.

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(3*2,1,2)) as v
      union all
      select
        case z when 1 then i+1 when 2 then i-1 end, 
        n+1,
        z 
      from s where n<3
    )
    select * from s order by i;

    Aquí iestá el orden y el znúmero de la secuencia (o la mitad de la secuencia si lo desea). Entonces, para la secuencia 1 estamos aumentando el orden de 1 a 3 y para la secuencia 2 estamos disminuyendo el orden de 6 a 4. Y finalmente

  6. Multiplica la serie por m

    (ver la primera consulta en la respuesta)


3

Si desea una solución portátil, debe darse cuenta de que esto es básicamente un problema matemático .

Dado @n como el número más alto de la secuencia y @x como la posición del número en esa secuencia (comenzando con cero), la siguiente función funcionaría en SQL Server:

CREATE FUNCTION UpDownSequence
(
    @n int, -- Highest number of the sequence
    @x int  -- Position of the number we need
)
RETURNS int
AS
BEGIN
    RETURN  @n - 0.5 * (ABS((2*((@x % (@n+@n))-@n)) +1) -1)
END
GO

Puede verificarlo con este CTE:

DECLARE @n int=3;--change the value as needed
DECLARE @m int=4;--change the value as needed

WITH numbers(num) AS (SELECT 0 
                      UNION ALL
                      SELECT num+1 FROM numbers WHERE num+1<2*@n*@m) 
SELECT num AS Position, 
       dbo.UpDownSequence(@n,num) AS number
FROM numbers
OPTION(MAXRECURSION 0)

(Explicación rápida: la función utiliza MODULO () para crear una secuencia de números repetidos y ABS () para convertirla en una onda en zig-zag. Las otras operaciones transforman esa onda para que coincida con el resultado deseado.)


2

En PostgreSQL, esto es fácil,

CREATE OR REPLACE FUNCTION generate_up_down_series(n int, m int)
RETURNS setof int AS $$
SELECT x FROM (
  SELECT 1, ordinality AS o, x FROM generate_series(1,n) WITH ORDINALITY AS t(x)
  UNION ALL
  SELECT 2, ordinality AS o, x FROM generate_series(n,1,-1) WITH ORDINALITY AS t(x)
) AS t(o1,o2,x)
CROSS JOIN (
  SELECT * FROM generate_series(1,m)
) AS g(y)
ORDER BY y,o1,o2
$$ LANGUAGE SQL;

2

Esto funciona en MS-SQL y creo que se puede modificar para cualquier versión de SQL.

declare @max int, @repeat int, @rid int

select @max = 3, @repeat = 4

-- create a temporary table
create table #temp (row int)

--create seed rows
while (select count(*) from #temp) < @max * @repeat * 2
begin
    insert into #temp
    select 0
    from (values ('a'),('a'),('a'),('a'),('a')) as a(col1)
    cross join (values ('a'),('a'),('a'),('a'),('a')) as b(col2)
end

-- set row number can also use identity
set @rid = -1

update #temp
set     @rid = row = @rid + 1

-- if the (row/max) is odd, reverse the order
select  case when (row/@max) % 2 = 1 then @max - (row%@max) else (row%@max) + 1 end
from    #temp
where   row < @max * @repeat * 2
order by row

2

Una forma de hacerlo en SQL Server utilizando un cte recursivo.

1) Genere el número requerido de miembros en la serie (para n = 3 ym = 4 sería 24, que es 2 * n * m)

2) Después de usar la lógica en una caseexpresión, puede generar la serie requerida.

Sample Demo

declare @n int=3;--change the value as needed
declare @m int=4;--change the value as needed

with numbers(num) as (select 1 
                      union all
                      select num+1 from numbers where num<2*@n*@m) 
select case when (num/@n)%2=0 and num%@n<>0 then num%@n 
            when (num/@n)%2=0 and num%@n=0 then 1  
            when (num/@n)%2=1 and num%@n<>0 then @n+1-(num%@n)  
            when (num/@n)%2=1 and num%@n=0 then @n
       end as num
from numbers
OPTION(MAXRECURSION 0)

Como lo sugiere @AndriyM .. la caseexpresión se puede simplificar a

with numbers(num) as (select 0
                      union all
                      select num+1 from numbers where num<2*@n*@m-1) 
select case when (num/@n)%2=0 then num%@n + 1
            when (num/@n)%2=1 then @n - num%@n
       end as num
from numbers
OPTION(MAXRECURSION 0)

Demo


2

Usando solo Math + - * /y Modulo básicos :

SELECT x
    , s = x % (2*@n) +
         (1-2*(x % @n)) * ( ((x-1) / @n) % 2)
FROM (SELECT TOP(2*@n*@m) x FROM numbers) v(x)
ORDER BY x;

Esto no requiere un SGBD específico.

Con numbersser una tabla de números:

...; 
WITH numbers(x) AS(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
    FROM (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n0(x)
    CROSS JOIN (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n1(x)
    CROSS JOIN (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n2(x)
)
...

Esto genera una tabla de números (1-1000) sin usar un CTE recursivo. Ver muestra . 2 * n * m debe ser menor que el número de filas en números.

Salida con n = 3 ym = 4:

x   s
1   1
2   2
3   3
4   3
5   2
6   1
7   1
8   2
... ...

Esta versión requiere una tabla numérica más pequeña (v> = ny v> = m):

WITH numbers(v) AS(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
    FROM (VALUES(1), (2), (3), (4), (5), (6), ...) AS n(x)
)
SELECT ord = @n*(v+2*m) + n
    , n*(1-v) + ABS(-@n-1+n)*v
FROM (SELECT TOP(@n) v FROM numbers ORDER BY v ASC) n(n)
CROSS JOIN (VALUES(0), (1)) AS s(v)
CROSS JOIN (SELECT TOP(@m) v-1 FROM numbers ORDER BY v ASC) m(m)
ORDER BY ord;

Ver muestra .


2

Una función básica usando iteradores.

T-SQL

create function generate_up_down_series(@max int, @rep int)
returns @serie table
(
    num int
)
as
begin

    DECLARE @X INT, @Y INT;
    SET @Y = 0;

    WHILE @Y < @REP
    BEGIN

        SET @X = 1;
        WHILE (@X <= @MAX)
        BEGIN
            INSERT @SERIE
            SELECT @X;
            SET @X = @X + 1;
        END

        SET @X = @MAX;
        WHILE (@X > 0)
        BEGIN
            INSERT @SERIE
            SELECT @X;
            SET @X = @X -1;
        END

        SET @Y = @Y + 1;
    END

    RETURN;
end
GO

Postgres

create or replace function generate_up_down_series(maxNum int, rep int)
returns table (serie int) as
$body$
declare
    x int;
    y int;
    z int;
BEGIN

    x := 0;
    while x < rep loop

        y := 1;
        while y <= maxNum loop
            serie := y;
            return next;
            y := y + 1;
        end loop;

        z := maxNum;
        while z > 0 loop
            serie := z;
            return next;
            z := z - 1;
        end loop;

        x := x + 1;
    end loop;

END;
$body$ LANGUAGE plpgsql;

1
declare @n int = 5;
declare @m int = 3;
declare @t table (i int, pk int identity);
WITH  cte1 (i) 
AS ( SELECT 1
     UNION ALL
     SELECT i+1 FROM cte1
     WHERE  i < 100  -- or as many you might need!
   )
insert into @t(i) select i from cte1 where i <= @m  order by i
insert into @t(i) select i from @t order by i desc
select t.i --, t.pk, r.pk 
from @t as t 
cross join (select pk from @t where pk <= @n) as r
order by r.pk, t.pk
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.