奇特なブログ

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

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

えっと、久々に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環境構築手順

 

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

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

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

 

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

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

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

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

 

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

薄利多売だとスピード重視にならざるを得ないんじゃないの?

結論だけ先に。

1.差別化は、薄利多売でも高単価でも大事(競合をよく調べるべし)

2.薄利多売だと、ブラックになりやすく、また多角化しないと中長期的には厳しい

3.サービスに愛着があると、客観的なジャッジが難しくなり、損切りしにくくなるので注意

4.広告・レコメンド系は、もう少し細かくオプトインの条件を設計すれば良いのでは?

5.倫理観を捨てないと稼げない場合は、モデルがそもそも優秀ではないと思われるので、早目に次のモデルを考えるか、倫理観を捨てても稼げる様に改良していかないと、特に風評被害に注意。

6.倫理観捨てないと稼げないケースの時、逆に消費者にバレると都合が悪いので、教育しない方が都合が良いので注意

 

で、タイトルの続きですけど、なので、まず「ビジネスモデルが薄利多売型なのか、客単価が高い(これをなんと言うのか知らんが、高付加価値ビジネス?)モデルなのかを判断する」が一点目。

判断する為の計算式とかあると良いんですけどね。「すぐ離れやすい・Twitterや2ちゃんで不満が多数(○○はクソとか理由が書かれているかどうかも判断基準)出ている」だと、薄利多売系かなぁとかはボンヤリ。

 

閑話休題

 

二点目。薄利多売型なら多くの事業に小さく手を出していかないと、多くの利益が確保出来なくなり(一つの事業での利益が少ないから)。

 

三点目。多くの利益が確保出来ないから、従業員の給料や余暇に回せず(少ない高スキルの人数で回しているならいけると思うが)、結果ブラック企業っぽくなり、風評被害で印象が悪く(最近だとマクドナルドかな)なったり、過労した従業員が辞めたり(特に、その人に依存している場合は代替コストが凄く高い)、引継ぎコスト(簡単なマニュアルで出来る仕事なら大きな問題にはならないと思うが)が嵩んだりするので中長期的には危ない」って所でしょうかね。

 

だから、以下のGE(100年以上の歴史って頭おかしいですね)みたく、リスクヘッジも考慮して、多角的にやれば良いのにっていう。そうすると、嫌でもサーフィン必須になりますけどね。ただ、サーフィンやったら、間違いなく中途半端になるので、その事業ではあまり稼げないでしょうけど、時流の変化には強いでしょうね。むしろ、その中で、「どれが薄利多売路線から抜け出せそうな事業か」を、見極めるのが重要でしょうと。

 

ゼネラル・エレクトリック - Wikipedia

 

あと、特にBtoCに顕著だと思いますけど、最近の消費者は流行り廃りとか多様性とか、一言で言うと「移り変わりが激しい」ので、むしろ高単価ビジネス(お金が無い人も多いでしょうし)は難しいでしょうねって所が、エンジニア(どう見ても、他の職種に比べたら高単価かと)には不利でしょうね。

 

次に、スピード重視になってしまう理由について。

差別化をしていない場合、値段を安くしたり競合他社に合わせて新機能とかをリリースしていかないと"あっちには有るけど、こっちには無いし、似ていてそんなに魅力も感じないから止めた"となり、利益が減少してしまうので、スピード感を持って事業を進めていくしかない。

 

・・・これ、限りなく「必然」に近い気も。

ココに限らないですけど、特にツッコミ希望はココですね。

ということで、薄利多売だけでいきたい場合は「多角的に事業を行う」。

あと、「多売」なので、当然ユーザー数も増やさないとダメですね。

昨今の、「スピード重視で速くユーザー数を増やして!」がよく分かる話じゃないかと。

逆に、高単価を狙いたい場合は「差別化」じゃあないかと。

あ、でも「市場規模」が小さいので、「低単価だけど差別化(これがまた、鋭すぎましてね)して且つ、他の事業(こっちも鋭かったわ)もやって稼いでいる」会社も最近あったので、あんまり関係ないか。コミュニケーション部分「だけ」引っ掛かりましたけど、この辺の詳細は不明。

・・・「市場規模が大きい場合は、あまり差別化を考えなくても、客が来るから大丈夫」って所でしょうか。まさに今のソシャゲがそうですし。

一応、差別化って話だと、以下の永井孝尚さんの本が良いと思いますね。

 

Amazon.co.jp: 永井 孝尚:作品一覧、著者略歴

 

あと、これは翻訳だけど、個人的には次は以下を読みたいかなと。

 

www.amazon.co.jp

 

