遅いSQLで勝つための魔法の武器 | JD Logistics Technical Team

大きなプロモーションの準備において、サービスの円滑な運用にとって最も有害な SQL の遅さは、日常業務でアプリケーション全体にジッターを引き起こすことがよくある最大の隠れた危険の 1 つです。日々の開発で SQL が遅い? SQL が遅い問題を解決するには、どのようなアイデアを使用する必要があるかを知る必要があります。この記事では主に、SQL が遅い場合のトラブルシューティングと解決策のアイデアを紹介し、問題をより迅速かつ正確に特定して解決できるように、実践的な例を通じて詳細な分析と概要を提供します。

解決策のステップ

step1. SQLを観察する

歴史的な理由により、一部の SQL クエリは非常に複雑になる場合があり、多くのテーブルを同時に関連付けたり、いくつかの複雑な関数やサブクエリを使用したりする必要があります。このような SQL は、データベース内のデータ量が比較的少ないため、データベースに大きな影響を与えません。プロジェクトの初期段階ではプレッシャーがかかりますが、時間とビジネス開発の蓄積により、これらの SQL は徐々に遅い SQL に変化し、データベースのパフォーマンスに一定の影響を与えることになります。

このような SQL については、まずビジネス シナリオを理解し、関係を整理し、SQL をいくつかの単純な小さな SQL に分解してメモリ内で結合することをお勧めします。

step2. 問題を分析する

遅い SQL を分析するときに最も一般的に使用されるツールは間違いなく Explain ステートメントです。以下は Explain ステートメントの実行出力です。

一般的に、最も注意を払う必要があるインジケーターには、type、 possible_keys、key、rows、および extra が含まれます。

type は接続タイプで、次の値があります。パフォーマンスは次のように最良から最悪の順に並べられます。

  • system: このテーブルには行が 1 つだけあります (システム テーブルと同等)。system は const 型の特殊なケースです。
  • const: 主キーまたは一意のインデックスに対する同等のクエリ スキャン。最大でも 1 行のデータのみを返します。const クエリは 1 回しか読み取らないため、非常に高速です。
  • eq_ref: この型は、インデックスのすべてのコンポーネントが使用され、インデックスが PRIMARY KEY または UNIQUE NOT NULL である場合にのみ使用され、そのパフォーマンスは system と const に次ぐものです。
  • ref: インデックスの左端のプレフィックス ルールが満たされる場合、またはインデックスが主キーまたは一意のインデックスではない場合に発生します。使用されるインデックスが少数の行のみに一致する場合、パフォーマンスは良好です。

チップ

左端のプレフィックスの原則は、インデックスが左端の最初の方法でインデックスと一致することを意味します。たとえば、結合インデックス (column1、column2、column3) が作成される場合、クエリ条件は次のようになります。

  • WHERE カラム 1 = 1、WHERE カラム 1= 1 AND カラム 2 = 2、WHERE カラム 1= 1 AND カラム 2 = 2 AND カラム 3 = 3 はすべてこのインデックスを使用できます。
  • WHERE カラム 1 = 2、WHERE カラム 1 = 1 AND カラム 3 = 3 はインデックスと一致できません。
  • fulltext: 全文インデックス
  • ref_or_null: このタイプは ref に似ていますが、MySQL はさらに NULL を含む行を検索します。このタイプはサブクエリの解析で一般的です
  • Index_merge: このタイプは、インデックス マージ最適化の使用を示し、クエリで複数のインデックスが使用されることを示します。
  • unique_subquery: このタイプは eq_ref に似ていますが、IN クエリを使用し、サブクエリは主キーまたは一意のインデックスです。例えば:

Index_subquery: unique_subquery に似ていますが、サブクエリが一意でないインデックスを使用する点が異なります。

