DISTINCT in PostgreSQL

25.08.2017

Ich habe sehr lange ausschließlich MySQL benutzt. Mein Chef bei busnetworx, wollte unbedingt PostgreSQL einsetzten. Aus diesem Grund habe ich mir PostgreSQL genauer angesehen. Das Schlüsselwort DISTINCT ist in PostgreSQL mächtiger als in MySQL und ich möchte es im einmal erklären.

SELECT DISTINCT

Ich habe eine einfache Mitarbeitertabelle angelegt, wie man sie in vielen Tutorials findet. Die Testdaten habe ich mit mockaroo generiert.

test=# \d employee
                                  Table "public.employee"
   Column   |         Type          |                       Modifiers                       
------------+-----------------------+-------------------------------------------------------
 id         | integer               | not null default nextval('employee_id_seq'::regclass)
 first_name | character varying(50) | 
 name       | character varying(50) | 
 department | character varying(50) | 
 salary     | integer               | 
Indexes:
    "employee_pkey" PRIMARY KEY, btree (id)

test=# SELECT * FROM employee limit 3;
 id | first_name |  name   | department  | salary 
----+------------+---------+-------------+--------
  1 | Francklin  | Gurr    | Services    |   4207
  2 | Cecily     | Churm   | Engineering |   3728
  3 | King       | Tribell | Training    |   3780
(3 rows)

Was macht DISTINCT

SELECT DISTINCT entfernt Duplikate aus der Ergebnismenge.

So erhält man eine Liste mit allen Abteilungen:

SELECT DISTINCT department FROM employee;
        department        
--------------------------
 Engineering
 Training
 Business Development
 Sales
 Marketing
 Accounting
 Services
 Support
 Research and Development
 Legal
 Product Management
 Human Resources
(12 rows)

Mir ist durchaus bewusst, dass das Schema nit normalisiert ist. Aber das Beispiel sollte sehr einfach sein, um das Verhalten von DISTINCT zu zeigen.

In diesem Beispiel erhält das identische Ergebnis, wenn man GROUP BY benutzt. GROUP BY kann im Gegegsatz zu DISTINCT potenziell einen Index nutzen, das ist auf großen Tabellen von Vorteil.

SELECT department FROM employee GROUP BY department;

DISTINCT ON

Nun möchte man wissen, welcher Mitarbeiter das höchste Gehalt in seine Anteilung hat. Dazu wird in einer Subquery das höchste Gehalt der jeweiligen Abteilungen bestimmt. Danach sucht man alle Mitarbeiter, welche dieses Gehalt in der jeweiligen Abteilung haben.

SELECT  
  *
FROM
  employee
WHERE
  (department, salary) IN (
    SELECT 
      department,
      MAX(salary)
    FROM
      employee
    GROUP BY
      department
    )
ORDER BY
    department;
  id  | first_name |   name    |        department        | salary 
------+------------+-----------+--------------------------+--------
 4841 | Caresse    | Simonian  | Accounting               |   9971
 4117 | Hedda      | Whifen    | Business Development     |   9936
 4433 | Delmar     | Pieper    | Engineering              |   9993
 5239 | Pebrook    | Roycraft  | Human Resources          |   9991
 4336 | Murry      | Cadwaladr | Legal                    |   9974
 5840 | Hayes      | Levee     | Marketing                |  10000
 3656 | Geno       | Culmer    | Marketing                |  10000
 3075 | Harland    | Dendle    | Product Management       |   9917
 5599 | Kayne      | Oppy      | Research and Development |   9984
 4065 | Alric      | de Zamora | Sales                    |   9993
 5178 | Brunhilde  | Glanton   | Services                 |   9995
 5988 | Grier      | Gotcliffe | Support                  |   9984
 5920 | Gray       | Bizley    | Training                 |   9955
(13 rows)

Wenn 2 Mitarbeiter das höchste Gehalt einer Abteilung haben, dann werden beide ausgegeben. So ist es bei der Abteilung Marketing der Fall.

Wenn man nur ein Datensatz pro Abteilung haben möcht, muss man mit den Window Functions arbeiten. Ich habe mit diesen Funktionen noch nie gearbeitet, ich kenne sie nur von Stackoverflow und der Dokumentation.

WITH ranked_employee AS (
  SELECT 
    ROW_NUMBER() OVER (
        PARTITION BY department ORDER BY salary DESC
      ) AS row,
      *
    FROM 
      employee
)
SELECT
  *
FROM
  ranked_employee
WHERE
  row = 1
ORDER BY 
  department;
 row |  id  | first_name |   name    |        department        | salary 
-----+------+------------+-----------+--------------------------+--------
   1 | 4841 | Caresse    | Simonian  | Accounting               |   9971
   1 | 4117 | Hedda      | Whifen    | Business Development     |   9936
   1 | 4433 | Delmar     | Pieper    | Engineering              |   9993
   1 | 5239 | Pebrook    | Roycraft  | Human Resources          |   9991
   1 | 4336 | Murry      | Cadwaladr | Legal                    |   9974
   1 | 3656 | Geno       | Culmer    | Marketing                |  10000
   1 | 3075 | Harland    | Dendle    | Product Management       |   9917
   1 | 5599 | Kayne      | Oppy      | Research and Development |   9984
   1 | 4065 | Alric      | de Zamora | Sales                    |   9993
   1 | 5178 | Brunhilde  | Glanton   | Services                 |   9995
   1 | 5988 | Grier      | Gotcliffe | Support                  |   9984
   1 | 5920 | Gray       | Bizley    | Training                 |   9955
