Holen Sie sich die letzten 5 unterschiedlichen Werte für jede ID

7

Ich arbeite mit PostgreSQL 9.4.

Ich habe eine Tabelle, die die folgenden Einträge enthält:

 id | postcode | date_created
 ---+----------+-----------------
 14 | al2 2qp  | 2015-09-23 14:46:57
 14 | al2 2qp  | 2015-09-23 14:51:07
 14 | sp2 8ag  | 2015-09-23 14:56:11
 14 | se4      | 2015-09-23 16:12:05
 17 | e2       | 2015-09-23 16:15:35
 17 | fk20 8ru | 2015-09-23 16:28:35
 17 | fk20 8ru | 2015-09-23 16:35:51
 17 | se2      | 2015-09-23 16:36:17
 17 | fk20 8ru | 2015-09-23 16:36:22
 17 | fk20 8ru | 2015-09-23 16:37:04
 17 | se1      | 2015-09-23 16:37:11
 17 | fk20 8ru | 2015-09-23 16:37:15
 17 | se1 8ga  | 2015-09-24 09:52:46
 17 | se1      | 2015-09-24 10:01:19
 17 | hp27 9rz | 2015-09-24 10:05:27
 17 | hp27 9rz | 2015-09-24 10:05:29
 17 | se1      | 2015-09-24 10:19:46
 14 | tn21 8qb | 2015-09-24 14:49:05
 14 | tn21 8qb | 2015-09-24 15:42:45
 14 | tn21 8qb | 2015-09-24 17:38:06
 14 | n4 1ny   | 2015-09-25 14:49:10

Was ich erreichen möchte, ist eine Abfrage, die die 5 neuesten eindeutigen Postleitzahlendatensätze für jede ID zurückgibt :

 id | postcode
 ---+---------
 14 | n4 1ny
 14 | tn21 8qb
 14 | se4
 14 | sp2 8ag
 14 | al2 2qp
 17 | se1
 17 | hp27 9rz
 17 | se1 8ga
 17 | fk20 8ru
 17 | se2

Was wäre der beste Weg, um dies zu erreichen? Ich habe mit Unterabfragen herumgespielt, aber immer wieder auf Wände gestoßen, wenn es darum geht, sie zu bestellen, während ich ein DISTINCTund mache GROUP BY.

RoboBex
quelle
1
Wie so oft hätte die genaue Tabellendefinition mit Datentypen und Einschränkungen sehr geholfen: Was Sie \d tblin psql erhalten.
Erwin Brandstetter
Danke @ErwinBrandstetter. Ich werde sicher in zukünftige Beiträge aufnehmen.
RoboBex

Antworten:

9

Es gibt wahrscheinlich viele Möglichkeiten, dies zu tun. Das erste, was mir in den Sinn kommt, ist die Verwendung von Fensterfunktionen:

SELECT 
    id, postcode
FROM
  ( SELECT id, postcode, 
           ROW_NUMBER() OVER (PARTITION BY id
                              ORDER BY MAX(date_created) DESC
                             ) AS rn
    FROM tablename
    GROUP BY id, postcode
  ) AS t
WHERE
    rn <= 5
ORDER BY 
    id, rn ;

Test bei SQLfiddle .

Wenn es Bindungen gibt, sagen wir die 5., 6. und 7. postcodefür eine idhaben die gleichen date_created, wird nur eine von ihnen (Auswahl wird willkürlich sein) in den Ergebnissen sein. Wenn Sie in diesen Fällen alle gebundenen Postleitzahlen möchten, verwenden Sie RANK()stattdessen anstelle von ROW_NUMBER().


Eine andere Möglichkeit ist die Verwendung der LATERALSyntax. Ich bin mir nicht sicher, welches effizienter sein wird. Es wird wahrscheinlich von der Werteverteilung der beiden Spalten ( idund postcode) abhängen , dh wie viele unterschiedliche IDs in der gesamten Tabelle, wie viele unterschiedliche Postleitzahlen pro ID und wie viele Zeilen pro (ID) , Postleitzahl) Kombinationen.

SELECT 
    t.id, ti.postcode
