Come generare una serie 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, ... in SQL standard o T-SQL?


11

Dati due numeri ne m, voglio generare una serie del modulo

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

e ripeterlo mvolte.

Ad esempio, per n = 3e m = 4, voglio una sequenza dei seguenti 24 numeri:

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

So come raggiungere questo risultato in PostgreSQL con uno dei due metodi seguenti:

Utilizzando la seguente query, che utilizza la generate_seriesfunzione, e alcuni trucchi per garantire che l'ordine sia quello giusto:

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 usa una funzione per lo stesso scopo, con loop aggiunti e annidati:

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 ;

Come potrei eventualmente fare l'equivalente in SQL standard o in Transact-SQL / SQL Server?

Risposte:


4

In Postgres, è facile usare la generate_series()funzione:

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 ;

In SQL standard - e supponendo che vi sia un limite ragionevole per la dimensione dei parametri n, m, ovvero inferiore a un milione - è possibile utilizzare una Numberstabella:

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

riempilo con il metodo preferito del tuo DBMS:

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

e poi usalo, invece di 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 ;

In pratica, non mi aspetto che quei numeri siano più grandi di 100; ma in teoria potrebbero essere qualsiasi cosa.
joanolo

10

Postgres

Puoi farlo funzionare con una matematica singola generate_series() e di base (vedi funzioni matematiche ).

Avvolto in una semplice funzione 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;

Chiamata:

SELECT * FROM generate_up_down_series(3, 4);

Genera il risultato desiderato. n e m possono essere qualsiasi numero intero in cui n * 2 * m non trabocca int4.

Come?

Nella sottoquery:

  • Genera il numero totale desiderato di righe ( n * 2 * m ), con un semplice numero crescente. Lo chiamo n2m. Da 0 a N-1 (non da 1 a N ) per semplificare la seguente operazione del modulo .

  • Prendilo % n * 2 ( %è l'operatore modulo) per ottenere una serie di n numeri ascendenti, m volte. Lo chiamo n2.

Nella query esterna:

  • Aggiungi 1 alla metà inferiore ( n2 <n ).

  • Per la metà superiore ( n2> = n ) specchio della metà inferiore con n * 2 - n2 .

  • Ho aggiunto ORDER BYper garantire l'ordine richiesto. Con le versioni attuali o Postgres funziona anche senza ORDER BYla semplice query, ma non necessariamente in query più complesse! Questo è un dettaglio dell'implementazione (e non cambierà) ma non è garantito dallo standard SQL.

Sfortunatamente, generate_series()Postgres è SQL specifico e non standard, come è stato commentato. Ma possiamo riutilizzare la stessa logica:

SQL standard

È possibile generare i numeri seriali con un CTE ricorsivo anziché generate_series(), o, in modo più efficiente per un uso ripetuto, creare una tabella con numeri interi seriali una volta. Chiunque può leggere, nessuno può scrivergli!

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;

Quindi, quanto sopra SELECTdiventa ancora più semplice:

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

Se hai bisogno di un semplice SQL. Teoricamente dovrebbe funzionare sulla maggior parte dei DBMS (testati su 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;

Spiegazione

  1. Genera serie 1..n

    Supponendo che n=3

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

    È abbastanza semplice e potrebbe essere trovato in quasi tutti i documenti sui CTE ricorsivi. Tuttavia abbiamo bisogno di due istanze di ciascun valore così

  2. Genera serie 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;

    Qui stiamo solo raddoppiando il valore iniziale, che ha due righe, ma il secondo gruppo è necessario nell'ordine inverso, quindi introdurremo un po 'l'ordine.

  3. Prima di introdurre l'ordine, osserva che anche questa è una cosa. Possiamo avere due righe nella condizione iniziale con tre colonne ciascuna, la nostra n<3è ancora una singola colonna condizionale. E stiamo ancora aumentando il valore di 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. Allo stesso modo, possiamo mescolarli un po ', guardare qui la nostra condizione iniziale cambiare : qui abbiamo 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. Genera serie 1..n, n..1

    Il trucco qui è generare la serie, (1..n) due volte, e quindi semplicemente cambiare l'ordinamento sul secondo set.

    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;

    Ecco l' iordine ed zè il numero della sequenza (o metà della sequenza, se lo si desidera). Quindi per la sequenza 1 stiamo aumentando l'ordine da 1 a 3 e per la sequenza 2 stiamo diminuendo l'ordine da 6 a 4. E infine

  6. Moltiplica la serie a m

    (vedi la prima query nella risposta)


3

Se si desidera una soluzione portatile, è necessario rendersi conto che questo è fondamentalmente un problema matematico .

Dato @n come il numero più alto della sequenza e @x come posizione del numero in quella sequenza (a partire da zero), la seguente funzione funzionerebbe in 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

Puoi verificarlo con questo 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)

(Spiegazione rapida: la funzione utilizza MODULO () per creare una sequenza di numeri ripetuti e ABS () per trasformarla in un'onda a zig-zag. Le altre operazioni trasformano quell'onda in modo che corrisponda al risultato desiderato.)


2

In PostgreSQL, questo è facile,

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

Funziona in MS-SQL e penso che possa essere modificato per qualsiasi tipo di 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

Un modo per farlo in SQL Server usando un cte ricorsivo.

1) Genera il numero richiesto di membri nella serie (per n = 3 e m = 4 sarebbe 24 che è 2 * n * m)

2) Successivamente, utilizzando la logica in caseun'espressione, è possibile generare le serie richieste.

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)

Come suggerito da @AndriyM .. l' caseespressione può essere semplificata

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

Utilizzando solo matematica + - * /e modulo di base:

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;

Questo non richiede un SGBD specifico.

Con l' numbersessere una tabella numerica:

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

Ciò genera una tabella numerica (1-1000) senza utilizzare un CTE ricorsivo. Vedi esempio . 2 * n * m deve essere inferiore al numero di righe nei numeri.

Uscita con n = 3 e m = 4:

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

Questa versione richiede una tabella numerica più piccola (v> = n e 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;

Vedi esempio .


2

Una funzione di base che utilizza iteratori.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.