Alle code-oefeningen
SQLPostgreSQLInstap6 vragen

Dubbele klanten opsporen — window-functies als ontdubbelaar

De klassieke opschoonquery — welke rij houd je, welke is de dubbele? Met ROW_NUMBER, FIRST_VALUE en een tiebreaker die belangrijker is dan hij lijkt.

Vroeg of laat staan er dubbele klanten in je database: dezelfde persoon die twee keer een account aanmaakte, één keer met een hoofdletter in het e-mailadres en één keer met een spatie erachter. Deze functie zoekt ze op — hij verwijdert niets, hij wijst alleen aan wat je zou verwijderen.

Dat onderscheid is belangrijk, want deze query wordt in de praktijk vrijwel altijd als basis voor een DELETE gebruikt. Twee van de vragen hieronder gaan precies over waarom dat gevaarlijk is.

CREATE OR REPLACE FUNCTION beheer.vind_dubbele_klanten(
    p_max_resultaten int DEFAULT 500
)
RETURNS TABLE (
    behouden_id   bigint,
    dubbele_id    bigint,
    email         text,
    naam_verschil boolean,
    orders_dubbel bigint
)
LANGUAGE sql
STABLE
AS $$
    WITH genormaliseerd AS (
        SELECT k.id,
               k.naam,
               lower(btrim(k.email)) AS email,
               k.aangemaakt_op
        FROM   crm.klanten k
        WHERE  k.email IS NOT NULL
          AND  k.verwijderd_op IS NULL
    ),
    gerangschikt AS (
        SELECT g.*,
               ROW_NUMBER() OVER (PARTITION BY g.email
                                  ORDER BY g.aangemaakt_op, g.id) AS rn,
               FIRST_VALUE(g.id)   OVER (PARTITION BY g.email
                                         ORDER BY g.aangemaakt_op, g.id) AS eerste_id,
               FIRST_VALUE(g.naam) OVER (PARTITION BY g.email
                                         ORDER BY g.aangemaakt_op, g.id) AS eerste_naam
        FROM   genormaliseerd g
    )
    SELECT r.eerste_id,
           r.id,
           r.email,
           r.naam IS DISTINCT FROM r.eerste_naam,
           (SELECT COUNT(*) FROM verkoop.orders o WHERE o.klant_id = r.id)
    FROM   gerangschikt r
    WHERE  r.rn > 1
    ORDER  BY r.email, r.rn
    LIMIT  p_max_resultaten;
$$;
Vraag 1Opwarmer

Waarom staat er lower(btrim(k.email)) in plaats van gewoon k.email?

Vraag 2Semantiek

Er staan drie window-functies met exact dezelfde PARTITION BY en ORDER BY. Wat doet elk van de drie, en waarom moet die ORDER BY identiek zijn?

Vraag 3NULL-gedrag

Waarom r.naam IS DISTINCT FROM r.eerste_naam en niet gewoon r.naam <> r.eerste_naam?

Vraag 4Randgeval

De sortering is ORDER BY g.aangemaakt_op, g.id. Wat gebeurt er als je die g.id weglaat?

Vraag 5Performance

(SELECT COUNT(*) FROM verkoop.orders o WHERE o.klant_id = r.id) staat in de SELECT-lijst. Wat is daar het probleem mee?

Vraag 6Valkuil

Deze functie verwijdert niets. Toch is hij gevaarlijk als je hem gebruikt om een DELETE op te baseren. Waarom — twee redenen.

Volgende oefening