質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

SQL Server

SQL Serverはマイクロソフトのリレーショナルデータベース管理システムです。データマイニングや多次元解析など、ビジネスインテリジェンスのための機能が備わっています。

SQLite

SQLiteはリレーショナルデータベース管理システムの1つで、サーバーではなくライブラリとして使用されている。

PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

SQL

SQL(Structured Query Language)は、リレーショナルデータベース管理システム (RDBMS)のデータベース言語です。大きく分けて、データ定義言語(DDL)、データ操作言語(DML)、データ制御言語(DCL)の3つで構成されており、プログラム上でSQL文を生成して、RDBMSに命令を出し、RDBに必要なデータを格納できます。また、格納したデータを引き出すことも可能です。

Q&A

解決済

4回答

3153閲覧

MySQLにて、複数Tableを結合して集計するSQL

kunai

総合スコア5405

MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

SQL Server

SQL Serverはマイクロソフトのリレーショナルデータベース管理システムです。データマイニングや多次元解析など、ビジネスインテリジェンスのための機能が備わっています。

SQLite

SQLiteはリレーショナルデータベース管理システムの1つで、サーバーではなくライブラリとして使用されている。

PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

SQL

SQL(Structured Query Language)は、リレーショナルデータベース管理システム (RDBMS)のデータベース言語です。大きく分けて、データ定義言語(DDL)、データ操作言語(DML)、データ制御言語(DCL)の3つで構成されており、プログラム上でSQL文を生成して、RDBMSに命令を出し、RDBに必要なデータを格納できます。また、格納したデータを引き出すことも可能です。

0グッド

0クリップ

投稿2018/10/25 03:19

編集2018/10/25 03:38

MySQL(5.5)にて、「商品」「売上」「仕入れ」の3テーブルについて、商品ごとの売上合計、仕入れ合計等を算出したいと考えています。
実現自体は出来ているのですが、処理に時間がかかっているため、もっとよい(高速で正しい値が取れる)方法がないかお知恵を貸していただきたく質問しました。

テーブル構造は以下の通りです。

product(商品)テーブル

idnameprice
1商品1100
2商品2200
3商品3300

sell(売上)テーブル (※is_back=1の場合、返品扱い)
|id|product_id|qty|is_back|datetime|
|:--|:--:|:--:|--:|
|1|1|2|0|2018-10-10 00:00:00|
|2|2|1|0|2018-10-10 00:00:00|
|3|3|3|0|2018-10-10 00:00:00|
|4|1|1|1|2018-10-10 00:00:00|
|5|2|1|1|2018-10-10 00:00:00|
|6|3|1|1|2018-10-10 00:00:00|

buy(仕入れ)テーブル(※is_back=1の場合、返品扱い)
|id|product_id|qty|is_back|datetime|
|:--|:--:|:--:|--:|
|1|1|5|0|2018-10-10 00:00:00|
|2|2|3|0|2018-10-10 00:00:00|
|3|3|10|0|2018-10-10 00:00:00|
|4|1|2|1|2018-10-10 00:00:00|
|5|2|1|1|2018-10-10 00:00:00|
|6|3|8|1|2018-10-10 00:00:00|
|7|1|1|0|2018-10-10 00:00:00|

最初に書いた(間違った)SQL

まず最初に、各TableをproductについてLeft Joinし、合算しました。

SQL

