Функция postgresql округления наполовину вниз

функция round(numeric,integer) в PostgreSQL округляет только в большую сторону:

round(cast (41.0255 as numeric),3) ==> 41.026

Так как нам нужна круглая функция, которая возвращает 41.025 и (что удивительно) такой функции нет в PostgreSQL (мы используем 9.1.5), мы написали функцию-обертку, которая в самой первой версии довольно наивна и грубо... но мы не нашли ничего лучше из-за отсутствия встроенной поддержки такого рода проблем в plpgsql.

Код показан ниже. Проблема в том, что он слишком медленный для наших целей. Не могли бы вы предложить лучший способ справиться с этой задачей?

Вот код:

    CREATE OR REPLACE FUNCTION round_half_down(numeric,integer) RETURNS numeric 
    AS $$
    DECLARE
      arg ALIAS FOR $1;
      rnd ALIAS FOR $2;
      tmp1 numeric;
      res numeric;
    BEGIN
      tmp1:=arg;
      IF cast(tmp1 as varchar) ~ '5$'  THEN res:=trunc(arg,rnd);
      ELSE res:=round(arg,rnd);
      END IF;

      RETURN res;
    END;
    $$ LANGUAGE plpgsql;

Мне нужно привести числовое значение и использовать регулярное выражение... это то, что (я полагаю) убивает производительность.

Просто чтобы вы знали: нам это нужно, потому что мы должны сравнивать числа, которые были сохранены в двух разных столбцах (в двух разных таблицах), но с разными числовыми типами данных: одно двойное, а другое действительное. Проблема в том, что при вставке в столбец с реальным типом данных PostgreSQL выполняет ROUND HALF DOWN, в то время как он не предоставляет такой возможности через свои математические функции!

РЕДАКТИРОВАТЬ:
Функция на самом деле прослушивается. Это была первая быстрая переработка как попытка улучшить производительность работающей функции, но очень медленная.

Поведение должно соответствовать следующему:
IF десятичная цифра, которая не учитывается при округлении, равна <=5 => trunc
ELSE округлению в большую сторону.

Несколько примеров:

select round_half_down(cast (41.002555 as numeric),3) -- 41.002 
select round_half_down(cast (41.002555 as numeric),4) -- 41.0025 
select round_half_down(cast (41.002555 as numeric),5) -- 41.00255 

в то время как функция раунда в PostgreSQL дает:

select round(cast (41.002555 as numeric),3) -- 41.003

person BangTheBank    schedule 21.09.2012    source источник
comment
Вы хотите округлить -41.0255 до -41.025 или -41.026? Также: вы хотите, чтобы 41.02551 округлялось до 41.025 или 41.026?   -  person Erwin Brandstetter    schedule 21.09.2012
comment
Привет, @erwin-brandstetter, Функция на самом деле прослушивается. Это была первая переработка для улучшения производительности рабочей функции, но очень медленная. Поведение должно соответствовать следующему: ЕСЛИ десятичная цифра, не подлежащая округлению, равна ‹=5 =› урезать, ИНАЧЕ округлить в большую сторону. Некоторые примеры: выберите round_half_down(приведение (41.002555 как числовое),3) -- 41.002 выберите round_half_down(приведение (41.002555 как числовое),4) -- 41.0025 выберите round_half_down(приведение (41.002555 как числовое),5) -- 41.00255 в то время как функция раунда в postgresql: выберите раунд (приведение (41.002555 как числовое), 3) -- 41.003   -  person BangTheBank    schedule 21.09.2012
comment
Я обновил свой ответ, чтобы он соответствовал вашим обновлениям. Кстати, такая информация должна быть в вашем вопросе, а не в комментарии, где ее трудно прочитать.   -  person Erwin Brandstetter    schedule 21.09.2012


Ответы (6)


Нам нужно сравнить числа, которые были сохранены в двух разных столбцах (в двух разных таблицах), но с разными числовыми типами данных: одно двойное, а другое вещественное.