range: 範囲スキャン。指定された範囲内の行が取得されたことを意味します。主に限定されたインデックス スキャンに使用されます。より一般的な範囲スキャンは、BETWEEN 句または WHERE 句と >、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、IN() などの演算子を使用するものです。

  • インデックス: フル インデックス スキャン。ALL と似ていますが、インデックス データ全体がインデックス スキャンされる点が異なります。このタイプは、クエリがインデックス内の列のサブセットのみを使用する場合に使用します。トリガーとなるシナリオは 2 つあります。
  • インデックス ツリーは、インデックスがクエリのカバー インデックスであり、クエリで必要なすべてのデータがインデックスによって満たされる場合にのみスキャンされます。このとき、Explain の Extra カラムの結果は、Using Index になります。通常、インデックスのサイズはテーブル データよりも小さいため、インデックスは ALL よりも高速です。
  • インデックス順にデータ行を検索するには、テーブル全体のスキャンが実行されます。現時点では、Uses インデックスは Explain の Extra 列の結果には表示されません。
  • ALL: フルテーブルスキャン、最悪のパフォーマンス。

possible_keys

現在のクエリで使用できるインデックスを示します。この列のデータは最適化プロセスの早い段階で作成されるため、一部のインデックスは後続の最適化プロセスでは役に立たない可能性があります。

MySQL によって実際に選択されたインデックスを示します。ファイルソートの使用と一時的な使用に注意することが重要です。前者は、ソート操作を完了するためにインデックスを使用できないことを意味します。データが小さい場合はメモリからソートされ、そうでない場合はメモリからソートされます。後者の MySQL では、結果を保存するために一時テーブルを作成する必要があります。

EXPLAIN を使用すると、SQL がインデックスを使用しているかどうか、使用されているインデックスが正しいかどうか、ソートが適切かどうか、およびインデックス列の区別を最初に特定することができ、これらを通じてほとんどの問題を基本的に特定できます。

step3.プランを指定する

SQL 自体では解決できない場合でも、ビジネス シナリオやデータ分散などの要因に基づいて合理的に変更計画を立てることができます。

ケースディスプレイ

1. この SQL には主に 2 つの問題があり、1 つはクエリ結果が約 20,000 件の大量のデータを含むこと、2 つ目は非インデックス フィールドoil_gun_price に従ってソートされ、ファイルソートが発生することです。変更オプションは 2 つあり、1 つはページング クエリに変換し、ID に従って昇順にソートし、ID オフセットに従ってディープ ページングの問題を回避する方法で、もう 1 つは、データの全量を直接取得する方法です。ソート方法を指定せずに条件を満たした場合、メモリ上でソートできます。このようなシナリオでは、並べ替えにデータベースを使用せず、並べ替えにインデックスを直接使用できない限り、すべてのデータをメモリに一度にロードするか、ページングでロードしてから並べ替えるようにしてください。

SELECT gs.id,
       gs.gas_code,
       gs.tpl_gas_code,
       gs.gas_name,
       gs.province_id,
       gs.province_name,
       gs.city_id,
       gs.city_name,
       gs.county_id,
       gs.county_name,
       gs.town_id,
       gs.town_name,
       gs.detail_address,
       gs.banner_image,
       gs.logo_image,
       gs.longitude,
       gs.latitude,
       gs.oil_gun_serials,
       gs.gas_labels,
       gs.status,
       gs.source,
       gp.oil_number,
       gp.oil_gun_price
FROM fi_club_oil_gas gs
LEFT JOIN fi_club_oil_gas_price gp ON gs.gas_code = gp.gas_code
WHERE oil_number = 95
  AND status = 1
  AND gs.yn = 1
  AND gp.yn=1
ORDER BY gp.oil_gun_price ASC;


2. この SQL の主な問題は、サブクエリが関連クエリの結合に使用されていることです。サブクエリには条件がほとんどありません。これは、最初にテーブル全体のスキャンを実行し、最初のクエリの結果をメモリにロードし、相関では、クエリ時間は 2.63 秒ですが、これは SQL が遅くなる一般的な原因であり、できるだけ回避する必要があります。ここでは、サブクエリが相関クエリに変更され、最終的な実行時間は 0.71 秒です。