あと、別の観点で未来予測(つまり、薄利多売が今後高単価系になっていくとか)もありますね。

・・・ただ未来予測は、以下の本にも少し書いてますけど、基本的には「出来ない(失敗についての予測の方が、出来ているケース多い様に思いますね。某図書館とか。)」様に思いますしね。だって、確か「ざっくり」って書いてましたし。

 

www.amazon.co.jp

 

ここまでで「薄利多売ダメって事?」って思った人、ちょっと待て。

というか、興味があったからクリックしたんでしょうし、「サービスに愛着があると損切り出来なく、結構失敗率も高い」を後で書くので。

 

逆に、高単価(最近は難しいかもしれないが)の場合。

一点目。質について満足した場合(なんだこの店?有り得ないぐらい味が良いし、アンケートで気に入らなかった点を堂々と書いてくださいとか、しつこいぐらいに言ってくるし)、"リピーターになりやすい"(もう、他の店行く気しねえや。病みつきになったので)。

ただ、ここで問題なのが、「質について認識出来るのか?」という部分。

以下の店とか、いきなり食べて分かるのかなぁと。

 

渋谷店の特徴|とんかつと旬のお料理 かつ吉 渋谷店|店舗紹介|かつ吉 菩提樹

 

とんかつは・・・多分20~30店舗ぐらい行ってるのかな。ココがダントツでしたね。ただ、「食べ比べない」と、多分わからないんですよね。そういう意味で、高いので上司が奢る(最近は、すっかり無くなったでしょうね)はアリかと。

という意味で、以下の様な事が広まってほしいなと。

 

kitoku-magic.hatenablog.com

 

ただ、上記って、以下とも根っ子は似てる気もしますね。

といっても、以下の場合、上記と違って「どのくらい教えられるのか」ってのも、あると思いますけどね。その点、上記は簡単ですね(笑)

 

 

ただ、上記の「アンケートで気に入らなかった点を堂々と書いてくださいとか、しつこいぐらいに言ってくるし」っていうのは、基本的には良い姿勢だと思いますけど、日本人は以下の通り「妖精なのでやらない」ので。

 

日本人 - chakuwiki

 

じゃあ、やる人は「大体(全員じゃないのでそこは注意)」は「クレーマー」ですね。

で、やらなかった人は、「居酒屋」とか「Twitter」とか「2ちゃんねる」とか「食べログ(これは適当だが)」とかで愚痴ってますと。

そういう意味で、何とも「商売のしづらい人種」ですね。

・・・妥当なツッコミだったら、飯などを奢るとか、「インセンティブ」を与えれば良いのでは。素晴らしい(笑)あ、勿論この記事も対象で(笑)

そういう意味で、この一つ前のエントリーのコメントは、「合格(上記のとんかつとかぐらいならOK)」ですね。

この辺は、以下の記事の後半を少し応用してみました。

 

モラルある行動は教育で生まれるのではなくシステムで作るもの - ベンチャー役員三界に家なし

 

ただ、「なんで、このツッコミが妥当じゃねえんだよ!」ってのもありそうですし、「なんだ、たったこれだけのサービスかよ!(って、やってもらう方が言うなって話ですけど)」ってのは「道徳」的な話でしょうけど。

 

閑話休題

 

二点目。「質が認識出来た場合」が前提ですけど。

すぐには客はいなくならず(といっても、ずっと何もしなければ、流石にいなくなると思うので、スピード感が無さすぎるのも、また問題)、当面の利益が確保出来る(従業員の数とかにもよるが。あと、最初から黒字なんて、まず有り得ませんのでVCや銀行などからの出資などは重要)ので、無理に残業をする必要がない(利益率がどのくらいかにもよるが)。

 

三点目。無理に残業する必要がないので。

品質を高める為の努力もしやすい(平日夜と休日全部)ので、今よりも更に良いサービスを提供出来る確率が上がり、よって好循環で長続きしやすい。

といってもコレって、従業員の努力もゼロじゃダメ(当たり前だが)ですけどね。

あと、努力が「収益が上がる」努力である必要も。

 

まぁ、以下なんか見ると、残業が少ない(つまり高単価型)方を望んでいる人の方が多いんでしょうね。戦後直後ならともかく、比較的平和で且つ、成熟してる部分多数でしょうし、そういう人の方が大多数かと僕も思いますね。

 

www.lifehacker.jp

 

でも、現状は薄利多売が主流っぽいので、「横に広げる」しか無いでしょうね。リスクヘッジにもなりますし。といっても、「質が認識出来る(大体、オタクやマニアが多いですね。そのジャンルを壊す可能性もありますけど)」も捨てがたいのも確か(未来予測も出来ませんし)なので、「平行にやれば良いんじゃないの?」って思うんですけど、でも、あんまりそれを見ないという(苦笑)

 

