日向夏特殊応援部隊

俺様向けメモ

あけましておめでとうございます

皆様あけましておめでとうございます。本年も宜しくお願い致します。

早いものでDeNA に入社してから二年経ってしまいました。実はプログラマとなってから二年間在籍したのはこの会社が初めてで、まだまだ楽しい出来事が待っていそうだなと思える会社だなと改めて思います。

今年は恐らくスマートフォンにおけるビジネスが本格化する年になるんじゃないかなと思います。そちらの方も少しずつキャッチアップしていけたらなと思っております。

自分も今年、35歳となるので今後の事を見据えて気を引き締めて頑張って行こうと思います。ブログに関してはもう少し頑張って書かないとなーと。

SQL だけで動的に ADD PARTITION する

DEALLOCATE PREPARE し忘れていたので修正しました。id:tokuhirom++

結論から言えば プリペアド ステートメントの為の SQL 構文 を使えば出来る。

CREATE TABLE `timeline` (
  `id` int(10) unsigned NOT NULL,
  `guid` int(10) unsigned NOT NULL,
  `title` varchar(140) DEFAULT NULL,
  `created_on` datetime NOT NULL,
  PRIMARY KEY (`id`,`created_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY RANGE (TO_DAYS(created_on))
(PARTITION p20101110 VALUES LESS THAN (734452))
ENGINE = InnoDB;

DROP PROCEDURE add_partition;
DELIMITER //
CREATE PROCEDURE add_partition(IN a_table TEXT, IN a_date DATE)
  BEGIN
    SELECT CONCAT(
      'ALTER TABLE timeline ADD PARTITION ( PARTITION ', 
      DATE_FORMAT(a_date, 'p%Y%m%d'), 
      ' VALUES LESS THAN(', TO_DAYS(a_date + INTERVAL 1 DAY), ') )') INTO @ddl;
    PREPARE ddl_stmt FROM @ddl;
    EXECUTE ddl_stmt;
    DEALLOCATE PREPARE ddl_stmt;
  END;
//
DELIMITER ;

CALL add_partition('timeline', '2010-11-11');

みたいな感じ。PREPARE の FROM 句は文字列リテラルかユーザー変数*1しか使えないのでプロシージャのローカル変数*2は使えないので注意。

PREPARE 構文を使うと SQL で生成した SQL 文が使えるようになるのが中々面白い。

*1:@なんちゃらって奴

*2:DECLARE して局所化した変数

Time::Piece::MySQL とタイムゾーン (訂正あり)


重要

id:holidays-l さんがこの記事の誤りと、ちゃんとした解説を書いてくれているので、そっちを参照して下さいませ。

以下、そのつもりで読んで下さい。

MySQL から UNIX_TIMESTAMP() と NOW() の値をこんな感じで出します。

[12:13:13 root@bopobo/test :4] SELECT UNIX_TIMESTAMP(), NOW();
+------------------+---------------------+
| UNIX_TIMESTAMP() | NOW()               |
+------------------+---------------------+
|       1288926767 | 2010-11-05 12:12:47 |
+------------------+---------------------+
1 row in set (0.00 sec)

最近 Time::Piece を使っているので、MySQL の datetime 型を扱う際には Time::Piece::MySQL を使ってこんな感じにします。

$ perl -MTime::Piece -MTime::Piece::MySQL -e 'my $t = Time::Piece->from_mysql_datetime("2010-11-05 12:12:47"); warn $t;'
Fri Nov  5 12:12:47 2010 at -e line 1.

これは正しいんですけど、

$ perl -MTime::Piece -MTime::Piece::MySQL -e 'my $t = Time::Piece->from_mysql_datetime("2010-11-05 12:12:47"); warn $t->epoch;'
1288959167 at -e line 1.

は元の timestamp と異なる値 (gmtime) になっちゃうと。これは $ENV{TZ} を変更しても全然変わらない。

$ perl -MTime::Piece -MTime::Piece::MySQL -e 'my $t = Time::Piece->from_mysql_datetime("2010-11-05 12:12:47"); warn $t->epoch - 9 * 60 * 60;'
1288926767 at -e line 1.

なら正しいけどかっこわるい。

$ perl -MTime::Piece -MTime::Piece::MySQL -e 'my $t = Time::Piece->from_mysql_datetime("2010-11-05 12:12:47"); warn $t->strftime("%s")'
1288926767 at -e line 1.

これなら正しいみたい。詳しくは man strftime すれば分かります。

余談ですが DateTime::Format::MySQL だと、

$ perl -MDateTime::Format::MySQL -e 'my $dt = DateTime::Format::MySQL->parse_datetime("2010-11-05 12:12:47"); warn $dt->set_time_zone("Asia/Tokyo")->epoch'
1288926767 at -e line 1.

とかで書けます。

YAPC Asia 2010 でのスライド公開と補足、あと感想など

はい、@941 さんを初めとして、@lestrrat さん、JPA の皆様って僕もですけど、それとボランティアスタッフの皆さん、スピーカーの皆さん、ご来場者の皆さん3日間お疲れ様でした。そして、今年も盛り上がりましたね。

さて、後半だいぶ時間が押して早口になってしまいましたが当日のスライドを公開します。

さてと特に後半の Q4M を複数台構成にした際の負荷のばらつきですが、INSERT の方は DNSRR とかでやればいいのですが、queue_wait() して実際に何かタスクを実行する際に、処理時間に偏りがある場合、queue_wait() の sleep が無視出来なくなって片側に接続が偏ってしまう現象が何度か起きました。

これを解決するために、ひとつのQ4Mに接続する worker のプロセス数が常に固定となるように改修しましたってのをアニメーション交えて解説するはずだったのにまったく時間が足りずでしたorz..

TRIGGER + Q4M ってのもちょっとした実験中で、枠組みそのものは当然出来るんですが障害時のリカバリプランとかは要検討だなーとか思ってます。こちらに関しては追々。

MySQL Partitioning に関しては結果的に purge が楽とかそういう理由で使ってるケースがほとんどだなーと。一方で unique 制約が妙なことになるので、わざと unique 性を担保する為だけに別のテーブル作ってそこに暗黙的に trigger で insert することによって unique 性の担保をしてみたりとか割と実験的なこともやってるんですけど、制約必要ならそもそも partitioning するなとかとか。。。

とりあえず、久しぶりに長時間のプレゼンだとか思ってたら完璧に時間配分間違えたのと、もうちょっとスライドが手軽に作れるようにしないとなーとか思ったしだいなのでした。

さて、最後の @miyagawa さんの KeyNote は最高でしたねー。社内でもあの KeyNote でやる気出たとか何人かいました。自分もそのうちの一人です。
改めて思い立ったらコード書くぞーって思った訳なのです。

と、とりとめもないですがまた来年の YAPC Asia も無事開催なるかっ!乞うご期待。

Partitioning と information_schema.PARTITIONS のメモ

例によって information_schema に partitioning されたテーブルの情報がある訳ですが、ちょっとメモ。ちなみに SQL としてはこんな感じです。

SELECT PARTITION_NAME, SUBPARTITION_NAME, PARTITION_ORDINAL_POSITION, SUBPARTITION_ORDINAL_POSITION, PARTITION_METHOD, SUBPARTITION_METHOD, PARTITION_EXPRESSION, SUBPARTITION_EXPRESSION, PARTITION_DESCRIPTION, PARTITION_COMMENT, NODEGROUP, TABLESPACE_NAME FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = 'test_partition' AND TABLE_NAME = 'k1'

で抽出はこんな感じ。

perl -MDBI=neat -e 'my $dbh = DBI->connect("dbi:mysql:dbname=test_partition", "root", ""); my @columns = qw(PARTITION_NAME SUBPARTITION_
NAME PARTITION_ORDINAL_POSITION SUBPARTITION_ORDINAL_POSITION PARTITION_METHOD SUBPARTITION_METHOD PARTITION_EXPRESSION SUBPARTITION_EXPRESSION PARTITION_DE
SCRIPTION PARTITION_COMMENT NODEGROUP TABLESPACE_NAME); my @short_names = qw(name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup
tablespace_name); my $rs = $dbh->selectall_arrayref("SELECT " . join(",", @columns) . " FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = ? AND TABLE_
NAME = ?", undef, @ARGV); sub print_row { print "|| " . join(" || ", @_) . " ||\n" } print_row(@short_names); for (@$rs) { print_row(map { defined $_ ? $_ :
 neat $_ } @$_) }' test_partition k1

Key

15.2.4. KEY パーティショニング からテーブル定義を拝借。

CREATE TABLE `k1` (
  `id` int(11) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
/*!50100 PARTITION BY KEY ()
PARTITIONS 2 */
name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup tablespace_name
p0 undef 1 undef KEY undef undef undef default undef
p1 undef 2 undef KEY undef undef undef default undef

HASH

15.2.3. HASH パーティショニング より

CREATE TABLE `employees` (
  `id` int(11) NOT NULL,
  `fname` varchar(30) DEFAULT NULL,
  `lname` varchar(30) DEFAULT NULL,
  `hired` date NOT NULL DEFAULT '1970-01-01',
  `separated` date NOT NULL DEFAULT '9999-12-31',
  `job_code` int(11) DEFAULT NULL,
  `store_id` int(11) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
/*!50100 PARTITION BY HASH ( YEAR(hired))
PARTITIONS 4 */
name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup tablespace_name
p0 undef 1 undef HASH undef YEAR(hired) undef undef default undef
p1 undef 2 undef HASH undef YEAR(hired) undef undef default undef
p2 undef 3 undef HASH undef YEAR(hired) undef undef default undef
p3 undef 4 undef HASH undef YEAR(hired) undef undef default undef

LIST

15.2.2. LIST パーティショニング

CREATE TABLE `employees2` (
  `id` int(11) NOT NULL,
  `fname` varchar(30) DEFAULT NULL,
  `lname` varchar(30) DEFAULT NULL,
  `hired` date NOT NULL DEFAULT '1970-01-01',
  `separated` date NOT NULL DEFAULT '9999-12-31',
  `job_code` int(11) DEFAULT NULL,
  `store_id` int(11) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
/*!50100 PARTITION BY LIST (store_id)
(PARTITION pNorth VALUES IN (3,5,6,9,17) ENGINE = MyISAM,
 PARTITION pEast VALUES IN (1,2,10,11,19,20) ENGINE = MyISAM,
 PARTITION pWest VALUES IN (4,12,13,14,18) ENGINE = MyISAM,
 PARTITION pCentral VALUES IN (7,8,15,16) ENGINE = MyISAM) */
name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup tablespace_name
pNorth undef 1 undef LIST undef store_id undef 3,5,6,9,17 default undef
pEast undef 2 undef LIST undef store_id undef 1,2,10,11,19,20 default undef
pWest undef 3 undef LIST undef store_id undef 4,12,13,14,18 default undef
pCentral undef 4 undef LIST undef store_id undef 7,8,15,16 default undef

RANGE

15.2.1. RANGE パーティショニング

CREATE TABLE `employees3` (
  `id` int(11) NOT NULL,
  `fname` varchar(30) DEFAULT NULL,
  `lname` varchar(30) DEFAULT NULL,
  `hired` date NOT NULL DEFAULT '1970-01-01',
  `separated` date NOT NULL DEFAULT '9999-12-31',
  `job_code` int(11) NOT NULL,
  `store_id` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
/*!50100 PARTITION BY RANGE (store_id)
(PARTITION p0 VALUES LESS THAN (6) ENGINE = MyISAM,
 PARTITION p1 VALUES LESS THAN (11) ENGINE = MyISAM,
 PARTITION p2 VALUES LESS THAN (16) ENGINE = MyISAM,
 PARTITION p3 VALUES LESS THAN (21) ENGINE = MyISAM) */
name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup tablespace_name
p0 undef 1 undef RANGE undef store_id undef 6 default undef
p1 undef 2 undef RANGE undef store_id undef 11 default undef
p2 undef 3 undef RANGE undef store_id undef 16 default undef
p3 undef 4 undef RANGE undef store_id undef 21 default undef

Sub Partitioning

15.2.5. サブパーティショニング

CREATE TABLE `ts` (
  `id` int(11) DEFAULT NULL,
  `purchased` date DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=sjis
/*!50100 PARTITION BY RANGE ( YEAR(purchased))
SUBPARTITION BY HASH ( TO_DAYS(purchased))
(PARTITION p0 VALUES LESS THAN (1990)
 (SUBPARTITION s0 ENGINE = MyISAM,
  SUBPARTITION s1 ENGINE = MyISAM),
 PARTITION p1 VALUES LESS THAN (2000)
 (SUBPARTITION s2 ENGINE = MyISAM,
  SUBPARTITION s3 ENGINE = MyISAM),
 PARTITION p2 VALUES LESS THAN MAXVALUE
 (SUBPARTITION s4 ENGINE = MyISAM,
  SUBPARTITION s5 ENGINE = MyISAM)) */
name s_name ord_pos s_ord_pos method s_method exp s_exp desc comment nodegroup tablespace_name
p0 s0 1 1 RANGE HASH YEAR(purchased) TO_DAYS(purchased) 1990 default undef
p0 s1 1 2 RANGE HASH YEAR(purchased) TO_DAYS(purchased) 1990 default undef
p1 s2 2 1 RANGE HASH YEAR(purchased) TO_DAYS(purchased) 2000 default undef
p1 s3 2 2 RANGE HASH YEAR(purchased) TO_DAYS(purchased) 2000 default undef
p2 s4 3 1 RANGE HASH YEAR(purchased) TO_DAYS(purchased) MAXVALUE default undef
p2 s5 3 2 RANGE HASH YEAR(purchased) TO_DAYS(purchased) MAXVALUE default undef

Iteration あれこれ

sharding やら partitioning とかし始めると段々とその key が特定のノートやパーティションに収まるように分類した上で処理とかしたくなる訳です。
最近、モバゲーのオープンプラットフォームのプロダクト全てで使っている、DBIx-DBHResolver にちょこちょこ新機能を入れたりしてるんですが、これに resolve_node_keys ってメソッドを最近つけたりしました。

#!/usr/bin/perl

use strict;
use warnings;
use feature qw(say);
use Data::Dump qw(dump);
use DBIx::DBHResolver;

my $resolver = DBIx::DBHResolver->new;
$resolver->config(
    +{
        clusters => +{
            TIMELINE =>
              +{ nodes => [qw/TIMELINE001 TIMELINE002/], strategy => 'Key' }
        },
        connect_info => +{
            TIMELINE001 => +{},
            TIMELINE002 => +{},
        }
    }
);

my %node_keys = $resolver->resolve_node_keys( TIMELINE => [ 1 .. 20 ] );
while ( my ( $node, $keys ) = each %node_keys ) {
    say $node;
    say dump $keys;
}

まぁこんな感じで使うんですが、

TIMELINE001
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
TIMELINE002
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

という出力となります。各ノードに分割アルゴリズムに則った形でノート名とキーの配列として返してくれます。余談ですがこの分割アルゴリズムは現在は Key, Range, List に対応してます。

このキーが今度はまた万単位であってそれらのキーを元に bulk insert するとかそんな処理が出てきたりする訳ですが、毎回のごとく

my @all_values = ( 1 .. 100000 );
my @values;
while ( ( @values = splice(@all_values, 0, 1000) ) > 0 ) {
  ### bulk insert by each @values
}

みたいな処理を書いて居た訳ですがどうにもスマートじゃないなぁと前から思ってました。

でこんな風にしてみるのはどうかなと思い立ったのがこちら。

#!/usr/bin/perl

use strict;
use warnings;
use feature qw(say);

use Array::AsHash;
use Data::Dump qw(dump);
use Data::Util qw(is_scalar_ref is_number);
use Iterator::Simple qw(iterator);
use SQL::Abstract;
use SQL::Abstract::Plugin::InsertMulti;

sub bulk_insert_iterator {
    my ( $values, $size ) = @_;
    $size ||= 100;
    iterator {
        my @next = splice( @$values, 0, $size );
        return unless ( @next > 0 );
        \@next;
    };
}

sub as_sql {
    my ( $stmt, @bind ) = @_;
    for my $v (@bind) {
        if ( is_scalar_ref $v ) {
            $stmt =~ s/\?/%s/;
        }
        elsif ( is_number $v ) {
            $stmt =~ s/\?/%d/;
        }
        else {
            $stmt =~ s/\?/'%s'/;
        }
    }
    sprintf( $stmt, @bind );
}

my $sql = SQL::Abstract->new;
my ( $stmt, @bind );
my %node_keys = (
    PEOPLE001_MASTER => [ map { $_ * 2 } ( 1 .. 25 ) ],
    PEOPLE002_MASTER => [ map { ( $_ * 2 ) - 1 } ( 1 .. 25 ) ]
);
my @cols = qw( id ref_id created_on );

my $ah = Array::AsHash->new( +{ array => [%node_keys] } );

while ( my ( $node, $keys ) = $ah->each ) {
    say $node;
    say '-' x 100;
    my $iter =
      bulk_insert_iterator( [ map { [ 1, $_, \'NOW()' ] } @$keys ], 10 );
    while ( my $values = $iter->next ) {
        ( $stmt, @bind ) = $sql->insert_multi( 'hidek', \@cols, $values );
        say as_sql( $stmt, @bind );
    }
    say '-' x 100;
}

で出力はこんな風になります。

PEOPLE001_MASTER
----------------------------------------------------------------------------------------------------
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 2, NOW() ), ( 1, 4, NOW() ), ( 1, 6, NOW() ), ( 1, 8, NOW() ), ( 1, 10, NOW() ), ( 1, 12, NOW() ), ( 1, 14, NOW() ), ( 1, 16, NOW() ), ( 1, 18, NOW() ), ( 1, 20, NOW() )
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 22, NOW() ), ( 1, 24, NOW() ), ( 1, 26, NOW() ), ( 1, 28, NOW() ), ( 1, 30, NOW() ), ( 1, 32, NOW() ), ( 1, 34, NOW() ), ( 1, 36, NOW() ), ( 1, 38, NOW() ), ( 1, 40, NOW() )
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 42, NOW() ), ( 1, 44, NOW() ), ( 1, 46, NOW() ), ( 1, 48, NOW() ), ( 1, 50, NOW() )
----------------------------------------------------------------------------------------------------
PEOPLE002_MASTER
----------------------------------------------------------------------------------------------------
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 1, NOW() ), ( 1, 3, NOW() ), ( 1, 5, NOW() ), ( 1, 7, NOW() ), ( 1, 9, NOW() ), ( 1, 11, NOW() ), ( 1, 13, NOW() ), ( 1, 15, NOW() ), ( 1, 17, NOW() ), ( 1, 19, NOW() )
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 21, NOW() ), ( 1, 23, NOW() ), ( 1, 25, NOW() ), ( 1, 27, NOW() ), ( 1, 29, NOW() ), ( 1, 31, NOW() ), ( 1, 33, NOW() ), ( 1, 35, NOW() ), ( 1, 37, NOW() ), ( 1, 39, NOW() )
INSERT INTO hidek ( id, ref_id, created_on ) VALUES ( 1, 41, NOW() ), ( 1, 43, NOW() ), ( 1, 45, NOW() ), ( 1, 47, NOW() ), ( 1, 49, NOW() )
----------------------------------------------------------------------------------------------------

まぁ最初の Array::AsHash は each でも一向に構わない訳ですが、何となく試しに使ってみました。

汎用的な Iterator を作りたいのであれば、Iterator::Simple が一番軽量で汎用的だなーという印象。

今日の MySQL - Partitioning 編 -

さてと、ありがちな下記のようなテーブルを作ってみます。ちなみに 5.1.45 で試してます。

DELIMITER ;

DROP TABLE IF EXISTS diary;

CREATE TABLE diary (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `subject` varchar(64) NOT NULL,
  `content` text NOT NULL,
  `created_on` datetime NOT NULL,
  `updated_on` datetime NOT NULL,
  PRIMARY KEY (`id`,`updated_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY RANGE ( to_days(updated_on))
(
  PARTITION p20100810 VALUES LESS THAN (TO_DAYS('2010-08-11 00:00:00')) COMMENT = '2010-08-11 00:00:00' ENGINE = InnoDB
);

ALTER TABLE diary ADD PARTITION ( PARTITION p20100811 VALUES LESS THAN (TO_DAYS('2010-08-12 00:00:00')) COMMENT = '2010-08-12 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100812 VALUES LESS THAN (TO_DAYS('2010-08-13 00:00:00')) COMMENT = '2010-08-13 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100813 VALUES LESS THAN (TO_DAYS('2010-08-14 00:00:00')) COMMENT = '2010-08-14 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100814 VALUES LESS THAN (TO_DAYS('2010-08-15 00:00:00')) COMMENT = '2010-08-15 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100815 VALUES LESS THAN (TO_DAYS('2010-08-16 00:00:00')) COMMENT = '2010-08-16 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100816 VALUES LESS THAN (TO_DAYS('2010-08-17 00:00:00')) COMMENT = '2010-08-17 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100817 VALUES LESS THAN (TO_DAYS('2010-08-18 00:00:00')) COMMENT = '2010-08-18 00:00:00' ENGINE = InnoDB );
ALTER TABLE diary ADD PARTITION ( PARTITION p20100818 VALUES LESS THAN (TO_DAYS('2010-08-19 00:00:00')) COMMENT = '2010-08-19 00:00:00' ENGINE = InnoDB );

DELIMITER //

CREATE TRIGGER diary_on_after_delete AFTER DELETE ON diary
FOR EACH ROW BEGIN
  DELETE LOW_PRIORITY FROM comment WHERE diary_id = OLD.id;
END;
//

DELIMITER ;

DROP TABLE IF EXISTS comment;
CREATE TABLE comment (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `diary_id` int(10) unsigned NOT NULL,
  `content` text NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `comment_on_diary_id` (`diary_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

まずは SHOW CREATE TABLE diary をしてみると、

CREATE TABLE `diary` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `subject` varchar(64) NOT NULL,
  `content` text NOT NULL,
  `created_on` datetime NOT NULL,
  `updated_on` datetime NOT NULL,
  PRIMARY KEY (`id`,`updated_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE ( to_days(updated_on))
(PARTITION p20100810 VALUES LESS THAN (734360) COMMENT = '2010-08-11 00:00:00' ENGINE = InnoDB,
 PARTITION p20100811 VALUES LESS THAN (734361) COMMENT = '2010-08-12 00:00:00' ENGINE = InnoDB,
 PARTITION p20100812 VALUES LESS THAN (734362) COMMENT = '2010-08-13 00:00:00' ENGINE = InnoDB,
 PARTITION p20100813 VALUES LESS THAN (734363) COMMENT = '2010-08-14 00:00:00' ENGINE = InnoDB,
 PARTITION p20100814 VALUES LESS THAN (734364) COMMENT = '2010-08-15 00:00:00' ENGINE = InnoDB,
 PARTITION p20100815 VALUES LESS THAN (734365) COMMENT = '2010-08-16 00:00:00' ENGINE = InnoDB,
 PARTITION p20100816 VALUES LESS THAN (734366) COMMENT = '2010-08-17 00:00:00' ENGINE = InnoDB,
 PARTITION p20100817 VALUES LESS THAN (734367) COMMENT = '2010-08-18 00:00:00' ENGINE = InnoDB,
 PARTITION p20100818 VALUES LESS THAN (734368) COMMENT = '2010-08-19 00:00:00' ENGINE = InnoDB) */

となり、各 partition の条件になっている部分の TO_DAYS は展開されている事が分かります。それとコメント付けておくと分かりやすくて良いですね。

さて、日付で Range Partition してるので DROP PARTITION を想定している訳ですが、試しに trigger を使って関連 comment が消されるかどうか確認してみます。

INSERT INTO diary(subject, content, created_on, updated_on) VALUES('hidek lost glasses', 'but he bought contact lens', '2010-08-10 12:00:00', '2010-08-10 12:00:00');
INSERT INTO diary(subject, content, created_on, updated_on) VALUES('hidek is great guy', 'he always play dj music midnight', '2010-08-11 12:00:00', '2010-08-11 12:00:00');

と言う日記を投稿して、

INSERT INTO comment( diary_id, content ) VALUES( 1, 'be careful!' );
INSERT INTO comment( diary_id, content ) VALUES( 1, 'I feel good' );
INSERT INTO comment( diary_id, content ) VALUES( 2, 'dancing' );
INSERT INTO comment( diary_id, content ) VALUES( 2, 'I am listening his dj play.' );

念のため transaction を使って comment が消されるかどうか試してみます。

root@localhost[mypage]:7> BEGIN;
Query OK, 0 rows affected (0.00 sec)
root@localhost[mypage]:8> SELECT COUNT(*) FROM comment;
+----------+
| COUNT(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)
root@localhost[mypage]:9> DELETE FROM diary WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
root@localhost[mypage]:10> SELECT COUNT(*) FROM comment;
+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.00 sec)
root@localhost[mypage]:11> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)

で、ALTER TABLE DROP PARTITON で 2010-08-10 分の Patition を消してみます。

root@localhost[mypage]:18> ALTER TABLE diary DROP PARTITION p20100810;
Query OK, 0 rows affected (0.91 sec)
Records: 0  Duplicates: 0  Warnings: 0

root@localhost[mypage]:19> SELECT COUNT(*) FROM comment;
+----------+
| COUNT(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)

ですよねー><

まとめ

  • PARTITON の条件に使った関数は定数化される
  • 各 PARTITION に COMMENT 振ると後で見た時に便利なので振るべし
  • DROP PARTITION では当然 DELETE の TRIGGER は発火しない
    • 恐らく foreign key 使った on delete cascade なんかも同様の挙動をすると思われる。
    • ついでに言うと partition で使う column は unique 制約に含まれてないと駄目と言う制約のせいで foreign key は甚だ使いづらい
  • 親子関係にあるテーブルは同等の条件で partitioning されている方が便利

5.5 だと multi column partition (って呼び方かどうか知らないけど) が使えるはずなんで、もう少し便利になってるのかなーなんて思いつつ 5.5 はまだ使った事がございません!

追記 (2010-08-17T17:33:45Z)

ブクマコメより。id:sh2 さんありがとうございます。

まとめの通りcommentもパーティション化してメンテナンスするのがよさそう。5.5からはDATETIME列で直接パーティション化できます。あと残念ながらパーティション化したテーブルにはそもそも外部キーが付与できません><

だそうで、そもそも外部キーは使えないようです。