[ トップページ ]

« JavaScript によるタイマー | メイン | DSMS によって開催中のオークションをもとめる問題の解法 »

データベース

Perl による関係データベースのクエリのシミュレーション

関係データベースがひろくつかわれるようになっているが,SQL によって記述したクエリがどのように実行されているかは不透明である. クエリは通常,DBMS の内部でインタプリタ的に実行される. 機械語にコンパイルせずに実行されるおもな理由は,クエリの対象であるテーブルの性質をみて実行時にクエリを最適化することが重要だからである. しかし,ここではクエリの実行のしくみをみるために,あえてそれを Perl のプログラム (“コンパイル・コード”) でシミュレートしてみる.

目次

はじめに

SQL 結合」 というサイトには,SQL の文とその実行例がいろいろ書かれている. そこにある例題をすこしだけかえて,Perl に翻訳してみることにする. このページのなかにひととおりのプログラムを収容しているが,全部をつないだ ソースコード もある.

使用するデータ

「SQL 結合」 でつかわれているデータを一部だけ変更したものをしめす. データベースは通常ディスクに格納されるが,ここでは主記憶上にあることを前提とする. データベースを揮発性のメモリにおくことはかんがえられないが,主記憶が不揮発性であれば可能な選択肢である.

my @Store_Information =
    ({store_name => 'Los Angeles', Sales => 1500, Date => 'Jan-05-1999'},
     {store_name => 'San Diego', Sales => 250, Date => 'Jan-07-1999'},
     {store_name => 'San Francisco', Sales => 300, Date => 'Jan-08-1999'},
     {store_name => 'Los Angeles', Sales => 300, Date => 'Jan-08-1999'},
     {store_name => 'Boston', Sales => 700, Date => 'Jan-08-1999'});

my @Geography =
    ({region_name => 'East', store_name => 'Boston'},
     {region_name => 'East', store_name => 'New York'},
     {region_name => 'West', store_name => 'Los Angeles'},
     {region_name => 'West', store_name => 'San Diego'});

my @Internet_Sales =
    ({Date => 'Jan-07-1999', Sales => 250},
     {Date => 'Jan-10-1999', Sales => 535},
     {Date => 'Jan-11-1999', Sales => 320},
     {Date => 'Jan-12-1999', Sales => 750});

ここで Store_Information 表は店舗ごとの売り上げの記録,Geography 表は店舗とそれが存在する地域との関係,Internet_Sales 表は 1 日ごとのインターネット上での売り上げの記録である. 表は配列によって表現し,その要素すなわちレコード (tuple) は Perl のハッシュによって表現している.

単純なクエリ

まずもっとも単純な SELECT 文を例とする. もとの SQL の文はつぎのとおりである. この文は Store_Information 表 (ハッシュの配列) から店舗名 (store_name) だけをとりだす.

SELECT store_name FROM Store_Information

これを Perl に翻訳するとつぎのようになる.

sub select_store_name() {
    print_result(@Store_Information, ['store_name']);
}

サブルーティン select_store_name は Store_Information 表の各行から 'store_name' というキーをもつ要素だけをとりだして印刷する. print_result はこれ以降も使用する汎用のサブルーティンであり,あたえられたハッシュの配列から指定された (1 個または複数個の) キーをもつ要素だけをとりだして印刷する.

sub print_result(\@$) {
    my ($result, $keys) = @_;
    foreach my $key (@$keys) {
	print "\t$key";
    }
    print "\n\t----------------\n";

    foreach my $record (@$result) {
	foreach my $key (@$keys) {
	    print "\t$record->{$key}";
	}
	print "\n";
    }
}

サブルーティン select_store_name() を実行すれば,つぎのような結果がえられる.

	store_name
	----------------
	Los Angeles
	San Diego
	San Francisco
	Los Angeles
	Boston

Store_Information 表には “Los Angeles” というなまえの (ことなる) 店舗が 2 つあるため,それが 2 回表示されている.

これ以降の例 (たとえば WHERE 節つきののクエリ) と比較するには,select_store_name をつぎのようにかきかえるとよい.

sub select_store_name() {
    my @result = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	push(@result, $record);
    }

    print_result(@result, ['store_name']); # SELECT store_name
}

