0

postgres テーブルに次の形式のデータがあります。

Col1   Col2    Col3              Col4
id1    a       b                 c 
id2    id1     timeBegin         1###-##-##
id2    id1     timeEnd           22##-##-##
id3    id4     id5               id6
id6    id3     timeBegin         2##-##-##
id7    id3     timeEnd           200-3-## 
id13   id8     id14              id15
id8    id9     timeBegin         -2-1-1
id10   id11    id12              id13

ここで 1###-##-## は、(1000-01-01 から 1999-12-31) までの時間の不確実性を意味します。

および 22##-##-## は、(2200-01-01 から 2200-12-31) までの時間の不確実性を意味します。

および 2##-##-## は、(200-01-01 から 200-12-31) までの時間の不確実性を意味します。

および 200-3-## は、(200-3-01 から 200-3-31) までの時間の不確実性を意味します。

および 20-3-## は、(20-3-01 から 20-3-31) までの時間の不確実性を意味します。

および 200-3-## は、(200-3-01 から 200-3-31) までの時間の不確実性を意味します。

および -200-3-## は、(-200-3-31 から -200-3-01) までの時間の不確実性を意味します。

次に、col1==col2 の 3 つの行を次のいずれかの形式にマージします。

Col1   Col2    Col3              Col4       timeBegin      timeEnd
id1    a       b                 c          1000-01-01     2200-12-31 
id3    id4     id5               id6        200-01-02      200-3-31
id10   id11    id12              id13       NULL           NULL
id13   id8     id14              id15       2-1-1 BC       9999-12-12

col1==col2 の timeEnd が指定されていない場合、9999-12-12 が timeEnd と見なされます。

col1==col2 の timeBegin が指定されていない場合、01-01-01 が timeBegin と見なされます。

つまり、マージ中に timeBegin の最小値と timeEnd の最大値を取りたいと考えています。

postgresでこの結合操作を行うことは可能ですか. つまり、これを SQL 結合クエリとして記述できますか?

Python のようなプログラミング言語を使用して (効率的な方法で) 目的を達成できれば、それは素晴らしいことです。

4

3 に答える 3

1

これが最善の方法であるとは確信していませんが、パターンを最小日付と最大日付に変換するという難しい部分を行う Postgres 関数のペアを次に示します。

Example Fiddle

Create Function preprocessPattern(pat varchar(11), out cpat varchar(10), out neg boolean) as $$
declare
  y varchar(4);
  m varchar(2);
  d varchar(2);
  i int;
begin
  neg = false;
  if left(pat, 1) = '-' then
    neg = true;
    pat = right(pat, -1);
  end if;

  i = position('-' in pat);

  y = right('000' || left(pat, i - 1), 4);
  pat = right(pat, -i);
  i = position('-' in pat);
  m = right('0' || left(pat, i - 1), 2);
  pat = right(pat, -i);
  d = right('0' || pat, 2);
  cpat = y || '-' || m || '-' || d;
end;
$$ Language plpgsql;

Create Function dateFromFmt(fmt varchar(10), neg boolean) returns date as $$
begin
  if neg then
    return to_date(fmt || ' BC', 'yyyy-mm-dd BC');
  else
    return to_date(fmt, 'yyyy-mm-dd');
  end if;
end;
$$ Language plpgsql;

Create Function minDateFromPattern(pat varchar(11)) returns date as $$
declare
    i int;
    neg boolean;
    n varchar(10);
begin
    select * into pat, neg from preprocessPattern(pat);
    i = position('#' in pat);
    if i = 0 then
      return dateFromFmt(pat, neg);
    else
      n = left(pat, i - 1) || right('0000-00-00', 0 - position('#' in pat) + 1);
      n = replace(n, '-00', '-01');
      return dateFromFmt(n, neg);
    end if;
end;
$$ Language plpgsql;


Create Function maxDateFromPattern(pat varchar(11)) returns date as $$
declare
    i int;
    y int;
    m int;
    d int;
    x varchar(10);
    neg boolean;
    res date;
begin
    select * into pat, neg from preprocessPattern(pat);
    i = position('#' in pat);
    if i = 0 then
        return dateFromFmt(pat, neg);
    elsif i = 1 then
        return date '9999-12-31';
        -- from here down, pick the next highest mask, convert to min date then subtract one day
    elsif i <= 6 then -- just add 1 to year
        if i = 6 then i = 5; end if; -- skip - char
        x = cast(cast(left(pat, i - 1) as int) + 1 as varchar) || right(pat, 0 - i + 1);
    else
      y = cast(left(pat, 4) as int);
      if i = 7 then
          m = cast(substr(pat, 6, 1) as int) + 1;
          if m = 2 then
              m = 0;
              y = y + 1;
          end if;
          x = left(to_char(y, 'FM0000'), 4) || '-' || to_char(m, 'FM0') || '#-##';
      elsif i = 9 then
          m = cast(substr(pat, 6, 2) as int) + 1;
          if m > 12 then
              m = 1;
              y = y + 1;
          end if;
          x = left(to_char(y, 'FM0000'), 4) || '-' || to_char(m, 'FM00') || '-##';
      elseif i = 10 then
          m = cast(substr(pat, 6, 2) as int);
          d = cast(substr(pat, 9, 1) as int) + 1;
          if (m = 2 and d = 3) or d = 4 then
              m = m + 1;
              d = 0;
              if m > 12 then
                  m = 1;
                  y = y + 1;
              end if;
          end if;
          x = left(to_char(y, 'FM0000'), 4) || '-' || to_char(m, 'FM00') || '-' || to_char(d, 'FM0') || '#';
      end if;
    end if;
    -- the original logic looks a little silly now as we're preprocessing twice
    res = minDateFromPattern(x) - interval '1 day';
    if neg then
      return dateFromFmt(to_char(res, 'yyyy-mm-dd'), neg);
    else
      return res;
    end if;
