Comment générer une série 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1,… en SQL standard ou T-SQL?


11

Étant donné deux nombres net m, je veux générer une série du formulaire

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

et répétez-le mfois.

Par exemple, pour n = 3et m = 4, je veux une séquence des 24 chiffres suivants:

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

Je sais comment obtenir ce résultat dans PostgreSQL par l'une des deux méthodes suivantes:

Utilisation de la requête suivante, qui utilise la generate_seriesfonction, et quelques astuces pour garantir que la commande est la bonne:

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 utilisez une fonction dans le même but, avec des boucles adjointes et imbriquées:

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 ;

Comment pourrais-je éventuellement faire l'équivalent dans SQL standard ou dans Transact-SQL / SQL Server?

Réponses:


4

Dans Postgres, il est facile d'utiliser la generate_series()fonction:

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 standard - et en supposant qu'il existe une limite raisonnable sur la taille des paramètres n, m, c'est-à-dire moins d'un million - vous pouvez utiliser une Numberstable:

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

remplissez-le avec la méthode préférée de votre SGBD:

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

puis l'utiliser, au lieu 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 pratique, je ne m'attends pas à ce que ces chiffres soient supérieurs à 100; mais en théorie, ils pourraient être n'importe quoi.
joanolo

10

Postgres

Vous pouvez le faire fonctionner avec une seule generate_series() mathématique et de base (voir fonctions mathématiques ).

Enveloppé dans une simple fonction 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;

Appel:

SELECT * FROM generate_up_down_series(3, 4);

Génère le résultat souhaité. n et m peuvent être n'importe quel entier où n * 2 * m ne déborde pas int4.

Comment?

Dans la sous-requête:

  • Générez le nombre total de lignes souhaité ( n * 2 * m ), avec un simple nombre croissant. Je le nomme n2m. 0 à N-1 (pas 1 à N ) pour simplifier l' opération modulo suivante .

  • Prenez-le % n * 2 ( %est l'opérateur modulo) pour obtenir une série de n nombres ascendants, m fois. Je le nomme n2.

Dans la requête externe:

  • Ajoutez 1 à la moitié inférieure ( n2 <n ).

  • Pour la moitié supérieure ( n2> = n ) miroir de la moitié inférieure avec n * 2 - n2 .

  • J'ai ajouté ORDER BYpour garantir la commande demandée. Avec les versions actuelles ou Postgres, il fonctionne également sans ORDER BYpour la simple requête - mais pas nécessairement dans les requêtes plus complexes! C'est un détail d'implémentation (et ça ne va pas changer) mais pas garanti par la norme SQL.

Malheureusement, generate_series()Postgres est spécifique à SQL et non standard, comme cela a été commenté. Mais on peut réutiliser la même logique:

SQL standard

Vous pouvez générer les numéros de série avec un CTE récursif au lieu de generate_series(), ou, plus efficacement pour une utilisation répétée, créer une table avec des nombres entiers en série une fois. Tout le monde peut lire, personne ne peut y écrire!

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;

Ensuite, ce qui précède SELECTdevient encore plus 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 vous avez besoin de SQL simple. Théoriquement, il devrait fonctionner sur la plupart des SGBD (testés sur PostgreSQL et 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;

Explication

  1. Générer la série 1..n

    En admettant que n=3

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

    Il est assez simple et peut être trouvé dans presque tous les documents sur les CTE récursifs. Cependant, nous avons besoin de deux instances de chaque valeur afin

  2. Générer les 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;

    Ici, nous doublons simplement la valeur initiale, qui a deux lignes, mais le deuxième groupe dont nous avons besoin dans l'ordre inverse, nous allons donc introduire l'ordre dans un peu.

  3. Avant d'introduire la commande, notez que c'est aussi une chose. Nous pouvons avoir deux lignes dans la condition de départ avec trois colonnes chacune, notre n<3est toujours une seule colonne conditionnelle. Et, nous augmentons toujours la valeur 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. De même, nous pouvons les mélanger un peu, regardez notre condition de départ changer ici : nous avons ici 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. Générer les séries 1..n, n..1

    L'astuce consiste à générer deux fois la série (1..n), puis à modifier simplement l'ordre sur le deuxième ensemble.

    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;

    Voici l' iordre et le znuméro de la séquence (ou la moitié de la séquence si vous le souhaitez). Donc, pour la séquence 1, nous augmentons l'ordre de 1 à 3 et pour la séquence 2, nous diminuons l'ordre de 6 à 4. Et enfin

  6. Multipliez la série pour m

    (voir la première requête dans la réponse)


3

Si vous voulez une solution portable, vous devez vous rendre compte qu'il s'agit essentiellement d'un problème mathématique .

Étant donné que @n est le numéro le plus élevé de la séquence et @x la position du numéro dans cette séquence (en commençant par zéro), la fonction suivante fonctionnerait dans 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

Vous pouvez le vérifier avec ce 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)

(Explication rapide: la fonction utilise MODULO () pour créer une séquence de nombres répétitifs et ABS () pour la transformer en une vague en zig-zag. Les autres opérations transforment cette onde pour correspondre au résultat souhaité.)


2

Dans PostgreSQL, c'est 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

Cela fonctionne en MS-SQL et je pense qu'il peut être modifié pour n'importe quelle saveur 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

Une façon de le faire dans SQL Server en utilisant un cte récursif.

1) Générer le nombre requis de membres dans la série (pour n = 3 et m = 4 ce serait 24 qui est 2 * n * m)

2) Après cela, en utilisant la logique dans une caseexpression, vous pouvez générer la série requise.

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)

Comme suggéré par @AndriyM .. l' caseexpression peut être simplifiée en

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

En utilisant uniquement les mathématiques de base + - * /et Modulo:

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;

Cela ne nécessite pas de SGBD spécifique.

En numbersétant une table numérique:

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

Cela génère une table numérique (1-1000) sans utiliser de CTE récursif. Voir l' exemple . 2 * n * m doit être inférieur au nombre de lignes en chiffres.

Sortie avec n = 3 et m = 4:

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

Cette version nécessite une table numérique plus petite (v> = n et 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;

Voir l' exemple .


2

Une fonction de base utilisant des itérateurs.

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.