奇特なブログ

「殊勝に値する行いや心掛け」を意味する、奇特な人になる為のブログです

Webアプリにおけるプログラムのパフォーマンスチューニング

さて、ちょっと予定には無かったんですけど、

最近この辺を考える事がやけに多く、

ということで、こんな記事を書いてみようかなぁと思います。


ちなみに、非エンジニアがエンジニアを評価(出来るのか?って話もありますが)するにあたって、

チューニングをした結果の「レスポンスが10秒→1秒になった」というのは、

スキルジャッジとしては分かりやすい指標だろうなぁとは思うので、

そういう意味でも「チューニング実績」というのは良いのかもと思いますね。


では、本題です。

が、急いでいる人?の為に、結論を先に。


1.JOINが出来るなら、まずはJOINを検討。あと、OFFSETとLIMITが使えるなら、よりベター。

2.JOINが出来ないなら、NoSQLを上手く使う。その際、出来るだけループ内でデータ取得処理を行わない様に工夫する

で、基本は、上記の流れで良いんじゃぁないかと。


というのが結論なので、どうしてこういう結論になったかは、以下を読んでくださいということで。

あと、最近読んでいる以下の本(かなり面白い)を見て思ったんですけど、

チューニングと一言でいっても色々あるわけでして。

で、今回はタイトルにもある通り、

プログラム(といっても大体SQLのSELECT関連)サイドのチューニングになります。


www.amazon.co.jp


あと、以前に以下の論理削除の記事でも若干触れた、

「マスタデータを何処に持つか」という話も若干絡みますね。


kitoku-magic.hatenablog.com


あと、地味かもしれないですが、

以下の記事もかなり役に立ってます。

というか、この記事見て完全にピンと来たんですけどね。


php-sql-gdgd.jugem.jp


では、「どういう時のボトルネックに対するチューニングなのか」ですが、

「ユーザーIDをキーにして、DBのトランザクションデータからデータを取得(データA)して、その後データAの中のマスタデータのIDをキーにしてマスタデータからデータを取得(データB)する」という様なプログラムです。

勿論、更に続きでデータC・データDと取得したりする事もザラではありますけど。

で、プログラム自体はよくあるプログラムだと思うので、

結構色々な環境で使えるんじゃないかとも思うんですよね。

じゃなきゃ、わざわざ書かないんですけどね(笑)


では、サンプルプログラム(合計9パターン)のご紹介です。

殆ど変わらないのもあるんで、5~6パターンでも良い気もするんですが、折角なので全部。


先にパターンと実行平均時間と高速度順の順位だけ紹介しておきますと、以下の通りです。


【1.トランザクションデータとマスタデータをJOINする】0.0067554741秒(1位)

【2.DBサーバーが別だとJOIN出来ないので、クエリを分ける】0.5855079975秒(8位)

【3.DBサーバーが別だとJOIN出来ないので、クエリを分ける & MySQL複数接続】0.6397109158秒(9位)

【4.マスタをMemcachedにキャッシュする】0.5397023411秒(7位)

【5.マスタをMemcachedにキャッシュする & キャッシュのキーが一つ】0.0144977431秒(2位)

【6.マスタをRedisにキャッシュする & キャッシュのキーが一つ】0.015953728秒(3位)

【7.先にマスタデータを全件取っておく】0.1823107789秒(6位)

【8.マスタデータを事前にAPCに突っ込んでおく】0.122105834秒(4位)

【9.マスタデータを事前にRedisに突っ込んでおく】0.180010453秒(5位)


まずは、DB関連から。


【0.テーブル定義とデータ投入】