Это должно быть очень быстро и просто:

SELECT dp_col, real_col
FROM   tbl
WHERE  dp_col::real = real_col

По сути, просто приведите число double precision к real для сравнения.


Если это не работает для вас, эта функция SQL должна выполнять правильную работу и работать быстрее:

CREATE OR REPLACE FUNCTION round_half_down1(numeric, int)
  RETURNS numeric LANGUAGE sql AS
$func$
SELECT CASE WHEN abs($1%0.1^$2) < .6 * 0.1^$2 THEN
         trunc($1, $2)
    ELSE round($1, $2) END;
$func$

Теперь исправлены отрицательные числа с вводом от @sufleR в комментариях.
Вы также можете просто использовать содержащееся выражение CASE.

% .. оператор по модулю
^ .. возведение в степень


Вот быстрый тест, который вы можете использовать для сравнительного анализа:

SELECT n                                   -- Total runtime: 36.524 ms
      ,round_half_down1(n,3)               -- Total runtime: 70.493 ms
      ,round_down_to_decimal_places(n,3)   -- Total runtime: 74.690 ms
      ,round_half_down(n,3)                -- Total runtime: 82.191 ms
FROM  (SELECT random()::numeric AS n FROM generate_series(1,10000)) x
WHERE  round_down_to_decimal_places(n,3)
    <> round_half_down1(n,3)

Это также демонстрирует, как функция @Parveen и ваша отредактированная версия ошибаются в расчетах - они trunc() там, где не должны.

person Erwin Brandstetter    schedule 21.09.2012
comment
@ user500501: Итак, приведение к real сработало для вас или функции? - person Erwin Brandstetter; 21.09.2012
comment
большое спасибо за ответ. Кастинг, как ни странно, не работает идеально. Я делаю больше тестов, чтобы выяснить, почему, так как это звучит правильно (и просто) для моей проблемы. Как ни странно, он не соответствует всем строкам, которые должен, и требуется целая вечность, чтобы найти несоответствующие, чтобы обнаружить проблему. По сути, все значения в столбце REAL должны совпадать с преобразованными в REAL значениями в столбце DOUBLE PRECISION (потому что именно это произошло, когда значения были вставлены в столбец REAL), но это не так! 73761 строка из 4267717 не сопоставляется. - person BangTheBank; 21.09.2012
comment
не работает для отрицательных значений. CASE WHEN $1%0.1^$2 должно быть CASE WHEN ABS($1%0.1^$2) - person sufleR; 04.02.2015
comment
@sufleR: Хороший улов. Спасибо, исправлено сейчас. - person Erwin Brandstetter; 04.02.2015

Метод очень быстрый без создания новой ФУНКЦИИ, которая может округлить половину, как показано ниже:

-- округлить до половины

round($n, 3)

-- округлить до половины

round($n-0.5, 3)
person congtinit    schedule 21.10.2014
comment
Это будет работать только без цифры после знака точки. Для 1 цифры используйте 0,05 и 2 цифры, 0,005 и так далее. - person divinedragon; 13.06.2019

Это просто:

Попробуйте использовать эту функцию

CREATE OR REPLACE FUNCTION ROUND_HALF_DOWN(NUMERIC)
  RETURNS NUMERIC LANGUAGE SQL AS
$FUNC$
  SELECT CASE WHEN ($1%1) < 0.6 THEN FLOOR($1) ELSE CEIL($1) END;
$FUNC$
person Adonias Vasquez    schedule 11.07.2015

Вот гораздо более простой подход

CREATE OR REPLACE FUNCTION roundHalfDown(value NUMERIC, prec INTEGER)
RETURNS NUMERIC AS $$
BEGIN
  RETURN trunc(value * 10^prec + 0.5 - 0.000000001) / 10^prec;
END
$$ LANGUAGE 'plpgsql';
person user3336056    schedule 21.02.2014

create or replace function round_down_to_decimal_places(numeric,integer)
returns numeric stable language sql as $$ 

