Wie kann der nächstgelegene Wert anhand der Nachschlagetabelle abgerufen werden?


7

Ich versuche, eine Abfrage zu erstellen, die den nächstgelegenen Wert aus einer Tabelle findet und deren ID in die resultierende Tabelle zurückgibt.

Unten finden Sie ein Beispiel, das die Situation besser beschreiben sollte.

Beispieldaten

Diese beiden Tabellen sind in der SQL-Datenbank vorhanden.

Haupttisch

+----+-------------+
| ID | Measurement |
+----+-------------+
|  1 | 0.24        |
|  2 | 0.5         |
|  3 | 0.14        |
|  4 | 0.68        |
+----+-------------+

Nachschlagwerk

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

Tor

Dies ist das Ergebnis einer Abfrage. Die Messungen sollten nicht an der Grenze erfolgen (z. B. 0,25).

+----+-------------+-----------+
| ID | Measurement | Lookup ID |
+----+-------------+-----------+
|  1 | 0.24        |         2 |
|  2 | 0.5         |         5 |
|  3 | 0.14        |         1 |
|  4 | 0.68        |         7 |
+----+-------------+-----------+

Gibt es eine Abfrage, die ein solches Ergebnis zurückgeben könnte?


Dies klingt sehr nach einer räumlichen "nächstgelegenen" Suche, die implementiert werden kann, um bestimmte spezielle Indextypen zu verwenden.
Colin 't Hart

Benötigen Sie dies für eine ganze Tabelle auf einmal oder nur für eine einzelne oder ausgewählte Zeile? Geben Sie außerdem immer Ihr RDBMS und Ihre Version an.
Erwin Brandstetter

Schön, all die verschiedenen Ansätze hier zu sehen. Und es ist auch schön, Lösungen für verschiedene DBMS zu haben.
Colin 't Hart

Ich würde das RDBMS und die Version angeben, wenn ich könnte. Ich arbeite daran, eine Excel-Datei mit mehreren Tabellen auf eine Art Datenbank zu migrieren. Dies ist jedoch ein Praktikum und Zweifel, dass ein RDBMS ausgewählt wird, bevor ich gehe. Derzeit mache ich ein
Modell

Antworten:


5

Einige Abfragen, die für Postgres 9.3 getestet und optimiert wurden. Alle geben das gleiche zurück, alle sind im Grunde Standard-SQL, aber kein RDBMS unterstützt den Standard vollständig.

Insbesondere verwendet der erste einen LATERAL JOIN, der in Oracle oder MySQL fehlt. Test, der am besten funktioniert.
Alle verwenden nur Index-Scans für die lookupTabelle in Postgres. Muss natürlich lookup.nominal_valueindiziert werden. Ich schlage vor , es zu machen , UNIQUEweil es scheint , als ob die Spalt sollten eindeutig sein, und weil das schafft auch den wichtigen Index automatisch.

LATERAL JOIN

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;

Alle Klammern erforderlich für UNION. Verwandte Antwort:
Postgres 9.2 wählt mehrere spezifische Zeilen in einer Abfrage aus

Korrelierte Unterabfragen in einer Unterabfrage

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;

Korrelierte Unterabfragen in einem 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;

Verschachtelte korrelierte Unterabfragen

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 Fiddle.


1
Oracle unterstützt seitliche apply
Verknüpfungen

4

Sie sind sich nicht sicher, welches DBMS Sie verwenden, aber heutzutage gibt es einige Funktionen des Support-Fensters:

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;

1

Dies ist durchaus möglich, obwohl der einzige Weg, den ich mir vorstellen kann, um dies zu lösen, ziemlich ineffizient ist und wirklich nicht sehr gut skaliert.

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

Eine andere Lösung, die möglicherweise besser skaliert / leistungsfähiger ist, verwendet geordnete Fensterfunktionen (verfügbar unter SQL Server 2012 und 2014 sowie einigen anderen Datenbankplattformen, jedoch nicht unter 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);

Wenn bei dieser Abfrage weiterhin Leistungsprobleme auftreten, erstellen Sie eine temporäre Nachschlagetabelle, füllen Sie sie mit den Zeilen aus "lkp" und verbinden Sie "t" und "lkp" wie oben beschrieben. Ich würde der temporären Tabelle wahrscheinlich einen Index wie geben

CREATE UNIQUE INDEX IX_temptable ON #temptable (fromValue) INCLUDE (toValue, ID);

Welche Lösung für Sie am besten geeignet ist, hängt in erster Linie davon ab, wie viele Daten Sie haben. Probieren Sie die verschiedenen Lösungen aus.


1

Ich hoffe, ich vermisse nichts Offensichtliches, aber die Art und Weise, wie ich dies abfragen würde, um es für eine sehr große Nachschlagetabelle zu skalieren, ist Folgendes zu beachten:

Es ist möglich, ein kompetentes DBMS (ich weiß, dass PostgreSQL dies kann) dazu zu bringen, einen Index zu verwenden

  • Suchen Sie nach dem größten Suchwert, der kleiner als unsere Messung ist, und nach
  • Suchen Sie nach dem kleinsten Suchwert, der größer als unsere Messung ist.

Sobald wir diese beiden Werte haben, können wir bestimmen, welcher der beiden näher ist.

Also so etwas wie ungetestet:

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;

sollte blitzschnell sein - es sind im Grunde immer zwei Index-Lookups und nichts weiter.

Nachdem Sie dies alles geschrieben haben, sollte es möglich sein, eine Fensterfunktion zu verwenden, um nur einen Index-Scan für die beiden Kandidatenwerte auf beiden Seiten des "Mess" -Werts durchzuführen. Der obige Ansatz erfordert jedoch keine Fensterfunktionen und sollte für keine funktionieren DBMS, das einen Index "durchlaufen" kann, anstatt einen auszuführen order by.


Es ist spät hier. Ich hoffe, dass das oben genannte nicht Brainfart ist.
Colin 't Hart

0

Ich habe Lennarts Antwort verwendet und musste nur in der Reihenfolge von absteigend auf aufsteigend ändern. Es hat wunderbar funktioniert und war nicht allzu kompliziert.

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.