// トランザクションデータが入るテーブル
CREATE TABLE `test` (
`user_id` int(11) NOT NULL,
`code` int(11) NOT NULL,
PRIMARY KEY (`user_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


// マスタデータが入るテーブル
CREATE TABLE `code_master` (
`code` int(11) NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

// データ投入シェルスクリプト
#!/bin/sh

file_name='insert_code_master.csv'

# マスタテーブルに10万件のデータを投入
for i in `seq 1 100000`
do
  echo "${i},${i}" >> $file_name
done

file_name='insert_test.csv'

# トランザクションテーブルに1億件のデータを投入
for i in `seq 1 100000`
do
  for j in `seq 1 1000`
  do
    echo "${i},${j}" >> $file_name
  done
done


LOAD DATA LOCAL INFILE 'insert_code_master.csv' INTO TABLE code_master FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';

LOAD DATA LOCAL INFILE 'insert_test.csv' INTO TABLE test FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';


まず、ここまでです。

ちなみに、以下のインデックスも張ろうかと思いましたが、

結果的に誤差レベルの差で殆ど変わらなかった(今回のクエリではですが)ので、

張ってません。


CREATE INDEX test_code ON test (code);


次に、以下から実際のコードになりますが、

実行方法は以下の様な事をして、EXCELで平均秒を算出しています。


■実行方法
for i in `seq 1 1 1000`; do php {PHPファイル名}.php >> result_{PHPファイル名}.xls; done &


// apcuの時のみ以下
for i in `seq 1 1 1000`; do curl http://localhost/apcu.php >> result_apcu.xls; done &


【1.トランザクションデータとマスタデータをJOINする】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT cm.name FROM test t LEFT JOIN code_master cm ON t.code = cm.code WHERE t.user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($name);

while (true === $stmt->fetch())
{
    //echo $name . "\n";
    $result_array[] = $name;
}

echo microtime(true) - $start . "\n";


1000回平均:0.0067554741秒


う~ん、速い?

というか、他のプログラムと比較しないと分からないので、次に。


【2.DBサーバーが別だとJOIN出来ないので、クエリを分ける】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$code_array = array();
while (true === $stmt->fetch())
{
    $code_array[] = $code;
}

$stmt->close();
$sql = "SELECT name FROM code_master WHERE code = ?";
$stmt = $conn->prepare($sql);
foreach ($code_array as $code)
{
    //echo $code . "\n";
    $stmt->bind_param('i', $code);
    $stmt->execute();

    $stmt->bind_result($name);

    while (true === $stmt->fetch())
    {
        //echo $name . "\n";
        $result_array[] = $name;
    }
}

echo microtime(true) - $start . "\n";


1000回平均:0.5855079975秒


あら、JOINに比べてだいぶ遅くなりました。

ちなみに遅い原因は、「ループの中でSQL投げてるから」でしょう。

一回一回のSQLは「軽い」と思います(上記だとPKのインデックス効いてますし)けど、

でも「千回も一万回」も投げたら流石にねぇと。


ところが、つい先日までの僕自身の書き方もそうだったんですけど、

こういうプログラム、かなりの現場に潜んでいるんじゃなかろうか?と。

というか、これまでの経験だけでも、結構見覚えが・・・

いや、勿論この速度はデータ量にもよりますし、

そもそも0.6秒が問題にならないシステムなら良いんですけど、

案件によってはね・・・っていう。


ひとまず、次にいきましょう。


【3.DBサーバーが別だとJOIN出来ないので、クエリを分ける & MySQL複数接続】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn || $conn->connect_errno) {
    die('データベース接続エラー: ' . $conn->connect_error);
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$conn1 = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');
if (!$conn1  || $conn1->connect_errno) {
    die('データベース接続エラー: ' . $conn1->connect_error);
    exit();
}
$sql = "SELECT name FROM code_master WHERE code = ?";
$stmt1 = $conn1->prepare($sql);
while (true === $stmt->fetch())
{
    //echo $code . "\n";
    $stmt1->bind_param('i', $code);
    $stmt1->execute();

    $stmt1->bind_result($name);

    while (true === $stmt1->fetch())
    {
        //echo $name . "\n";
        $result_array[] = $name;
    }
}

echo microtime(true) - $start . "\n";


1000回平均:0.6397109158秒


こちらは、2番のプログラムの中の「$code_array」配列に要素を突っ込むのと、

その後に、$code_array配列内の要素を一つずつ取り出す部分の、

「繰り返し処理を一つにまとめた」パターンなんですが、

1ユーザーでDB接続数2以上って事で、逆に遅くなってますかね。

どっちにしても、1ユーザーでDB接続数2以上って時点で速くてもイヤですけど。

ちなみに、繰り返し処理を一つにまとめる他の方法として、

2番のプログラムの最初の$stmt->fetch()辺りの箇所を、

$stmt->get_result()とすれば、また同じ$stmt変数使えて繰り返し処理も一つに出来ます。

ただ、それをしても大体「0.59秒」ぐらいということで、特に変化無かったですね。

では、次。


【4.マスタをMemcachedにキャッシュする】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$code_array = array();
while (true === $stmt->fetch())
{
    $code_array[] = $code;
}

$stmt->close();
$sql = "SELECT name FROM code_master WHERE code = ?";

$memcached = new Memcached();
$memcached->addServer('Memcachedホスト名', ポート番号);

foreach ($code_array as $code)
{
    //echo $code . "\n";
    $cache_key = $user_id . '_' . $code;
    $name = $memcached->get($cache_key);
    if (false === $name)
    {
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('i', $code);
        $stmt->execute();

        $stmt->bind_result($name);

        while (true === $stmt->fetch())
        {
            //echo '$name1:' . $name . "\n";
            $result_array[] = $name;
            $memcached->set($cache_key, $name);
        }
    }
    else
    {
        //echo '$name2:' . $name . "\n";
        $result_array[] = $name;
    }
}

echo microtime(true) - $start . "\n";


1000回平均:0.5397023411秒


おお、流石Memcachedと言いたい所ですけど、

でも、それでも「JOINより遅い」んですよね。

・・・ふと思ったんですけど、

これ「ネットワークの通信(あるいは回線?)速度(あるいは回数?)」も、結構影響大きいのでは?(今回、Web・DB・KVS全てが別サーバ)とも思いますね。

いや、今回その辺までは調べてませんけど。

ひとまず、次に。


【5.マスタをMemcachedにキャッシュする & キャッシュのキーが一つ】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$code_array = array();
while (true === $stmt->fetch())
{
    $code_array[] = $code;
}

$stmt->close();
$sql = "SELECT name FROM code_master WHERE code = ?";

$memcached = new Memcached();
$memcached->addServer('Memcachedホスト名', ポート番号);
$cache_key = 'code_master' . '_' . $user_id;
$name_array = array();
$result = $memcached->get($cache_key);

foreach ($code_array as $code)
{
    //echo $code . "\n";
    if (false === $result || false === isset($result[$code]))
    {
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('i', $code);
        $stmt->execute();

        $stmt->bind_result($name);

        while (true === $stmt->fetch())
        {
            //echo '$name1:' . $name . "\n";
            $name_array[$code] = $name;
            $result_array[] = $name;
        }
    }
    else
    {
        //echo '$name2:' . $name . "\n";
        $name_array[$code] = $result[$code];
        $result_array[] = $result[$code];
    }
}
if (0 < count($name_array))
{
    $memcached->set($cache_key, $name_array);
}

echo microtime(true) - $start . "\n";


1000回平均:0.0144977431秒


あら~なんと、これはJOINとほぼ同じですね。

ただ、このプログラムの問題は「メモリ食う」ですね。

・・・あ、これ、$resultと$name_arrayは変数を一つにまとめても問題無さそうですね。

これを書いている途中で気付きました(苦笑)

ということで、まだ速くなりそうですし、別にメモリもさほど食わないかも(苦笑)

あと、ついでにもう一つ書いてる途中に気付いた事として、

ループ内で「$stmt = $conn->prepare($sql);」と書いてある場合は、

大抵はループ外に出せますね(元々が「準備された文」だから当然か)。

ひとまず、次行きましょう。


【6.マスタをRedisにキャッシュする & キャッシュのキーが一つ】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$code_array = array();
while (true === $stmt->fetch())
{
    $code_array[] = $code;
}

$stmt->close();
$sql = "SELECT name FROM code_master WHERE code = ?";

$name_array = array();
$redis = new Redis();
$redis->connect('Redisホスト名', ポート番号);
$cache_key = 'code_master' . '_' . $user_id;
$result = $redis->get($cache_key);
if (false !== $result)
{
    $result = json_decode($result, true);
}

foreach ($code_array as $code)
{
    //echo $code . "\n";
    if (false === $result || false === isset($result[$code]))
    {
        $stmt = $conn->prepare($sql);
        $stmt->bind_param('i', $code);
        $stmt->execute();

        $stmt->bind_result($name);

        while (true === $stmt->fetch())
        {
            //echo '$name1:' . $name . "\n";
            $name_array[$code] = $name;
            $result_array[] = $name;
        }
    }
    else
    {
        //echo '$name2:' . $name . "\n";
        $name_array[$code] = $result[$code];
        $result_array[] = $result[$code];
    }
}
if (0 < count($name_array))
{
    $redis->set($cache_key, json_encode($name_array));
}

echo microtime(true) - $start . "\n";


1000回平均:0.015953728秒


Memcachedと殆ど同じという事で。

ただ、こっちはjson_encode & decode(Redisの他のデータ型なら不要かもしれないですけど)の分、

処理がメンドイと。

っていうのもあるし、Redisは無駄に多機能な事を考えても、

今回の様な用途なら、Memcachedの方が良いのではとは思いますね。

次。


【7.先にマスタデータを全件取っておく】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

$code_array = array();
while (true === $stmt->fetch())
{
    $code_array[$code] = $code;
}

$stmt->close();

$redis = new Redis();
$redis->connect('Redisホスト名', ポート番号);

$redis_key = 'code_master' . '_' . $user_id;

$all_master_data = $redis->get($redis_key);
if (false === $all_master_data)
{
    $all_master_data = array();
    $sql = "SELECT code, name FROM code_master";
    $stmt = $conn->prepare($sql);
    $stmt->execute();

    $stmt->bind_result($code, $name);

    while (true === $stmt->fetch())
    {
        $all_master_data[$code] = $name;
    }
    if (0 < count($all_master_data))
    {
        $redis->set($redis_key, json_encode($all_master_data));
    }
}
else
{
    // json_decodeは、第二引数をtrueにすると連想配列形式のデータを返す(デフォルトはstd_class)
    $all_master_data = json_decode($all_master_data, true);
}

foreach ($code_array as $code)
{
    // もし、ここで、$all_master_data[$code]にデータが無かったら、マスタに無いコードがtestテーブルに入ってる
    $result_array[] = $all_master_data[$code];
}

echo microtime(true) - $start . "\n";


1000回平均:0.1823107789秒


さて、こちらは前述の通り「ループ内でSQLを投げる」のが遅いということなので、

「ループ外でSQL1回投げて全件取って、ループ内でSQLは投げない様にする」という改良を入れたプログラムです。

で、上記の平均はRedisのキャッシュ効いている状態の平均ですが、

このプログラムをRedisだけ使わない様に修正しても、「0.35秒ぐらい」ということで、

以下の3つがAND条件で全てYesの場合には、

今回のパターン内では一番速い(ただ、マスタ全件を配列に格納するので、メモリ不足には注意)ですね。


・JOIN出来ない

・KVSも無い

・後述のAPCも無い


ということで、まぁまぁオススメかと。

では、次。


【8.マスタデータを事前にAPCに突っ込んでおく】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$apc_key = 'code_master' . '_' . $user_id;

$all_master_data = apc_fetch($apc_key, $success);
if (false === $all_master_data || false === $success)
{
    $all_master_data = array();
    $sql = "SELECT code, name FROM code_master";
    $stmt = $conn->prepare($sql);
    $stmt->execute();

    $stmt->bind_result($code, $name);

    while (true === $stmt->fetch())
    {
        $all_master_data[$code] = $name;
    }
    if (0 < count($all_master_data))
    {
        apc_store($apc_key, $all_master_data);
    }
    $stmt->close();
}

$start = microtime(true);
$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

while (true === $stmt->fetch())
{
    $result_array[] = $all_master_data[$code];
}

echo microtime(true) - $start . "\n";


1000回平均:0.122105834秒


まず、このプログラムのマスタから「事前に」って言ってる所についての補足ですが、

「事前に」っていうのは、この「個別」処理を行う前の「共通」処理で事前に行うって意味です。

なので、このプログラムは、「$start = microtime(true);」の位置が途中からになっています。

その点、ご注意を。

で、まぁ、速度的に悪くはないわけですけど、

「マスタ全件をキャッシュ」よりは、

「存在したマスタのID分のデータだけキャッシュ」の方が当然、効率は良いですよね。

ただ、このプログラム「だけ」ならそうなんですけど、「全体」で見た場合にはどうかなと。

「共有可能なデータ」は、共有した方が良いでしょうし。

まぁ、共有したけど使われない処理が殆どだったってケースも考えられるので、「共有するデータの選定」がポイントでしょうけど。

あと、APCに関しては、チラっとしか見てないですけど、

キャッシュがクリアされないだの何だのといったトラブル事例(回避策も同時に載っていた記憶もかすかにあるが)もチラホラ見かけましたので、

まぁ、あんまりじゃあないかという気もしますね。

あと、APCってWebサーバ毎にキャッシュ持つと思うんですけど、

そうすると、キャッシュのヒット率は低いのではってのもありますかね?

なので、特に理由無ければMemcachedなどの方が良いかなぁと。

じゃあ、次で最後です。


【9.マスタデータを事前にRedisに突っ込んでおく】

<?php

ini_set('memory_limit', -1);

$conn = new mysqli('DBホスト名', 'ユーザー名', 'パスワード', 'データベース名', 'ポート番号');

if (!$conn) {
    die('データベース接続エラー: ' . mysql_error());
    exit();
}

$user_id = 859;
$start = microtime(true);

$redis = new Redis();
$redis->connect('Redisホスト名', ポート番号);
$cache_key = 'code_master' . '_' . $user_id;

$all_master_data = $redis->get($cache_key);
if (false === $all_master_data)
{
    $all_master_data = array();
    $sql = "SELECT code, name FROM code_master";
    $stmt = $conn->prepare($sql);
    $stmt->execute();

    $stmt->bind_result($code, $name);

    while (true === $stmt->fetch())
    {
        $all_master_data[$code] = $name;
    }
    if (0 < count($all_master_data))
    {
        $redis->set($cache_key, json_encode($all_master_data));
    }
    $stmt->close();
}
else
{
    $all_master_data = json_decode($all_master_data, true);
}

$result_array = array();
$sql = 'SELECT code FROM test WHERE user_id = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $user_id);
$stmt->execute();

$stmt->bind_result($code);

while (true === $stmt->fetch())
{
    $result_array[] = $all_master_data[$code];
}

echo microtime(true) - $start . "\n";


1000回平均:0.180010453秒


APCをRedisに変えただけなので特に。

Redisの方が遅いのは意外でしたけど。

ということで、結論に。


【結論】


1.JOINが出来るなら、まずはJOINを検討。あと、OFFSETとLIMITが使えるなら、よりベター。

2.JOINが出来ないなら、NoSQLを上手く使う。その際、出来るだけループ内でデータ取得処理を行わない様に工夫する

基本は、上記の流れで良いんじゃぁないかと。

あと、とにかく重要なのが、「ループ内でデータ取得(SQLも、NoSQLのget()も)を出来るだけ控えろ」という点。

更に、画面設計的にページング(スマホだとスクロールが同義語?)処理にするとかも大事ですね、って基本ですけど。


あと最後に、チューニングにおける「鉄則中の鉄則」ですけど、

「推測するな、計測せよ」ってのは、

「JOINが遅いのも、ケースバイケース」ということからも感じた所ですので、

胸に刻み込んで、日々の業務に携わるのが良いんじゃないかと思いましたね。


いつもの事ですが、何かご意見やご感想などありましたら、遠慮無くいただければと思います。

性犯罪をしにくくしつつ性欲を満たすビジネスモデルの草案

相当に厄介なテーマなんですけどね。

ただ、考えてみたら、結構昔から時々書いているネタでもあるので。

 

まず、情報提供を希望したいんですけど、「国内・国外問わず、性犯罪率を低下させる目的のWebサービス(サービス名やトップのURL)」の情報提供を希望します。

僕が思い付いたぐらいですから、既にある様に思うんですよね、だから。

で、これを書いた理由は、最後まで読むと分かると思いますけどね。

だから最後にもまた書きますけど。

 

では、本題に入ります。

あと、今回は草案なのでざっくり(それでも長いけど)です。

 

まず、インターネットの特徴を考えてみますと、

「情報の公開範囲(制限しなければ全世界に公開)」

「情報と情報の繋ぎ合わせが容易(リンク機能)」

アーカイブ(情報の保持)性が高い(消されなければずっと残る)」

というものがあると思います。

いや、これで全部だとは思いませんけど。

 

で、プラスの側面からいきますと、

「美味しい料理の作り方」とか、

「ヒーリングに良い音楽」とか、

システム開発における、開発ノウハウ」とか、

おそらく真剣に書きだしたらとんでもない量になると思いますけど、

そういった「知識や、良いノウハウの共有と永続性」があると思います。

 

ただ、逆に困る(相性が悪いともいう)のが、今回のテーマの様な時。

例えば、以下の様なケース。

「リアルでミニスカートを履いていてたまたま転んだ結果、パンツが見えた状況を盗撮されネットの画像・動画サイトにアップロードされて、ずっとそのまま」

「被害の当事者的には、その時は気付かなかったが、あとになって気付く」

といった被害報告的なページも、多々存在しています。

 

で、こういった現状に対して、「忘れられる権利」とか「ペチパンツを開発」とか「シャッター音が鳴るカメラ」とか、色々な業界が色々な手法を用いて解決策を見出そうとしていますけど、データ的には以下の様な状況の様です。

 

matome.naver.jp

 

上記ページ内の以下のリンクにもある様に、

「犯罪件数」自体は減ってるんですけどね。

なので、「内訳」が問題と。

 

www.hazardlab.jp

 

増えた原因は、まぁスマホのカメラはあるんでしょうけど、

正直この辺は「使い方の問題」でしょうと。

個人的には、仙台とか伊豆とか大阪とかに旅行に行った時の写真がありましたね。

夜景とか色々ありますけど。

 

他に考えられる事として、まぁ一杯あると思いますけど、

個人的に取り上げたいのは、「即効性」と「遅効性」の話(この記事の元ネタになった)ですね。

 

www.asahi.com

 

結構、ここら辺は印象度が強いんですけど、

ヌード(全裸)は、ストレートで即効性は高いんでしょうけどね。

でも、「深みが無いので飽き易い」。

パンチラとパンモロ(こっちの方が深みが無い)も一緒。

あと、上記はログインしないと見れないので、以下も貼っておきます。

 

toyokeizai.net

 

「今ではクリックひとつで想像の及ぶあらゆる性行為がタダで見られる。そんな時代に(ヌード写真は)時代遅れだ」

 

ここは意見が違います(今回の記事と対立してますし)けど、

単純な性欲解消にあまり興味が無いので、ヌードを失くすという大枠の方向性自体は賛成ですね。

なので、まずは、この「深み」の存在があります。

 

それと、女性側からの観点として、

上記のペチパンツもそうですけど、

性犯罪防止観点(だとしか思えないが)から、

下着が進化していると。

これは、歴史を見ると早いですね。

 

www.cocoros.jp

 

いや、勿論下着に限らず、

スポーツ着メーカー(ブルマとかね)とか、

学校側の取り組み(体操着の選定とかね)とか、

AV(これは否定的な意見もありそうですが)とか、

色々な取り組みがされているわけですけど、

性犯罪は全く無くならない(むしろ上記の通り増えている)。

・・・商品の進化に、人間(この場合、男ですけど)が追い付いていない感がありますね。

ちなみにココ、セキュリティ業界でよく言われる「イタチごっこ」と、構図が全く一緒かと。

「男性が攻撃側」で、「女性が防御側」という意味で。

なので、ピンときた方はご存知の通り、この話「人類が滅亡しない限り、永久に終わらない」と予想しています。

ということで、商品は「深み」に対応してはいるので、

人間も深みに対応しなければいけないのではないかというのも、ポイントかと。

・・・一番、妥当な手段はAVなんですけどね。

一番簡単だと思いますし。

ただ、AVで済まない人向けには、

深みに対応する必要があるかなと。

 

で、この「深みに対応」するのに必要な能力として、

「妄想力」を磨く必要がありまして。

 

例えば、上記のペチパンツを題材にしましょう。

以下の様なもの(確かに最近ちょくちょく見かけますね)です。

 

www.peachjohn.co.jp

 

で、例えばリアルで、この服装の女性を「襲わず盗撮せず何もせずに(言うまでもない)」、どうやって性欲を解消するのか?

 

ポイントは、言葉(文章や声)と心理(羞恥心)。

え、キモい・・・それは最後のQ&Aをご覧下さい。

・・・この分野、「綺麗な解決策」の実践だけでの解決って難しそうなんですよね(苦笑)

 

話を戻しまして、言葉と心理について。

まず、「何故、この人はペチパンツを履いているのか?」という「心理(動機といった方が良いかも?)」を予測します。

どうやって予測するかは、上記のページなどの「口コミ」とか。

で、簡単な予測結果としては、パンチラ防止(他にもあると思うけど、今回は省略)を目的としていると。

 

で、ここからが「妄想(頭の中だけ。手や体は勿論動かない)」です。

・・・人によって全然違うでしょうし、何パターンも思い付くし何とも言えないですけど、例えば以下。

「ズボンじゃなくてスカートが好きなのか?で、スカートが好きってことは、お尻が大きいから隠したいのか?でも、スカートだと今度はパンチラが怖いので、ペチパンツを履いてパンチラ防止と。じゃあ、ペチパンツを履いてまで隠したいパンツは、どんなパンツだろう?{自分の好みのパンツのデザイン}に違いない!じゃあ、なんで{自分の好みのパンツのデザイン}を履いているかというと・・・キリが無いので省略」

・・・途中で噴いてしまいましたね(苦笑)

ただ、基本的にはこういう様なアプローチを取って、「口コミを書いた人(大体のケースにおいて、何も知らない人のはず)」と「対象者(奥様でも、今好きな人でも、過去に好きだった人でも)をイメージ」して「結び付けて、その人がこの口コミを書いた事にして」、「前述の妄想に更に妄想を重ねて興奮度がどんどんどんどん上がってMAXになった所」で、「マスターベーション」して終了です。とんでもない(苦笑)

ちなみに、結び付けに関しては、知ってる人ほど「アイツは、こんな口コミ書かないな」となって結び付けしにくい(どっかで無理が出る)ので、どっちかというと「知らない人」をお勧めします。だから、通りすがりの{自分の好みの見た目の女性}が一番良いですね。・・・いやでも、ちょっと知ってるぐらいの人の時もありますね。ただ、少なくとも、「知ってる度合いが高ければ高いほど、妄想展開に無理が出てきやすいので、除外されやすい」のは確かですね。

 

で、ビジネス的には、この辺が題材です。

つまり、「色々な質問によって、現在のエロ妄想力を診断して、診断結果に応じて妄想力を磨いて、マスターベーションを行い性欲を解消(よって遅漏解消などにも)し、性犯罪に向かう衝動を抑える」という「Webサービス(ただし、オフラインも絡むと思われます)」。

で、こういうの無いんですか?というのが、一番最初の「情報提供」の話です。

あと、マスターベーションは、やり過ぎはダメでしょうけど、以下の様な健康効果もある様ですね。やっぱり(笑)

 

spotlight-media.jp

 

あと、上記の質問内容はどういうものかについて、色々用意はある(といっても数がまだ足りなさそう)んですけど、例えば以下の様な感じです。あと、おそらくは複数回答のものが多くなる気もします。

 

Q1.知り合い(奥様・恋人・友達など)の下着のデザインに、あなたは意見を挟みますか?

 

1.本人の勝手にすれば良いので、一切口を挟まない

2.女性側が希望したなら、意見を挟む

3.女性側の希望の有無に関係なく、意見を挟む

4.意見を挟みつつ、女性用の下着売り場に同行する

5.・・・夢が叶った。女性用の下着売り場に行って・・・以下省略(苦笑)

 

Q2.ミニスカートを履いておきながら、パンチラ防止に努める女性について、あなたはどう思いますか?

 

1.どうせブスでしょ?そうじゃなくても自意識過剰過ぎだし、ミニスカ履いてるなら、パンチラは諦めろよ

2.大切な部分を守るという乙女心について考えていると思うので、好感が持てる

3.マナーとして普通だと思う

4.何か過去に辛い体験あったのかなと想像し、可愛そうに思う

5.上記のペチパンツの妄想と同じ(苦笑)

 

Q3.最近、Twitterのデザインで、★マークから♥マークに変わった所がありますが、あなたはどう思いますか?

 

1.改悪である

2.相手がどう解釈しようが、特に使い方を変える気もないので、何も気にしない

3.Twitter婚増加間違いなし!

4.草食系の男女が、過度に意識し過ぎて、利用度合いが減りそう

5.本当は♥マークを押したいのに、愛情表現と勘違いされそうで恥ずかしいから押さない女性とかクッソ可愛いわ!

 

・・・今回は、「・・・」の多い記事になってますが、

「・・・」の箇所の大半は、アンフェアの雪平夏見の「バカかお前は」に置き換えておいて下さい(苦笑)

いやでも、「多角的」に質問しないと、診断って難しいのではと思いますんで。

ちなみに、一見安全そうな「1~2辺りの回答」でも、例えば本音を語らない様な人(プライドや社会的立場とかから)なら、そう安心でもないと思いますね。勿論、1~2でも全く問題ない(多分、ここが大多数?)人もいらっしゃるでしょうし、4~5だから危ないともいえない(この話は、倫理観が最大のポイント)わけですので、結構難しいです。

 

で、診断結果(ここまでは無料)出ました。で、その後の性欲解消コース?が「有料」です。で、ここは「オンライン(難しいぞコレ)・オフライン」併用な感じです。先程書いた様な手法(他にも色々とあるわけですけど。といってもこっちも数が足り無さそう)によって、個別に解決策を考えていきましょうと。カウンセリングっぽいですね。

ここは、値段高くなりそうなので、何種類かコース(簡単なものや本格的なもの)を用意した方が良さそうですね。

 

あと、個人的に思っているポイントとしては、以下かなと。

 

1.ネットの特徴(一番最初に書いた話)を考えると、エロは大変に相性が悪い(拡散しやすいし、永久に残る)ので、Webサービスを使って、この辺のおとしまえを付けられないか

 

2.ネット上の画像と動画(勿論、本人の同意が無いもの)に頼らず(AVで解決するなら、それが一番穏当かと)、テキストなどからの妄想力アップによって、性欲不満を解消する

 

3.上記の1番抜きにしても、マスターベーションからの健康対策に良いのでは?

 

ひとまず、「草案」としてはこんな所です。

最後にQ&Aで締めます。

 

 

1.AVとか風俗とかパンツゲーとかがあるから、性犯罪が増えるので、即刻全て失くすべき

 

さすがに今時・・・とは思いますけど、仮に法令などでそうなったら「この国出ていきましょうか」。

「本格的なディストピア突入間違いなし」でしょうけど、逆に言うと今はまだなっていない(と思うが)ので、「言われている程、悪い国(別に、全てが良い国とも言いませんけど)」では無いと思いますね。

 

2.言葉から好き勝手に妄想して女性の心理を読むとか、あり得ないしキモ過ぎる。そもそも、男性がスケベな事を考えなければ良いし。あと誰も言葉を言わなくなったり、口コミを書かなくなるよ?

 

「男性がスケベな事を考えなければ良い」は、上記1と本質一緒。「性欲のコントロールの問題」でしか無いので。あと、「誰も言葉を言わなくなったり、口コミを書かなくなる」については、じゃあ「言わなくなったり書かなくなった原因を予測する」となる(だから、セキュリティのイタチごっこと一緒)だけですね。あと、別に「ネットのテキストに限定していない」ですし。

と言いますか、以下のどっちが「まだマシ」だと思います?

 

(1)リアルで盗撮されて、知らない間にネットにアップされて拡散されて、それを見た人とリアルでバッタリ会って教えられた(或いは、そのまま性犯罪に巻き込まれた)が、既に削除申請も間に合わず、何処かで永遠に残る。対象は、自分の場合もあれば、自分の子供の場合もある

 

(2)リアルでもネットでも何の接点もない人が、自分の全く知らない所で勝手に妄想して性欲を解消しているが、犯罪に巻き込まれることはない

 

で、僕が推奨しているのは(2)の方で、要するに(1)に対する対策ですね。

綺麗にいくなら、「適切な性教育」「他人が嫌がっている(ちなみに、ストックホルム症候群を考えると、嫌がっていなくても微妙)のにやるな」とかでしょうけど、それが出来ているなら苦労しないですね。

 

で、コレ、いじめと同じで「やる方が悪い」んです。だから、それ(今回の場合は性欲)をコントロールする為に、妄想力を鍛える(じゃなくて、普通の治療とかでも良いんですけど)という話なんです。

 

「妄想力とかキモイ」のは間違いないでしょうし、綺麗な対策が「全」国民に行き届くのが一番良いわけですけど、上記の統計データの通りなわけで、そこは我慢出来ないものですかね。この点は、仮にネットが無くなっても変わらないと思いますし。

 

3.女性に配慮し過ぎでは無いか?

 

まぁ、以下みたいなのもあるみたいですし。いわゆる、「リベンジポルノ」ってやつ(僕は、オヤジ狩りを思い出しましたけど)ですかね。リベンジポルノは、男性が女性に対してってイメージ強いかもしれないですけど、以下の通り、逆だって有り得るでしょうし。

 

girlschannel.net

 

4.ビジネスチックに語っているけど、そもそもは卑猥な動機ですよね?

 

「卑猥」が何を指し示すかによる様な。性欲解消が「卑猥」ですかね。

あと、ビジネスって、いわゆる社会的責任(CSR)を持って社会的問題解決や社会の発展に繋げるって事だと思いますし、じゃあAV製造メーカーや風俗もNGになりませんかね。

 

5.ブラとか、上半身の話が全然無いような?

 

ココ悩ましい問題で。上半身は「全然」興味が無いものでしてね。もし、やるなら本格的に勉強はしてみますけど。

 

6.倫理(ここでは、やってはいけない事という意味合い)の問題が一番大きいし、根っ子なのでは?

 

ええ、全くその通りかと。「女性は好きじゃないのか?」「好きな相手が嫌がっているのに、どうしてやるのか?」ってのが、個人的に「サッパリ」分からないんですけどね。

 

「女性に騙されたから」は、すいません僕も昔キャバクラで騙された(わけじゃないが、貢ぎまくった(苦笑))けど、「良い社会勉強(それにしても、高い出費でしたが)だったかなぁ」と。

「女性が自分に全く相手してくれないから」は、それは貴方にも何がしかの問題があるんじゃないでしょうかね。僕が言っても、ちっとも説得力無いでしょうけど(苦笑)

 

7.性欲を満たすだけで、性犯罪は起こさなくなるのか?

 

多分、上記6の倫理が根っ子と思われるので、完全には微妙。でも、減るとは思うんですけど、どうでしょうかね。要分析かと。

 

8.難易度高過ぎないか?

 

上記のペチパンツはまぁ確かに。

AVの、中の女優を「自分の好きな人」に置き換えるのが、一番簡単かなと思いますね。

 

9.リアルではちょっとした発言だけでもセクハラ扱いされる時もあるが、ネットでは大丈夫なのか?

 

この記事もそうですけど、「女性に対して直接言わなければ良い」のでは。被害妄想強過ぎる女性(といっても、コレも元の原因は男にあったりもするみたいですし。全部ではないでしょうけど)だと、ちょっと厄介でしょうけど。実際、直接話したケースって、相当昔に女性がエロ(というより恋愛だった気がするが)ネタにノッテきた時に、話したぐらいですし。

 

10.逆に、kitoku_magic的に、性欲解消が困難なケースってどういうケース?

 

きっついのだと、「ロボット的対応(感情表現を一切出さない)」。

つまり、この記事に対して「何の反応もしない」がベストですね、女性から見れば。一方で、反応しなければ(男だけの問題じゃないので)問題解決に繋がりにくくもなるんですけど。・・・まぁ、「女性とバレなければ良い」んですけどね。

ただ、仮に全ての女性がロボット的対応をしたとしても、「過去の記憶」が無くならない以上は、「思い出の青春時代」とか使えば良いんじゃないでしょうか(苦笑)

 

11.どうして、kitoku_magicは、こんな風になっちゃったの?

 

・・・・・・・・・・・・・・・分かりません(苦笑)

ただ、合法の範囲内で性欲解消を「継続的に」するのに、相当に苦しんだ人間が編み出した手法ではあるんでしょうけどね。

 

まだまだまだまだまだ話はありますが、一旦締めます。

あと、一番最初に書いた件ですが、「国内・国外問わず、性犯罪率を低下させる目的のWebサービス(サービス名やトップのURL)」の情報提供を希望します。

 

案件の状況とか排他制御とか色々

えっと、久々にITシステム関連の現状について色々書くことにします。

まず、主に先月観察していた案件状況から。

 

1.言語間での単価の違い

 

まだ観察が少ない気もするんですけど、

やっぱり結構この辺もあるのかなと。

具体的には、RubyPythonが高いケースが多く、

PHPPerlPerlは殆ど見ていないが)が低いケースが多い印象です。

CとかC++とかJavaは全然見ていないので分からないですけど。

あと、GoやErlangも数が少ないので分からないですね。

 

予想では、開発者の「数」が影響しているんだろうと思いますね。

勿論、数が少なくても需要が無いとダメではありますけど。

あと、言語毎の特徴(出来る事)や性能(速度)は、

そんなに重要視されてはいない様にも感じますね。

だったら、CやC++Javaがもう少しWebで使われても良いでしょうから。

 

RubyPythonならなぁ・・・個人的にはPythonの方が好みかな。

全然と言っていい程知らないですけど、

言語にコーディング規約が存在するっていうのは気に入りましたしね。

と言いつつ、PHPでも月70万ぐらいだったらあったりもしたので、

取引先次第な部分も多いとは思いますけどね。

 

2・準委任VS請負

 

結構前からボンヤリとは思ってはいたんですけどね、この辺は。

まず、基本的には以下の様な傾向があると思っています。

 

1.発注者はシステムの完成まで面倒見て欲しいので請負を好む

2.ただ、システム開発の場合、システムを完成して納品という形態が馴染まない部分も多く、準委任などの成果物を問わない形態の契約の方が相性が良かったりもする

3.金額的には、請負の方が高い場合が多いが、見積もり次第な部分もある

 

まぁ、相性悪い部分があるにしても、

それでも発注側としては作り切ってからお金を払いたいというニーズは間違いなくあるでしょうから、

ポイントなのはやっぱり「成果物の定義」という事になってはくるだろうと思うんですけど、ココが揉める所ですしねぇ(苦笑)

まぁ、「要件」って基本的に難しいとは思うんですけどね。

個人的には、「こんな家に住みたい」「こんなゲームがしたい」「こんな人と結婚したい」「こんな人生を送りたい」とかを考えるのと、システム開発の要件を考えるのって、「自分の希望を整理する」って意味で似ていると思ってまして、で、前述の難しさを考えてみても難しいだろうなぁと思いますし。

 

ということもあり?最近は、設計や運用以外の開発工程でも、準委任の契約案件が増えている?し、そういうエンジニア(これは実際に何人か見た)も増えている印象ですね。でも、金額は安くなりがちと。

 

金額に関しては、以下のツイートをした事があるんですけど、

この辺は、「国の景気」や「業界の景気」も関係してくるのかなとも思いますね。

リーマンショックの時に案件激減したのを見ても。

 

kitoku_magic on Twitter: "@tyonekura 「平均」(なので一概には言えない)だと、確かにそのぐらいな気もしますね。あと、個人的には、日本は「上限」が低い(1500万以上とかになると、かなり厳しい)印象の方もありますね。"

 

だから、ビジネスに関して成功して欲しいというのは常々あるんですけど、

現状見てると、「短期的な視点"しか"ない」のと、

「一つのサービスに対する期待値が高過ぎる」が殆どなんですよね。

特に、最近後者に関してよく思うんですけど、

「そのサービス、もう伸びしろあんまり無いと思うよ」って思うサービスなのに、

「まだまだ売り上げ上げるんだ!」というノリで過剰に働き続けたにも関わらず、

思うような結果が出ないということで、モチベもダウンしていくという。

多分、こういう時にポイントなのは、

勿論売り上げを維持するっていうのは大事なんですけど、

どっかのタイミングで「損切り」をすることなんだろうと思いますね。

で、この辺が上手なのが経営上手なんでしょうと、きっと。

 

この辺、引き続き見ていきますけど、とりあえず現状ではこんな印象ですね。

 

3.直接契約

 

結局、契約には至らなかったものの、

エンジニアを企業と結びつける会社であるにも関わらず、

契約は、エンジニアと企業が直接行うというケースがありまして。

まぁ、この人材紹介サービスというか、

この商売形態もそんなに稼げる商売ではないと思いますし、

確かにそんなに労力を割く必要も無い気はしますね。

実際、上記の会社も、他の事業も合わせてやって稼いでいるとの事でしたから。

 

4.GitHub釣り

 

どうやら、一部のスタートアップ企業とか、

あと、ちょっと前から大企業も追随し始めているみたいですけど、

採用方面において、GitHubの重要度がかなり高くなっている様で。

 

ただ、ココは結構以前から、

職務経歴書とか、短い面談時間だけじゃ判断難しい」と思ってはいましたし、

上記のGitHubと類似と思われる「面談時に技術の問題のクイズを解く」というのにも、

基本的に賛成だったので、良い傾向だとは思いますけどね。

ただ、GitHubだと海外のエンジニアも対象になるので、

そこが厄介ではあるんですけど。

 

過剰にセルフブランディングとか意識せよとまでは思わないですけど、

とはいえ、Webで何にもやっていないのは、

裏で色々やっていたとしても、相手から認識してもらうのが難しいので、

特に、WebのエンジニアならWebを活用せよとは思いますかね。

 

「Webに何かを公開する」って観点って、個人的には

「書店に行った時に、Webの未購入の本の一覧を見て、どれを買うか考える」とか、

「自分のPC以外で仕事する時に、Webのvimrcの内容をコピーしてくる」とか、

色々便利なんですけどね(笑)

あと、まだ殆どやっていないですけど、

考察系記事の内容を、仕事中に他の人に見せたりも出来ますし。

 

ということで個人的には、

もっとWebを(リアルもだが)有効活用していきたいと思った次第でした。

 

5.サーフィンはこんな所にも効果が

 

比較的成功しているスタートアップ経営者の話を聞く機会があったんですけど、

どうやら、サービスを立ち上げる時に、

日本は勿論、海外の競合事例などを"あり得ないぐらい"沢山調べているとの事で、

「あぁ、情報収集のサーフィンが半端ないんだな」と。

あと、技術系のイベントのLTを聞いていて、

「あぁ、皆さん技術で"遊んで"モチベ管理しているのか」と思ったりとか。

特に、後者については、

僕は最近技術で遊ぶという観点が薄れてきていたので、

だいぶん参考になりましたね。

 

6.排他制御について

 

時々書いている、「NoSQLでトランザクション管理」の話なんですけど、

一応、以下のソースを書いてはみたんですけどね。

 

final_magic/kvs_handle.php at master · kitoku-magic/final_magic · GitHub

 

final_magic/kvs_handle_memcache.php at master · kitoku-magic/final_magic · GitHub

 

final_magic/kvs_handle_redis.php at master · kitoku-magic/final_magic · GitHub

 

ただ、ソース内にも書いている以下のリンクの内容もそうですし、

「ロックを取得した後にトランザクションが失敗したら、ロックは解放するのか」とか、

「複雑な更新処理にしてしまうと、ロールバックが難しくなるので注意」とか、色々と考える必要がある点も別途ある印象でして、

そこまで考える必要あるなら、やっぱりRDBでやった方がという気もしますね。

 

dekokun.github.io

 

元々、「パフォーマンスを良くしたい」って目的からこの話って出てきているんだと思うんですけど、

正直「諸刃の剣(かどうかも怪しい、どれくらい効果があるか未知数ですし)」と思われるこの機能を充実させるよりも、

サーバの増設・増強、言語やフレームワーク自体の性能改善、プログラムのデータ構造やアルゴリズムの工夫、RDBSQLの性能改善などなど、他の比較的リスクの低いチューニングをやった「後に」手を付けた方が良い印象ですね。

でも、上記の1番の言語もそうですけど、

色々見ている限り、そこまではチューニングしていないケースの方が多い印象ですし、そっちが先じゃないかと思いましたね。

あぁ、そういう意味では、先日書いた論理削除の話も同じですね。

というかこっちは、パフォーマンスに好影響ある?って印象なんですけど。

 

ということで、出来れば使いたくない機能ですけど、実装してみたという変な話でした(笑)

論理削除の考察

えっと、以前に以下に書いたんですけど、
ちょっと今回、この点について真剣に考えてみましょうということで。


kitoku-magic.hatenablog.com


ひとまずは、考えについて整理するということで、
上記記事のリンク先の内容について突っ込んでいってから、
まとめを書きたいと思います。

ただ、「物凄く長い記事」なので、結論だけ知りたい方はこちらを。


http://mike-neck.hatenadiary.com/entry/2015/03/24/231422 から引用。

カーディナリティが云々で検索条件につけても性能上的にもよくない

0と1という二つの値しか取り得ないから、インデックス貼っても大して速くならないといった所でしょうか。
ただ、この記事の最後に書いてある

二つの値でも片方が極端に少ない、片方が極端に多い場合は意味あるらしい

というのもあるみたいですね。


論理削除というのはまだ要件的に未確定な要素が隠されていることを示すフラグ

これ、この後も沢山出てくるんですけど、結構ポイントだと思いますね。


「社員」はマスターと言っていましたが、マスター(僕はリソースと呼んでいる)はシステムの開始から未来永劫追加・変更・削除することができないデータ

ここちょっと興味あったんですけど、「マスター」ってどういうテーブルをイメージします?
個人的には、「商品・社員・アプリ上で表示するメッセージ」といった、
「管理や運営側は変更出来るけど、ユーザーが直接変更出来ないデータ」を「マスター」と呼んでいるんですけどね。
というか、上記の「システムの開始から未来永劫追加・変更・削除することができないデータ」というのがピンとこなかった。
「追加・変更・削除しないデータ」ってあるかなぁと。
まぁ、あんまり本題と関係ないので次へ。


http://blog.kazuhooku.com/2015/03/blog-post_26.html から引用。

「現在の状態を表現するビュー」を実体化したものとして表現されること

あぁ、どうやらココ、「DBA視点」と「アプリ(といってもサーバ側だが)エンジニア視点」で、
「DBに対する考え方が違う」のかなと。
僕は、アプリ(しつこいけどサーバ側)エンジニアなわけですけど、
ビューとかトリガーとかストアドとかファンクションとかって基本否定的でして。
というのも、「ロジックをDB側に書きたくない」というのがあるんですよね。
あと、トリガーに限っていえば、過去に2~3日ハマった事があるというのもあるんですけど(苦笑)
DBは、「データ格納倉庫」として使いたいんですよ。
ただ、後で散々出てくる「制約(ユニーク制約とか)」は使いたいんですけどね。
でも、「手続き型の処理(ストアドやファンクションの事)とか、AをしたらBをする(トリガーの事)」とかは、
プログラム側に一元化したいなぁ。それで「仕事が増えた」としてもね。
・・・ビューは、「ビューのビュー」とか、「複雑化しませんか?」といった懸念や、
以下の記事辺りから、「まぁあんまり」という感じですね。


ビューの功罪


で、多分、「DBの方でやらないでアプリの方でやる」の一番大きな理由としては、「仕様変更に対応しやすくしたいから」だと思いますね。
・・・といってもまぁ、アプリエンジニア間でも、意見が割れそうではありますけどね(苦笑)

なので、上記の話については、特に否定もしないけど肯定もしないといった所ですね。
そもそもの前提が違うので。


http://frsyuki.hatenablog.com/entry/2015/03/26/080627 から引用。

WebサービスとかTreasure Dataのようなサービス事業者だと、テーブルを最初に設計するときにあまり時間をかけられないので、後から見直して最適化していくことが多い

この記事も、基本さっきの話と同じ感じなんですけど、上記については
「だったら、尚更テーブル設計はシンプルにしておきたくないか?」と思うんですよね。
あと、この記事の場合、ポスグレに依存してるのでってのもあるかなと。
いや、MySQLにも対応させたいですからね。


でもPlazmaDBの実装は、ほぼ全部FUNCTIONになっている

これ、リンク先も読んでみたんですけど、「どこにFUNCTIONなんて書いてある?」のか、ちょっと見つけられなかったですね。
単純に、僕が気付かなかった可能性も高そうですけど。


http://blog.webcreativepark.net/2008/05/26-103152.html から引用。

user_mst テーブルに対して、deleted_user_mst テーブルを用意しておいて、ユーザの削除時は、user_mst テーブルから DELETE すると同時に、deleted_user_mst テーブルに INSERT するという方法です

コメントからの引用ですけど、
これ、この後も沢山出てきますね。
というか、僕の知り合いも同じ様な事言ってましたけど。


開発時はこまめにバックアップを取る

開発サーバは何とも言えないですけど、本番でバックアップ取らないってのは全然イメージ無いですね。
正直、「良い度胸してるな」って感じです(苦笑)


テストや検証の工数が取れない場合

この辺が、削除フラグ導入の一因になってる可能性も高そうですね(苦笑)
でもコレ、その時は良くても、後からじわじわと困っていきますしねぇ。


http://qiita.com/Jxck_/items/156d0a231c6968f2a474 から引用。

「最適な設計は DELETE_FLAG ではない」という場合が有って、その場合は、その最適な設計を探すのが正しいだろ

これは、この後も散々出てくるんですけど、「とても大事なポイント」だと思いますね。
「状態」とか、他の意味が隠されているんじゃないの?って。


大抵、色々仕様を見返すと「こういう状態もありますよね?」とこちらが気づくくらいには見落としがあって、
これは放っておいたら、状態が発見されるたびに FLAG 列が追加

で、名前が「~FLAG2」とか、数字の連番になっていくわけですよね(苦笑)


しかし、これはマスタ系のテーブルだと上手く行きません。
例えば社員表から退職者を移すなどと言う場合、その退職者の ID は多くのテーブルから参照されており、
その参照をどうするかが最大の課題となります

あぁ、なるほど。
・・・ゲームの案件(だけ?)で良く見るんですけど、
「マスター系のデータは、DBではなくCSVの中に入れて、使いたい時には、CSVをインポートしてパースする」なんてのがありますね。
で、こっちだと、「社員が退職したらCSVから行を削除」でいけないですかね。
当然、外部キーは貼ってないわけですし。
といっても、結構この辺モヤってますけど。
ただ、こっちの場合、プログラム内で扱うデータの量が増えるので、「メモリに注意」とはなるんですけどね。

・・・あぁ、社員が退職に限っていえば、「退職」って多分ステータスだと思うので(在籍とか出向ってステータスもあるかと)、
単純に、社員テーブルに「在籍状態」ってカラムを用意して、それでさばくとか。
カードゲームのユーザーが所有しているカードのテーブルなら、
「売却(といっても、こっちはマスターじゃないので、削除テーブルに突っ込むで良いのでは?)」とか。

まぁ、上記の方法のいずれでもダメなら、最悪「マスター系だけは削除フラグ使う」となるかもですけど。


パーティションのキーとなる状態があればいい

それこそ、さっきの「在籍状態」とか。
といっても、これはOracleでしか使えない?ので今一つですけど。


問題は、本当にそんな場合があるのか、安易なフラグを付けることによって設計にしわ寄せがいったとしてもフラグを付けるだけの価値があるか、物理設計のツケを論理設計で払うのが本当に良いのか、つける前に検証する価値は有ると思います。推測しないで計測せよという話です

ということで、「計測」してみました。
あと、ここから「物凄く長い」ので注意です。
計測結果による結論だけ知りたいって方は、こちらを。


まず、MySQLのバージョンは「5.5.29」です。
・・・余談ですけど、はてなブログHTML微妙に書きにくい(苦笑)


1.以下のテーブルを用意


// 有効なユーザーのみ格納するテーブル
CREATE TABLE `users_test1` (
`user_id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`user_id`)
) engine = InnoDB default charset = utf8;