1select 2 product.id, 3 product.price, 4 ifnull(sum(sell_out.qty), 0) as summed_sell, 5 ifnull(sum(sell_in.qty), 0) as summed_back, 6 ifnull(sum(buy_out.qty), 0) as summed_buy, 7 ifnull(sum(buy_in.qty), 0) as summed_return, 8 9 /** 実際にはこれらの値も取得しているが、説明の為割愛 10 ifnull(sum(sell_out.qty * product.price), 0) as summed_sell_price, 11 ifnull(sum(sell_in.qty * product.price), 0) as summed_back_price, 12 ifnull(sum(buy_out.qty * product.price), 0) as summed_buy_price, 13 ifnull(sum(buy_in.qty * product.price), 0) as summed_return_price 14 **/ 15from 16 product 17 left join 18 sell as sell_out 19 on 20 sell_out.product_id = product.id 21 and sell_out.is_back = 0 22 and sell_out.`datetime` > '2018-10-01 00:00:00' 23 24 left join 25 sell as sell_in 26 on 27 sell_in.product_id = product.id 28 and sell_in.is_back = 1 29 and sell_in.`datetime` > '2018-10-01 00:00:00' 30 31 left join 32 buy as buy_out 33 on 34 buy_out.product_id = product.id 35 and buy_out.is_back = 1 36 and buy_out.`datetime` > '2018-10-01 00:00:00' 37 38 left join 39 buy as buy_in 40 on 41 buy_in.product_id = product.id 42 and buy_in.is_back = 0 43 and buy_in.`datetime` > '2018-10-01 00:00:00' 44group by 45 product.id;

このSQLでは、全てLEFT JOINで結合されるため、商品1(product.id=1)は仕入れ(buy)が2件あるため、JOINされたrow数が2つずつになり、結果として売上、返品等の件数がすべて倍になっています。

product.idproduct.pricesummed_sellsummed_backsummed_buysummed_return
11004246
22001113
330031810

そこで、JOINする前にGroup Byで集計し、その値を出すようにしまして、正しい値が取れるようになりました。

SQL

1select 2 product.id, 3 product.price, 4 sell_out.summed_sell as summed_sell, 5 sell_in.summed_back as summed_back, 6 buy_out.summed_buy as summed_buy, 7 buy_in.summed_return as summed_return, 8 9 /** 実際にはこれらの値も取得しているが、説明の為割愛 10 ifnull(sum(sell_out.summed_sell * product.price), 0) as summed_sell_price, 11 ifnull(sum(sell_in.summed_back * product.price), 0) as summed_back_price, 12 ifnull(sum(buy_out.summed_buy * product.price), 0) as summed_buy_price, 13 ifnull(sum(buy_in.summed_return * product.price), 0) as summed_return_price 14 **/ 15 16from 17 product 18 left join 19 (select 20 ifnull(sum(sell.qty), 0) as summed_sell, 21 sell.product_id 22 from 23 sell 24 where 25 sell.is_back = 0 26 and sell.`datetime` > '2018-10-01 00:00:00' 27 group by 28 sell.product_id 29 ) as sell_out 30 on 31 sell_out.product_id = product.id 32 33 left join 34 (select 35 ifnull(sum(sell.qty), 0) as summed_back, 36 sell.product_id 37 from 38 sell 39 where 40 sell.is_back = 1 41 and sell.`datetime` > '2018-10-01 00:00:00' 42 group by 43 sell.product_id 44 ) as sell_in 45 on 46 sell_in.product_id = product.id 47 48 left join 49 (select 50 ifnull(sum(buy.qty), 0) as summed_buy, 51 buy.product_id 52 from 53 buy 54 where 55 buy.is_back = 1 56 and buy.`datetime` > '2018-10-01 00:00:00' 57 group by 58 buy.product_id 59 ) as buy_out 60 on 61 buy_out.product_id = product.id 62 63 left join 64 (select 65 ifnull(sum(buy.qty), 0) as summed_return, 66 buy.product_id 67 from 68 buy 69 where 70 buy.is_back = 0 71 and buy.`datetime` > '2018-10-01 00:00:00' 72 group by 73 buy.product_id 74 ) as buy_in 75 on 76 buy_in.product_id = product.id 77group by 78 product.id;

↓結果

product.idproduct.pricesummed_sellsummed_backsummed_buysummed_return
11002126
22001113
330031810

ただ、こちらの場合は処理にかなり時間がかかってしまい、とても残念な感じです。
なんとか高速に正しい値が処理出来るようにならないでしょうか。

ちなみに、上記に記載したテーブル構造・データは下記のSQLで構築出来ますので、是非ご検証いただけると助かります。
※実際には、件数が万件単位になっている場合に処理時間が体感出来るのですが。。

