# データベース基礎演習(2/3) ## ファイルからの実行 先ほどまではSQLをすべて手動で入力してきましたが、 あらかじめファイルに保存したSQLを実行できたほうが便利です。 ### echoによるファイル作成 `.exit`でunixのターミナルに戻って、ファイルを作成します。 ``` $ echo "select * from book where price < 1200;" > sale.sql $ cat sale.sql select * from book where price < 1200; $ ll sale.sql ``` `echo`コマンドは入力した内容をそのまま出力します。`> [ファイル名]`と ある場合、unixはファイルを作成して出力内容を書き込みます。 作成したSQLはunixターミナル(外側)から次のように実行できます。 ``` $ sqlite3 -header my.db < sale.sql id|title|price 2|manual of neko|1000 3|manual of neko|500 ``` ### テーブル形式での表示 研修用のサーバ上には、表組に変換する`tbl`コマンドがインストールされているので、 以下のように`| tbl`を末尾に追加すると見やすく出力することもできます。 ``` $ sqlite3 -header my.db < sale.sql | tbl +----+----------------+-------+ | id | title | price | +----+----------------+-------+ | 2 | manual of neko | 1000 | +----+----------------+-------+ | 3 | manual of neko | 500 | +----+----------------+-------+ ``` ### sedによるファイル修正 作成したSQLは価格1200円未満のレコードを出力するものでした。 この金額を1000円未満に変更したいとします。色々な方法がありますが、 `sed`コマンドを利用することもできます。 ``` $ sed -i -e 's/1200/1000/' sale.sql $ cat sale.sql select * from book where price < 1000; ``` ### viによるファイル編集 テスト用にデータを追加するため、`insert`文を何件か並べたファイルを作成しましょう。 `vim`コマンドを利用します。`vim`はターミナルから利用できるテキストエディタで、若干操作に癖がありますが、便利で強力な機能がそろっています。 ``` $ vim newbooks.sql ``` 全画面表示になり、左下に"newbooks"[New File]と表示されます。 ### 入力モード キーボードの`i`を押して、挿入モードに変更します。 左下が-- INSERT ---に変わりました。 まずは1行入力してみましょう。 ``` insert into book (title, price) values ("a yellow bear", 1200); ``` 入力したら`esc`キーを押します。挿入モードから編集モードに戻りました。 ### 行コピー `yy9p`と入力してみましょう。`yy`は行コピー、`9p`は9回ペーストを意味します。 insert文が10行に増えました。 ``` insert into book (title, price) values ("a yellow bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ..省略 insert into book (title, price) values ("a yellow bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ``` ### 移動と検索 2行目の"yellow bear"を"red bear"に、金額を3300円に変更しましょう。 `gg`を押すと先頭行に移動します。`j`と押すと1行下へ移動。 `/yellow`と押すと修正箇所へ移動します。 ``` insert into book (title, price) values ("a [カーソル]yellow bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ..省略 insert into book (title, price) values ("a yellow bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ``` ### 単語変更 `cw`を押すと"yellow"を 削除して挿入モードになるので、`red`と入力して`esc`を押します。 ``` insert into book (title, price) values ("a red bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ..省略 insert into book (title, price) values ("a yellow bear", 1200); insert into book (title, price) values ("a yellow bear", 1200); ``` ### 連続検索 次の行の"bear"を"rat"に変えましょう。`/bear`と入力して`enter`を押すと、 同じ行の"bear"にカーソルが移動します。ここで`n`と入力すると、次の検索場所に 移動するので、二行目を修正できます。 1200->3300の変更も同様に実施できます。 ### 保存 保存するときには、`:wq`と入力して`enter`を押します。 ### マニュアルなど vimのコマンドは独特ですが、色々なサマリーが公開されているので、 自分にあった使いやすいものを見つけて利用すると良いでしょう。 [これからvimコマンドを使おうとしている方々へ](https://qiita.com/HiromuMasuda0228/items/5ef842ee50e7ac5e85f2) LevelbossMike/vim_shortcut_wallpaper ![LevelbossMike/vim_shortcut_wallpaper](https://i.imgur.com/ufkKmi0.png) ## 複数テーブルの作成 書籍を表すbookテーブルに加えて、書店を表すshopテーブルを作成します。 それぞれの本は、いずれかの書店に必ず所属するものとします。 以下のような更新用のSQLファイル、add_shop.sqlを作成して テーブルの作成およびデータの追加を一括して行ってみましょう。 vimを利用する場合、キーワードへの色付けや、インデントの自動付与など 入力補助機能が利用できます。 ```sql create table shop ( id integer not null primary key autoincrement, name text not null ); insert into shop(name) values("meguro"); insert into shop(name) values("gotanda"); alter table book add shop_id integer default 1; ``` ``` $ sqlite3 my.db < add_shop.sql $ sqlite3 my.db sqlite> .schema CREATE TABLE book ( ...省略 ``` `alter`コマンドで変更したテーブルの構造は、`.schema`コマンドで 確認することができます。 ## 複数テーブルの結合表示 `select`文では、複数のテーブルを結合して表示することができます。 ``` > select * from shop,book; ...省略 > select * from shop,book where shop.id = book.shop_id; ...省略 ``` `where`句が無いと、関係のないお店と本が並べて表示されてしまいます。 本が4冊、お店が3店舗あるとき、関連を無視して4x3レコードが表示されます。 `where shop.id = book.shop_id`と制約することにより、関連した 本とお店を並べて取得することができます。 ## 集計表示 各お店の本の在庫は何冊でしょうか。次のような集計関数で求めることができます。 ``` > select shop.id, shop.name, count(*) from shop,book where shop.id = book.shop_id; ``` `count`は件数を集計しますが、合計を集計する`sum`なども使用することができます。 ## NULLを許容するテーブル結合 古書店では特別会員向けに、予約販売を開始することになりました。 会員を表すuserテーブルを作成し、bookテーブルに予約販売日カラムと、販売先会員番号を追加します。 ただし、予約販売の対象でない本については、それぞれ値をNULLとします。 次のような更新用SQLを作成して実行してみましょう。 ```sql create table user ( id integer not null primary key autoincrement, name text not null ); insert into user(name) values("taro"); insert into user(name) values("hanako"); alter table book add sell_date date; alter table book add user_id integer; update book set sell_date = '2021-04-04', user_id = 1 where id = 1; ``` NULL値は`select`文の結果として非表示になってしまうので、 sqliteのコンソールで`.nullvalue NULL`と表示設定を行います。 ``` sqlite> .nullvalue NULL sqlite> select * from book; 1|book of inu|1000|1|2021-04-04|1 1|manual of neko|1000|1|NULL|NULL 1|manual of neko|1000|1|NULL|NULL ``` 予約販売の予定がない本については、カラムの値がNULLになっています。 ## NULLを条件とした表示 本のうち、予約販売日がNULLであるレコードを表示してみましょう。 ``` > select * from book where sell_date = NULL; Error: near "where" ``` エラーになってしまいました。実はNULLは比較の値としては利用できません。 NULLであるか、ないかの判定は、専用の構文が用意されています。試してみましょう。 ```sql select * from book where sell_date is null; select * from book where sell_date is not null; ``` ## 外部結合 全ての本の一覧を表示します。ただし、予約販売があれば予約者の名前を表示し、 無ければ空欄にします。次のSQLで実現できるでしょうか。 ```sql select book.id, book.title, user.name from book,user where book.user_id = user.id; ``` 販売予定のない本が表示されません。 それでは、販売予定がない=user_idがnullである先は 必ず表示させる以下のSQLではどうでしょうか。 ```sql select book.id, book.title, user.name from book,user where book.user_id = user.id or book.user_id is null; ``` ところが、この方法だと販売予定の無い書籍が、全会員と結合して表示されます。 (where句なしのテーブル結合を思い出しましょう) 次のように複数の問い合わせ結果を取得する`union`句を使えば実現できます。 ```sql select book.id, book.title, user.name from book,user where book.user_id = user.id union select book.id, book.title, null from book where book.user_id is null; ``` しかし書き方が冗長ですし、実は性能的にも問題があります。("sql union 性能"などで調べてみましょう) 次のように、`left outer join`句を使用してテーブルの結合方法を指定すると、簡潔に期待通りの出力を得ることができます。 ```sql select book.id, book.title, user.name from book left outer join user on book.user_id = user.id; ``` # 練習問題 実例を通じて今まで学んだことを活用してみましょう。 問題ではテーブル名やカラム名は指定されませんが、適切な名前をつけることができる力は重要です。練習してみましょう。 ## 問題1 次のような動物園を管理するテーブルを作成して下さい。{}は繰り返し項目を表します。 `展示室(展示室番号、展示室名、{動物番号、動物名、動物種別})` 次のような問い合わせを実行できるSQLを作成して下さい。 ・展示室と動物の一覧、ただし空の展示室であっても表示する ・1体の動物の展示室を変更する ## 問題2 次のような従業員の勤務状況を管理するテーブルを作成して下さい。 `従業員(従業員番号、従業員名、{出勤日、出金状況})` ・従業員の、ある月における、欠勤日の日数を集計して下さい。 ・ある日に欠勤している従業員の一覧を出力して下さい。 ヒント:`where id between 1 and 4`で1以上4以下を指定できます。 ## 問題3 次の様な商品売上伝票を管理するテーブルを作成して下さい。 `売上(売上番号、売上日、顧客番号、顧客名、{商品番号、商品名、商品数量、商品単価})` ・各売上の売上金額を表示して下さい。 ヒント:`select a * b, a + b from ~`など、カラム同士を計算することができます。 ・特定の顧客の売上金額合計を表示して下さい。 ヒント:`select sum(a+b) from `など、計算結果を更に合計することができます。 ・特定の日付の、特定の商品の売上数量、合計金額を表示して下さい。 ## 参考情報 postgresqlの実行計画。 同一のSQLであっても、外部表の参照回数により、Index Scanではなく、 Hash Joinが使用されることがある。 極端な例では下記の通り、条件式の1件の違いにより、処理時間が1000倍程度相違することがある。 ``` tamura=# select count(*) from book; count --------- 1000000 (1 row) tamura=# select count(*) from customer; count -------- 100000 (1 row) tamura=# explain analyze select * from book, customer where book.customer_id = customer.id and book.id < 577; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.72..2943.27 rows=598 width=40) (actual time=0.042..2.932 rows=577 loops=1) -> Index Scan using book_pkey on book (cost=0.42..25.89 rows=598 width=22) (actual time=0.029..0.278 rows=577 loops=1) Index Cond: (id < 577) -> Index Scan using customer_pkey on customer (cost=0.29..4.88 rows=1 width=18) (actual time=0.004..0.004 rows=1 loops=577) Index Cond: (id = book.customer_id) Planning time: 0.432 ms Execution time: 3.061 ms (7 rows) tamura=# explain analyze select * from book, customer where book.customer_id = customer.id and book.id < 578; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=2887.43..2914.48 rows=599 width=40) (actual time=35.522..36.054 rows=578 loops=1) Hash Cond: (book.customer_id = customer.id) -> Index Scan using book_pkey on book (cost=0.42..25.91 rows=599 width=22) (actual time=0.032..0.161 rows=578 loops=1) Index Cond: (id < 578) -> Hash (cost=1637.00..1637.00 rows=100000 width=18) (actual time=34.138..34.139 rows=100000 loops=1) Buckets: 131072 Batches: 1 Memory Usage: 5994kB -> Seq Scan on customer (cost=0.00..1637.00 rows=100000 width=18) (actual time=0.018..11.248 rows=100000 loops=1) Planning time: 0.463 ms Execution time: 36.553 ms (9 rows) ```