しかし,追加されたループは実質的な仕事をなにもしていない (配列をコピーしているだけな) ので,これを除去すると最初にしめしたプログラムのようになる.

DISTINCT 演算子つきのクエリ

つぎの例では上記の例と同様に Store_Information 表から店舗名をとりだすが,重複する店舗名を除去する.

SELECT DISTINCT store_name FROM Store_Information

これを Perl に翻訳するとつぎのようになる.

sub select_distinct_store_name() {
    my %Distinct_Store_Information = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	$Distinct_Store_Information{$record->{store_name}} = $record; # DISTINCT
    }
    my @result = ();
    foreach my $store_name (keys %Distinct_Store_Information) { # DISTINCT
	my $record = $Distinct_Store_Information{$store_name};  # DISTINCT
	push(@result, $record);
    }

    print_result(@result, ['store_name']); # SELECT store_name
}

重複した店舗名を排除するためにハッシュ表 %Distinct_Store_Information を使用している. Perl にはハッシュがくみこまれているので,容易に実現することができる. 配列 @Store_Information においては重複していた “Los Angeles” が @result においてはひとつにまとめられる.

select_distinct_store_name() をよびだすと,結果はつぎのように表示される.

	store_name
	----------------
	San Francisco
	San Diego
	Los Angeles
	Boston

すなわち,“Los Angeles” の重複がなくなっている.

WHERE 節つきのクエリ

つぎの例も Store_Information 表から店舗名を抽出するが,販売量が 1000 (ドル) をこえる店舗だけをえらびだす.

SELECT store_name
FROM Store_Information
WHERE Sales > 1000

これを Perl に翻訳するとつぎのようになる.

sub select_store_name_where() {
    my @result = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if ($record->{Sales} > 1000) { # WHERE Sales > 1000
	    push(@result, $record);
	}
    }

    print_result(@result, ['store_name']); # SELECT store_name
}

このサブルーティンを,ループを追加したサブルーティン select_store_name とくらべると,WHERE 節に対応する条件文がひとつ追加されているだけであることがわかる.

select_store_name_where() をよびだすと,結果はつぎのように表示される.

	store_name
	----------------
	Los Angeles

AND 演算子, OR 演算子を使用した WHERE 節つきのクエリ

前記の例と同様に WHERE 節をつかい,そのなかで演算子 AND や OR を使用した例をしめす.

SELECT store_name
FROM Store_Information
WHERE Sales > 1000 OR (Sales < 500 AND Sales > 275)

これを Perl に翻訳するとつぎのようになる.

sub select_store_name_and_or() {
    my @result = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if ($record->{Sales} > 1000 ||
	    $record->{Sales} < 500 && $record->{Sales} > 275) {
		# WHERE Sales > 1000 OR (Sales < 500 AND Sales > 275)
	    push(@result, $record);
	}
    }

    print_result(@result, ['store_name']); # SELECT store_name
}

select_store_name_and_or() をよびだすと,結果はつぎのように表示される.

	store_name
	----------------
	Los Angeles
	San Francisco
	Los Angeles

IN 演算子を使用した WHERE 節つきのクエリ

前記の例と同様に WHERE 節をつかい,そのなかで演算子 IN を使用した例をしめす.

SELECT *
FROM Store_Information
WHERE store_name IN ('Los Angeles', 'San Diego')

Perl ではつぎのように表現することができる.

sub select_store_name_in() {
    my @result = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if (grep ($_ eq $record->{store_name},
		  'Los Angeles', 'San Diego')) {
		# WHERE store_name IN ('Los Angeles', 'San Diego')
	    push(@result, $record);
	}
    }

    print_result(@result, ['store_name', 'Sales', 'Date']); # SELECT *
}

リストの検索に grep を使用している点と,店舗名とあわせて売り上げや日付をあわせて表示している点をのぞくと,まえの例とほとんどかわらない.

select_store_name_in() をよびだすと,結果はつぎのように表示される.

	store_name	Sales	Date
	----------------
	Los Angeles	1500	Jan-05-1999
	San Diego	250	Jan-07-1999
	Los Angeles	300	Jan-08-1999

LIKE 演算子を使用した WHERE 節つきのクエリ

前記の例と同様に WHERE 節をつかい,そのなかで演算子 LIKE を使用した例をしめす.