select
case
when $1 >= 0 then
case when $1 - round($1, 3) < 0 then round($1, 3) - 0.001 else 
round($1, 3) end
else
case when $1 - round($1, 3) > 0 then round($1, 3) + 0.001 else 
round($1, 3) end
end

$$;

Вы можете использовать универсальный, изменив (0,001 для 3 десятичных знаков, 0,0001 для 4 десятичных знаков и т. д.)

ОТРЕДАКТИРОВАНО OP: @Parveel: я изменил вашу функцию, чтобы она работала в обычном режиме.

create or replace function round_half_down(numeric,integer)
returns numeric stable language sql as $$ 

select
case
when $1 >= 0 then
    case 
        when ($1 - round($1, $2)) < 0 then cast((round($1, $2) - (1.0/(10^$2))) as numeric) 
    else round($1, $2) end
else
    case 
        when ($1 - round($1, $2)) > 0 then cast((round($1, $2) + (1.0/(10^$2))) as numeric)
    else round($1, $2) end
end

$$;
person Parveen Sharief    schedule 21.09.2012
comment
Обе ваши функции просчитываются. Я добавил демо к своему ответу. - person Erwin Brandstetter; 21.09.2012
comment
@Erwin: на самом деле вторая была обобщенной версией функции Парвина, которую я предложил :) - person BangTheBank; 21.09.2012
comment
@BangTheBank: было бы лучше добавить свой собственный ответ, если у вас есть что-то существенно отличающееся (например, здесь). SO не препятствует ответу на ваш собственный вопрос. - person Erwin Brandstetter; 21.09.2012
comment
@ErwinBrandstetter, спасибо, что указали на это. Я понимаю вашу точку зрения, но в данном случае я просто взял оригинальную идею Парвина и попытался ее обобщить. - person BangTheBank; 21.09.2012
comment
@BangTheBank: все еще слишком существенно для редактирования, следует выделить отдельный ответ. И если вы редактируете, вы должны четко указать, что вторая половина написана вами (BangTheBank), а не первоначальным автором (Parveen). Текст, который я изменил... становится понятен читателям только после изучения истории правок. Не так, как должно быть. - person Erwin Brandstetter; 21.09.2012
comment
@ErwinBrandstetter: хорошо, понял. Это первый раз, когда я активно использую сайт стека и не хочу загрязнять не совсем оригинальными ответами, независимо от читабельности. - person BangTheBank; 21.09.2012
comment
@BangTheBank: Я вижу ваши добрые намерения. Иначе я бы не тратил время на обучение. Добро пожаловать в СО! :) Теперь исправим так или иначе. - person Erwin Brandstetter; 21.09.2012
comment
@ErwinBrandstetter: Спасибо за ваши усилия и время, которое вы тратите на это :) Я не могу ответить на свои вопросы, так как у меня нет достаточного количества очков репутации (мне нужно подождать один день для этого). Приходилось делать это здесь. - person BangTheBank; 21.09.2012

CREATE OR REPLACE FUNCTION public.round_half_down (numeric,integer)
RETURNS numeric AS
$body$
DECLARE
  arg ALIAS FOR $1;
  rnd ALIAS FOR $2;
  tmp1 numeric;
  res numeric;
  vra varchar;
  inta integer;
  ifa boolean;
 BEGIN
    tmp1:= arg;
    vra := substr(cast((arg - floor(arg)) as varchar),3);
    ifa := null;
    FOR i IN 1 .. length(vra) LOOP
        inta := CAST((substr(vra,i,1)) as integer);
        IF (i > rnd) THEN
            IF ((ifa is null) AND inta >= 6)THEN
                ifa := true;
            ELSE
                ifa := false;
            END IF;
        END IF;
    END LOOP;

    IF ifa THEN 
        res:=trunc(arg,rnd);
    ELSE 
        res:=round(arg,rnd);
    END IF;
    RETURN res;
END;
END;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

Попробуйте это, надеюсь, вам поможет

person borneo    schedule 27.09.2013