(12 rows)

Wie man sieht, enthält diese Ausgabe keine Duplikate mehr. In PostgreSQL kann man diese Anfrage auch sehr elegant mit DISTINCT ON schreiben. Das ist aber nicht Teil des SQL-Standards. Diese Lösung finde ich sehr elegant und einfach.

SELECT DISTINCT ON (department)
  *
FROM
  employee
ORDER BY
  department,
  salary DESC;

SELECT DISTINCT ON ( expression [, …] ) keeps only the first row of each set of rows where the given expressions evaluate to equal.

IS DISTINCT FROM

In diesen Abschnitt wird die folgende Tabelle verwendet:

CREATE TABLE t AS (
  SELECT 1 AS a, 1 AS b UNION ALL
  SELECT 1, 2 UNION ALL
  SELECT NULL, 1 UNION ALL
  SELECT NULL, NULL
);

In SQL kann eine Abfrage die 3 verschiedene Ergebisse haben: TRUE, FALSE und NULL.

SELECT
  a,
  b,
  a = b AS equals
FROM
  t;
  a   |  b   | equals 
------+------+--------
    1 |    1 | t
    1 |    2 | f
 NULL |    1 | NULL
 NULL | NULL | NULL

Wie man sieht, kann man in SQL nicht mit NULL auf Gleichheit vergleichen. Sowie man mit NULL vergleicht, kommt immer NULL heraus. In vielen Fällen möchte man aber trotzdem mit NULL vergleichen und TRUE bzw. FALSE erhalten. Das funktioniert mit einen komplexen Ausdruck.

SELECT
  a,
  b,
  a = b AS equals,
  (
    (a IS NULL AND b IS NULL) 
    OR
    (a IS NOT NULL AND b IS NOT NULL AND a = b)
  ) AS full_condition
FROM
  t;
  a   |  b   | equals | full_condition 
------+------+--------+----------------
    1 |    1 | t      | t
    1 |    2 | f      | f
 NULL |    1 | NULL   | f
 NULL | NULL | NULL   | t

Der verwendete Ausdruck ist sehr komplex. Die meisten sind wahrscheinlich nicht in der Lage ihn schnell aufzuschreiben. Im SQL Standard gibt es einen Operator für einen sicheren Vergleich mit NULL.

SELECT
  a,
  b,
  a = b as equal,
  a IS DISTINCT FROM b AS is_distinct_from
FROM
  t;
  a   |  b   | equal | is_distinct_from 
------+------+-------+------------------
    1 |    1 | t     | f
    1 |    2 | f     | t
 NULL |    1 | NULL  | t
 NULL | NULL | NULL  | f

Im PostgreSQL Wiki findet die Wertetabellen für IS DISTINCT FROM.

ARRAY_AGG(DISTINCT)

Die Funktion ARRAY_AGG verkettet alle Argumente zu einem Array.

SELECT
    department,
    ARRAY_AGG(first_name || ' ' || name) AS employees
FROM
    employee
GROUP BY
    department
ORDER BY
    department;
     department     |              employees               
--------------------+--------------------------------------
 Accounting         | {"Devlin Klulik"}
 Engineering        | {"Cecily Churm","Nevin Melladew"}
 Legal              | {"Camey Speers","Gottfried Carneck"}
 Product Management | {"Glenda Trout"}
 Services           | {"Francklin Gurr"}
 Training           | {"King Tribell"}

Nun sieht man, welche Mitarbeiter in einer Abteilung arbeiten. Man kann auch DISTINCT verwenden, um das höchste Gehalt innerhalb einer Abteilung zu finden.

SELECT
    department,
    ARRAY_AGG(DISTINCT salary) AS salaries
FROM
    employee
GROUP BY
    department
ORDER BY
    department;
     department     |  salaries   
--------------------+-------------
 Accounting         | {3785}
 Engineering        | {3728,4033}
 Legal              | {3962}
 Product Management | {3982}
 Services           | {4207}
 Training           | {3780}

Man sieht, dass die Legal-Abteilung 2 Mitarbeiter hat, aber es git nur ein Gehalt. Daraus folgt, dass alle Mitarbeiter dasselbe Gehalt haben müssen. Die ORDER BY department Befehle, sorgen dafür, dass die Ausgabe immer gleiche Reihenfolge hat. Sie sind für die Anfragen nicht notwendig.

Schluss

Ich wollte einmal auf die verschiedenen Verwendungen von DISTINCT hinweisen. Auf einige Verwendungen bin ich erst beim Schreiben des Artikels gestoßen. Was ich mitnehme (und ihr mitnehmen solltet), man sollte öfter einmal in die Dokumention von PostgreSQL schauen, anstatt umständliches SQL zu schreiben oder die Daten noch einmal in der Applikation verarbeiten.

Kategorien: Datenbank
Tags: #PostgreSQL