1つのテーブルから最も近い値を見つけ、そのIDを結果のテーブルに返すクエリを作成しようとしています。
以下は、状況をよりよく説明する例です。
これらの2つのテーブルはSQLデータベースに存在します。
+----+-------------+
| ID | Measurement |
+----+-------------+
| 1 | 0.24 |
| 2 | 0.5 |
| 3 | 0.14 |
| 4 | 0.68 |
+----+-------------+
+----+---------------+
| ID | Nominal Value |
+----+---------------+
| 1 | 0.1 |
| 2 | 0.2 |
| 3 | 0.3 |
| 4 | 0.4 |
| 5 | 0.5 |
| 6 | 0.6 |
| 7 | 0.7 |
| 8 | 0.8 |
| 9 | 0.9 |
+----+---------------+
これはクエリの結果になります。測定値は境界線上に配置しないでください(たとえば、0.25)。
+----+-------------+-----------+
| ID | Measurement | Lookup ID |
+----+-------------+-----------+
| 1 | 0.24 | 2 |
| 2 | 0.5 | 5 |
| 3 | 0.14 | 1 |
| 4 | 0.68 | 7 |
+----+-------------+-----------+
この種の結果を返すことができるクエリはありますか?
Postgres 9.3用にテストおよび最適化されたいくつかのクエリ。すべてが同じものを返し、すべてが基本的に標準SQLですが、RDBMSが標準を完全にサポートすることはありません。
特に、最初のものは LATERAL JOIN
、 OracleまたはMySQLにはありません。 最高のパフォーマンスを発揮するテスト。
それらはすべて、Postgresのlookup
テーブルでインデックスのみのスキャンを使用します。明らかに、lookup.nominal_value
にインデックスを付ける必要があります。列は一意である必要があるようであり、重要なインデックスも自動的に作成されるため、UNIQUE
にすることをお勧めします。
SELECT m.id, m.measurement, l.nominal_value
FROM measurement m
JOIN LATERAL (
(
SELECT nominal_value - m.measurement AS diff, nominal_value
FROM lookup
WHERE nominal_value >= m.measurement
ORDER BY nominal_value
LIMIT 1
)
UNION ALL
(
SELECT m.measurement - nominal_value, nominal_value
FROM lookup
WHERE nominal_value <= m.measurement
ORDER by nominal_value DESC
LIMIT 1
)
ORDER BY 1 -- NULLS LAST is default
LIMIT 1
) l ON TRUE;
UNION
にはすべての括弧が必要です。関連する回答:
Postgres 9.2は1つのクエリで複数の特定の行を選択します
SELECT id, measurement
,CASE WHEN hi - measurement > measurement - lo
THEN lo
ELSE COALESCE(hi, lo) -- cover all possible NULL values
END AS nominal_value
FROM (
SELECT id, measurement
,(SELECT nominal_value
FROM lookup
WHERE nominal_value >= m.measurement
ORDER BY nominal_value
LIMIT 1) AS hi
,(SELECT nominal_value
FROM lookup
WHERE nominal_value <= m.measurement
ORDER by nominal_value DESC
LIMIT 1) AS lo -- cover possible NULL values
FROM measurement m
) sub;
WITH cte AS (
SELECT id, measurement
,(SELECT nominal_value
FROM lookup
WHERE nominal_value >= m.measurement
ORDER BY nominal_value
LIMIT 1) AS hi
,(SELECT nominal_value
FROM lookup
WHERE nominal_value <= m.measurement
ORDER by nominal_value DESC
LIMIT 1) AS lo
FROM measurement m
)
SELECT id, measurement
,CASE WHEN hi - measurement > measurement - lo
THEN lo
ELSE COALESCE(hi, lo) -- cover all possible NULL values
END AS nominal_value
FROM cte;
SELECT id, measurement
,(SELECT nominal_value FROM (
(
SELECT nominal_value - m.measurement, nominal_value
FROM lookup
WHERE nominal_value >= m.measurement
ORDER BY nominal_value
LIMIT 1
)
UNION ALL
(
SELECT m.measurement - nominal_value, nominal_value
FROM lookup
WHERE nominal_value <= m.measurement
ORDER by nominal_value DESC
LIMIT 1
)
ORDER BY 1
LIMIT 1
) sub
) AS nominal_value
FROM measurement m;
どのDBMSを使用しているかはわかりませんが、最近はかなりの数のサポートウィンドウ機能があります。
SELECT id, measurement, lookupid
FROM (
SELECT t1.id, t1.measurement, t2.id as lookupid
, row_number() over (partition by t1.id
order by abs(t1.measurement - t2.nominal) desc
) as rn
FROM main t1
CROSS JOIN lookup t2
) AS T
WHERE rn = 1;
これは完全に可能ですが、これを解決するために私が考えることができる唯一の方法は非常に非効率的であり、実際にはあまりスケーリングしません。
SELECT t.ID, t.Measurement,
(SELECT TOP 1 lkp.ID
FROM lookupTable AS lkp
ORDER BY ABS(lkp.NominalValue-t.Measurement)) AS LookupID
FROM mainTable AS t
スケーリング/パフォーマンスが向上する可能性がある別のソリューションは、順序付けされたウィンドウ関数を使用します(SQL Server 2012および2014、および他のいくつかのデータベースプラットフォームで使用可能ですが、Azureは使用できません)。
WITH lkp AS (
SELECT ID,
--- fromValue is the average of the previous NominalValue and this one:
(NominalValue+LAG(NominalValue, 1) OVER (ORDER BY NominalValue))/2.0 AS fromValue,
--- toValue is the average of the next NominalValue and this one:
(NominalValue+LEAD(NominalValue, 1) OVER (ORDER BY NominalValue))/2.0 AS toValue
FROM dbo.LookupTable)
SELECT t.ID, t.Measurement, lkp.ID AS LookupID
FROM MainTable AS t
LEFT JOIN lkp ON
--- The first lookup value will have fromValue=NULL
(t.Measurement>=lkp.fromValue OR lkp.fromValue IS NULL) AND
--- The last lookup value will have toValue=NULL
(t.Measurement<lkp.toValue OR lkp.toValue IS NULL);
このクエリでもパフォーマンスの問題が発生する場合は、一時的なルックアップテーブルを作成し、「lkp」の行を入力してから、上記のように「t」と「lkp」を結合してください。私はおそらく一時テーブルに次のようなインデックスを与えるでしょう
CREATE UNIQUE INDEX IX_temptable ON #temptable (fromValue) INCLUDE (toValue, ID);
どのソリューションが最適かは、主にデータの量に依存します。さまざまな解決策を試してください。
私は明らかなものを見逃していないことを願っていますが、非常に大きなルックアップテーブルに対してスケーリングするためにこれをクエリする方法は、次のことを観察することです。
有能なDBMS(PostgreSQLでできることを知っています)を取得して、インデックスを使用して
これらの2つの値を取得したら、どちらが近いかを判断できます。
したがって、未テストのようなもの:
with candidates as (
select id, nominal_value
from lookup_table
where nominal_value >= measurement
order by nominal_value
limit 1
union
select id, nominal_value
from lookup_table
where nominal_value <= measurement
order by nominal_value desc
limit 1
)
select id
from candidates
order by abs(nominal_value - measurement)
limit 1;
非常に高速である必要があります-常に基本的に2つのインデックス検索であり、それ以上のものはありません。
これをすべて記述したら、ウィンドウ関数を使用して、「測定」値の両側にある2つの候補値に対して1回だけインデックススキャンを実行できるはずですが、上記のアプローチはウィンドウ関数を必要とせず、すべてで機能するはずです。 order by
を実行する代わりにインデックスを「ウォーク」できるDBMS。
私はLennartの回答を使用しました。必要なのは、descをascに変更することだけです。それは美しく機能し、過度に複雑ではありませんでした。
SEL
ECT id, measurement, lookupid
FROM (
SELECT t1.id, t1.measurement, t2.id as lookupid
, row_number() over (partition by t1.id
order by abs(t1.measurement - t2.nominal) asc
) as rn
FROM main t1
CROSS JOIN lookup t2
) AS T
WHERE rn = 1