SELECT count(0)
FROM trans_scheduler_base tsb
INNER JOIN
  (SELECT scheduler_code,
          vehicle_number,
          vehicle_type_code
   FROM trans_scheduler_calendar
   WHERE yn = 1
   GROUP BY scheduler_code) tsc ON tsb.scheduler_code = tsc.scheduler_code
WHERE tsb.type = 3
  AND tsb.yn = 1;

----------修改后--------------
SELECT count(distinct(tsc.scheduler_code))
FROM trans_scheduler_base tsb
LEFT JOIN trans_scheduler_calendar tsc ON tsb.scheduler_code = tsc.scheduler_code
WHERE tsb.type = 3
  AND tsb.yn = 1
  AND tsc.yn=1


3. この SQL は比較的典型的なもので、見落とされがちですが頻繁に出現する遅い SQL です。SQLではcarrier_codeとtrader_codeの両方にインデックスが存在しますが、最終的にupdate_timeインデックスが使用されるのは、MYSQLオプティマイザの最適化結果により、実際の実行で使用されるインデックスが想定と異なる場合があるためです。実際、クエリ SQL は、並べ替え方法、クエリ フィールド、返される項目数など、多くの場合完全には適用できません。そのため、異なるビジネス ロジックでは、個別に定義された独自の SQL を使用することをお勧めします。解決策としては、force_index を使用してインデックスを指定するか、状況に応じて並べ替え方法を変更することが考えられます。

SELECT id,
       carrier_name,
       carrier_code,
       trader_name,
       trader_code,
       route_type_name,
       begin_province_name,
       begin_city_name,
       begin_county_name,
       end_province_name,
       end_city_name,
       end_county_name
FROM carrier_route_config
WHERE yn = 1
  AND carrier_code ='C211206007386'
  AND trader_code ='010K1769496'
ORDER BY update_time DESC
LIMIT 10;


group by と order by を含む制限 N 個の SQL ステートメント (order by と group by のフィールドには使用可能なインデックスがあります) の場合、MySQL オプティマイザーは既存のインデックスの順序性を利用してソートを削減しようとします。これは、 SQL 実行プランの最適な解決策ですが、実際の効果はまったく異なる場合があります。SQL 実行プランが ID による順序のインデックスを選択し、インデックスを使用する代わりにテーブル全体をスキャンしてしまうケースに誰もが遭遇したことがあると思います。 where 条件でデータを検索およびフィルタリングすると、クエリが非常に非効率になる可能性があります (もちろん、クエリが非常に効率的になる場合もありますが、これはテーブル内のデータの特定の分布に関係します)。

制限による順序による最適化がプラスの役割を果たすことができるという前提は、まず順序付きインデックスと順序なしインデックスが無関係であると仮定し、次にデータが均等に分散していると仮定することです。

これら 2 つの前提は、ソートされたインデックスを介したアクセスのコストを見積もるための前提です (ただし、実際の運用環境では、これら 2 つの前提はほとんどのシナリオに当てはまらないため、ほとんどのシナリオでインデックスの選択が誤ることになります)。条件付きインデックス フィルタリングの時間は数十ミリ秒ですが、インデックス ソート スキャンには 1 時間かかります。これは MySQL のバグと考えられます。

4. SQL の制限も、SQL の速度低下につながる原因の 1 つです。制限を使用して SQL を制限する場合、SQL で使用される制限が残りのエントリの総数より大きく、使用されるインデックス条件が適切に機能しない場合があります。順序付けされた特性の場合、MYSQL はテーブル全体のスキャンを実行する可能性があります。たとえば、次の SQL では、SQL は実行時に create_time インデックスを使用しますが、条件に create_time が条件として含まれていないため、SQL 結果の総数は 6 であり、現時点での制限結果の 10 よりも少ないです。したがって、MYSQL はフル テーブル スキャンを実行しますが、これには時間がかかります。2.19 秒、制限を 6 に変更すると、条件を満たす 6 つの結果がクエリされたときに MYSQL が直接返すため、SQL 実行時間は 0.01 秒になります。また、テーブル全体のスキャンは実行されません。したがって、ページング クエリ データが 1 ページに収まらなくなった場合は、limit パラメータを手動で設定することをお勧めします。