SELECT *
FROM Store_Information
WHERE store_name LIKE '%AN%'

LIKE は正規表現をつかって文字列のパターンマッチをする演算子である. これも Perl の得意とする機能なので,容易に表現することができる.

sub select_store_name_like() {
    my @result = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if ($record->{store_name} =~ /.*AN.*/i) { # WHERE store_name LIKE '%AN%'
	    push(@result, $record);
	}
    }

    print_result(@result, ['store_name', 'Sales', 'Date']); # SELECT *
}

select_store_name_like() をよびだすと,結果はつぎのように表示される.

	store_name	Sales	Date
	----------------
	Los Angeles	1500	Jan-05-1999
	San Diego	250	Jan-07-1999
	San Francisco	300	Jan-08-1999
	Los Angeles	300	Jan-08-1999

ORDER BY 節を使用したクエリ

売り上げがおおい順に結果を整列するには,つぎのように ORDER BY 節を使用する.

SELECT store_name, Sales, Date
FROM Store_Information
ORDER BY Sales

Perl ではつぎのように表現することができる.

sub select_store_name_order_by() {
    my @result = ();
    @result = sort {$b->{Sales} <=> $a->{Sales}} @Store_Information; # ORDER BY Sales

    print_result(@result, ['store_name', 'Sales', 'Date']);
    # SELECT store_name, Sales, Date
}

すなわち,sort をつかって整列してから表示すればよい.

select_store_name_order_by() をよびだすと,結果はつぎのように表示される.

	store_name	Sales	Date
	----------------
	Los Angeles	1500	Jan-05-1999
	Boston	700	Jan-08-1999
	San Francisco	300	Jan-08-1999
	Los Angeles	300	Jan-08-1999
	San Diego	250	Jan-07-1999

SUM 関数による値の集約

ここまでの例には複数のレコードをつかった計算がふくまれなかったが,つぎの例では関数 SUM をつかって売り上げの合計をもとめる.

SELECT SUM(Sales) FROM Store_Information

Perl ではつぎのように表現することができる.

sub select_sales_sum() {

    my $SUM_Sales = 0;
    foreach my $record (@Store_Information) { # FROM Store_Information
	$SUM_Sales += $record->{Sales};
    }

    print "\tSUM(Sales)\n";
    print "\t$SUM_Sales\n";
}

select_sales_sum() をよびだすと,結果はつぎのように表示される.

	SUM(Sales)
	3050

COUNT 関数, AVG 関数による値の集約

SUM と同様の例だが,レコード数を COUNT 関数によってかぞえ,平均値 (AVG 関数) を計算する例をしめす.

SELECT COUNT(store_name), AVG(Sales)
FROM Store_Information
WHERE store_name is not NULL

Perl ではつぎのように表現することができる.

sub select_store_name_count() {
    my $COUNT = 0;
    my $SUM_Sales = 0;
    foreach my $record (@Store_Information) { # FROM Store_Information
	if ($record->{store_name}) { # WHERE store_name is not NULL
	    $COUNT++; # COUNT(store_name)
	    $SUM_Sales += $record->{Sales};
	}
    }

    print "\tCOUNT(store_name)\tAVG(Sales)\n";
    print "\t$COUNT\t", $SUM_Sales / $COUNT, "\n";
}

中間値としては SUM 関数とおなじ値をもとめ,最後にレコード数によってわることによって平均をもとめている.

select_store_name_count() をよびだすと,結果はつぎのように表示される.

	COUNT(store_name)	AVG(Sales)
	5	610

COUNT 関数と DISTINCT 演算子をくみあわせたクエリ

つぎは COUNT と DISTINCT とをくみあわせた例である.

SELECT COUNT(DISTINCT store_name)
FROM Store_Information

Perl ではつぎのように表現することができる.

sub select_store_name_count_distinct() {
    my %Distinct_Store_Information = (); # DISTINCT
    foreach my $record (@Store_Information) { # FROM Store_Information
	$Distinct_Store_Information{$record->{store_name}} = $record; # DISTINCT
    }
    my $COUNT_store_name = 0;
    foreach my $store_name (keys %Distinct_Store_Information) { # DISTINCT
	$COUNT_store_name++; # COUNT(DISTINCT store_name)
    }

    print "COUNT(DISTINCT store_name)\n";
    print $COUNT_store_name, "\n";
}

