DBのインデックスについて

データを高速に検索するにはどうすればよいか

DBからデータを取得する場合、通常順次検索して条件に合ったものを取ってくる。 順次検索するより、索引を作成して、索引を基準とした検索方法の方が検索スピードが速くなる。 つまりDBにもインデックスを張るカラムを指定する事と、索引の格納している位置を明示する事で、 順次検索するよりも、素早く対象の値を取得する事ができる。

インデックスを張る事で検索スピードが上がるDBの特徴

  • データが大量に格納されている。 少量のデータだと順次検索したほうが早い場合がある。

  • 値が重複していない。 インデックスを張るカラムに格納されているデータが重複していると、索引を付ける箇所が少なく、効果が薄い

データ構造

データの特性や検索の仕方によって、インデックスのデータ構造は変わってくる。

  • B木 ツリー構造をしていて、根から検索を開始して、子ノードを条件によって分岐してデータを求める構造 BETWEEN等を用いた範囲指定検索において有効です

  • B+木 B木と同じツリー構造をしているが、データの持ち方が異なり、データはリーフノードに持つ。 B+木の場合、最後のリーフノードまで検索しないと、データを取得できないので、B+木の2分木の特徴を持ちながら、順次検査をしていく形を取ります。 ANDやOR操作だけで行う検索やNOTを用いた否定検索において有効

B木とB+木の違い B TreeとB+ Treeの違い - Carpe Diem

  • ビットマップインデックス ビットマップを使った検索方法で、値とレコード番号(値を特定する値)を図表にして求める値を特定していく。 格納されている値のパターンが少ない場合に有効 OR検索にも有効

↓わかりやすい記事 ビットマップインデックスの仕組み - Qiita

  • ハッシュインデックス Hash関数を用意して検索時にその関数を通して検索する。 例えば、値を100で割ったときの余り返す関数をHash関数とした場合、ID=234のHash値は34になり、この34に対応する位置にHash値とTIDを格納します。

第2回「Hashインデックス」 | NTTデータ先端技術株式会社

参考サイト データベース性能を向上させる「インデックス」を理解する:「データベーススペシャリスト試験」戦略的学習のススメ(26) - @IT

リーダブルコード1章2章

1章

コードが読みやすいとは、他人がコードを見たときに理解しやすく、理解するまでの時間が短い

2章

名前を付けるという事は短いコメントを書くような意識を持ち、名前に情報を埋め込んでいく

  • 明確な単語を選ぶ

  • 例えば汎用的な単語を使う場合、getだとどこからgetするデータかわからないし、sizeはどんな量、数なのかわからないので、汎用的な単語は使わないようにする。

  • 他にもrtnvalなどに関数の返り値用の変数名を用意するより、どんな変数なのか目的や値を明確に情報を表示したほうが良い。

  • ただ、本当に一時的なデータを取り扱うという意味の場合、tmpなど汎用的な方がわかりやすい場合もある。

  • 基本的には短い名前が理想だが、スコープが長い場合は情報も多いので、明確に名前に情報を持たせる。

  • 名前は短いので無理に詰め込まないようにする。1つで無理ならば、2つに分割できないか考える。

プログラマー転職時に聞かれた技術的な質問

一覧

メモリリーク

メモリリーク(memory leak)とは - IT用語辞典 e-Words

プログラムを実行するためにメモリ領域を確保する必要がある。プログラム処理終了後に領域を解放する必要があるが、 解放するのを忘れて、使えるメモリ領域が少ない状態になるバグ。

デッドロック

複数のプロセスが互いに相手の占有している資源の解放を待ってしまい、処理が停止してしまうこと。データベースの排他制御の不備などが原因で起こる。

2つのプログラムA,Bがある。またa、bの2種類のデータがあるとする。 ABどちらもトランザクションをかけていて、一連の処理が終了するまで、データを確保する。 Aはまずaのデータを処理する。並行して、Bはbのデータを処理する。 続いて、Aはaの次にbのデータを処理しようとするが、Bが確保しているので処理できない。 また、Bもbの次にaのデータを処理しようとするが、Aが確保しているので処理できない。 こうして、AもBも実行中の状態になり処理が終わらず停止してしまう。

MySQLのデッドロック対処 おまけでギャップロック│システムガーディアン株式会社

  • N+1問題 N+1問題とは、データベースからデータを取り出す際に、大量のSQLが実行されて動作が重くなるという問題

N+1問題について - 理解するブログ

プロセスとスレッド

プログラムを実行する単位をプロセスという。1つのプロセスの中には1つ、もしくは複数のスレッドが含まれており、このスレッドが実際にCPUで実行される単位となる。

第3回 プロセスとスレッド:Windows OS入門(1/2 ページ) - @IT

スタックとキュー

