web-dev-qa-db-fra.com

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?

Étant donné deux nombres n et 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 m fois.

Par exemple, pour n = 3 et 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:

À l'aide de la requête suivante, qui utilise le generate_series fonction, 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?

11
joanolo

Dans Postgres, c'est facile d'utiliser la fonction 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, 
    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 à la taille des paramètres n, m, c'est-à-dire moins d'un million - vous pouvez utiliser une table Numbers:

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 utilisez-le, 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 ;
4
ypercubeᵀᴹ

Postgres

Vous pouvez le faire fonctionner avec un singlegenerate_series() et les mathématiques 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 peut être n'importe quel entier où n * 2 * m fait 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 BY Pour garantir la commande demandée. Avec les versions actuelles ou Postgres, il fonctionne également sans ORDER BY Pour la requête simple - 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 non garanti par la norme SQL.

Malheureusement, generate_series() est spécifique à Postgres et non SQL 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 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, le SELECT ci-dessus devient 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;
10
Erwin Brandstetter

Si vous avez besoin de SQL simple. Théoriquement, il devrait fonctionner sur la plupart des SGBD (testé 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<3 est 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 : ici nous avons 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ération des 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;
    

    Ici i est la commande et z est le numé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 en m

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

5
Abelisto

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 zigzag. Les autres opérations transforment cette onde pour correspondre au résultat souhaité.)

3
Twinkles

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
Evan Carroll

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;
2
McNets

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
Jules

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

1) Générez 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 expression case, 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'expression case 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
vkp

Utiliser 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.

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 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 exemple .

2
Julien Vavasseur
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
1
paparazzo