select_store_name_count_distinct() をよびだすと,結果はつぎのように表示される.

COUNT(DISTINCT store_name)
4

“Los Angeles” という店舗が 2 つあるので,ここではそれをあわせてひとつとかぞえている.

GROUP BY 節を使用したクエリ

同名の店舗をグループ化して,グループごとの売り上げを表示させる.

SELECT store_name, SUM(Sales)
FROM Store_Information
GROUP BY store_name

Perl ではつぎのように表現することができる.

sub select_store_name_group_by() {
    my %Grouped_Store_Information = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if (!$Grouped_Store_Information{$record->{store_name}}) { # GROUP BY store_name
	    $Grouped_Store_Information{$record->{store_name}} =
	    {store_name => $record->{store_name},
	     SUM_Sales => $record->{Sales}};
	} else {
	    $Grouped_Store_Information{$record->{store_name}}->{SUM_Sales} +=
		$record->{Sales};
	}
    }
    my @result = ();
    foreach my $store_name (keys %Grouped_Store_Information) {
	push(@result, $Grouped_Store_Information{$store_name});
    }

    print "store_name SUM(Sales)\n";
    print_result(@result, ['store_name', 'SUM_Sales']); # SELECT store_name, SUM(Sales)
}

最初のループ内に条件文があるが,その then 側であたらしいレコードをつくっている. これが SUM の計算のための中間値および最終値を保持するためのレコードである. これまでの例においてはこのような値を保持する必要がなかったため,既存のレコードへのポインタを使用すればよく,あたらしいレコード (ハッシュ) を生成する必要はなかった. その条件文の else 側では SUM の中間値の更新だけをおこなっている.

select_store_name_group_by() をよびだすと,結果はつぎのように表示される.

	store_name	SUM_Sales
	----------------
	San Francisco	300
	San Diego	250
	Los Angeles	1800
	Boston	700

“Los Angeles” という名の 2 つの店舗の売り上げが加算されているが,それ以外は Store_Information 表にふくまれる値とかわらない.

GROUP BY 節と HAVING 節を使用したクエリ

“GROUP BY” によってグルーピングした結果に条件をつけるため,“HAVING” を使用する.

SELECT store_name, SUM(sales)
FROM Store_Information
GROUP BY store_name
HAVING SUM(sales) > 1500

ここでは,グループの売り上げの和が 1500 をこえるものを表示する.

Perl ではつぎのように表現することができる.

sub select_store_name_having() {
    my %Grouped_Store_Information = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	if (!$Grouped_Store_Information{$record->{store_name}}) { # GROUP BY store_name
	    $Grouped_Store_Information{$record->{store_name}} =
	    {store_name => $record->{store_name},
	     SUM_Sales => $record->{Sales}};
	} else {
	    $Grouped_Store_Information{$record->{store_name}}->{SUM_Sales} +=
		$record->{Sales};
	}
    }
    my @result = ();
    foreach my $store_name (keys %Grouped_Store_Information) {
	my $record = $Grouped_Store_Information{$store_name};
	if ($record->{SUM_Sales} > 1500) { # HAVING SUM(sales) > 1500
	    push(@result, $record);
	}
    }

    print "store_name SUM(Sales)\n";
    print_result(@result, ['store_name', 'SUM_Sales']); # SELECT store_name, SUM(sales)
}

WHERE 節の条件ははじめのループにはいっているが,HAVING 節の条件はグルーピングのあとで適用されるため,あとのループにはいっている.

select_store_name_having() をよびだすと,結果はつぎのように表示される.

	store_name	SUM_Sales
	----------------
	Los Angeles	1800

JOIN 演算子を使用したクエリ

JOIN は関係データベースの演算のなかでももっとも重要なものである.

SELECT Geography.region_name, SUM(Store_Information.Sales)
FROM Geography, Store_Information
WHERE Geography.store_name = Store_Information.store_name
GROUP BY Geography.region_name

この例では 2 つの表がともに店舗名をふくんでいるので,それをつかって 2 つの表を結合する. すなわち,地域 (region_name) ごとの売り上げを計算する.