SQL

1-- 2-- Table structure for table `buy` 3-- 4 5DROP TABLE IF EXISTS `buy`; 6CREATE TABLE `buy` ( 7 `id` int(11) NOT NULL AUTO_INCREMENT, 8 `product_id` int(11) NOT NULL, 9 `qty` int(11) DEFAULT NULL, 10 `is_back` tinyint(4) DEFAULT '0', 11 `datetime` datetime DEFAULT NULL, 12 PRIMARY KEY (`id`) 13) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 14 15-- 16-- Dumping data for table `buy` 17-- 18 19LOCK TABLES `buy` WRITE; 20/*!40000 ALTER TABLE `buy` DISABLE KEYS */; 21INSERT INTO `buy` VALUES (1,1,5,0,'2018-10-10 00:00:00'),(2,2,3,0,'2018-10-10 00:00:00'),(3,3,10,0,'2018-10-10 00:00:00'),(4,1,2,1,'2018-10-10 00:00:00'),(5,2,1,1,'2018-10-10 00:00:00'),(6,3,8,1,'2018-10-10 00:00:00'),(7,1,1,0,'2018-10-10 00:00:00'); 22/*!40000 ALTER TABLE `buy` ENABLE KEYS */; 23UNLOCK TABLES; 24 25-- 26-- Table structure for table `product` 27-- 28 29DROP TABLE IF EXISTS `product`; 30CREATE TABLE `product` ( 31 `id` int(11) NOT NULL AUTO_INCREMENT, 32 `name` varchar(45) DEFAULT NULL, 33 `price` int(11) DEFAULT NULL, 34 PRIMARY KEY (`id`) 35) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 36 37-- 38-- Dumping data for table `product` 39-- 40 41LOCK TABLES `product` WRITE; 42/*!40000 ALTER TABLE `product` DISABLE KEYS */; 43INSERT INTO `product` VALUES (1,'商品1',100),(2,'商品2',200),(3,'商品3',300); 44/*!40000 ALTER TABLE `product` ENABLE KEYS */; 45UNLOCK TABLES; 46 47-- 48-- Table structure for table `sell` 49-- 50 51DROP TABLE IF EXISTS `sell`; 52CREATE TABLE `sell` ( 53 `id` int(11) NOT NULL AUTO_INCREMENT, 54 `product_id` int(11) NOT NULL, 55 `qty` int(11) DEFAULT NULL, 56 `is_back` tinyint(4) DEFAULT '0', 57 `datetime` datetime DEFAULT NULL, 58 PRIMARY KEY (`id`) 59) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 60 61-- 62-- Dumping data for table `sell` 63-- 64 65LOCK TABLES `sell` WRITE; 66/*!40000 ALTER TABLE `sell` DISABLE KEYS */; 67INSERT INTO `sell` VALUES (1,1,2,0,'2018-10-10 00:00:00'),(2,2,1,0,'2018-10-10 00:00:00'),(3,3,3,0,'2018-10-10 00:00:00'),(4,1,1,1,'2018-10-10 00:00:00'),(5,2,1,1,'2018-10-10 00:00:00'),(6,3,1,1,'2018-10-10 00:00:00'); 68/*!40000 ALTER TABLE `sell` ENABLE KEYS */; 69UNLOCK TABLES; 70

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

SQLの作りより、インデックスが無いのが根本的に低速な理由かと思われます。
sell,buy共に、
(datetime, is_back, qty, product_id)
または
(product_id, datetime, is_back, qty)
のインデックスを追加してみて下さい。
※多分効果を生むのは前者で、その場合product_idは無くても変わらない気がしますが。

SQLはyamabejpさんのご提示分で良いとして、売値と仕入値は実際には分かれているんですよねきっと。

投稿2018/10/25 05:06

編集2018/10/25 05:58
sazi

総合スコア25195

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

こんな感じですね

  • 元データ

SQL

