現在oracle+PHPで作られたプログラムの改修を行っています。
その内容が、oracleからMySQLへのDBの変更といった内容なのですが、そこで問題になったのがoracle独自の関数を用いて作られたSQLです。
今、問題なっているのが、
sql
1select * from ( 2 select t1.TENPO_CD as TENPO_CD, 3 t1.TODOFUKEN_CD as TODOFUKEN_CD, 4 t1.DATE as DATE, 5 t1.KBN as KBN, 6 t1.TANKA as TANKA, 7 trim(format(t1.RESULT,0)) as RESULT, 8 t1.XX_CD as XX_CD, 9 t1.XX_NAME as XX_NAME, 10 t1.XX_NO as XX_NO, 11 (select count(*) +1 from XXtable as t2 where 1 and CAST(CAST(t2.RESULT as UNSIGNED) as SIGNED) > CAST(CAST(t1.RESULT as UNSIGNED) as SIGNED) and concat(t2.KBN,t2.TANKA)= concat(t1.KBN,t1.TANKA)) as rank 12 from XXtable as t1 13 where TODOFUKEN_CD = '23' 14 and DATE like '2016-03-12%' 15 and DEL_FLG = 0 16 order byKBN asc,TANKA desc) as A1 17where rank <= 20; 18
といったSQLです。
もともとはoracleのrow_numberを使って記載されていたものをMySQLに対応させてみました。
ラッピングしている中身だけであれば1秒かからず処理ができるのですが、
where rank <= 20;
の部分を対応させると30秒近くかかってしまいます。
もともとデータをほぼ全読みするものであるため、インデックスなども付けられず、件数はテストデータでは8000件程度です。
本番ではこの7倍が想定されていますので、到底耐えうるものではなくなってしまいました。
何か高速化する手段はないものでしょうか?
MySQLのバージョン(5.1.73)
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/03/15 01:30
回答9件
0
2016/03/15 15:08に退会済みユーザが投稿してる
ユーザ変数を使う方法ですが
OracleACEのAketiJyuuzouさんとyoku0825さんと
日本オラクルの木村明治さんによると
MySQLのユーザ変数は評価順序が未定義です。
http://qiita.com/AketiJyuuzou/items/cced9b70cc714b382d98
なので、結果が保証されませんので、
納品物件などでは、問題外の品質です。
投稿2016/04/22 06:08
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
興味深く参照させていただきました。
XXtableの1レコードに対して、
XXtableを全読みした場合のオーダーはn*n
ただ、
XXtableの全読みに対して、インデックスが利けば
実質nで済むので、
インデックスが利くようなSQL文を検討してみてはどうでしょうか?
OracleのRow_Numberだと
クイックソートでも使ってるでしょうから
n*log(n)なんでしょうね。
せめて、MySQLで
SQL
1select colA 2from XXtable a 3where (select count(*) 4 from (select 1 from XXtable b 5 where b.colA=a.colA --順位を決める条件 6 Limit 21) as TmpV ) <= 20;
と書ければいいのですが、
サブクエリの2段ネストでは、aはスコープ外になるので不可のようです。
投稿2016/03/15 06:16
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
MySQLのOffSetを忘れてました。
MySQLでもこれなら通りますね。
SQL
1select colA 2from XXtable a 3where not exists(select 1 from XXtable b 4 where b.colA> a.colA --順位を決める条件 5 OffSet 19 Limit 1) 6
ということで、
1位から20位を取りたいのですから
21位以降は正確な順位は不要ということをふまえて
LimitとOffSetを使ってみました。
サブクエリでCountStopかかるので、
多少は早くなるはずです。
SQL
1select t1.TENPO_CD as TENPO_CD, 2 t1.TODOFUKEN_CD as TODOFUKEN_CD, 3 t1.DATE as DATE, 4 t1.KBN as KBN, 5 t1.TANKA as TANKA, 6 trim(format(t1.RESULT,0)) as RESULT, 7 t1.XX_CD as XX_CD, 8 t1.XX_NAME as XX_NAME, 9 t1.XX_NO as XX_NO 10from XXtable as t1 11where TODOFUKEN_CD = '23' 12 and DATE like '2016-03-12%' 13 and DEL_FLG = 0 14 and not exists(select 1 from XXtable t2 15 where CAST(CAST(t2.RESULT as UNSIGNED) as SIGNED) 16 > CAST(CAST(t1.RESULT as UNSIGNED) as SIGNED) 17 and concat(t2.KBN,t2.TANKA)= concat(t1.KBN,t1.TANKA) 18 OffSet 19 Limit 1) 19order by KBN asc,TANKA desc;
投稿2016/03/16 01:45
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
テーブルCREATE
sql
1CREATE TABLE XXtable ( 2 id MEDIUMINT NOT NULL AUTO_INCREMENT, 3 TENPO_CD int(10), 4 TODOFUKEN_CD int(10), 5 DATE DATETIME, 6 KBN CHAR(3), 7 TANKA int(10), 8 RESULT CHAR(10), 9 XX_CD int(10), 10 XX_NAME CHAR(12), 11 XX_NO int(10), 12 DEL_FLG tinyint(1), 13 PRIMARY KEY (id) 14)
テストデータ作成
python
1# coding:utf-8 2import random 3import time 4 5# 0~100の乱数を生成 6def randomAge(): 7 return random.randint(0, 100) 8 9def randomTempo(): 10 return random.randint(100, 999) 11 12def randomTodou(): 13 return random.randint(1, 47) 14 15def randomTanka(): 16 return random.randint(500, 2000) / 100 * 500 17 18# startからendの間でランダムな日付生成 19def randomDate(start, end): 20 format = '%Y-%m-%d %H:%M:%S' 21 stime = time.mktime(time.strptime(start, format)) 22 etime = time.mktime(time.strptime(end, format)) 23 ptime = stime + random.random() * (etime - stime) 24 return time.strftime(format, time.localtime(ptime)) 25 26# 出力するファイル名 27OUTPUT_FILE = "TestData.sql" 28 29# 登録するデータ件数 30RECORD_COUNT = 8000 31 32# 実行するSQLコマンド文字列 33sqlCommands = "" 34 35# 使用するデータベースを指定(今回はCreateTestData) 36sqlCommands += "USE goods;\n" 37 38# 登録するデータの数だけINSERT文を生成 39for _ in range(RECORD_COUNT): 40 41 # 登録するランダムなデータの生成 42 date = randomDate("2015-12-12 00:00:00", "2015-12-20 00:00:00") 43 result = str(randomAge()) + '.' + str(randomAge()) 44 45 tenpo = randomTempo() / 100 * 10 46 tdou = randomTodou() 47 kbn = randomTempo() / 100 * 10 48 tanka = randomTanka() 49 50 # ランダムなデータからInsert文を生成 51 sqlCommands += "INSERT INTO XXtable" \ 52 "(TENPO_CD, TODOFUKEN_CD, DATE, KBN, TANKA, RESULT, DEL_FLG) " \ 53 "VALUES ('{}', '{}', '{}', '{}', '{}', '{}', 0);\n"\ 54 .format(tenpo, tdou, date, kbn, tanka, result) 55 56# 生成したSQLコマンドをファイルに書き出す 57f = open(OUTPUT_FILE, 'w') 58f.write(sqlCommands) 59f.close()
追加のSQL
SQL
1-- KBN 昇順 TANKA 降順 のインデックス 2alter table XXtable ADD KBN_TANKA int; 3update XXtable set KBN_TANKA = KBN * 10000000 + ( 10000000 - TANKA); 4 5-- RESULT 降順 のインデックス 6alter table XXtable ADD RESULT_SORT int; 7update XXtable set RESULT_SORT = 10000000 - RESULT * 1000; 8 9-- RESULT KBN TANKA 10 create index index_kbntankaresult_sort on XXtable (KBN_TANKA, RESULT_SORT);
MySQLのインデックスは、降順が出来ず、指定しても無視され昇順で、格納されるので、
降順で並ぶように細工が必要になります。
区分、TANKA 毎のランキング 上位20位抽出SQL
SQL
1select * from 2 ( select 3 *, 4 @kbn_rank := IF(@current_kbn = KBN_TANKA, @kbn_rank + 1, 1) AS kbn_rank, 5 @current_kbn := KBN_TANKA 6 from (SELECT @kbn_rank := -1) s, 7 (SELECT @current_kbn := -1) c, 8 XXtable 9 order by KBN_TANKA, RESULT_SORT 10 ) ranked 11where 12 kbn_rank <= 20 13 and TODOFUKEN_CD = '23' 14 and DATE >= '2015-12-17' and DATE < '2015-12-18' 15 and DEL_FLG = 0 ;
投稿2016/03/15 06:08
編集2016/03/15 06:35退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2016/03/15 06:20
退会済みユーザー
2016/03/15 06:34
退会済みユーザー
2016/03/15 07:05
退会済みユーザー
2016/03/15 07:41
2016/03/15 08:37
退会済みユーザー
2016/04/04 08:22
0
terionさんの指摘も組み込んで、まとめてみました。
下記いかがでしょうか?
mysql
1 select t1.TENPO_CD as TENPO_CD, 2 t1.TODOFUKEN_CD as TODOFUKEN_CD, 3 t1.DATE as DATE, 4 t1.KBN as KBN, 5 t1.TANKA as TANKA, 6 trim(format(t1.RESULT,0)) as RESULT, 7 t1.XX_CD as XX_CD, 8 t1.XX_NAME as XX_NAME, 9 t1.XX_NO as XX_NO, 10 (select count(t2.TEMPO_CD) +1 --count(*)より、主キーやNOT NULLのインデックスのはられた列をcountした方が高速です。 11 from 12 XXtable as t2 13 where 14 1 15 and CAST(CAST(t2.RESULT as UNSIGNED) as SIGNED) > CAST(CAST(t1.RESULT as UNSIGNED) as SIGNED) --ここでやりたいことってどういうことなんでしょうか?冗長な匂いがします。 16 and concat(t2.KBN,t2.TANKA)= concat(t1.KBN,t1.TANKA)) as rank 17 from XXtable as t1 18 where TODOFUKEN_CD = '23' 19 and DATE >= '2016-03-12' and DATE < '2016-03-13' 20 and rank <= 20 --rankを内側に入れ、ネストを1つ減らしました。 21 and DEL_FLG = 0 --絞り込みに使いにくい条件はWhereの外側に、絞り込める条件を内側に書いた方が早くなる場合あります。 22 order byKBN asc,TANKA desc) as A1
投稿2016/03/14 15:52
総合スコア907
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/03/15 01:37
2016/03/15 02:01
2016/03/15 05:42
2016/03/15 06:03
2016/03/15 10:26
0
グループ毎の上位20件を抽出するってことですよね?
難しいですね。
所で、サブクエリだけなら1秒で終わるんですか?count(*)の要素まで含めても?
それならソートをサブクエリの外に移したらどうでしょう?
サブクエリ内でソートしたものを、外側でrankで絞り込むためにまた、rankでソートしたりしてるんじゃないかなと思ったり。
※後、外にwhereを置いたSQLだとサブクエリ内のソートは無意味だったような気がします。(うろ覚え)
投稿2016/03/14 12:02
編集2016/03/14 12:02総合スコア2068
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/03/14 12:11
2016/03/14 12:23 編集
2016/03/14 12:52
2016/03/14 14:15 編集
2016/03/15 01:39
0
ベストアンサー
MySQL でrankを出すとき
変数を使用すると、早くなります。
http://stackoverflow.com/questions/11084668/finding-rank-of-student-in-table
追記1
日付はLIKE検索してはいけません。
LIKE検索の場合はインデックスが使用されずフルテーブルスキャンが走ってしまいます。
SQL
1 SELECT * FROM test WHERE c2 >= '2014-01-02' AND c2 < '2014-01-03';
投稿2016/03/14 11:07
編集2016/03/14 11:24退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/03/14 11:11
退会済みユーザー
2016/03/14 11:25
2016/03/14 11:42
退会済みユーザー
2016/03/15 00:08
退会済みユーザー
2016/03/15 01:06
退会済みユーザー
2016/03/15 01:13
退会済みユーザー
2016/03/15 02:28
2016/03/15 05:45
退会済みユーザー
2018/09/16 06:46
0
深く読み込んだわけではないですが、下記のように外側のSelect句はいらないように思いますが、いかがでしょうか?
mysql
1select t1.TENPO_CD as TENPO_CD, 2 t1.TODOFUKEN_CD as TODOFUKEN_CD, 3 t1.DATE as DATE, 4 t1.KBN as KBN, 5 t1.TANKA as TANKA, 6 trim(format(t1.RESULT,0)) as RESULT, 7 t1.XX_CD as XX_CD, 8 t1.XX_NAME as XX_NAME, 9 t1.XX_NO as XX_NO, 10 (select count(*) +1 from XXtable as t2 where 1 and CAST(CAST(t2.RESULT as UNSIGNED) as SIGNED) > CAST(CAST(t1.RESULT as UNSIGNED) as SIGNED) and concat(t2.KBN,t2.TANKA)= concat(t1.KBN,t1.TANKA) ) as rank 11 from XXtable as t1 12 where TODOFUKEN_CD = '23' 13 and DATE like '2016-03-12%' 14 and DEL_FLG = 0 15 and rank <= 20 16 order by KBN asc,TANKA desc
投稿2016/03/14 10:55
編集2016/03/14 15:03総合スコア907
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/03/14 11:07
2016/03/14 15:23 編集
0
ORDER BYした結果を副問い合わせしてLIMITで件数を絞ればいいのではないでしょうか。
http://www.dbonline.jp/mysql/select/index12.html
たぶん、row_numberを改変した質問文のやり方ですと、8000件分のSELECT COUNT(*)が走ると思うので激重だと思います。
投稿2016/03/14 10:52
総合スコア249
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。