SELECT cva.id,
       cva.carrier_vehicle_approval_code,
       dsi.driver_erp,
       d.driver_name,
       cva.vehicle_number,
       cva.vehicle_type,
       cva.vehicle_kind,
       cva.fuel_type,
       cva.audit_user_code,
       dsi.driver_id,
       cva.operate_type,
       dsi.org_code,
       dsi.org_name,
       dsi.prov_code,
       dsi.prov_name,
       dsi.area_code,
       dsi.area_name,
       dsi.node_code,
       dsi.node_name,
       dsi.position_name,
       cva.create_user_code,
       cva.audit_status,
       cva.create_time,
       cva.audit_time,
       cva.audit_reason,
       d.jd_pin,
       d.call_source,
       cv.valid_status
FROM driver_staff_info dsi
INNER JOIN carrier_vehicle_approval cva ON cva.driver_id = dsi.driver_id
INNER JOIN driver d ON dsi.driver_id = d.driver_id
INNER JOIN carrier_vehicle_info cv ON cv.vehicle_number = cva.vehicle_number
WHERE dsi.yn = 1
  AND d.yn = 1
  AND cva.yn = 1
  AND cv.yn = 1
  AND dsi.org_code = '3'
  AND dsi.prov_code = '021S002'
  AND cva.carrier_code = 'C230425013337'
  AND cva.yn = 1
  AND cva.audit_status = 0
  AND d.call_source IN ('kuaidi',
                        'kuaiyun')
ORDER BY cva.create_time DESC
LIMIT 10


5. 次の SQL テーブルには関連付けが多すぎるため、比較的大量のデータがデータベースにロードされます。実際の状況に応じて、最初に 1 つのテーブルのデータを基本データとして検索し、その後埋め込むことを選択できます。残りのフィールドはテーブルの接続条件に従って入力されます。大量のデータを含むテーブルに多数のテーブルを関連付けることはお勧めできませんが、適切な冗長フィールドや幅の広いテーブルの処理に置き換えることができます。

SELECT blsw.bid_line_code,
         blsw.bid_bill_code,
         blsw.bid_line_name,
         blsw.step_code,
         blsw.step_type,
         blsw.step_type_name,
         blsw.step_weight,
         blsw.step_weight_scale,
         blsw.block_price,
         blsw.max_weight_flag,
         blsw.id,
         blsw.need_quote_price,
         bbs.step_item_code,
         bbs.step_item_name,
         bbs.step_seq,
         bl.bid_line_seq
FROM bid_line_step_weight blsw
LEFT JOIN bid_bill_step bbs
    ON blsw.bid_bill_code = bbs.bid_bill_code
        AND blsw.step_code = bbs.step_code
        AND blsw.step_type = bbs.step_type
LEFT JOIN bid_line bl
    ON blsw.bid_line_code = bl.bid_line_code
        AND blsw.bid_bill_code = bl.bid_bill_code
WHERE blsw.yn = 1
        AND bbs.yn = 1
        AND bl.yn=1
        AND blsw.bid_bill_code = 'BL230423051192'; 


6. この SQL では、時間範囲インデックスとして update_time を使用していますが、ホット データが過度に集中し、クエリ データが非常に多くなり、SQL では直接解決できない複雑なソート条件が発生する問題がないか注意する必要があります。最適化。一方で、ホットデータの過剰集中の問題を解決する必要がありますが、他方では、データ量を削減するためにいくつかのデフォルト条件を追加するなど、ビジネス シナリオに応じて最適化する必要があります。