データ構造の一つで、保管したデータをどのように取り出すのかを考える。 スタックは最後に追加したデータを最初に取り出すように扱う。例えばWebブラウザの訪問履歴 キューは最初に追加したデータを最初に取り出すように扱う。例えば航空券予約キャンセル待ち スタックとキューを極める! 〜 考え方と使い所を特集 〜 - Qiita

Webアプリのセキュリティ

Webアプリを構築する上でどのようなセキュリティに気を付けておくべきかを複数挙げる。

SQLインジェクション

formなどに悪意のあるSQLを入力して、データベースを改ざんやユーザー情報を取得する攻撃のこと 例えば

SELECT * FROM books WHERE auther = 'O'Reilly'

このSQL文は「'」で囲えてないので、「Reilly'」の代わりに悪意のあるSQLを挿入すると実行されてしまう。 なので、以下のようにプレースホルダを組み立てて、SQL文の変更を防ぐなどがある。

SELECT * FROM books WHERE auther = ? 
クロスサイト・リクエストフォージェリCSRF

攻撃用のサイトをユーザーがアクセスする。 そのサイトに別サイトへの攻撃用のスクリプトを用意していて、ユーザーのログイン状態を利用して、 別サイトで不正操作される

bit演算

bitとはコンピューターが扱う最小単位で、0、1のどちらかの値が入る情報のこと。 bit演算はそのbitを使用する計算方法。フラグ管理などで使われる考え方。

例えば 各桁の値をそれぞれ論理演算で比較して、2進数を10進数に表示する 45 OR 25 = 0b111101 = 61 となる。 45 = 0b101101 25 = 0b011001 0b111101 = 1(2**5) + 1(24) + 1*(23) + 1(2**2) + 1(2**0) = 32+16+8+4+1=61

論理演算 

AND演算
入力された2つの値のうち、「真」かつ「真」(「1・1」)であれば「1」となるが、それ以外の「1・0」「0・1」「0・0」の場合はすべて「0」となる。

OR演算
二つの入力のいずか一方あるいは両方が1のとき出力が1となり、いずれも0の場合に0となる。

もっと詳しい事は以下のサイト参考

ビット演算 (bit 演算) の使い方を総特集! 〜 マスクビットから bit DP まで 〜 - Qiita

オブジェクト指向の重要な概念3つ

継承、ポリモーフィズムカプセル化

継承

あるクラスが別のクラスの性質を受け継いでいること。 親クラスからサブクラスへとメソッドやプロパティなどが引き継がれる事。

継承(インヘリタンス)とは - IT用語辞典 e-Words

ポリモーフィズム

関数作って、その関数に引数、返り値、データの型などを用意して、これらに異なる値を入れても使い分けができる事。 関数の中に入るデータが別でも、違う処理を行える事。

ポリモーフィズム(多相性)とは - IT用語辞典 e-Words

カプセル化

できるだけ他のプログラムからアクセスされない仕組みのこと オブジェクト指向で設計をすると、クラスからインスタンスを生成して、データを処理するという仕組みを考える。 クラスのフィールドには【private】アクセス修飾子を付与して、他のプログラムからのクラスの設定をいじられないように守るように設計したほうが、わかりやすくバグが出にくくなる。

カプセル化とはなにか?超わかりやすく解説します! | Webpia

オブジェクト指向と10年戦ってわかったこと - Qiita

find_or_initialize_byを使ってみる

要約

find_or_initialize_byはRailsのメソッドで、同時にfind_byとnewを行ってくれる。

find_or_initialize_by | Railsドキュメント

モデル.find_or_initialize_by(条件)

find_byで検索してない場合、条件に入れた値を基に新しいインスタンスが生成される。 createではなくnewなので、saveしないと保存されない。

Userモデルにid=1のレコードが一個入ってるテーブルで以下実行してみた。