あと、「高単価で稼ぎまくっているよ」で注意なのが、「確かに稼いではいるが、倫理観が怪しい」ケース。広告とレコメンド系が共通してますね。

で、この場合、「学ばない→理解出来ない→認識出来ない」で気付けないので、むしろ「消費者が学ばない方が稼げる」という(苦笑)

「倫理観をちゃんとしたら、あまり稼げなくなった」は、要するにモデルがその程度って事だと思いますけどね。なので、これも「横に広げる」か、「高単価で差別化が図れないか」検証するしかないですね。

といっても、完全に新規のビジネスモデルの場合は、しばらくは大丈夫でしょうけど、徐々に怪しくなっていくというのはあるでしょうね。

といっても、レコメンド系に関して(といっても広告も、そう変わらないか)は、「デフォルトはオプトアウトだが、ジャンル別(技術書はオプトインだが、生活用品はオプトアウトとか)にチェックボックスを分けてチェックしてもらうとか、基本観測外のジャンルの情報を、"毎日一回"・"週に一回"・"月に一回"毎に提供する」とか。あと、情報提供「する」情報と、「受け取りたい」情報も別にして。

要するに、「個人によって、どの辺がプライバシーと感じるかが違う」というのに対応と。

・・・一回、ネットリサーチ会社利用して、アンケート取ってみれば良いんじゃないかと思うんですねコレ。

個人的には、エロ系が分かりやすいですけど、情報提供「しない(基本的に勘違いされるので)」けど、でも情報は「受け取りたい」ので(笑)

というのがあるので、「きめ細かく」すると。

あと、以下の記事、最近読んで「最初はしっくりきた」んですけど、

その後、某所(ってFBだが)で議論した結果、

「どう頑張って設計したり実装しても、漏れたら個人と紐付くと思うんだが?」って結論になった次第でして、「統計処理」の詳細を知りたいなと。

 

海老澤澄夫の個人ブログ: レコメンドはプライバシーを侵害するか? 高木浩光氏への異論

 

なので、やるのは良いけど、前提として「オプトイン・オプトアウトは付けてね」っていう。

・・・リサーチ結果を、つくづく見たい所ですねココは。

 

あと、ここまで書いたので、いい加減、経営本を読みたい所ですね。

基本的に、「暴走を止めたり、スピードを若干緩めたりなどをするのが役割」でしょうし。

で、以下の本を読む限り、多少は「読む本も絞り込めそう」なので、

そうしましょうか。大体読んでいる暇がない(苦笑)

 

www.amazon.co.jp

 

次。

「薄利多売でも高単価でも良いけど、事業が失敗した(もしくはしそう)になったらどうすんだ?」は、「止めよう」。

で、ココで問題になるのが、サービスに愛着があると損切り出来なく、結構失敗率も高い」かなと。

以下に書いてますね。

 

d.hatena.ne.jp

 

でも無理ですよねぇ、大好きなもの止めるって。

これ逆に極論言っちゃえば、「俺、この商売"大っ嫌い"なんだけどさ、でも"有り得ない"ぐらい稼げるから、"仕方なく"やってるんだよね」が良いんでしょうけど、これは(精神力的に)難しい。

「愛着と客観性」がポイントだと思うんですけど、

「好きでやっている」のは良いけど、その時に「冷静な目でジャッジする」のも同時に行うと。

上記の、スーパージェネラリストの本の中でいう、「もう一人が、遠くから冷静に見ている」的な話ですね。かなりオカルトっぽいですけど(苦笑)

あと、もう一つの方法として、「あまりにも愛着があり過ぎて、結果として他人に全く理解されず、"どうして僕の事を誰も理解してくれないんだ!"と中二病っぽくなるが、しばらくして、何でなんだろう?ちょっと調べたり聞いたりしてみるかとなり、冷静に他人の感想などを聞ける」ケース。

分野によっては、僕はこっちですね(苦笑)

だから愛着も「有り得ないぐらいある」か、

もしくは「好きでいて且つ、客観的に見れる」なら、

損切りも出来るかなと。

 

 

最後に、今後の課題を書いて終わりにしましょう。

 

一つ目。中単価ビジネスって無いのかね?っていう。・・・これ、どっちかというと、BtoCよりBtoBな感がありますね。

 

二つ目。

「エンジニアって高単価な場合多いけど、でも薄利多売ビジネスで且つ、多角的に事業を展開してないと、一人当たりの手取りって減るし、相性悪いよね」

これは困った・・・が疲れたので、今日はココで終了にしましょう。

