2010年1月19日 (火)

PostgreSQL トランザクションの隔離性

PostgreSQL のトランザクションで使用できる隔離性 isolation level は, read committed と, serializable の 2 種類です. これらは, それぞれ, 文単位での読み取り一貫性と, トランザクション単位での読み取り一貫性を保証しようとするものです. 1

さて, 以下のようなテーブル m があります.

mydb=# select * from m;

 m_id | m_name | m_upd_time
------+--------+------------
    1 | foo    |
    2 | bar    |
    3 | baz    |
(3 rows)

ここに, 以下のようなタイミングで, 2 つのトランザクション, Trans1, Trans2 を実行します.

Time Trans1 Trans2
1 select from m -
2 - update m
3 select from m -

すると, それぞれの隔離性に対して, 以下の実行結果が得られます.

------- Read Committed Isolation Level
begin trans2
end trans2
begin trans1
begin select
trans1:bar
end select
begin trans2
end trans2
begin select
trans1:fizzbuzz
end select
end trans1
------- Serializable Isolation Level
begin trans2
end trans2
begin trans1
begin select
trans1:bar
end select
begin trans2
end trans2
begin select
trans1:bar
end select
end trans1

なお, 各隔離性の実行結果について, 先頭の Trans2 の実行は無視してください. テーブル値の初期化に使用しています.

以下は, 実験に使用したソースコード.

###
### pg-trans1.rb
###
require 'rubygems'
require 'pg'

class PgTrans
  def initialize(*conn)
    @conn = PGconn.open(*conn)
  end

  def x(sql, *rest)
    @conn.exec(sql, *rest)
  end

  private :x
end

class Trans1 < PgTrans
  def execute(is_serial = false)
    begin
      puts 'begin trans1'
      x 'begin'
      x 'set transaction isolation level serializable' if is_serial
      puts 'begin select'
      r = x 'select * from m where m_id = 2'
      r.each {|t| puts "trans1:#{t['m_name']}" }
      puts 'end select'
      sleep 0.2
      puts 'begin select'
      r = x 'select * from m where m_id = 2'
      r.each {|t| puts "trans1:#{t['m_name']}" }
      puts 'end select'
      x 'commit'
      puts 'end trans1'
    rescue
      x 'rollback'
      STDERR.puts "pg:#$!"
    end
  end
end

class Trans2 < PgTrans
  def execute(mname)
    begin
      puts 'begin trans2'
      x 'begin'
      x "update m set m_name = $1 where m_id = 2", [mname]
      x 'commit'
      puts 'end trans2'
    rescue
      x 'rollback'
      STDERR.puts "pg:#$!"
    end
  end
end

def go_trans(is_serial)
  Trans2.new(:dbname => 'mydb').execute('bar')
  threads = []
  threads.
    push(Thread.new() do
           Trans1.new(:dbname => 'mydb').execute(is_serial)
         end
         )
  threads.
    push(Thread.new() do
           Trans2.new(:dbname => 'mydb').execute('fizzbuzz')
         end
         )
  threads.each {|t| t.join }
end

puts '------- Read Committed Isolation Level'
go_trans(false)
puts '------- Serializable Isolation Level'
go_trans(true)

1. 完全に保証するわけではない. 13.2.2.1. Serializable Isolation versus True Serializability, PostgreSQL 8.4.2 Documentation

| | コメント (0) | トラックバック (0)

2010年1月17日 (日)

PHP の超便利オブジェクト指向,および PHP 所感

サンプルコード:

<?php
class Foo {
}

$foo = new Foo();
$foo->bar = 'Hello World!!';
var_dump($foo);

実行結果:

object(Foo)#1 (1) {
  ["bar"]=>
  string(13) "Hello World!!"
}

な,なんだってー!!!

...

人気が急上昇した言語はグーグルの「Go」と「PHP」、メジャーなのは静的型付け言語。オランダのTIOBEが発表, Publickey

確かに,何か, PHP 使った開発案件の引き合いが増えてるんですよね.思うに,ユーザサイドでアプリをメンテナンスしたい,という意向が背景にあるのでしょう.「ホームページ」的なノリで.内製化ってやつですかね.

