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つの問題は.*
、内部クエリでのinの使用は少し危険であり、そのクエリによって返される列名が一意であることを実際に保証する必要があるということです。(現在、列名が異なる場合でも、これらのテーブルの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 <
述語を追加して、キャッシュテーブルのサイズを大幅に削減できます。