20190405のSQLに関する記事は3件です。

10以上の文字列のORDER BYは気をつけないとダメ

数値でソートするカラムが数値型なら問題ないですが、
文字列型の場合のソートは挙動が変わります。

数値型の場合
1,2,3,4,5,6,7,8,9,10,11
文字列型の場合
1,10,11,2,3,4,5,6,7,8,9

原因としては文字列で見てしまうので最初の文字が「1」なので「1」「10」「11」が「2」より優先されてしまいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Django]管理画面の一覧表示部分に別テーブルのフィールドを表示する

はじめに

「[Django]管理画面の一覧表示部分に、(表示対象のテーブルで外部キーなどでの関連もさせていない)別テーブルのフィールドを表示する」

最終的に結構単純でしたが、同じようなことをしている人が少なくて割と悩んだのでメモ。
多分ドキュメントに丁寧に目を通した人にとっては当たり前のことかも。

やりたいこと

上の画面を下のような表示にしたかったのですが、この時、DBの構成は下記のER図のようになっており、日時系の値とそれ以外の値は別DBのフィールドのため、単純にlist_displayで追加することができなかった。

[やりたい画面レイアウト]
図1.png

[ER図]
(記事用に適当に書き換えたので、ER図と下の方のコードの整合性保ってない部分もあるかも)
スクリーンショット 2019-04-05 14.14.53.png

要点

特に重要なのは下記の部分ですが、一応全貌を載せておきます。

重要部分1
    def plan_start_datetime(self, obj):
        list_got_objects = list(Datetime.objects.all().values_list('plan_start_datetime', flat=True))

        return list_got_objects

※1:取得したい情報の条件(例:「id=1」など)

重要部分2
    list_display = (
                    'plan_start_datetime',
                   )
要点1.

<テーブル名>.objects.all() の戻り値はQuerySetなので、values_list() で値を取得する必要がある。

要点2.

values_list() で取得した値をスライスして取得した「文字列」をlist_display に渡す。

以下、全貌

models.py
# coding: utf-8
from django.db import models
from django.utils import timezone
from django.core.validators import MaxValueValidator


# コードがforeign keyとして使用されるため、このクラスを先に定義している
class M_branch(models.Model):

    def __str__(self):
        return self.name

    code = models.CharField(max_length=4, primary_key=True)
    name = models.CharField(max_length=128)

# コードがforeign keyとして使用されるため、このクラスを先に定義している
class M_workplace(models.Model):

    def __str__(self):
        return self.name

    branch_code = models.ForeignKey(M_branch, on_delete=models.CASCADE)
    code = models.CharField(max_length=4)
    name = models.CharField(max_length=128)


class Report(models.Model):

    def __str__(self):
        return self.application_id

    application_id = models.CharField(max_length=32, primary_key=True)
    branch_code = models.ForeignKey(M_branch, on_delete=models.DO_NOTHING)
    workplace_code = models.ForeignKey(M_workplace, on_delete=models.DO_NOTHING)


class Datetime(models.Model):

    def __str__(self):
        return self.plan_start_datetime

    application_id = models.OneToOneField(Report, on_delete=models.CASCADE)
    plan_start_datetime = models.DateTimeField()
    plan_end_datetime = models.DateTimeField()
    actual_start_datetime = models.DateTimeField()
    actual_end_datetime = models.DateTimeField()

admin.py
from django.contrib import admin

from .models import Report, Datetime, Participant, M_branch, M_workplace


# TODO DatetimeやParticipantテーブルから必要な情報を取得して同じ表に表示する
class ReportAdmin(admin.ModelAdmin):
    fieldsets = [
        ('申請ID', {'fields': ['application_id']}),
        ('支部コード', {'fields': ['branch_code']}),
        ('職場コード', {'fields': ['workplace_code']}),
    ]
    list_display = ('application_id',
                    'branch_code',
                    'workplace_code',
                    'plan_start_datetime',
                    'plan_end_datetime',
                    'actual_start_datetime',
                    'actual_end_datetime',
                    )
    def plan_start_datetime(self, obj):
        list_got_objects = list(Datetime.objects.all().values_list('plan_start_datetime', flat=True))
        return plan_start_datetime
    plan_start_datetime.short_description = 'plan_start_datetime'

    def plan_end_datetime(self, obj):
        list_got_objects = list(Datetime.objects.all().values_list('plan_end_datetime', flat=True))
        return plan_end_datetime
    plan_end_datetime.short_description = 'plan_end_datetime'

    def actual_start_datetime(self, obj):
        list_got_objects = list(Datetime.objects.all().values_list('actual_start_datetime', flat=True))
        return actual_start_datetime
    actual_start_datetime.short_description = 'actual_start_datetime'

    def actual_end_datetime(self, obj):
        list_got_objects = list(Datetime.objects.all().values_list('actual_end_datetime', flat=True))
        return actual_end_datetime
    actual_end_datetime.short_description = 'actual_end_datetime'