ただ,本格的な入力系アプリを,ホームページ的ノリで作ってしまうと,プロでもメンテナンスできなくなってしまう巨大スパゲッティが出現するのは必至であるわけでして.

入力系アプリは, MVC フレームワーク等を使って, データベースアプリの 3 層 アーキテクチャを採用する一方,単純な read only のホームページ的画面は HTML ベタで作る,というのが良いのかな.エンドユーザも,複雑なアプリの部分は,どうやっても手が出せないでしょうし.

でも, PHP でロジック組むのは,やっぱりいやだ.プレゼン層だけに, PHP の利用を限定する単純でうまい方法ってないかな.

| | コメント (0) | トラックバック (0)

2010年1月 4日 (月)

SQL でヒストグラムを作成 - 訂正 -

よくみてみると, 各階級の結果が微妙におかしかったようです. 丸めが適当にすぎたのが原因でした. 以下のとおり, 謹んで訂正をさせていただきます.

コード

select
  x_from, x_to,
  count(*) as y_freq
from (
  select
    (cx - 1) * u_val as x_from,
    cx * u_val as x_to
  from (
    select
      u_val,
      ceil(o_total / u_val) as cx
    from (
      select
        ceil(max_val / breaks) as u_val
      from (
        select
          max(o_total) as max_val,
          1 + log(2, count(*)) as breaks
        from orders
      ) s0
    ) s1,
    orders
  ) s2
) s3
group by x_from, x_to
order by x_from

 

実行結果

 x_from | x_to | y_freq
--------+------+--------
      0 |   23 |   1435
     23 |   46 |   1698
     46 |   69 |   1649
     69 |   92 |   1667
     92 |  115 |   1686
    115 |  138 |   1663
    138 |  161 |   1650
    161 |  184 |   1676
    184 |  207 |   1754
    207 |  230 |   1707
    230 |  253 |   1700
    253 |  276 |   1672
    276 |  299 |   1631
    299 |  322 |   1719
    322 |  345 |   1641
    345 |  368 |    972
(16 rows)

 

前バージョンとの差分

--- hist_v_0.0.1.sql  2010-01-04 03:07:20.372303040 +0900
+++
hist_v_0.0.2.sql  2010-01-04 03:10:55.108940398 +0900
@@ -3,15 +3,15 @@
   count(*) as y_freq