SELECT r.id,
         r.carrier_code,
         r.carrier_name,
         r.personal_name,
         r.status,
         r.register_org_name,
         r.register_org_code,
         r.register_city_name,
         r.verify_status,
         r.cancel_time,
         r.reenter_time,
         r.verify_user_code,
         r.data_source,
         r.sign_contract_flag,
         r.register_time,
         r.update_time,
         r.promotion_erp,
         r.promotion_name,
         r.promotion_pin,
         r.board_time,
         r.sync_basic_status,
         r.personal_verify_result,
        r.cert_verify_result,
        r.qualify_verify_result,
        r.photo_verify_result,
         d.jd_pin,
         d.driver_id,
         v.vehicle_number,
         v.vehicle_type,
         v.vehicle_length,
         r.cancellation_code ,
         r.cancellation_remarks
FROM carrier_resource r
LEFT JOIN carrier_driver d
    ON r.carrier_code = d.carrier_code
LEFT JOIN carrier_vehicle v
    ON r.carrier_code = v.carrier_code
WHERE r.update_time >= '2023-03-26 00:00:00'
        AND r.update_time <= '2023-04-02 00:00:00'
        AND r.yn = 1
        AND v.yn = 1
        AND d.yn = 1
        AND d.status != -1
        AND IFNULL(r.carrier_individual_type,'') != '2'
ORDER BY  (case r.verify_status
    WHEN 30 THEN
    1
    WHEN 20 THEN
    2
    WHEN 25 THEN
    3
    WHEN 35 THEN
    4
    WHEN 1 THEN
    5
    ELSE 6 end), r.update_time desc, if((v.driving_license_time IS null
        AND d.driver_license_time IS null), 0, 1) desc, if(((v.driving_license_time IS NOT null
        AND v.driving_license_time < NOW())
        OR (d.driver_license_time IS NOT null
        AND d.driver_license_time < NOW())), 2, 0) DESC LIMIT 10; 


実際の開発プロセスでは、過剰なクエリ データの読み込み、過剰なテーブル データ量、重大なデータ スキューなど、SQL 自体からの最適化が困難なシナリオが多数あります。ビジネスに基づいて必要な保護措置や制限を実装するようにしてください。ビジネスの状況では、クエリに ES を使用するなどの代替案を探すことができますが、実際のシナリオに基づいて別のソリューションを選択する必要があります。

7. 大量のデータを含む一部のテーブルでは、ページング クエリを実行すると結果がすぐに返されますが、アイテムの合計数のページング カウントを実行すると非常に遅くなることがよくあります。これは、ページング中に pageSize 制限があるためです。クエリ。MYSQL が項目数を満たすデータをクエリすると、そのデータが直接返されます。カウントする場合は、条件に従ってテーブル全体をクエリします。条件に含まれるデータの量が多すぎる場合は、テーブル全体をクエリします。 SQL のパフォーマンスを制限します。この場合、ページング ロジックを書き換えて count と selectList を分離することをお勧めします。ES を count データ ソースとして適用することを検討するか、特定の条件下でアイテムの総数が既に存在する場合は、カウントを長くし、ページング カウントの数を減らします。一方、ページングの深さを制限して、深いページングを回避します。

全体最適化原理

  • 適切なインデックスを作成する
  • 列への不要なアクセスを減らす
  • カバリングインデックスを使用する
  • ステートメントの書き換え
  • データ繰り越し
  • ソートする適切な列を選択します
  • 適切な列冗長性
  • SQL分割
  • ESの適切な適用

著者: JD Logistics Li Wenhao

出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください

Bunが正式バージョン1.0をリリース、 JavaScriptがZigによって書かれたランタイム時の Windowsファイルエクスプローラーの魔法のバグ、1秒でパフォーマンスが向上 JetBrainsがRust IDEをリリース:RustRover PHPの最新統計:市場シェアは70%を超え、CMSの王様が Pythonプログラムを移植Mojo、パフォーマンスは 250 倍向上し、C よりも高速です 。.NET 8 のパフォーマンスは大幅に向上し、.NET 7 をはるかに上回っています。 JS の 3 つの主要なランタイム: Deno、Bun、Node.js の比較 Visual Studio Code 1.82 NetEase Fuxi は従業員の「バグのため人事に脅されて亡くなった」に応じました。 Unity エンジンは来年からゲームのインストール数に応じて課金されるようになります (ランタイム料金)。
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10110517