web-dev-qa-db-ja.com

ルックアップテーブルに基づいて最も近い値を取得する方法は?

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 |
+----+-------------+-----------+

この種の結果を返すことができるクエリはありますか?

7
pjbollinger

Postgres 9.3用にテストおよび最適化されたいくつかのクエリ。すべてが同じものを返し、すべてが基本的に標準SQLですが、RDBMSが標準を完全にサポートすることはありません。

特に、最初のものは LATERAL JOINOracleまたは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;

CTEの相関サブクエリ

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;

SQLフィドル

5

どの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;
4
Lennart

これは完全に可能ですが、これを解決するために私が考えることができる唯一の方法は非常に非効率的であり、実際にはあまりスケーリングしません。

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);

どのソリューションが最適かは、主にデータの量に依存します。さまざまな解決策を試してください。

1

私は明らかなものを見逃していないことを願っていますが、非常に大きなルックアップテーブルに対してスケーリングするためにこれをクエリする方法は、次のことを観察することです。

有能な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。

1
Colin 't Hart

私は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
0
NMDJ