class M_branchAdmin(admin.ModelAdmin):
    fieldsets = [
        ('支部コード', {'fields': ['code']}),
        ('支部名', {'fields': ['name']}),
    ]
    list_display = ('name', 'code')


class M_workplaceAdmin(admin.ModelAdmin):
    fieldsets = [
        ('支部コード', {'fields': ['branch_code']}),
        ('職場コード', {'fields': ['code']}),
        ('職場名',    {'fields': ['name']}),
    ]
    list_display = ('name', 'code', 'branch_code')

admin.site.register(Report, ReportAdmin)
admin.site.register(M_branch, M_branchAdmin)
admin.site.register(M_workplace, M_workplaceAdmin)

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLのInnoDBのトランザクション周りについて調べたことをまとめた

元々NoSQLしか使用していなく、今年に入って転職して初めて業務でMySQLを使用しだした。
雰囲気で使ってる感じがしてよくないなと思ったので、色々調べた。

今回まとめたのは、MySQLのInnoDBのトランザクションについての一部。
この記事でまとめる対象の事柄は、既に詳しく素晴らしい記事が存在するが、それなりに難しい内容なのでそれらを読みながら理解の補助的に、またメモ的にまとめたものが本記事。

また、「この概念を理解するには、事前に〇〇の概念の理解が必要で、それを理解するにはこの記事がわかりやすい」といった形で自分が後から振り返られるようにもまとめた。

この記事はあくまでも理解の補助と、どの概要を学んだ方が良いかということをまとめた記事なので、詳細な理解は紹介している記事を読んだ方が良い。

トランザクションの分離レベル

トランザクションにはACID属性というものがあって、その中のI(Isolation)が分離性と呼ばれるもの。

トランザクションの分離にはレベルが存在する。
詳細は以下の記事等を参照。

大事なのは、どの分離レベルにおいて、どんな不都合な読み込みが行われるかを認識することだと思う。

トランザクション分離レベル - Wikipedia

[RDBMS][SQL]トランザクション分離レベルについて極力分かりやすく解説 - Qiita

MySQLのInnoDBの Repeatable Read

MySQLのデフォルトのエンジンであるInnoDBのデフォルトのトランザクションの分離レベルはRepeatable Readである。

一般的に、Repeatable Readは、ダーティリードとファジーリードは防ぐことができるけれども、ファントムリードは防ぐことができない。

ファントムリードは、以下のようだとある。

別のトランザクションで挿入されたデータが見えることにより、一貫性がなくなる現象。

引用元: [RDBMS][SQL]トランザクション分離レベルについて極力分かりやすく解説 - Qiita

一般的なRepeatable Readの場合のシミュレーションすると以下のような感じ(実際にやったわけではない)

事前に以下のような操作を行なっているとする。

CREATE TABLE programming_langs (
    id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
    name VARCHAR(20)
);

show tables;

+-------------------+
| Tables_in_sample  |
+-------------------+
| programming_langs |
+-------------------+

INSERT INTO programming_langs (name) VALUES ('Go');

Repeatable Readの場合のトランザクションのシミュレーション

phantom.png

上記のように、左側のトランザクションがInsertしたデータを右側のトランザクションが読み込めてしまう。(ファントムリード)

しかし、MySQLの InnoDB は、 Repeatable Read であっても、別のトランザクションで挿入されたデータをうまいことやりくりしており、一貫性があるように見える。

実際にやってみた。

事前に以下のような操作を行なっているとする。

CREATE TABLE programming_langs (
    id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
    name VARCHAR(20)
);

show tables;

+-------------------+
| Tables_in_sample  |
+-------------------+
| programming_langs |
+-------------------+

INSERT INTO programming_langs (name) VALUES ('Go');

Repeatable ReadのMySQL InnoDBでの操作を図にしたもの
(実際にMySQLでやってみたが、見易さを考慮して図にしている)

mvcc.png

ファントムリードが生じていないように見える。

これはなぜだろうか。実は、MVCC(MultiVersion Concurrency Control)という仕組みを用いているからである。

MVCC

MySQLのドキュメントによれば

Acronym for “multiversion concurrency control”. This technique lets InnoDB transactions with certain isolation levels perform consistent read operations; that is, to query rows that are being updated by other transactions, and see the values from before those updates occurred. This is a powerful technique to increase concurrency, by allowing queries to proceed without waiting due to locks held by the other transactions.

This technique is not universal in the database world. Some other database products, and some other MySQL storage engines, do not support it.

引用元: MySQLのドキュメント