SMTPプロトコルで直接サーバーと対話するメール送信ライブラリをPHPで書いた件

GitHub対象ファイル(以下のmail~で始まる4つのファイル)

github.com

 

えっと、まずこの話は、実は2010年頃に社内案件(というのとは、また違うか)でこのプログラムを書いていた(ただ、業務では結局使っていない)んですけど、その後すっかり忘れまして(苦笑)

ただ今年ふと、自分の外付けHDDに入っているプログラム(許可は勿論取りましたが)を見ていたら、このプログラムがありまして、もうちょっとで出来上がるし書いてみるかという。

 

そもそも、「何でPHPにmail関数有るのに、こんな事をするの?」という点についてですけど、以下の通り、関数の戻り値がTRUE or FALSEしか無いんですね。

 

PHP: mail - Manual

 

あと、mb_send_mailやimap_mail関数でも一緒みたいですし。

PEARもTRUEかPEAR_ERRORの様ですし。

と思ったら、「PHPMailer」というライブラリがあり、

そこにはErrorInfoという関数があるみたいですけどね(苦笑)

どういうエラー情報取れるかってのがありますけど。

ちなみに、このライブラリだと、以下の様なメッセージ(これは成功時ですけど)です。

 

CONNECTED: 220 smtp.gmail.com ESMTP un2sm9860205pac.28 - gsmtp
EHLO/HELO: 250-smtp.gmail.com at your service, [{IPアドレス}]
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN XOAUTH
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8
AUTH LOGIN: 334 {ユーザー名を暗号化とかハッシュ化とかエンコードとかした値?}
USERNAME: 334 {パスワードを暗号化とかハッシュ化とかエンコードとかした値?}
PASSWORD: 235 2.7.0 Accepted
MAIL FROM: 250 2.1.0 OK un2sm9860205pac.28 - gsmtp
RCPT TO: 250 2.1.5 OK un2sm9860205pac.28 - gsmtp
DATA: 354 Go ahead un2sm9860205pac.28 - gsmtp
CONTENTS: 250 2.0.0 OK 1441867397 un2sm9860205pac.28 - gsmtp
QUIT: 221 2.0.0 closing connection un2sm9860205pac.28 - gsmtp

 

あと、このライブラリ、フレームワークの中に入ってますけど、他のファイルに依存していない作りにしてますので、単体で使えます。

 

といってもまぁ、この辺

http://www.atmarkit.co.jp/fnetwork/rensai/netpro03/mail-header.html のヘッダーとか大部分は現状では対応してないですし、

あと、書いた後に気付いたけど、ezwebが送信元で認証(宛先が携帯は分かるんですけど)ってとかもありますし、

あと、コード的に、以下の書き方が大変に気に食わないというのもありますし。

 

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

 

どうも、以下と同じ話だと思い、色々やってみたんですけど、どうもダメでしてね(そういえば、以下のコードをそのまま実行してなかった)。

 

PHPでの対話的なソケット通信についてのメモ | ブログ :: Web notes.log

 

あと、メールといえば、メールヘッダインジェクションがありますけど、

以下を見て、「ヘッダー部分に改行があったらエラー、ヌルバイトは空白に置換」としました。

 

メール送信のセキュリティ | yohgaki's blog

 

ただ、呼び出し情報全て改行だと、本文の改行も改行も無くなってしまうので、

本文は何もしていないです。

あと、メアドのチェックも一緒で、この辺はアプリによって変わるのではと思いましたので、呼び出し側で考えてくれの方が良いんじゃないかと。

 

使い方のイメージとしては、「会員登録後のメール送信」や「お問い合わせでのメール送信」辺りを想定しています。

僕個人は今、本番サーバーのアクセスパスとアクセス数をまとめた情報を自分のメアド宛に送ってますけど、これは何となく思っただけなので、あまり意味はないかと。

あと、大量のメール配信だと、以下の様なサービスじゃないと、配信するサーバーの問題もあるだろうし、現状では難しいかもしれないですね。

 

メルマガ・メール配信ASP|アクセスメール

 

あと、これはまだ本番では対応していないですけど、「メール送信の結果(要するに上記にあったログ情報)をcronに渡して、そこからPostfixとmailコマンド使って、その情報をまた送る」というのも予定しています。

「失敗(成功の時でも来ますけど、これは用途によると思われ)した時の情報が自動でメールで来たら良いでしょ」と思いまして。夜中にその対応ってのは、「お金を出していただければ良い」と思いますし。ただ、Postfixがかなり時間かかりそうですけどね。

 

そんな所です。

あと、他にもソース見て気付いた点がありましたら、突っ込んでいただければと思います。