É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?
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 ;
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
.
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:
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;
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;
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
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.
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;
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;
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
Multipliez la série en m
(voir la première requête dans la réponse)
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é.)
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;
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;
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
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.
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)
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 .
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