FROM
    ( SELECT DISTINCT id
      FROM tablename
    ) AS t
  CROSS JOIN LATERAL
    ( SELECT tt.postcode,
             MAX(tt.date_created) AS date_created
      FROM tablename AS tt
      WHERE tt.id = t.id
      GROUP BY tt.postcode
      ORDER BY date_created DESC
      LIMIT 5
    ) AS ti 
ORDER BY 
    t.id, ti.date_created DESC;

Ein Index hinzuzufügen (id, postcode, date_created)wäre auch eine gute Idee - oder weiter (id, postcode, date_created DESC).

ypercubeᵀᴹ
quelle
Danke @ypercube. Wir haben einen Index hinzugefügt und arbeiten jetzt mit Ihrem ersten Vorschlag.
RoboBex
5

Normalerweise haben Sie eine andere Tabelle (nennen wir sie tbl) mit allen unterschiedlichen idWerten in separaten Zeilen. Wenn Sie dies nicht tun, erstellen Sie es:

CREATE TABLE tbl AS 
SELECT DISTINCT id FROM postcode ORDER BY id;  -- ORDER is optional

Oder ersetzen Sie die tblunten stehende Abfrage durch dieselbe SELECTwie die Unterabfrage, aber das ist (viel) teurer.

Wenn es mehrere Zeilen pro Zeile geben kann id, sollte ein rekursiver CTE am schnellsten sein:

WITH RECURSIVE cte AS (
   SELECT t.id, 1 AS rnk, p.*, ARRAY[postcode] AS arr
   FROM   tbl t
        , LATERAL (
      SELECT postcode, date_created
      FROM   postcode
      WHERE  id = t.id
      ORDER  BY date_created DESC NULLS LAST
      LIMIT  1
      ) p

   UNION ALL
   SELECT t.id, rnk + 1, p.*, arr || p.postcode
   FROM   cte t
        , LATERAL (
      SELECT postcode, date_created
      FROM   postcode
      WHERE  id = t.id
      AND    date_created < t.date_created
      AND    postcode <> ALL (t.arr)
      ORDER  BY date_created DESC NULLS LAST
      LIMIT  1
      ) p
   WHERE rnk < 5
   )
SELECT id, rnk, postcode, date_created
FROM   cte
ORDER  BY id, rnk;

Angenommen postcode, textoder varchar. Bei dieser speziellen Abfrage können Probleme auftreten, wenn postcodeTypmodifikatoren (ähnlich varchar(50)oder ähnlich) vorhanden sind:

Ein Index für (id, date_created)ist für die Leistung bei großen Tabellen von entscheidender Bedeutung:

CREATE INDEX postcode_foo_idx ON postcode(id, date_created DESC NULLS LAST);

SQL Fiddle.

Sie können NULLS LASTüberall überspringen , wenn dies date_createddefiniert ist NOT NULL.

Wenn deutlich mehr als 5 Zeilen pro ida rare Fall @ ypercube der Anfragen wird schneller sein. Test mit EXPLAIN ANALYZE.

Der Unterschied: Mein rCTE ist mit mehr Overhead verbunden, aber die Leistung wird kaum von älteren überschüssigen Zeilen beeinflusst (diese werden in der Abfrage nicht berührt). Beide Abfragen von @ ypercube haben weniger Overhead, werden jedoch langsamer mit mehr Zeilen pro id.

Grundlagen mit Links und mehr Erklärung:

Wenn Sie keinen Tisch haben tbl, können Sie eine ähnliche Technik verwenden, um sich idvon postcodeder ersten zu unterscheiden:

Erwin Brandstetter
quelle
2
@ypercube hat unsere Kommentare zu einem möglichen Fehler mit Typmodifikatoren in einen Chat verschoben . Hier ist die sich daraus ergebende Folgefrage .
Erwin Brandstetter
Danke für deine Antwort. Wir arbeiten momentan mit einer kleinen Datenmenge, daher habe ich zunächst die erste Abfrage von ypercube durchgeführt. Die Daten werden jedoch erheblich zunehmen, so dass ich auch Ihren Weg versuchen werde. Ich habe vorher noch nicht mit rekursiv gearbeitet, also werde ich zuerst ein wenig recherchieren. Danke noch einmal.
RoboBex