JOIN の実行法にはさまざまあり,その最適化 (効率化) は 1980 年代ころのデータベース研究のもっとも重要なテーマのひとつだった. しかし,ここではひとつの方法だけをとりあげる. hash join といわれる,ハッシュを使用した効率化をおこなった方法である.

sub select_store_geo_join() {
    my %Distinct_Geography = ();
    foreach my $record (@Geography) { # FROM Geography
	push(@{$Distinct_Geography{$record->{store_name}}}, $record); # hash chain
    }
    my %Grouped = ();
    foreach my $record_S (@Store_Information) { # FROM Store_Information
	foreach my $record_G (@{$Distinct_Geography{$record_S->{store_name}}}) {
	    if (!$Grouped{$record_G->{region_name}}) { # GROUP BY region_name
		$Grouped{$record_G->{region_name}} =
		{region_name => $record_G->{region_name},
		 Sales => $record_S->{Sales}};
	    } else {
		$Grouped{$record_G->{region_name}}->{Sales} +=
		    $record_S->{Sales};
	    }
	}
    }
    my @result = ();
    foreach my $region_name (keys %Grouped) {
	push(@result, $Grouped{$region_name});
    }

    print_result(@result, ['region_name', 'Sales']); # SELECT store_name
}

最初のループにおいてハッシュ表をつくっている. “DISTINCT” や “GROUP BY” の例などでもハッシュ表をつかっているが,それらにおいてはハッシュ表のひとつのエントリに対してひとつだけの結果 (SUM の値など) が対応していた. それに対してこの例においてはひとつのエントリが複数のレコードを (配列として) 保持するため,やや複雑になっている. それでも,Perl をつかえば hash join のアルゴリズムをこのようにみじかい行数で表現することができる.

2 つめのループは 2 重ループになっているが,内側のくりかえし回数は通常はすくない. このループ内に条件文があるが,その then 側であたらしいレコードをつくっている. これが 2 つのテーブルのレコードを結合したレコードである. その条件文の else 側ではそのレコードの更新だけをおこなっている.

select_store_geo_join() をよびだすと,結果はつぎのように表示される.

	region_name	Sales
	----------------
	West	2050
	East	700

SELECT が入れ子になったクエリ

サブクエリをふくむ例,すなわち SELECT が入れ子になった例をしめす.

SELECT SUM(Sales) FROM Store_Information
WHERE Store_name IN
(SELECT store_name FROM Geography
 WHERE region_name = 'West')

この文においては西の店舗の売り上げだけを合計する. Perl ではつぎのように表現することができる.

sub select_store_subquery() {
    my @work = ();
    foreach my $record (@Geography) { # FROM Geography
	if ($record->{region_name} eq 'West') { # WHERE region_name = 'West')
	    push(@work, $record->{store_name}); # SELECT store_name
	}
    }
    my $SUM_Sales = 0;
    foreach my $record (@Store_Information) { # FROM Store_Information
	if (grep ($_ eq $record->{store_name}, @work)) { # WHERE store_name IN ...
	    $SUM_Sales += $record->{Sales};
	}
    }

    print "SUM_Sales\n";
    print $SUM_Sales, "\n";
}

はじめのループが入れ子の内側の SELECT に対応し,つぎのループが外側の SELECT に対応している.

select_store_subquery() をよびだすと,結果はつぎのように表示される.

SUM_Sales
2050

UNION 演算子を使用したクエリ

複数のクエリの結果の和をとる例である.

SELECT Date FROM Store_Information
UNION
SELECT Date FROM Internet_Sales

Perl ではつぎのように表現することができる.

sub select_date_union() {
    my %work = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	$work{$record->{Date}} = 1; # SELECT DISTINCT Date
    }
    foreach my $record (@Internet_Sales) {    # FROM Internet_Sales
	$work{$record->{Date}} = 1; # UNION SELECT DISTINCT Date
    }
    my @result = ();
    foreach my $Date (keys %work) {
	push(@result, {Date => $Date});
    }

    print_result(@result, ['Date']);
}

UNION 演算子においては重複した値を排除する必要があるため,ハッシュ表を使用している. ハッシュ表がふくむ値は null ('' または 0) か 1 かのいずれかである. 1 がふくまれている要素だけが UNION の結果にふくまれる.

