Code Metaphor

Programming, Writing, Reading, Thoughts…

PostgreSQL에서 interval을 원하는 resolution의 정수로 가져오기

제목이 긴데, 한마디로 interval '3 years 4 months 8 days 13 hours 27 min 36 sec'에 대해 저게 초로 환산하면 대체 몇초인지

106964856 = (60 × ((60 × (24 × (((365 × 3 = 1095) + (30 × 2 = 60) + (31 × 2 = 62)1 + 8) + 13 = 1238) = 29712) = 1782720) + 27 = 1782747) = 106964820) + 36

이런 식으로 구하고 싶다는 것이다. 비슷하게 앞과 같은 interval 값에 대해 날자수로 환산하면 대체 몇일이나 되는지는

1238 = ((365 × 3 = 1095) + (30 × 2 = 60) + (31 × 2 = 62) + 8) + 13

이런 식으로 구한다. 그러니까 정리하자면, 년, 월, 일, 시, 분, 초 가운데 원하는 시간 단위의 정수로 interval을 환산하고 싶다. PostgreSQL에 extract라는 기능이 있긴 하지만 우리가 원하는 그런 기능은 아니다.

dahlia=# select extract(year from interval '1 year');
 date_part
-----------
         1
(1 row)

dahlia=# select extract(year from interval '1 year 2 month');
 date_part
-----------
         1
(1 row)

dahlia=# select extract(month from interval '1 year 2 month');
 date_part
-----------
         2
(1 row)

보다시피 extract는 해당 단위의 정수를 가져오긴 하지만(정확히는 float), 해당 단위보다 큰 단위를 합쳐서 가져오진 않는다. 마지막 표현식에 대해 우리가 원하는 값은 2가 아니라 14다.

처음에는 PostgreSQL에 왜 저런 거 해주는 함수 하나 없냐고 투덜대며 직접 함수를 만들기 시작했는데, 만들다보니 왜 그런지 알 것 같아졌다. 사실 interval은 시각 정보를 담고 있지 않기 때문에, 정확히 다른 단위로 환산하기가 매우 힘들다. 그래서 PostgreSQL은 interval '1 year'interval '365 days'를 구분한다. 전자의 1년이 윤년인지 아닌지 알 수 없기 때문이다. 마찬가지로 interval '2 months'를 일수로 환산하기는 너무 모호하다. 저 두달이라는 것이 7월과 8월이면 62일이 되겠으나 1월과 2월이라면 그 해가 윤년이냐 아니냐에 따라 또 달라지게 된다.

하지만 내가 해당 interval을 특정 단위로 환산하고자 하는 것은 정확한 값이 아니라 근사값을 원하는 것이었기 때문에 내가 만들고자 하는 프로그램에서 허용할 수 있을만한 오차는 무시하고 적당히 실용적인 관점에서 구현해보았다.2 구현을 보면 하나도 어려운 것이 없으며, 윤년이나 2월에 대해서는 처리를 하지 않았거나 대충 넘어갔다;;;

Create Or Replace Function Interval_getMonths(v interval)
Returns bigint AS $$
    Begin
        Return extract(month From v) + 12 * extract(year From v);
    End;
$$ Language plpgsql;

Create Or Replace Function Interval_getDays(v interval)
Returns bigint AS $$
    Begin
        Return extract(day From v) + floor(
                   30 * extract(month From v) * 5 / 12.0 + -- months of 30 days
                   31 * extract(month From v) * 7 / 12.0 -- months of 31 days
               )
             + 365 * extract(year From v)
             + floor(extract(year From v) / 4); -- leap years (not exactly)
    End;
$$ Language plpgsql;

Create Or Replace Function Interval_getHours(v interval)
Returns bigint AS $$
    Begin
        Return extract(hour From v) + 24 * Interval_getDays(v);
    End;
$$ Language plpgsql;

Create Or Replace Function Interval_getMinutes(v interval)
Returns bigint AS $$
    Begin
        Return extract(minute From v) + 60 * Interval_getHours(v);
    End;
$$ Language plpgsql;

Create Or Replace Function Interval_getSeconds(v interval)
Returns bigint AS $$
    Begin
        Return extract(second From v) + 60 * Interval_getMinutes(v);
    End;
$$ Language plpgsql;

구현 상의 실수가 있거나, 좀더 개선될 수 있는 부분이 있으면 코멘트로 남겨주거나, 개인적으로 연락해주시길 바란다!


  1. 자세히 보면 4달을 둘로 나누어 30 × 2와 31 × 2를 더하는 식이다. 당연히 interval은 특정 시각 정보를 가지고 있지 않기 때문에 저렇게밖에 할 방법이 없다. 

  2. 원래는 VLAAH의 친구들 페이지에서 친구들이 쓴 코멘트들 사이의 작성 시각 interval들의 표준 편차를 구하려는 것이었는데, 실제로 초 단위까지는 필요 없고 분 정도의 정보만 알면 되었다. 게다가 두 코멘트 사이의 작성 시각 차이가 한 달이, 1년씩이나 되는 경우가 드물 거라고 판단했다. 그리고 이렇게 단위가 커질 경우 하루 이틀 정도의 오차가 별 의미가 없어진다. (내가 작성하려는 프로그램의 용도에 따르면.) 아, interval을 굳이 정수형으로 구하려고 했던 것은 stddev(standard deviation) 집계 함수(aggregate function)가 숫자만 받기 때문이다. 

This entry was posted on November 13, 2008 at 11:10 AM. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

Comments are closed.

Powered by WordPress. Styled by Hong, MinHee. XML Feed, Comments XML Feed.