1create table product(id int primary key,name varchar(20),price int); 2insert into product values 3(1,'商品1',100), 4(2,'商品2',200), 5(3,'商品3',300); 6 7create table sell(id int primary key,product_id int,qty int,is_back tinyint,`datetime` datetime); 8insert into sell values 9(1,1,2,0,'2018-10-10 00:00:00'), 10(2,2,1,0,'2018-10-10 00:00:00'), 11(3,3,3,0,'2018-10-10 00:00:00'), 12(4,1,1,1,'2018-10-10 00:00:00'), 13(5,2,1,1,'2018-10-10 00:00:00'), 14(6,3,1,1,'2018-10-10 00:00:00'); 15 16create table buy(id int primary key,product_id int,qty int,is_back tinyint,`datetime` datetime); 17insert into buy values 18(1,1,5 ,0,'2018-10-10 00:00:00'), 19(2,2,3 ,0,'2018-10-10 00:00:00'), 20(3,3,10,0,'2018-10-10 00:00:00'), 21(4,1,2 ,1,'2018-10-10 00:00:00'), 22(5,2,1 ,1,'2018-10-10 00:00:00'), 23(6,3,8 ,1,'2018-10-10 00:00:00'), 24(7,1,1 ,0,'2018-10-10 00:00:00');
  • 集計

SQL

1select * from product as t1 2left join 3(select product_id, 4sum(qty*(is_back=0)) summed_sell, 5sum(qty*(is_back=1)) summed_back 6from sell 7where `datetime` > '2018-10-01 00:00:00' 8group by product_id 9) as t2 on t1.id=t2.product_id 10left join 11(select product_id, 12sum(qty*(is_back=0)) summed_buy, 13sum(qty*(is_back=1)) summed_return 14from buy 15where `datetime` > '2018-10-01 00:00:00' 16group by product_id 17) as t3 on t1.id=t3.product_id

投稿2018/10/25 03:38

yambejp

総合スコア114839

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kunai

2018/10/25 03:42

あぁ、すみません。検証用データ構築用SQLの提供が遅れました。。 わざわざありがとうございます。 なんかカッコいい感じのSQLですが、WHERE句に書かれるような条件がSELECT句内で表現されているのを初めて見ましたので、何者なのか調べてきます。
kunai

2018/10/25 03:48

これ(select句内での条件記述)は、値を抽出しているのではなく、単純にIF文のように is_back=0 を満たすレコードの場合は1となり、つまりqty実数が加算され、 is_back=0 でなかったレコードの場合は0となり、つまりqty*0=0が加算されるため、 結果的に表に出る値は正しい値になる、という理屈なのだと理解して良いのでしょうか。
yambejp

2018/10/25 03:54

sellとbuyはほぼ同じ構造なので一つにまとめてもいいかもしれませんね。 また、戻しについてはis_back属性は必要だとして、戻しのqtyに-をつければ処理は楽になります
yambejp

2018/10/25 03:56

> sum(qty*(is_back=0)) これはMySQLの方言のようなもので (is_back=0)を評価しtrueかfalseを返し、trueは1falseは0で計算されるため 上記プラスの処理のqtyの合計を計算します
kunai

2018/10/25 04:06

方言ですか。なるほど。 テーブル構造については、何年も前から運用されているシステムで、既に膨大なデータが登録されているので今更どうにも、というところなのです。 正確には、仰る通りマイナスの値が入っているレコードもあったりするので、実際にはABS(qty)的な処理も入っています。 実際のデータにて検証してみます!ありがとうございます。
yambejp

2018/10/25 04:33

念の為ちゃんとしたSQLだと sum(qty*(is_back=0)) ↓↓↓ sum(qty*(case is_back when 0 then 1 else 0 end))
guest

0

タイトル名は「…のチューニング」ですね。

SQLのチューニングはベテランエンジニアにお願いする一大仕事なので、
片手間でチューニングとかどんなベテランにも不可能です。
チューニングを責任もってやってくれという話になったらクラウドワークスで金払ってくれみたいな話に膨らんでしまうので、ここはチューニングを頑張って勉強してみてください。