上記を意訳しつつ、まとめると以下のような感じ。

  • MVCCを使用すると、特定のトランザクションの分離レベルのInnoDBで、一貫性のある読み取り操作が可能に
    • 他のトランザクションが更新した行を照会する際に、更新前の状態を見ることができる
  • 他のトランザクションのロックを待たずに、クエリを進めることができるのでMVCCだと、同時実行性を高めることができる

他のトランザクションが更新した行を照会する際に、更新前の状態を見ることができる

これがまさにMySQLのInnoDBではRepeatable Readであっても別のトランザクションで挿入されたデータをうまいことやりくりして、一貫性があるように見える(ファントムリードが起きない)ことに寄与している。

これはファントムリードでは、トランザクション内で挿入したデータを別のトランザクション内で見えてしまうが、それを回避することができるということだ。

MVCCの仕組みの説明は、以下がわかりやすいのでこちらを参照。
MySQLのMVCC - Qiita

公式でも詳しく仕組みが記されている。

MVCCが故の注意

行ロックをすることなく一貫性を出せるのと、複数のトランザクションが同一のテーブルを操作するとき、お互いのロックを待つことなく操作できるので、同時実行性が上がる。

しかし、行ロックをしないが故に生じ得るLost Updateという問題も存在する。
以下に詳しく記述されているので参照。
漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点

詳しくは上記の記事を参照した方が良いが、記事を読んだ際に頭を整理するためにこれまで書いてきた内容と合わせて、以下のようなメモをしながら読んだので載せておく。

  • Lost Update
    • MVCCは、行ロックをとるわけではないので2つのトランザクションが重なったときには、後で更新した方の値が最終的に反映される
  • Locking Read
    • Lost Updateを防ぐためには、Locking Readを使用する
      • Lost Update は行にロックをかけていないから生じるわけで、それならば行をロックすればええやんという感じ
        • 排他ロックと共有ロックを行える構文が存在する
          • 排他ロックと共有ロックはこの記事がわかりやすい
      • 逆説的な話だが、MVCCでRepeatable Readでもファントムリードを防げているのは、MVCCによって、DB_ROLL_PTRの値を見ているからで、Locking Readの場合はそうでなくファントムリードが生じる

DB_ROLL_PTR ... そのレコードの過去の値を持つundo log recordへのポインタ

引用元: MySQLのMVCC - Qiita

ロックの仕組み

MySQLのロックにはレコードロック、ギャップロック、ネクストキーロックの3つがある。
以下の記事が非常にわかりやすいので、参照。

MySQL(InnoDB)のネクストキーロックの仕組みと範囲を図解する - 備忘録の裏のチラシ

ただ、「レコードロックっていうのは実際は、インデックスレコードのロックだよ」みたいな感じでインデックスの知識が必要。
ここで必要なインデックスの知識も、ただクエリの性能向上のための便利なものという理解ではなく内部の仕組みの理解が必要みたいだ。(特にクラスタインデックスとセカンダリインデックス)

なので、上記のロックの記事を読む前に以下の記事等でインデックスの仕組みについて学んでからの方が良さそう。

インデックスの記事
漢(オトコ)のコンピュータ道: 知って得するInnoDBセカンダリインデックス活用術!

INDEX FULL SCANを狙う

MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.13.2 クラスタインデックスとセカンダリインデックス

MySQL with InnoDB のインデックスの基礎知識とありがちな間違い - クックパッド開発者ブログ

MySQL(InnoDB)のインデックスについての備忘録 - What is it, naokirin?

参考にさせていただいたサイト

ACID属性

ACID (コンピュータ科学) - Wikipedia

トランザクション分離レベル

[RDBMS][SQL]トランザクション分離レベルについて極力分かりやすく解説 - Qiita

トランザクション分離レベル - Wikipedia

MVCC

MySQLのMVCC - Qiita

MySQLのドキュメント

ロック
MySQLでSELECT FOR UPDATEと行ロックの挙動を検証してみた - JUST FOR FUN

MySQL - InnoDBのロック関連まとめ - Qiita

世界の何処かで MySQL(InnoDB)の REPEATABLE READ に嵌る人を1人でも減らすために - KAYAC engineers' blog

doc/innodb.md at master · ichirin2501/doc

漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点

アプリケーションエンジニアが知っておくべきMySQLのロック - Qiita

MySQL(InnoDB)のネクストキーロックの仕組みと範囲を図解する - 備忘録の裏のチラシ

インデックス
漢(オトコ)のコンピュータ道: 知って得するInnoDBセカンダリインデックス活用術!

INDEX FULL SCANを狙う

MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.13.2 クラスタインデックスとセカンダリインデックス

MySQL with InnoDB のインデックスの基礎知識とありがちな間違い - クックパッド開発者ブログ

MySQL(InnoDB)のインデックスについての備忘録 - What is it, naokirin?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む