select_date_union() をよびだすと,結果はつぎのように表示される.

	Date
	----------------
	Jan-07-1999
	Jan-05-1999
	Jan-11-1999
	Jan-12-1999
	Jan-10-1999
	Jan-08-1999

日付の重複はないが,整列していないので順序はきまっていない.

UNION ALL 演算子を使用したクエリ

直前の例にちかいが,“UNION” のかわりに “UNION ALL” を使用する. すなわち,重複を排除しない.

SELECT Date FROM Store_Information
UNION ALL
SELECT Date FROM Internet_Sales

Perl ではつぎのように表現することができる.

sub select_date_union_all() {
    my @work = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	push(@work, {Date => $record->{Date}}); # SELECT Date
    }
    foreach my $record (@Internet_Sales) {    # FROM Internet_Sales
	push(@work, {Date => $record->{Date}}); # UNION ALL SELECT Date
    }
    my @result = ();
    foreach my $record (@work) {
	push(@result, $record);
    }

    print_result(@result, ['Date']);
}

UNION ALL 演算子においては重複した値を排除する必要がないため,UNION 演算子より容易に実現することができる.

select_date_union_all() をよびだすと,結果はつぎのように表示される.

	Date
	----------------
	Jan-05-1999
	Jan-07-1999
	Jan-08-1999
	Jan-08-1999
	Jan-08-1999
	Jan-07-1999
	Jan-10-1999
	Jan-11-1999
	Jan-12-1999

INTERSECT 演算子をふくむクエリ

UNION のかわりに INTERSECT (積) をふくむ例である.

SELECT Date FROM Store_Information
INTERSECT
SELECT Date FROM Internet_Sales

Perl ではつぎのように表現することができる.

sub select_date_intersect() {
    my %work = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	$work{$record->{Date}} = 1;
    }
    foreach my $record (@Internet_Sales) {    # FROM Internet_Sales
	if ($work{$record->{Date}}) {
	    $work{$record->{Date}} = 2;	# INTERSECT
	}
    }
    my @result = ();
    foreach my $Date (keys %work) {
	if ($work{$Date} == 2) {
	    push(@result, {Date => $Date});
	}
    }

    print_result(@result, ['Date']); # SELECT Date
}

INTERSECT 演算子は UNION 演算子よりやや実現しにくい. ここでは 2 をふくむハッシュ表の要素だけが INTERSECT の結果にふくまれる. しかし,この方法は演算式が複雑になったときはこのままでは適用できないとかんがえられる.

select_date_intersect() をよびだすと,結果はつぎのように表示される.

	Date
	----------------
	Jan-07-1999

MINUS 演算子をふくむクエリ

2 つの結果の差を計算する例である.

SELECT Date FROM Store_Information
MINUS
SELECT Date FROM Internet_Sales

Perl ではつぎのように表現することができる.

sub select_date_minus() {
    my %work = ();
    foreach my $record (@Store_Information) { # FROM Store_Information
	$work{$record->{Date}} = {Date => $record->{Date}};
    }
    foreach my $record (@Internet_Sales) {    # FROM Internet_Sales
	if ($work{$record->{Date}}) {
	    $work{$record->{Date}} = ''; # MINUS
	    # or, delete $work{$record->{Date}};
	}
    }
    my @result = ();
    foreach my $Date (keys %work) {
	if ($work{$Date}) {
	    push(@result, {Date => $Date});
	}
    }

    print_result(@result, ['Date']); # SELECT Date
}

select_date_minus() をよびだすと,結果はつぎのように表示される.

	Date
	----------------
	Jan-05-1999
	Jan-08-1999
Keywords: 関係データベース, リレーショナル・データベース, SQL, 結合, ジョイン

トラックバック

このエントリーのトラックバックURL:
https://www.kanadas.com/mt/mt-tb.cgi/3437

コメントを投稿

bulb403_7501-1.jpg

螺旋 3D 印刷技術を使用してつくったこのような「3D デザインランプ」を 3d-dl.com で売っています.

このページについて

2008-12-29 14:05 に投稿されたエントリーのページです。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。

Creative Commons License
このブログは、次のライセンスで保護されています。 クリエイティブ・コモンズ・ライセンス.
Powered by Movable Type