Como gerar uma série 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, ... no padrão SQL ou T-SQL?

11

Dados dois números ne m, quero gerar uma série do formulário

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

e repita isso mvezes.

Por exemplo, para n = 3e m = 4, quero uma sequência dos 24 números a seguir:

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

Eu sei como alcançar esse resultado no PostgreSQL por um dos dois métodos:

Usando a consulta a seguir, que usa a generate_seriesfunção, e alguns truques para garantir que o pedido seja o correto:

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 ;

... ou use uma função para a mesma finalidade, com loops adjuntos e aninhados:

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 ;

Como eu poderia fazer o equivalente no SQL padrão ou no Transact-SQL / SQL Server?

joanolo
fonte

Respostas:

4

No Postgres, é fácil usar a generate_series()função:

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 ;

No SQL padrão - e supondo que exista um limite razoável no tamanho dos parâmetros n, m, ou seja, menos de um milhão - você pode usar uma Numberstabela:

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

preencha-o com o método preferido do seu DBMS:

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

e depois use-o, em vez 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 ;
ypercubeᵀᴹ
fonte
Na prática, não espero que esses números sejam maiores que 100; mas em teoria eles poderiam ser qualquer coisa.
joanolo
10

Postgres

Você pode fazê-lo funcionar com uma matemática única generate_series() e básica (consulte funções matemáticas ).

Embrulhado em uma função SQL simples:

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;

Ligar:

SELECT * FROM generate_up_down_series(3, 4);

Gera o resultado desejado. n e m podem ser qualquer número inteiro em que n * 2 * m não transborda int4.

Quão?

Na subconsulta:

  • Gere o número total desejado de linhas ( n * 2 * m ), com um número crescente simples. Eu cito n2m. 0 a N-1 (não 1 a N ) para simplificar a seguinte operação do módulo .

  • Aceite % n * 2 ( %é o operador do módulo) para obter uma série de n números ascendentes, m vezes. Eu cito n2.

Na consulta externa:

  • Adicione 1 à metade inferior ( n2 <n ).

  • Para a metade superior ( n2> = n ), espelho da metade inferior com n * 2 - n2 .

  • Eu adicionei ORDER BYpara garantir o pedido solicitado. Nas versões atuais ou no Postgres, ele também funciona sem ORDER BYa consulta simples - mas não necessariamente em consultas mais complexas! Esse é um detalhe da implementação (e não vai mudar), mas não é garantido pelo padrão SQL.

Infelizmente, o generate_series()Postgres é específico e não o SQL padrão, como foi comentado. Mas podemos reutilizar a mesma lógica:

SQL padrão

Você pode gerar os números de série com um CTE recursivo em vez de generate_series(), ou, de forma mais eficiente para uso repetido, criar uma tabela com números inteiros de série uma vez. Qualquer um pode ler, ninguém pode escrever para ele!

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;

Então, o acima SELECTse torna ainda mais simples:

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;
Erwin Brandstetter
fonte
5

Se você precisar de SQL simples. Teoricamente, ele deve funcionar com a maioria dos DBMSs (testados no PostgreSQL e 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;

Explicação

  1. Gere a série 1..n

    Assumindo que n=3

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

    É bastante simples e pode ser encontrado em quase todos os documentos sobre CTEs recursivas. No entanto, precisamos de duas instâncias de cada valor para que

  2. Gerar séries 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;
    

    Aqui apenas dobramos o valor inicial, que tem duas linhas, mas o segundo agrupamento é necessário na ordem inversa, portanto, apresentaremos a ordem daqui a pouco.

  3. Antes de introduzirmos a ordem, observe que isso também é uma coisa. Podemos ter duas linhas na condição inicial com três colunas cada, a nossa n<3ainda é uma única coluna condicional. E ainda estamos apenas aumentando o 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. Da mesma forma, podemos misturá-los um pouco, observar nossa condição inicial mudar aqui : aqui temos um (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. Gere a série 1..n, n..1

    O truque aqui é gerar a série (1..n) duas vezes e simplesmente alterar a ordem no 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;
    

    Aqui iestá a ordem e o znúmero da sequência (ou metade da sequência, se você desejar). Portanto, para a sequência 1, estamos aumentando a ordem de 1 para 3 e, para a sequência 2, estamos diminuindo a ordem de 6 para 4. E finalmente

  6. Multiplique a série para m

    (veja a primeira consulta na resposta)

Abelisto
fonte
3

Se você deseja uma solução portátil, precisa entender que isso é basicamente um problema matemático .

Dado @n como o número mais alto da sequência e @x como a posição do número nessa sequência (começando com zero), a seguinte função funcionaria no 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

Você pode verificá-lo com 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)

(Explicação rápida: a função usa MODULO () para criar uma sequência de números repetidos e ABS () para transformá-lo em uma onda em zig-zag. As outras operações transformam essa onda para corresponder ao resultado desejado.)

Twinkles
fonte
2

No PostgreSQL, isso é 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;
Evan Carroll
fonte
2

Isso funciona no MS-SQL e acho que pode ser modificado para qualquer sabor 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
Jules
fonte
2

Uma maneira de fazer isso no SQL Server usando um cte recursivo.

1) Gere o número necessário de membros da série (para n = 3 em = 4, seria 24 o que é 2 * n * m)

2) Depois disso, usando a lógica em uma caseexpressão, você pode gerar as séries necessárias.

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)

Conforme sugerido por @AndriyM .. a caseexpressão pode ser simplificada para

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

vkp
fonte
2

Usando apenas matemática + - * /e módulo 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;

Isso não requer um SGBD específico.

Com numberssendo uma tabela 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)
)
...

Isso gera uma tabela numérica (1-1000) sem usar um CTE recursivo. Veja exemplo . 2 * n * m deve ser menor que o número de linhas em números.

Saída com n = 3 em = 4:

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

Esta versão requer uma tabela numérica menor (v> = ne 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;

Veja exemplo .

Julien Vavasseur
fonte
2

Uma função 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;
McNets
fonte
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
paparazzo
fonte