end;
$$ Language  plpgsql;

これは、いくつかのリファクタリングを使用できるほど十分な反復を経てきました

于 2013-10-06T22:30:18.380 に答える
0

質問には実際には2つの部分があります。1 つは、テーブルのデータを正しく配置することに対応します。もう 1 つは、数え切れないほどの日付形式の処理です。


ここでは、2 つの SQL 関数 begin_time() と end_time() を想定します。以下でそれらについて説明します。

データを整列するには、テーブルをそれ自体で 2 回左結合します。

select t.col1, t.col2, t.col3, t.col4,
       parse_begin_time(bt.col4) as timeBegin,
       parse_end_time(et.col4) as timeEnd
from yourtable t
left join yourtable as bt on begin_t.col2 = t.col1 and bt.col3 = 'timeBegin'
left join yourtable as et on end_t.col2 = t.col1 and et.col3 = 'timeEnd'
where t.col3 not in ('timeBegin', 'timeEnd');

複数のエントリが存在するために複数のエントリが生成される場合は、集計を使用します。

select t.col1, t.col2, t.col3, t.col4,
       min(parse_begin_time(bt.col4)) as timeBegin,
       max(parse_end_time(et.col4)) as timeEnd
from yourtable t
left join yourtable as bt on begin_t.col2 = t.col1 and bt.col3 = 'timeBegin'
left join yourtable as et on end_t.col2 = t.col1 and et.col3 = 'timeEnd'
where t.col3 not in ('timeBegin', 'timeEnd')
group by t.col1, t.col2, t.col3, t.col4;

注: 大量のデータがある場合、上記は特にうまく機能しないことが予想されます。create table as ... ステートメントでそれらを 1 回実行し、元のスキーマを削除するか、将来使用するためにマテリアライズド ビューを作成します。


次に、乱雑な timeBegin フィールドと timeEnd フィールドの書式設定について心配する必要があります。これらは、テキスト フィールドに格納されていると想定しています。次のようになります。

create or replace function parse_begin_time(text) returns date as $$
declare
  _input  text := $1;
  _output text;

  _bc     boolean := false;
  _y      text;
  _m      text;
  _d      text;

  _tmp    text;
  _i      int;
begin
  _input := trim(both from _input);

  -- PG is fine with '200-01-01 BC' as a date, but not with '-200-01-01'
  if left(_input, 1) = '-'
  then
    _bc    := true;
    _input := right(_input, -1);
  end if;

  -- Extract year, month and day
  _tmp := _input;
  _i   := position(_tmp for '-');
  _y   := substring(_tmp from 1 for i - 1);

  _tmp := substring(_tmp from i);
  _i   := position(_tmp for '-');
  _m   := substring(_tmp from 1 for i - 1);

  _tmp := substring(_tmp from i);
  _i   := position(_tmp for '-');
  _d   := substring(_tmp from 1 for i - 1);

  if _tmp <> '' or left(trim(left '0' from _y), 1) = 'X'
  then
    raise exception 'invalid date input: %', _input;
  end if;

  -- Prevent locale-specific text to date conversion issues with one or two digit years
  -- e.g. rewrite 1-2-3 as 0001-02-03.
  if length(_y) < 4
  then
    _y := lpad(_y, 4, '0');
  end if;

  if length(_m) < 2
  then
    _m := lpad(_m, 2, '0');
  end if;

  if length(_d) < 2
  then
    _d := lpad(_m, 2, '0');
  end if;

  -- Process year, month, day
  -- Add suitable logic here per your specs, using string and date functions
  -- http://www.postgresql.org/docs/current/static/functions-string.html
  -- http://www.postgresql.org/docs/current/static/functions-formatting.html
  -- http://www.postgresql.org/docs/current/static/functions-datetime.html
  -- for end-of-months, use the built-in arithmetics, e.g.:
  -- _date := _date + interval '1 month' - interval '1 day'

  -- Build _output
  _output := _y || '-' || _m || '-' || _d;

  if _bc
    _output := _output || ' BC';
  end if;

  return _output::date;
end;
$$ language plpgsql strict stable;

言語に慣れている場合は、代わりに plpython または plpythonu を使用できます。あなたは私よりもこれらの 2 つについてよく知っていると思います。また、必要なコードを作成するのに十分な Python の知識があることも確かです。ローレンスのコードは、plpgsql に何かを入れたい場合のもう 1 つの良い出発点です。

このstrictステートメントは、Postgres に、null 入力でわざわざ関数を呼び出さず、すぐに null を返すように指示します。おそらく end_time 関数には必要ありません。

于 2013-10-10T18:39:00.837 に答える