from (
   select
-    ceil((cx - 1) * u_val) as x_from,
-    ceil(cx * u_val) as x_to
+    (cx - 1) * u_val as x_from,
+    cx * u_val as x_to
   from (
     select
       u_val,
       ceil(o_total / u_val) as cx
     from (
       select
-        max_val / breaks as u_val
+        ceil(max_val / breaks) as u_val
       from (
         select
           max(o_total) as max_val,

 

解説の続き

PostgreSQL の width_bucket 関数ですが, これは, 範囲の上限・下限, 階級の数, 対象値を渡すと, その対象値の階級を返す関数ですね. ちょっと使い途のよくわからない関数です. これを調べていて間違いに気づいたわけですけど.

| | コメント (0) | トラックバック (0)

SQL でヒストグラムを作成

前提条件

select
version();
                                    version
--------------------------------------------------------------------------------
PostgreSQL 8.4.2 on i686-pc-linux-gnu, compiled by GCC gcc (GCC) 4.3.4, 32-bit
(1 row)
select
min(o_total),
max(o_total),
count(*)
from orders;
 min  |  max   | count
------+--------+-------
1.11 | 359.80 | 25920
(1 row)

 

コード

select
  x_from, x_to,
  count(*) as y_freq
from (
  select
    ceil((cx - 1) * u_val) as x_from,
    ceil(cx * u_val) as x_to
  from (
    select
      u_val,
      ceil(o_total / u_val) as cx
    from (
      select
        max_val / breaks as u_val
      from (
        select
          max(o_total) as max_val,
          1 + log(2, count(*)) as breaks
        from orders
      ) s0
    ) s1,
    orders
  ) s2
) s3
group by x_from, x_to
order by x_from

 

実行結果

 x_from | x_to | y_freq
--------+------+--------
      0 |   23 |   1432
     23 |   46 |   1696
     46 |   69 |   1651
     69 |   92 |   1662
     92 |  115 |   1690
    115 |  138 |   1651
    138 |  161 |   1652
    161 |  184 |   1673
    184 |  207 |   1754
    207 |  230 |   1699
    230 |  253 |   1699
    253 |  276 |   1678
    276 |  299 |   1631
    299 |  322 |   1716
    322 |  345 |   1634
    345 |  368 |   1002
(16 rows)

 

解説

といっても, breaks を求めている, この式しかありません.

1 + log(2, count(*)) as breaks

PostgreSQL には, width_bucket という関数がありますね.

return the bucket to which operand would be assigned in an equidepth histogram with count buckets, in the range b1 to b2

Table 9-3. Mathematical Functions, PostgreSQL 8.4.2 Documentation

この関数は, 似たようなことをやっていそうですが, 動作は確認していません.

| | コメント (0) | トラックバック (0)

2010年1月 3日 (日)

PostgreSQL で フィボナッチ数

使用したのは, PostgreSQL 8.4.2 です.

コード

with recursive fib(y1, y2) as (
    values (0, 1)
  union all
    select
      y2,
      y1 + y2 as y3
    from fib
)
select y1 from fib limit 20;

 

実行結果

  y1
------
    0
    1
    1
    2
    3
    5
    8
   13
   21
   34
   55
   89
  144
  233
  377
  610
  987
1597
2584
4181
(20 rows)

 

わかったこと

正確には, 再帰ではないらしいです.

Note: Strictly speaking, this process is iteration not recursion, but RECURSIVE is the terminology chosen by the SQL standards committee.

7.8. WITH Queries, Chapter 7. Queries, PostgreSQL 8.4.2 Documentation

色々と試していて, "iteration not recursion" の意味がわかりました. つまり, with recursive query では, 前 1 回分の計算結果しか受け取ることができません. また, 再帰箇所も 1 箇所に限定されます.

この iteration を末尾再帰と同一視してよいかはわかりませんが, iteration が末尾再帰になるのは確かでしょう.

もともと, SQL という言語は, 「再帰が使えない」という点にこだわりがあるみたいでしたので(停止性を保証することに何か意味があるらしい), 再帰が導入されたらしいと知って少々意外に思っていましたが. かなり限定的な「再帰」のようです.

データベース上で使うものだと考えれば, これはこれで良いのでしょう.

| | コメント (0) | トラックバック (0)

2009年12月30日 (水)

SQL は高水準言語

当たり前の話ですが、SQLは現在流通している中では、最も人間に近い高水準言語で、それを低水準言語でラップしても意味がない。

ベンチャー社長で技術者で: 白熱(炎上)したのでまとめ。

SQL isn't mentioned around here all that often, so I am glad we can at least remind ourselves from time to time of the most heavily used declarative language out there by posting SQL puzzles and hacks...

Solving a Sudoku with one SQL-statement, Lambda the Ultimate

Lambda the Ultimate 引用の拙訳:

SQL については、この界隈では、全くと言ってよいほど語られない。そのため、 SQL パズルやハックをここで紹介することで、この最も広く使われている宣言型プログラミング言語について、ときどきは思い出すことができる、ということだけでも良いことだと思う。

オブジェクト指向プログラミングもそうかもしれませんけど、プログラミング言語がより抽象化の方向へと向かったとしても、誰でもできるようになる、というものでもないのかもしれませんね。

| | コメント (0) | トラックバック (0)

2009年12月 3日 (木)

SQL を実行時毎に組み立てなければならないとき

まあ、全くないわけじゃないのですけど。

例えば、アプリに、項目の並び順を、ユーザに指定させるような機能があるときとか。そもそもパラメータが使えない構文要素ですと、どうしようもないわけで。

ただ、検索条件の指定みたいに、SQL の 条件句に入ってくるのは、パラメータを使えば、 SQL の組み立ては、 基本、 1 回で済むはずなのですよね。

例えば、商品(書籍)に対して、以下の条件を使って検索できる機能が、アプリにあるとします。

  • タイトル
  • 発売日
  • 出版社
  • 著者名

この場合、以下のように SQL を作ればよいですね。

require 'pg'

SQL_ITEM_SEARCH = <<EOS
select
i_title,
i_pub_date,
i_publisher,
a_fname,
a_lname,
a_mname
from item i
inner join author a
on i.i_a_id = a_id
where
(i_title = $1 or $1::varchar is null)
and (i_pub_date >= $2 or $2::date is null)
and (i_pub_date <= $3 or $3::date is null)
and (i_publisher = $4 or $4::varchar is null)
and (a_fname = $5 or $5::varchar is null)
and (a_lname = $6 or $6::varchar is null)
and (a_mname = $7 or $7::varchar is null)
order by i_pub_date desc, i_title
EOS

conn = PGconn.open(
:dbname => 'dbt1',
:hostaddr => '127.0.0.1',
:user => 'postgres')
conn.set_client_encoding('SJIS')

res = conn.exec(
SQL_ITEM_SEARCH,
[nil, '2000-01-01', nil, nil, nil, nil, nil])

res.each { |t| p t }

| | コメント (7) | トラックバック (0)

2009年12月 2日 (水)

初心者向けプログラミング言語

そもそも、この言葉自体、なんか、おかしいですよね。

どんなプログラミング言語を使おうと、それがチューリング完全であれば、計算できることは同じわけでして。

初心者がプログラミングできるようになる言語

というのは、存在しないのですよ。

誰でも英語ができるようになる教材

と同類ではないですかねえ。

本屋にいけば、プログラミングの入門書が平積みになっているのを見ることができるのですけど。これって、英語の入門書が平積みになっているのと同じではないかと。それだけ、入門レベルで挫折する人が多い、ということを示唆しているのではないですかね。

| | コメント (0) | トラックバック (0)

PHP をやって過去の謎が解ける

以下のようなコードの「出身地」は、 PHP ではなかろうか。

if (x == true ) { // do something. }

以前、 Java の Web アプリで、

  • 変数がすべて文字列型
  • SQL を毎回メソッド呼び出し時に String で構築
  • ていうか、なんで PreparedStatement 使わない?

というのを見たことがあるのですけど、これも「 PHP 風」ですね。

PHP には MySQL 関数群というのがあって、これが、

  • パラメータ使えない
  • クエリ結果を全部文字列の配列で返す

という、かなり変わった特徴があるみたいです。

ただし、PHP 5 からは、 MySQLi ( MySQL Improved Extension ) というのが入ってます。これは、パラメータが使えて、一見 DBI 風。が、クエリ結果は文字列の配列で返すみたい。なんで?

<?php
$a = array(true, false, NULL, 0, 1, '', '0', '1',
  0.0, 1.0, array(), array(1));
var_dump($a);
foreach ($a as $k => $v) {
  echo "$k: ";
  if ($v) { echo "true\n"; }
  if (!$v) { echo "false\n"; }
}

カオスな実行結果:

array(12) {
  [0]=>
  bool(true)
  [1]=>
  bool(false)
  [2]=>
  NULL
  [3]=>
  int(0)
  [4]=>
  int(1)
  [5]=>
  string(0) ""
  [6]=>
  string(1) "0"
  [7]=>
  string(1) "1"
  [8]=>
  float(0)
  [9]=>
  float(1)
  [10]=>
  array(0) {
  }
  [11]=>
  array(1) {
    [0]=>
    int(1)
  }
}
0: true
1: false
2: false
3: false
4: true
5: false
6: false
7: true
8: false
9: true
10: false
11: true

| | コメント (0) | トラックバック (0)

2009年9月 8日 (火)

SQL クエリのスタイル、もしくは、 なぜ SQL クエリは読みにくいのか

しばらくの間、 SQL で書かれた、様々な集計クエリを読んでいたのですが。どうも、クエリが読みにくくていけない、と思いまして、いまさらながらですが、 SQL クエリの書き方について考えました。

SQL クエリの読みにくさの原因というのは、いくつかあるのですが、一番大きいのは、 FROM 句と、 WHERE 句が離れてしまうことにあると思います。クエリを作るときには、テーブル/リレーションごとに条件を考えて、クエリを組み立てていくと思います。これを、 昔ながらのスタイルで SQL に書くと、 FROM 句にリレーションが並び、 WHERE 句にリレーションの条件が並ぶ、という、リレーションと条件とが分離した形になります。

クエリの読み手がクエリを読む際には、 クエリを作るときの手順を逆に行なうことになります。 WHERE 句に並んでいる条件が、 FROM 句のどのリレーションにかかっているのか、 FROM 句のリレーションごとに、 WHERE 句の条件を並べなおして、クエリからどのような結果が出力されるかを読み取ろうとするわけです。

このようなクエリは、例えば、以下のようなものになります 1

select
  a.A_ID,
  trim(a.A_LNAME) || ',' ||
    trim(a.A_MNAME) || ',' ||
    trim(a.A_FNAME) as a_name,
  ol.OL_QTY
from
  CUSTOMER c,
  ADDRESS ad,
  COUNTRY co,
  ORDERS o,
  ORDER_LINE ol,
  ITEM i,
  AUTHOR a
where
  ad.ADDR_ID = c.C_ADDR_ID and
  co.CO_ID = ad.ADDR_CO_ID and
  co.CO_NAME = 'Japan' and
  o.O_C_ID = c.C_ID and
  o.O_STATUS = 'SHIPPED' and
  ol.OL_O_ID = o.O_ID and
  i.I_ID = ol.OL_I_ID and
  i.I_PUB_DATE >= date '2007-08-01' and
  i.I_PUB_DATE < date '2009-09-01' and
  a.A_ID = i.I_A_ID and
  c.C_DISCOUNT >= 0.1
order by A_ID

このように、いちいち、 WHERE 句と、 FROM 句を頭の中で照合するのは、リレーションの数、条件の数が増えてきますと、やっておれなくなってきます。このため、別途、メモを作成して、リレーションごとに条件をまとめてみたりするわけですが、 無用な手間、という感じなんですよね。

そこで、私がこうしたクエリを書くときにやるのが、 WHERE 句にある条件を、すべて JOIN 句に書く、というやり方です。 さらに、 FROM 句の一番目のテーブルの条件だけは、 WHERE 句に残ってしまうため、あえて、サブクエリにします。

こうしますと、条件がリレーションごとにまとまりますので、かなり読みやすくなるのではないかと思います。

select
  a.A_ID,
  trim(a.A_LNAME) || ',' ||
    trim(a.A_MNAME) || ',' ||
    trim(a.A_FNAME) as a_name,
  ol.OL_QTY
from (
  select C_ID, C_ADDR_ID
  from CUSTOMER
  where C_DISCOUNT >= 0.1) c
inner join ADDRESS ad on
  ad.ADDR_ID = c.C_ADDR_ID
inner join COUNTRY co on
  co.CO_ID = ad.ADDR_CO_ID and
  co.CO_NAME = 'Japan'
inner join ORDERS o on
  o.O_C_ID = c.C_ID and
  o.O_STATUS = 'SHIPPED'
inner join ORDER_LINE ol on
  ol.OL_O_ID = o.O_ID
inner join ITEM i on
  i.I_ID = ol.OL_I_ID and
  i.I_PUB_DATE >= date '2007-08-01' and
  i.I_PUB_DATE < date '2009-09-01'
inner join AUTHOR a on
  a.A_ID = i.I_A_ID
order by A_ID

というわけで、 私は、 WHERE 句ではなく、 JOIN 句に条件を書く、というスタイルを推奨してみたいわけです。


1. データベース・スキーマは、 OSDL DBT-1 より。

| | コメント (0) | トラックバック (0)

より以前の記事一覧