MySQLのチューニングに関しては、MySQLの神様みたいな人間が書いた神記事があるので丸投げします。
ちょっと古いですが私もこれ見て勉強しました。
MySQLのEXPLAINを徹底解説!! - 漢のコンピュータ道

まずこれを見ながらExplain文を発行して「実行結果」を確認してください。
まぁ、質問文のSelect文の頭にexplainという枕詞を引っ付けるだけで表示されると思います。

MySQLは集計処理はC++で行っており、下手なスクリプト言語でfor文回して集計頑張るよりよっぽど速度が出るはずです。
にもかかわらず遅いのはほぼディスクアクセス、つまりテーブルに適切なインデックスが張られていない事が原因で、今回の理由もインデックスの張り忘れが原因なんじゃないの?と思ってます

商品ごとの売上合計、仕入れ合計等を算出したいと考えています。

返品という概念があり、売上合計と仕入れ合計のロジックが違うのが複雑怪奇になっている原因でしょう。
シンプルさを考慮するなら副問合せを行って最後にマージするか、
大人しくSQLを2つに分けるかの二択にすべきじゃないかなと思います。

もしかして一撃SQLを作ってるのですか?
一撃SQLを作る事は頭の体操になり、MySQL用のSQLを書く技術が上達しますから、
もし私が先輩であなたが後輩ならば、今日一日使って良いから一撃で出来ないか考えるよう指示を飛ばすかもしれません。

ただ、こういう公共の場で一撃SQLを作れません助けて下さいとなると
「お前アホだろ?素直に2個に分けろ」という回答がメインになるでしょう。
簡潔で良いSQLを2個発行出来るというのも立派な技術力ですから、SQLを分ける事も検討してみてください。

投稿2018/10/25 04:11

編集2018/10/25 04:17
miyabi-sun

総合スコア21158

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sazi

2018/10/25 04:50

処理を分割せざるをえない場合には、分割した方がトータルでは高速という場合は有りますけど、 問い合わせ回数を減らすというのは、チューニングの基本です。
miyabi-sun

2018/10/25 05:26

なるほど、つまり今回の質問者さんは一撃SQLを作ったが数十秒〜数百秒掛かって辛いと言ってるのに、 クエリ回数を1→2に増やすと数ms遅くなるから1個のまま頑張れと言ってるわけですね。 ボトルネックを探すために実行計画を確認し、それに対応する事を行うのがチューニングの仕事だと勘違いしていました。 saziさんの方がよくご存知みたいなのでそちらにおまかせします。
sazi

2018/10/25 05:54 編集

「数MS遅くなるから」というコメントから察するに、ご納得はされていないようですが、 一撃で行えるということは、分割した際には、その共通部分が実行計画上分割回数分実行されるので、トータルすると、遅くなるのが殆どなのです。 その際の時間差が数MSかどうかは物によります。
guest

0

IS_BLACKの値により、SELLテーブルとBUYテーブルをそれぞれ2つに分けてJOINしているのが無駄です。
1回JOINして

SQL

1select 2 ... 3 ifnull(sum(case when sell.is_black = 0 then sell.qty end), 0) as summed_sell, 4 ifnull(sum(case when sell.is_black = 1 then sell.qty end), 0) as summed_black, 5 ...

などとすれば良いです。

テーブル定義はCREATE TABLE, インデックス定義は CREATE INDEX で提示した方が適切なコメントが付き易いです。
SQL実行計画の疑問解決には「とりあえずEXPLAIN」しよう で適切なインデックスが設定されているかも確認されては?

投稿2018/10/25 03:52

編集2018/10/25 05:25
Orlofsky

総合スコア16415

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kunai

2018/10/25 04:07

Explainは勿論してます! Case文を使った方法も検証してみます。 ありがとうございます。
Orlofsky

2018/10/25 04:16

質問に現状のEXPLAINも載せるとより適切なコメントが付くかと。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問