Как создать серии 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1,… в стандартном SQL или T-SQL?


11

Учитывая два числа nи m, я хочу создать серию вида

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

и повтори это mраз.

Например, для n = 3и m = 4я хочу последовательность из следующих 24 чисел:

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

Я знаю, как добиться этого результата в PostgreSQL любым из двух способов:

Используя следующий запрос, который использует generate_seriesфункцию, и несколько хитростей, чтобы гарантировать, что порядок правильный:

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 ;

... или использовать функцию для той же цели с присоединенными и вложенными циклами:

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 ;

Как я мог сделать эквивалент в стандартном SQL или в Transact-SQL / SQL Server?

Ответы:


4

В Postgres легко использовать 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 ;

В стандартном SQL - и при условии, что существует разумный предел размера параметров n, m, т. Е. Менее миллиона - вы можете использовать Numbersтаблицу:

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

заполните его предпочитаемым методом вашей СУБД:

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

и затем используйте его вместо 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 ;

На практике я не ожидаю, что эти цифры будут больше 100; но в теории они могут быть чем угодно.
Жоаноло

10

Postgres

Вы можете заставить его работать с одной generate_series() основной математикой (см. Математические функции ).

Обернут в простую функцию 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;

Вызов:

SELECT * FROM generate_up_down_series(3, 4);

Создает желаемый результат. n и m может быть любым целым числом, где n * 2 * m не переполняется int4.

Как?

В подзапросе:

  • Создайте желаемое общее количество строк ( n * 2 * m ) с простым возрастающим числом. Я называю это n2m. От 0 до N-1 (не от 1 до N ), чтобы упростить следующую операцию по модулю .

  • Возьмем % n * 2 ( %это оператор по модулю), чтобы получить последовательность из n возрастающих чисел, m раз. Я называю это n2.

Во внешнем запросе:

  • Добавьте 1 к нижней половине ( n2 <n ).

  • Для верхней половины ( n2> = n ) зеркало нижней половины с n * 2 - n2 .

  • Я добавил, ORDER BYчтобы гарантировать запрошенный заказ. С текущими версиями или Postgres это также работает без ORDER BYпростого запроса - но не обязательно в более сложных запросах! Это деталь реализации (и она не изменится), но не гарантируется стандартом SQL.

К сожалению, generate_series()это специфический для Postgres, а не стандартный SQL, как уже было сказано. Но мы можем использовать ту же логику:

Стандартный SQL

Вы можете генерировать серийные номера с помощью рекурсивного CTE вместо generate_series()или, более эффективно для многократного использования, создать таблицу с серийными целыми числами один раз. Любой может читать, никто не может писать в него!

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;

Тогда вышеупомянутое SELECTстановится еще проще:

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

Если вам нужен простой SQL. Теоретически он должен работать на большинстве СУБД (проверено на PostgreSQL и 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;

объяснение

  1. Генерация серии 1..n

    При условии, что n=3

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

    Это довольно просто и может быть найдено почти в любой документации о рекурсивных CTE. Однако нам нужно два экземпляра каждого значения так

  2. Генерировать серии 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;

    Здесь мы просто удваиваем начальное значение, в котором есть две строки, но вторая группа нам нужна в обратном порядке, поэтому мы немного введем порядок.

  3. Прежде чем вводить порядок, обратите внимание, что это тоже вещь. У нас может быть две строки в начальном условии с тремя столбцами в каждом, наш n<3по-прежнему является условным столбцом. И мы все еще только увеличиваем ценность 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. Аналогично, мы можем немного их перепутать, посмотреть, как меняется наше начальное условие : здесь мы имеем (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. Генерация серии 1..n, n..1

    Хитрость заключается в том, чтобы сгенерировать серию (1..n) дважды, а затем просто изменить порядок во втором наборе.

    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;

    Здесь iпорядок и zномер последовательности (или половина последовательности, если хотите). Таким образом, для последовательности 1 мы увеличиваем порядок с 1 до 3, а для последовательности 2 мы уменьшаем порядок с 6 до 4. И, наконец,

  6. Умножьте ряд на m

    (см. первый запрос в ответе)


3

Если вы хотите портативное решение, вы должны понимать, что это в основном математическая проблема.

Учитывая @n как наибольшее число последовательности и @x как позицию числа в этой последовательности (начиная с нуля), в 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

Вы можете проверить это с этим 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)

(Краткое объяснение: функция использует MODULO () для создания последовательности повторяющихся чисел и ABS (), чтобы превратить ее в зигзагообразную волну. Другие операции преобразуют эту волну в соответствии с желаемым результатом.)


2

В PostgreSQL это легко,

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

Это работает в MS-SQL, и я думаю, что может быть изменено для любой разновидности 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

Способ сделать это в SQL Server с помощью рекурсивного cte.

1) Создайте необходимое количество членов в серии (для n = 3 и m = 4 это будет 24, что составляет 2 * n * m)

2) После этого, используя логику в caseвыражении, вы можете сгенерировать нужный ряд.

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)

Как предлагает @AndriyM .. caseвыражение может быть упрощено до

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

Используя только базовые математические + - * /и по модулю:

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;

Это не требует определенного SGBD.

С numbersтаблицей чисел:

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

Это создает таблицу чисел (1-1000) без использования рекурсивного CTE. Смотрите образец . 2 * n * m должно быть меньше номера строки в числах.

Выход с n = 3 и m = 4:

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

Для этой версии требуется таблица чисел меньшего размера (v> = n и 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;

Смотрите образец .


2

Основная функция с использованием итераторов.

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.