// 無効(削除とか)なユーザーを格納するテーブル
CREATE TABLE `delete_users` (
`delete_id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`name` varchar(50) NOT NULL DEFAULT '',
`delete_time` datetime NOT NULL,
PRIMARY KEY (`delete_id`),
KEY (`user_id`)
) engine = InnoDB default charset = utf8;


// delete_flag有りのユーザーのテーブル
CREATE TABLE `users_test2` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`name` varchar(50) NOT NULL DEFAULT '',
`delete_flag` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY (`user_id`, `delete_flag`)
) engine = InnoDB default charset = utf8;

// ユーザーIDの現在値(色々あって、こうせざるを得なかったんですよね)
CREATE TABLE `user_id_seq` (
`current_user_id` bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`current_user_id`)
) engine = InnoDB default charset = utf8;

INSERT INTO user_id_seq VALUES (0);

まぁ、テーブルをどうやって切るかも色々ありそうですけど、
ひとまず、こんな形に。


2.テーブルにデータ突っ込むPHP


// data_insert_users_test1.php
// users_test1テーブルに百万件のレコードを作成する
<?php

try
{
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'ユーザー名', 'パスワード', array(PDO::ATTR_EMULATE_PREPARES => false));
}
catch (PDOException $e)
{
exit('データベース接続失敗:' . $e->getMessage());
}

$pdo->beginTransaction();

for ($i = 1; $i <= 1000000; $i++)
{
if ($i >= 10000 && $i % 10000 === 1)
{
$pdo->beginTransaction();
}
$stmt = $pdo->prepare("INSERT INTO users_test1 (name) VALUES (:name)");
$stmt->bindValue(':name', uniqid('', true), PDO::PARAM_STR);

$ret = $stmt->execute();
if ($ret)
{
if ($i >= 10000 && $i % 10000 === 0)
{
$pdo->commit();
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}


// data_insert_delete_users.php
// delete_usersテーブルに二百万件のレコードを作成する
// 1ユーザーあたり、2件の削除レコードが存在する
<?php

try
{
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'ユーザー名', 'パスワード', array(PDO::ATTR_EMULATE_PREPARES => false));
}
catch (PDOException $e)
{
exit('データベース接続失敗:' . $e->getMessage());
}

$stmt = $pdo->prepare("SELECT * FROM users_test1 ORDER BY user_id");
$ret = $stmt->execute();

if ($ret)
{
$pdo->beginTransaction();
while ($result = $stmt->fetch(PDO::FETCH_ASSOC))
{
if ($result['user_id'] >= 10000 && $result['user_id'] % 10000 === 1)
{
$pdo->beginTransaction();
}
$stmt1 = $pdo->prepare("INSERT INTO delete_users (user_id, name, delete_time) VALUES (:user_id, :name, :delete_time)");
$stmt1->bindValue(':user_id', $result['user_id'], PDO::PARAM_INT);
$stmt1->bindValue(':name', $result['name'], PDO::PARAM_STR);
$stmt1->bindValue(':delete_time', date('Y/m/d H:i:s'), PDO::PARAM_STR);

$ret = $stmt1->execute();
if ($ret)
{
$stmt2 = $pdo->prepare("INSERT INTO delete_users (user_id, name, delete_time) VALUES (:user_id, :name, :delete_time)");
$stmt2->bindValue(':user_id', $result['user_id'], PDO::PARAM_INT);
$stmt2->bindValue(':name', $result['name'], PDO::PARAM_STR);
$stmt2->bindValue(':delete_time', date('Y/m/d H:i:s', strtotime('+1 day')), PDO::PARAM_STR);

$ret = $stmt2->execute();
if ($ret)
{
if ($result['user_id'] >= 10000 && $result['user_id'] % 10000 === 0)
{
$pdo->commit();
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}
}
else
{
echo "SELECT失敗\n";
exit;
}


// data_insert_users_test2.php
// users_test2テーブルに三百万件のレコードを作成する
// 百万件はdelete_flagが0で、残りはdelete_flagが1
<?php

try
{
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'ユーザー名', 'パスワード', array(PDO::ATTR_EMULATE_PREPARES => false));
}
catch (PDOException $e)
{
exit('データベース接続失敗:' . $e->getMessage());
}

$pdo->beginTransaction();

for ($i = 1; $i <= 1000000; $i++)
{
if ($i >= 10000 && $i % 10000 === 1)
{
$pdo->beginTransaction();
}
$name = uniqid('', true);
$stmt = $pdo->prepare("INSERT INTO users_test2 (user_id, name, delete_flag) VALUES (:user_id, :name, :delete_flag)");
$stmt->bindValue(':user_id', $i, PDO::PARAM_INT);
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
$stmt->bindValue(':delete_flag', 0, PDO::PARAM_INT);

$ret = $stmt->execute();
if ($ret)
{
$stmt1 = $pdo->prepare("INSERT INTO users_test2 (user_id, name, delete_flag) VALUES (:user_id, :name, :delete_flag)");
$stmt1->bindValue(':user_id', $i, PDO::PARAM_INT);
$stmt1->bindValue(':name', $name, PDO::PARAM_STR);
$stmt1->bindValue(':delete_flag', 1, PDO::PARAM_INT);

$ret = $stmt1->execute();
if ($ret)
{
$stmt2 = $pdo->prepare("INSERT INTO users_test2 (user_id, name, delete_flag) VALUES (:user_id, :name, :delete_flag)");
$stmt2->bindValue(':user_id', $i, PDO::PARAM_INT);
$stmt2->bindValue(':name', $name, PDO::PARAM_STR);
$stmt2->bindValue(':delete_flag', 1, PDO::PARAM_INT);

$ret = $stmt2->execute();
if ($ret)
{
if ($i >= 10000 && $i % 10000 === 0)
{
$pdo->commit();
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}

$pdo->beginTransaction();

$stmt = $pdo->prepare("UPDATE user_id_seq SET current_user_id = 1000000");

$ret = $stmt->execute();

if ($ret)
{
$pdo->commit();
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
}


ちなみに、今更気付いたんですけど、
削除テーブルに同じユーザーが二人って変だから、
ユーザーじゃない(履歴的なテーブルであれば良いわけですし)方が良かったですね(苦笑)
まぁ、今回の本筋とは関係ないと思うので、今回はこのままいきます。


3.SELECT文を投げて速度計測


// 有効なユーザーデータのみ取得
select user_id, name from users_test1 where user_id = 492650;

select user_id, name from users_test2 where user_id = 492650 and delete_flag = 0;


// 無効なユーザーデータも含めて取得
select user_id, name from users_test1 where user_id = 492650
UNION ALL
select user_id, name from delete_users where user_id = 492650;

select user_id, name from users_test2 where user_id = 492650;


mysql> set profiling = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> select user_id, name from users_test1 where user_id = 492650;

user_id name
492650 5600e36c5e6b81.87318120

1 row in set (0.00 sec)

mysql>
mysql> select user_id, name from users_test2 where user_id = 492650 and delete_flag = 0;

user_id name
492650 5600f48c5dade8.64925888

1 row in set (0.00 sec)

mysql> select user_id, name from users_test1 where user_id = 492650
-> UNION ALL
-> select user_id, name from delete_users where user_id = 492650;

user_id name
492650 5600e36c5e6b81.87318120
492650 5600e36c5e6b81.87318120
492650 5600e36c5e6b81.87318120

3 rows in set (0.00 sec)

mysql>
mysql> select user_id, name from users_test2 where user_id = 492650;

user_id name
492650 5600f48c5dade8.64925888
492650 5600f48c5dade8.64925888
492650 5600f48c5dade8.64925888

3 rows in set (0.00 sec)

mysql> select user_id, name from users_test1 where user_id = 492650;

user_id name
492650 5600e36c5e6b81.87318120

1 row in set (0.00 sec)

mysql>
mysql> select user_id, name from users_test2 where user_id = 492650 and delete_flag = 0;

user_id name
492650 5600f48c5dade8.64925888

1 row in set (0.00 sec)

mysql> select user_id, name from users_test1 where user_id = 492650
-> UNION ALL
-> select user_id, name from delete_users where user_id = 492650;

user_id name
492650 5600e36c5e6b81.87318120
492650 5600e36c5e6b81.87318120
492650 5600e36c5e6b81.87318120

3 rows in set (0.00 sec)

mysql>
mysql> select user_id, name from users_test2 where user_id = 492650;

user_id name
492650 5600f48c5dade8.64925888
492650 5600f48c5dade8.64925888
492650 5600f48c5dade8.64925888

3 rows in set (0.00 sec)

mysql> show profiles;

Query_ID Duration Query
1 0.00042075 select user_id, name from users_test1 where user_id = 492650
2 0.00025525 select user_id, name from users_test2 where user_id = 492650 and delete_flag = 0
3 0.00049375 select user_id, name from users_test1 where user_id = 492650

UNION ALL
select user_id, name from delete_users where user_id = 492650 |

4 0.00021150 select user_id, name from users_test2 where user_id = 492650
5 0.00029775 select user_id, name from users_test1 where user_id = 492650
6 0.00022225 select user_id, name from users_test2 where user_id = 492650 and delete_flag = 0
7 0.00048450 select user_id, name from users_test1 where user_id = 492650

UNION ALL
select user_id, name from delete_users where user_id = 492650 |

8 0.00020925 select user_id, name from users_test2 where user_id = 492650

8 rows in set (0.00 sec)

mysql>


UNION ALLが若干遅い(といっても、ミリ秒レベルの話ですけど)けど、
そんなに変わらんって所でしょうか。
ちなみに、同じSQL二回投げたのは、キャッシュを利かせて同じ条件にする為です。
キャッシュ利いていない同じ条件の場合は・・・また今度にしましょう。


3.ユーザーの削除処理を実行し速度計測


// data_delete_users_test1.php
// users_test1テーブルのレコードを削除し、delete_usersに追加する
<?php

// コマンドライン引数からユーザーIDを渡す前提
$user_id = $argv[1];
$time_start = 0;
$time_end = 0;

try
{
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'ユーザー名', 'パスワード', array(PDO::ATTR_EMULATE_PREPARES => false));
}
catch (PDOException $e)
{
exit('データベース接続失敗:' . $e->getMessage());
}

$time_start = microtime(true);

$stmt = $pdo->prepare("SELECT * FROM users_test1 where user_id = :user_id");
$stmt->bindValue(':user_id', $user_id, PDO::PARAM_INT);
$ret = $stmt->execute();

if ($ret)
{
$pdo->beginTransaction();
while ($result = $stmt->fetch(PDO::FETCH_ASSOC))
{
$stmt1 = $pdo->prepare("DELETE FROM users_test1 where user_id = :user_id");
$stmt1->bindValue(':user_id', $result['user_id'], PDO::PARAM_INT);

$ret = $stmt1->execute();

if ($ret)
{
$stmt1 = $pdo->prepare("INSERT INTO delete_users (user_id, name, delete_time) VALUES (:user_id, :name, :delete_time)");
$stmt1->bindValue(':user_id', $result['user_id'], PDO::PARAM_INT);
$stmt1->bindValue(':name', $result['name'], PDO::PARAM_STR);
$stmt1->bindValue(':delete_time', date('Y/m/d H:i:s'), PDO::PARAM_STR);

$ret = $stmt1->execute();
if ($ret)
{
$pdo->commit();
}
else
{
echo "データ追加失敗\n";
$pdo->rollback();
exit;
}
}
else
{
echo "データ削除失敗\n";
$pdo->rollback();
exit;
}
}
$time_end = microtime(true);
}

echo $time_end - $time_start . "\n";


// data_delete_users_test2.php
// users_test2テーブルdelete_flagが0の行を1に更新する
<?php

// コマンドライン引数からユーザーIDを渡す前提
$user_id = $argv[1];
$time_start = 0;
$time_end = 0;

try
{
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'ユーザー名', 'パスワード', array(PDO::ATTR_EMULATE_PREPARES => false));
}
catch (PDOException $e)
{
exit('データベース接続失敗:' . $e->getMessage());
}

$time_start = microtime(true);
$pdo->beginTransaction();
$stmt = $pdo->prepare("UPDATE users_test2 SET delete_flag = 1 where user_id = :user_id and delete_flag = 0");
$stmt->bindValue(':user_id', $user_id, PDO::PARAM_INT);

$ret = $stmt->execute();

if ($ret)
{
$pdo->commit();
}
else
{
echo "データ更新失敗\n";
$pdo->rollback();
exit;
}
$time_end = microtime(true);

echo $time_end - $time_start . "\n";


で、上記PHPを、以下のコマンドで実行しました。


for i in `seq 1 1 1000`; do php data_delete_users_test1.php $i >> result_data_delete_users_test1.xls; done


for i in `seq 1 1 1000`; do php data_delete_users_test2.php $i >> result_data_delete_users_test2.xls; done


で、エクセルファイルに書き込まれた実行時間の平均を取った結果、
users_test1が「0.1151350505秒」で、
users_test2が「0.1165583317秒」となりましたって、
削除フラグ無いusers_test1の方がむしろ速いんですけど。


とりあえず、計測の結論としては、
「ほとんど変わらん」のではないかと。
勿論、前提変われば結果も変わるとは思うので、
テーブル設計やプログラムが変って所があれば、ご指摘頂きたい所ではありますけど。

では、話を戻して続きを。


http://qiita.com/Jxck_/items/156d0a231c6968f2a474 から引用。

「論理削除した行が復活する事」は、オペレーションミスの訂正時にもありますね。
オペレーションミスの訂正は画面を閉じた後でも発生するので、アンドゥ機能では対応できません。

ここからは、コメントからの引用ですけど、
オペミスがあるから、delete_flagを付けると?
「管理画面で確認ダイアログ出す」とか、
「万が一オペミスした場合のレコード復旧機能を、別途作る」とか、
色々工夫は出来ると思うんですけどね。
ちなみに、どうやってレコードを復旧させるかについては、
「削除テーブルに入れてしまったレコードを、有効テーブルに戻す」プログラムを書けば良いと思います。
まぁ、削除テーブルのdelete_idが歯抜けにはなっちゃいますけど、
このID自体、特に業務用のIDでも無い(サロゲートキー)ですし、仕方ないかと。


ちなみに、私は、作成日・更新日も何も考えずに項目に入っているのも嫌です。
JSON形式として更新前イメージをすべてログを残しておくようにしています

この作成日・更新日も多いですね。
まぁ、JSON形式かどうかはともかく、確かにログに残す方が良いと思いますね。


しかし、実際は要件定義には手をつけられないのもよくあること。私はその場合、特段問題が無い限り削除フラグやバックアップテーブルなど「意図しない不要データ」を許容してしまいます。思考停止というより(開発では)QCDに見合わないですからね。運用段階で、運用改善、パフォーマンス改善として予算取りするという政治的解決が待っていますorz

まぁ、ぶっちゃけこういった「時間が無いから」ってケースも多そうですね(苦笑)
でも、後で面倒になりますから、やっぱり時間は割くべきかと。
「自分で自分の首を絞める形になる」前に。


昔アプリ不具合でselect文のwhere句部分が無くなる

これ、SQLインジェクションじゃないですかね。
まぁ、本題と全然関係ないですけど。


http://qiita.com/yuba/items/70165875cfe02b03513d から引用。

アドレスとstatusの組をキーに一意性制約をかけます。削除済みユーザはNULLがキーに含まれるので一意性の判定対象にならないという次第。表技感あふれてますね

そうそう、さっき上記のプログラム書いてた時に、ユニークに出来ないんだがと思っていたんですけど、
で、こういう方法があるよと。
といっても、NULLは「3値論理」がねぇ。
以下の通り、結構ややこしいですし、正直覚えてられん。


codezine.jp


http://dekasasaki.tumblr.com/post/69487259373/%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4%E3%81%8C%E5%A5%AA%E3%81%86%E3%82%82%E3%81%AE から引用。

「あるエンティティ定義に、論理削除有無を設定する属性が定義されている時点で、開発者は『ああ、この表のデータって削除していいんだ』という暗黙の了解に思考を縛られる」

なるほど、こういった心理的副作用は怖いですね。


http://www.infoq.com/jp/news/2009/09/Do-Not-Delete-Data から引用。

注文は削除されません。– キャンセルされるのです。キャンセルが遅かったら、料金が発生するでしょう。
従業員は削除されません。– 解雇されるのです(あるいは退職かもしれません)。 給与体系は頻繁に手を入れなければなりません 。
仕事も削除されません。– 完了されるのです(または、取り消されるかもしれません)。
どんな場合でも、着目すべきはユーザが行いたい作業であり、エンティティその他に対する技術的な処理ではありません。ユーザの作業に着目すれば、ほとんどすべての場合、1つ以上のエンティティを考慮しなければならないはずです。

どう見ても削除じゃないですね、コレ。
だから、「状態があるものを、削除フラグで扱うな」となる。


http://d.hatena.ne.jp/jflute/20121116/1353001525 から引用。

ユニーク制約がないと何が困るかって、
一言でずばり「なんでも入っちゃう」ってところ。
要は「FOO=A, BAR=B, DEL_FLG=false」も、
複数入ってしまう可能性があるってこと

ですねぇ、制約で防げる所は防ぎたいです。


ふとしたときに重複データが入ってしまって、
「一件検索が二件になってエラーになっちゃった」とか

はいはい(苦笑)


削除フラグだけじゃなく、削除日時みたいなカラムを用意して、
有効なデータには固定の "9999/12/31" とか入れて、
削除されたデータには削除した日時をそのまんま入れる。
「FOOとBARと削除日時」でユニーク制約

でも、これ「同じ日(あるいは、同じ時間)」だったらアウトになりますよね。
まぁ、同じ時間(秒まで同じ)はそうそうないのは確かですけど。


削除フラグにはDBコメントが絶対に必須

もし入れるなら、確かに必須レベルかと。


そもそも有効Table作っときゃいいだけなんじゃないですかね?

こんなTableが有ったとして
create table Master
(
id serial primary key,
column1 text,
column2 text,
column3 text
);

こんな感じで生存しているRecordのIDだけを保持する有効Tableを用意します。
create table ActiveMaster
(
  id integer references Master( id ) on delete cascade
);
Masterの検索削除を行う際は、この有効Table使って操作します。例えば削除する場合は、ActiveMasterのRecordだけをdeleteします。


ぱっと見、削除Flagと大差ありませんが、ActiveMasterに対しinsertとdeleteを行うことで追加削除ができ、やや直感的になります。また、検索時に分散Indexなんかを使わなくとも削除した項目を検索対象にしないので高速です。参照制約も活用しやすくなりますよ

これはコメントからですけど、ほほう、またさっきの僕のとは違うアプローチですね。
ただコレ、使い方がちょっと分かりにくい・・・

Masterテーブルには「削除されたレコードも含めた全件」が入っていて、
そのうち、有効なレコード「だけ」が、ActiveMasterに入ってるって事かと思うんですけど。
ちょっと、上記の僕のusersテーブルに合わせると、以下の様になりますかね。


CREATE TABLE `users_test3` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`name` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY (`user_id`)
) engine = InnoDB default charset = utf8;

CREATE TABLE `active_users_test3`
(
`id` bigint unsigned NOT NULL,
`user_id` bigint unsigned NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (user_id),
foreign key(id) references users_test3(id) on delete cascade,
foreign key(user_id) references users_test3(user_id) on delete cascade
) engine = InnoDB default charset = utf8;

CREATE TABLE `user_id_seq` (
`current_user_id` bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`current_user_id`)
) engine = InnoDB default charset = utf8;

INSERT INTO user_id_seq VALUES (0);

う~ん、user_id_seqが余分ですねぇ。
あと、出来るだけサロゲートキーも使いたくないしねぇ。
で、SELECTは、


select * from users_test3 ut where ut.user_id = (select aut.user_id from active_users_test3 aut where aut.user_id = ?);


という感じ(あくまで一例だが)でしょうか。
あと、INSERTは、「両方のテーブルに追加」が必要で、
UPDATEは、「users_test3の方だけ」で、
DELETEは、「active_users_test3の方だけ」でという感じでしょうか。
まぁ、悪くはないですけど、やはり前述のテーブルの方が良い様に思えますね。
いや、もしかしたら、使い方が間違っているのかもしれないですけど。

話を戻して次。


http://d.hatena.ne.jp/ryousanngata/20150329/1427647377 から引用。

1.名前から目的が明確にならない
2.バグの要因の一つ (認識の祖語を生みやすいし、SQLを注意して書かないと実装バグを起こしやすい。)
3.テストするにも論理削除状態のデータを1個置いてテストするようなことをしないといけない

1番目は、だからコメントをって話だと思いますし、
3番目は、なるほどこういう手間も確かにありますね。


ただし論理削除に慣れてきてしまうと、本当に削除していいのか判断ができなくなり、とりあえず論理削除を設計に盛り込むようになる。
他のシステムもやってるしーみたいなノリで入れ始める

確かに、こういったケースで導入してる場合もありそう(苦笑)


SELECT文の条件で意識しないといけない(この操作のときはチェックすべきなのか否か)

あぁ、そうそう、これ以前の仕事中にありましたね。
で、こういう時にメンドイ(というか迷う)と。


ものぐさには2つのテーブルを行ったり来たりする気力はないかもしれない。(自分はそう思います)

う~む、そうなのか?
いや、そこまで面倒じゃないと思うんですけどね・・・


この発想に至るのは「ログは1か所で管理しろ」という点に尽きる。つまり、DBはログを管理する場所ではない。

テキストベースのログとDBの論理削除されたレコードのタイムスタンプ比較して多分このレコードはここの操作にあたるな?とアタリをつけて調べるのは辛い。

ログは大量に出るので時系列で追おうとすると、途端に辛くなりますが、ユーザの識別子(ID)とかをログに出すようにすればgrepすればある程度は追いやすくなるし、最近はJSONにしてDBにぶっこんで調べるというのも実用レベルで聞くし、ログはログでまとめたほうが良いのではと思ってる

あぁ、確かにログは1か所の方が良いと思いますね。
で、ユーザの識別子をログに出してってのは、やってる現場ありましたね。
で、確かに調べやすかった。


http://blog.crossidea.co.jp/2013/501/ から引用。

間違えて登録したデータを完全に削除できないのは困る。
あるデータに関しては通常の運用で完全に削除したい。

あぁ、でもコレは論理削除使ってても、定期的にバッチ回して消すとかで対応可能ですけどね。
ただ、特定のデータだと、絞り込めないですね(delete_timeなら絞り込めるけど)。


情報資産というのはデータの所有者が、それを保有することに価値を見出している情報という意味です。一定の役目を終えたからと言って完全に削除してしまうと、新たなデータの参考にすることすらできなくなります。その場合は論理削除が良いのではないかと思っています

いや、それこそ履歴(削除テーブルの事)に持たせれば良いじゃないですか。


http://makopi23.blog.fc2.com/blog-entry-176.html から引用。

Webアプリとかでは、パフォーマンスの問題を考慮して物理削除を行わない場合がある。
秒間1億のトランザクションを捌くのに、物理削除は難しい。

これ、よく聞く論調の話ではあるんですけど、
上記の計測結果を見るとなぁって感じでもあるんですよね。
いや、勿論条件が全然違うのは分かりますけど、これは誰が言い始めたのかが気にならないでもないですね。


「製品名」が変更になった場合、そのレコードを新規追加し、更新次数を0とする。他のレコードは更新次数をすべて +1 してズラす

う~ん、話としては分かりますけど、ちょっと面倒じゃないですかねコレ。


レコードには「開始日付」と「終了日付」を持たせる。
あるレコードの「終了日付」と、次に新しいレコードの「開始日付」は、一致させること。
日付に連続性を持たせ、Select文のWhere句で "where 開始日付 <= Target and Target < 終了日付"のように指定する。
下限を ≦ にして上限を < にする点がポイント。これによりレコードの連続性を保つ。betweenは使えない。
あるレコードの開始日付と終了日付を同じ値にすることで、そのレコードを論理削除したことと同一になる。

要するに、履歴を管理したいって事だと思いますけど、
それは、削除テーブルのdelete_timeで出来ると思うんですけどね。


http://blogs.wankuma.com/kacchan6/archive/2007/08/21/91190.aspx から引用。

現在の業務アプリはJSOXの絡みもあってDELETEは一切ご法度。
UPDATEもすべてトリガでログするかなどの消える処理はご法度です

JSOX、ほほうまた新たな観点が。
といっても、それも削除テーブルを見れば良いんじゃないかと。


もう使わないマスタデータだけれども過去のデータでマスタ参照しているものもあるので、そういったものは論理削除(削除フラグ=On)

これがちょっとな・・・まだモヤってますね。


http://blog.mogmet.com/ronsakucasual-1/ から引用。

やってみると結構難しい
1個のテーブルだけではなく、お互いのテーブルの関係などを考えると結構大変

履歴テーブルで管理するのが難しいって事ですけど、
確かに最初は、論理削除と比べて手間がかかるとは思いますね。
なので、あまりにも時間がない時(それはそれで、別の問題がある様にも思うので、そっちの改善をした方が良いですが、それでもダメなら)には、
論理削除の方が良いとは思いますね。


絶版商品に削除フラグを付けて、一覧や検索で表示されないようにしたい

ほんとうに欲しかったのもはstatus = :絶版

またしても、フラグでは無かったパターンですね。


退会した会員はログイン出来ないように論理削除したい

そのまま残してstatus = :ログイン不可にする

これも同じですね。
で、どういうマスターかにもよりますけど、マスターもこれで良いのでは。


保管期間の過ぎたデータに削除フラグをたてたい

古いやつを消したいというニーズは新しいものを表示したいというニーズではないのか

これは、以前にポイントの履歴を格納するテーブルがありまして、
「ポイントは半年間のみ有効だが、それ以前は無効になる」なんて時に、
「無効になったポイントは、別のテーブルに移す」なんてのが有効じゃないかというのがありましたね。
勿論、SQLで期間指定する方法もあるけど、
定期的にバッチを実行して、移した方が良いんじゃないかと。


http://ledsun.hatenablog.com/entry/2015/03/27/015203/ から引用。

アプリケーションの作成工数や運用工数が、データ設計工数より低い

これは、「データ設計工数がたっぷり取られていて、散々検討した結果、不要な削除フラグは排除されているので、
そういった中で導入した削除フラグについては仕方ない」のでOKって事ですかねぇ。
まぁでも確かに、要件として含まれていた場合にはとかも考えると、これは良いのではないかと。
ただ、個人的経験ですと、
「むしろ、アプリの作成工数や運用工数の方が、データ設計工数より"圧倒的に"多いよ」って印象で、
でも、そんな現場で削除フラグで導入されていて、これまでに色々書かれていた問題が発生したりしているので、
上記の意見は満更でもないですね。

結論


長々と書いてきましたが、以下の点がポイントなんじゃないかと。


1.要件を整理すること。それは本当に「削除」なのか、「状態」ではないのかとか。


これまで散々書かれていた事で、これがやっぱり一番重要だと思いますね。
で、その上で削除フラグ(フラグじゃなくて時間かもしれないが)導入はアリかと。
あと、ココは工数が足りなくて~もあるかと思うので、
エンジニア以外のメンバーの協力も必要かと。


2.パフォーマンスについては「計測」すべし。


上記の計測の通りですね。
あと、パフォーマンスって別にDB(というかSQL)だけじゃなくて、
Webサーバ・プログラム(FWやORマッパーも含む)・クライアント側のプログラム・スケールアウト・スケールアップ・DBのパラメータチューニングとか、
他にも色々あると思うので、「全体」として見てみたい所ではありますね。


3.オペミスなどからの「データ復旧」は、オペミス発生しづらい仕組み(ダイアログ出すとか)作るなり、復旧機能を作るなり、バックアップから戻せ


手作業はやっぱり大変だと思うので。
バックアップは最終手段にしたいですけどね。


「マスターの一部と大規模Webサービスは削除フラグ導入せざるを得ない?」って所が特にモヤってますけど、
ひとまず、こんな所でしょうか。
個人的には、削除テーブル案をもう少し検証してみたいですけどね。


ご意見・ご感想などありましたら、コメント等でいただければと思います。

追記

記事の途中の速度計測をしている箇所について、
users_test1テーブルを一億件、
delete_usersテーブルを二億件、
users_test2テーブルを三億件にして再計測してみたんですけど、
users_test1が「0.1128752351秒」で、
users_test2が「0.1137793614秒」ということで、
計測前と殆ど変わらなかったという(苦笑)
だから、これはもう速度が遅いという理由で、
論理削除を使う理由はちっとも無い様に思いますね。

Linuxで色々な環境を構築する手順書を書いた件

かなり長くなってしまいましたけど(2600行以上)、

以下に書きました。

 

Linux環境構築手順

 

一番の注目点だと、「本番に上げる設定」ですかね。

皆さん、どうしてるんだろうなぁと。

いや、色々な現場で色々なやり方見てますけどね。

 

といってもコレ、「複数サーバーに対応した設定」とか、

モノによっては「本番サーバーに対応した設定」もありますし、

あと、もう少しコメントを追加したい所もありますね。

まぁ、ゆるゆるとやっていきたいと思います。

 

例によって、何か気になる点ありましたら、ツッコミいただければと思います。