レコードが存在する場合:idで検索
irb(main):001:0> new_user = User.find_or_initialize_by(id:1)
  User Load (7.0ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
irb(main):002:0> new_user
=> #<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">

レコードが存在する場合:id以外で検索
irb(main):011:0> new_user = User.find_or_initialize_by(uid:'test@example.com')
  User Load (2.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`uid` = 'test@example.com' LIMIT 1
irb(main):012:0> new_user
=> #<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">


レコードが存在しない場合:idで検索
irb(main):003:0> new_user = User.find_or_initialize_by(id:2)
  User Load (2.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
irb(main):004:0> new_user
=> #<User id: 2, provider: "email", uid: "", allow_password_change: false, name: nil, nickname: nil, image: nil, email: nil, created_at: nil, updated_at: nil>

レコードが存在しない場合saveする。validationが走る。
irb(main):005:0> new_user.save!
   (1.3ms)  BEGIN
  User Exists? (3.7ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` IS NULL AND `users`.`provider` = 'email' LIMIT 1
   (1.3ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):5
ActiveRecord::RecordInvalid (Validation failed: Password can't be blank, Email can't be blank)


レコードが存在しない場合:id以外で検索。検索条件を代入してインスタンス生成される。
irb(main):009:0> new_user = User.find_or_initialize_by(email:'test@example.com')
  User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE `users`.`email` = 'test@example.com' LIMIT 1
irb(main):010:0> new_user
=> #<User id: nil, provider: "email", uid: "", allow_password_change: false, name: nil, nickname: nil, image: nil, email: "test@example.com", created_at: nil, updated_at: nil>

Railsのfind、where、find_byの違い

要約

findはidを基に検索する。値がない場合、エラーを吐く

find_byはカラムを指定して検索する。値がない場合、nilを吐く

whereは条件を指定して検索する。値がない場合は#<ActiveRecord::Relation []>を吐く

find

findはidを基に検索する。値がない場合、エラーを吐く 例

irb(main):001:0> User.find(1)
  User Load (4.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">
irb(main):002:0> User.find(9)
  User Load (0.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 9 LIMIT 1
Traceback (most recent call last):
        1: from (irb):2
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=9)

find_by

find_byはカラムを指定して検索する。値がない場合、nilを吐く 例

irb(main):003:0> User.find_by(id:1)
  User Load (56.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">
irb(main):004:0> User.find_by(id:2)
  User Load (2.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
=> nil
irb(main):005:0> User.find_by(id:2).name
  User Load (0.8ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
Traceback (most recent call last):
        1: from (irb):5
NoMethodError (undefined method `name' for nil:NilClass)

where

whereは条件を指定して検索する。値がない場合は空の配列#<ActiveRecord::Relation []>を吐く

irb(main):006:0> User.where(id:2)
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 11
=> #<ActiveRecord::Relation []>
irb(main):007:0> User.where(id:1)
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">]>
irb(main):008:0> User.where(id:1).id
Traceback (most recent call last):
        1: from (irb):8
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 11
NoMethodError (undefined method `id' for #<User::ActiveRecord_Relation:0x0000561efed5dcc0>)
Did you mean?  ids
irb(main):009:0> User.where(id:1).first
  User Load (2.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
=> #<User id: 1, provider: "email", uid: "test@example.com", allow_password_change: false, name: "テストユーザー", nickname: nil, image: nil, email: "test-user+1@example.com", created_at: "2021-12-12 18:16:56", updated_at: "2021-12-14 08:00:35">
irb(main):010:0> User.where(id:1).first.id
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
=> 1

N+1問題について

要約

関連テーブルからデータを取得するときに、結合せずに、1つづつデータを取ると、負荷がかかる。

1つづつデータを取る場合とは、まず親となるテーブルから対象のデータ(N個)を取得するSQLが1回、 子テーブルの外部キーを条件にデータを取得するSQLがN回、合計1+N回SQLを発行する事になる。 このやり方だと負担がかかる。 対応策は必要なレコードはまとめて取得することを意識する。

データを取得する状況を考える

例えば、CompaniesテーブルとUsersテーブルがあって、 CompaniesはたくさんのUserを持っている状態とする。 各Companiesに紐づくUserのデータを表示したい場合を考える。

N+1の取り方

Comapaniesを取ってくる

Comapany.all

次に取ってきたCompaniesをeachで回してUsersの外部キーを条件に名前を取得する

Company.all.each do |c|
    puts User.find_by(company_id:c.id)
end

そうすると以下のようにSQLが複数発行される

# usersテーブルから取得(1回目)
SELECT  `users`.* FROM `users` WHERE `users`.id = 1 LIMIT 1
# usersテーブルから取得(2回目)
SELECT  `users`.* FROM `users` WHERE `users`.id = 2 LIMIT 1
# usersテーブルから取得(3回目)
SELECT  `users`.* FROM `users` WHERE `users`.id = 3 LIMIT 1

全体のCompanyが1回、NこのCompanyでN回、合計1+N回SQLが発行される。

対応策

テーブルを結合させて、まとめて取得する

Rails における内部結合、外部結合まとめ - Qiita

例えば、joinsを使うと以下のようになる。

Comapny.joins(:user)

githubにログインできなかったときにやったこと

githubの設定時

ローカルからgithubにユーザー名とパスワードでHTTPでログインしようとしたが、できなかった。 アクセストークンを作成して、パスワードの代わりにこれを代入したらできた。 やり方↓

githubでfatal: Authentication failedが出て急にpush,pullできなくなった

githubへpushするたびにログインしなくて済むようにする

pushとかpullすると、上記の状態だと毎回ユーザー名とログイン名を入れる必要がある。 自動的に通信するためにはssh接続を設定する。

やり方↓

GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiita

余談

WindowsでWSL上で開発していて、WSL上にソースコードを保存している。 SourceTreeで管理したかったが、Windows側とWSL側との間のアクセスが悪いらしく、 かなり重たくて使い物にならなかった。いい解決策は思いつかなかった。

同じような事で悩んでるっぽい人↓

docker - Why is Git on WSL2 with SouceTree so slow for me? - Stack Overflow