sql >> データベース >  >> RDS >> Mysql

MYSQLは距離がありますが、グループ化できませんか?

    GROUPBYがあなたが望む結果をもたらすとは思わない。残念ながら、MySQLは分析関数をサポートしていません(これが、OracleまたはSQL Serverでこの問題を解決する方法です)。

    ユーザー定義の変数を利用することで、いくつかの基本的な分析関数をエミュレートすることができます。

    この場合、エミュレートする必要があります:

    ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq
    

    そこで、元のクエリから始めて、doctor_idでソートされるようにORDERBYを変更しました。 最初に、次に計算されたdistance 。 (これらの距離がわかるまで、どれが「最も近い」かはわかりません。)

    このソートされた結果を使用して、基本的に各doctor_idの行に「番号を付け」、最も近い行を1、2番目に近い行を2というように続けます。新しいdoctor_idを取得したら、最も近いものを1からやり直します。

    これを実現するために、ユーザー定義変数を利用します。行番号の割り当てに1つ使用します(変数名は@iで、返される列のエイリアスはseqです)。前の行のdoctor_idを「記憶」するために使用するもう1つの変数。これにより、doctor_idの「中断」を検出できるため、行番号を1から再開するタイミングを知ることができます。

    クエリは次のとおりです:

    SELECT z.*
    , @i := CASE WHEN z.doctor_id = @prev_doctor_id THEN @i + 1 ELSE 1 END AS seq
    , @prev_doctor_id := z.doctor_id AS prev_doctor_id
    FROM
    (
    
      /* original query, ordered by doctor_id and then by distance */
      SELECT zip, 
      ( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) ) AS distance, 
      user_info.*, office_locations.* 
      FROM zip_info 
      RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip 
      RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id 
      WHERE user_info.status='yes' 
      ORDER BY user_info.doctor_id ASC, distance ASC
    
    ) z JOIN (SELECT @i := 0, @prev_doctor_id := NULL) i
    HAVING seq = 1 ORDER BY z.distance
    

    元のクエリが必要な結果セットを返していると仮定しています。行が多すぎて、各doctor_idの「最も近い」(距離の値が最小の行)以外のすべてを削除したいとします。

    元のクエリを別のクエリでラップしました。元のクエリに加えた唯一の変更は、結果をdoctor_idで並べ替えてから距離で並べ替え、HAVING distance < 50を削除することでした。 句。 (50未満の距離のみを返したい場合は、先に進んでその句をそのままにしておきます。それが意図したものなのか、それとも、doctor_idごとに行を1つに制限するために指定されたものなのかは明確ではありませんでした。)

    注意すべきいくつかの問題:

    置換クエリは2つの追加の列を返します。これらは、結果セットを生成する手段を除いて、結果セットでは実際には必要ありません。 (このSELECT全体を別のSELECTで再度ラップして、これらの列を省略することは可能ですが、それは価値があるよりも実際には厄介です。列を取得するだけで、無視できることがわかります。)

    もう1つの問題は、.*の使用です。 内部クエリでは、そのクエリによって返される列名が一意であることを実際に保証する必要があるという点で、少し危険です。 (現在、列名が異なる場合でも、これらのテーブルの1つに列を追加すると、クエリに「あいまいな」列例外が発生する可能性があります。これを回避するのが最善であり、.* 返される列のリストと、「重複する」列名のエイリアスを指定します。 (z.*の使用 zによって返される列を制御している限り、外部クエリでの問題はありません。 。)

    補遺:

    GROUP BYでは、必要な結果セットが得られないことに注意しました。 GROUP BYを使用したクエリで結果セットを取得することは可能ですが、正しい結果セットを返すステートメントは面倒です。 MIN(distance) ... GROUP BY doctor_idを指定できます 、そしてそれはあなたに最小の距離を与えるでしょう、しかしSELECTリストの他の非集計式が最小の距離を持つ行からのものであり、他の行からのものであるという保証はありません。 (MySQLは、GROUP BYと集計に関して危険なほど寛大です。MySQLエンジンをより慎重にするために(そして他のリレーショナルデータベースエンジンと一致させるために)、SET sql_mode = ONLY_FULL_GROUP_BY

    補遺2:

    Dariousによって報告されたパフォーマンスの問題「一部のクエリには7秒かかります。」

    処理を高速化するには、関数の結果をキャッシュすることをお勧めします。基本的に、ルックアップテーブルを作成します。例:

    CREATE TABLE office_location_distance
    ( office_location_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to office_location.id'
    , zipcode_id         INT UNSIGNED NOT NULL COMMENT 'PK, FK to zipcode.id'
    , gc_distance        DECIMAL(18,2)         COMMENT 'calculated gc distance, in miles'
    , PRIMARY KEY (office_location_id, zipcode_id)
    , KEY (zipcode_id, gc_distance, office_location_id)
    , CONSTRAINT distance_lookup_office_FK
      FOREIGN KEY (office_location_id) REFERENCES office_location(id)
      ON UPDATE CASCADE ON DELETE CASCADE
    , CONSTRAINT distance_lookup_zipcode_FK
      FOREIGN KEY (zipcode_id) REFERENCES zipcode(id)
      ON UPDATE CASCADE ON DELETE CASCADE
    ) ENGINE=InnoDB
    

    それはただのアイデアです。 (特定の郵便番号からoffice_location距離を検索していると思います。したがって、(zipcode、gc_distance、office_location_id)のインデックスは、クエリに必要なカバーインデックスです(計算された距離をFLOATとして保存することは避けます。 FLOATデータ型を使用したクエリパフォーマンス)

    INSERT INTO office_location_distance (office_location_id, zipcode_id, gc_distance)
    SELECT d.office_location_id
         , d.zipcode_id
         , d.gc_distance
      FROM (
             SELECT l.id AS office_location_id
                  , z.id AS zipcode_id
                  , ROUND( <glorious_great_circle_calculation> ,2) AS gc_distance
               FROM office_location l
              CROSS
               JOIN zipcode z
              ORDER BY 1,3
           ) d
    ON DUPLICATE KEY UPDATE gc_distance = VALUES(gc_distance)
    

    関数の結果がキャッシュされてインデックスが作成されると、クエリははるかに高速になります。

    SELECT d.gc_distance, o.*
      FROM office_location o
      JOIN office_location_distance d ON d.office_location_id = o.id
     WHERE d.zipcode_id = 63101
       AND d.gc_distance <= 100.00
     ORDER BY d.zipcode_id, d.gc_distance
    

    INSERT/UPDATEのHAVING述語をキャッシュテーブルに追加することを躊躇しています。 (緯度/経度が間違っていて、100マイル未満で誤った距離を計算した場合、緯度/経度が固定されて距離が1000マイルになった後の後続の実行...行がクエリから除外されている場合、その場合、キャッシュテーブルの既存の行は更新されません(キャッシュテーブルをクリアすることはできますが、実際には必要ありません。データベースとログに多くの余分な作業が必要です。メンテナンスクエリの結果セットも大きい場合は、zipcodeごと、またはoffice_locationごとに繰り返し実行するように分割できます。)

    一方、特定の値を超える距離に関心がない場合は、HAVING gc_distance <を追加できます。 述語を作成し、キャッシュテーブルのサイズを大幅に削減します。



    1. MYSQL:Where句があいまいです

    2. 1か月あたりのすべての日のリストを作成し、値を毎日均等に分割します

    3. PDO ::ATTR_EMULATE_PREPARESを無効にすると、「不明」の問題が発生します

    4. SQL制約とそのさまざまなタイプとは何ですか?