Praktische SQL-Tricks, die jeder Datenwissenschaftler kennen sollte

# Einführung

Konzentrieren Sie sich nur auf SELECT, WHEREUnd GROUP BY reicht für die grundlegende Aggregation aus, aber viele echte Analyseaufgaben erfordern Muster, die über einfache Abfragen hinausgehen. Beispiele hierfür sind die Erkennung aufeinanderfolgender Aktivitätsstränge, die Segmentierung von Kunden nach Ausgabenstufen, die Glättung verrauschter Zeitreihendaten oder die zeilenübergreifende Verfolgung von Plan-Improve-Pfaden.

Dieser Artikel geht über die Grundlagen hinaus durch sieben praktische SQL-Muster und konzentriert sich auf Techniken, die echte analytische Probleme lösen.

# Einrichten des Datensatzes

Wir verwenden eine Beispieltabelle für Kundentransaktionen eines fiktiven Software program-as-a-Service-Unternehmens (SaaS):

CREATE TABLE transactions (
    transaction_id   SERIAL PRIMARY KEY,
    customer_id      INT,
    plan_type        VARCHAR(20),   -- 'starter', 'professional', 'enterprise'
    quantity           NUMERIC(10,2),
    standing           VARCHAR(20),   -- 'accomplished', 'refunded', 'failed'
    created_at       TIMESTAMP
);

Der vollständige Datensatz von 36 Transaktionen bei 7 Kunden im Zeitraum September 2023 bis Juni 2024 ist verfügbar in seed.sql. Führen Sie es aus, bevor Sie mit den Abfragen fortfahren.

# 1. Messung der Zeit zwischen Ereignissen mit LAG()

LAG() Und LEAD() ermöglicht Ihnen den Zugriff auf den Wert einer vorherigen oder nächsten Zeile ohne Selbstverknüpfung. Sie sind besonders nützlich für die Berechnung von Lücken zwischen Ereignissen wie Verlängerungsrhythmen, Abwanderungssignalen und Verzögerungen bei der Wiederaufnahme.

Aufgabe: Berechnen Sie, wie viele Tage zwischen den aufeinanderfolgenden abgeschlossenen Transaktionen jedes Kunden vergangen sind.

SELECT
    customer_id,
    created_at,
    LAG(created_at) OVER (
        PARTITION BY customer_id
        ORDER BY created_at
    ) AS previous_transaction_at,
    ROUND(
        EXTRACT(EPOCH FROM (
            created_at - LAG(created_at) OVER (
                PARTITION BY customer_id
                ORDER BY created_at
            )
        )) / 86400
    ) AS days_since_last
FROM transactions
WHERE standing="accomplished"
ORDER BY customer_id, created_at;

Ausgabe (gekürzt):

customer_id |     created_at      | previous_transaction_at | days_since_last
-------------+---------------------+-------------------------+-----------------
        3317 | 2024-01-03 11:02:00 |                         |
        3317 | 2024-03-15 10:45:00 | 2024-01-03 11:02:00     |              72
        3317 | 2024-05-22 09:30:00 | 2024-03-15 10:45:00     |              68
        4482 | 2023-09-10 09:00:00 |                         |
        4482 | 2023-10-10 09:00:00 | 2023-09-10 09:00:00     |              30
        4482 | 2023-11-10 09:14:00 | 2023-10-10 09:00:00     |              31
        4482 | 2024-01-03 09:14:00 | 2023-11-10 09:14:00     |              54
        4482 | 2024-03-03 08:20:00 | 2024-01-03 09:14:00     |              60
        4482 | 2024-04-03 10:00:00 | 2024-03-03 08:20:00     |              31
        4482 | 2024-05-01 11:00:00 | 2024-04-03 10:00:00     |              28
        ...
        7891 | 2024-02-01 09:00:00 |                         |
        7891 | 2024-04-01 09:00:00 | 2024-02-01 09:00:00     |              60
        7891 | 2024-05-15 09:00:00 | 2024-04-01 09:00:00     |              44
        8810 | 2024-01-05 12:00:00 |                         |
        8810 | 2024-02-05 12:00:00 | 2024-01-05 12:00:00     |              31
        8810 | 2024-04-05 12:00:00 | 2024-02-05 12:00:00     |              60
(29 rows)

Die erste Reihe professional Kunde hat immer NULL Für beide Spalten gibt es kein vorheriges Ereignis, auf das verwiesen werden kann. EXTRACT(EPOCH ...) wandelt das Zeitstempelintervall in Sekunden um; dividieren durch 86400 gibt Tage.

LEAD() funktioniert auf die gleiche Weise, blickt jedoch vorwärts statt rückwärts, was es nützlich macht, die Zeit bis zur nächsten Verlängerung zu berechnen oder die letzte Transaktion vor der Abwanderung zu kennzeichnen.

# 2. Vergleichen einer Zeile mit anderen Zeilen in derselben Tabelle mit einem Self-Be part of

A selbst beitreten verknüpft Zeilen innerhalb derselben Tabelle miteinander. Es ist das richtige Device, wenn Sie zwei Ereignisse für dieselbe Entität im Zeitverlauf vergleichen müssen – Upgrades, Downgrades, Reaktivierungen oder ein beliebiges Vorher/Nachher-Muster.

Aufgabe: Finden Sie Kunden, die zu einem beliebigen Zeitpunkt vom Starter zum Professional (oder vom Professional zum Enterprise) gewechselt sind.

SELECT DISTINCT t1.customer_id
FROM transactions t1
JOIN transactions t2
    ON  t1.customer_id = t2.customer_id
    AND t1.plan_type="starter"
    AND t2.plan_type="professional"
    AND t2.created_at  > t1.created_at
WHERE t1.standing="accomplished"
  AND t2.standing="accomplished"
ORDER BY t1.customer_id;

Ausgabe:

customer_id
-------------
        4482
        6204
        7891
(3 rows)

Die Tabelle wird zweimal mit einem Alias ​​versehen (t1, t2), sodass jeder Alias ​​einen anderen Zeitpunkt für denselben Kunden darstellen kann. Der Zustand t2.created_at > t1.created_at erzwingt die zeitliche Reihenfolge – ohne sie würden Sie Kunden zuordnen, die einfach beide Plantypen in beliebiger Reihenfolge hatten, auch den falschen. DISTINCT Reduziert Fälle, in denen ein Kunde vor dem Improve mehrere Starttransaktionen hatte, die andernfalls zu doppelten Zeilen führen würden.

Dieselbe Struktur eignet sich zum Erkennen von Herabstufungen, zum Finden von Kunden, die abgewandert sind und zurückgekommen sind, oder zum Vergleichen zweier Zustände, die nach Zeit sortiert werden müssen.

# 3. Auswählen der obersten Reihe professional Gruppe mit ROW_NUMBER()

Wenn Sie die Prime-N-Zeilen professional Kategorie benötigen – höchste Transaktion professional Kunde, jüngstes Ereignis professional Konto, erster Kauf professional Kohorte – ROW_NUMBER() innerhalb eines gemeinsamen Tabellenausdrucks (CTE) ist der Standardansatz.

Aufgabe: Erhalten Sie die höchste abgeschlossene Transaktion jedes Kunden.

WITH ranked AS (
    SELECT
        customer_id,
        transaction_id,
        quantity,
        plan_type,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY quantity DESC, created_at DESC
        ) AS rn
    FROM transactions
    WHERE standing="accomplished"
)
SELECT customer_id, transaction_id, quantity, plan_type
FROM ranked
WHERE rn = 1
ORDER BY customer_id;

Ausgabe:

customer_id  | transaction_id  | quantity  | plan_type
-------------+----------------+--------+------------
        3317 |             12 |  19.00 | starter
        4482 |              8 | 299.00 | enterprise
        5901 |             19 | 299.00 | enterprise
        6103 |             25 | 299.00 | enterprise
        6204 |             28 |  79.00 | professional
        7891 |             32 |  79.00 | professional
        8810 |             36 |  79.00 | professional
(7 rows)

ROW_NUMBER() weist der Zeile, die innerhalb jeder Partition zuerst sortiert wird, 1 zu. Die äußere Abfrage filtert dann nur auf diese Zeilen. Die sekundäre Sortierung created_at DESC fungiert als Tiebreaker; Wenn zwei Transaktionen den gleichen Betrag haben, gewinnt die neuere.

Wenn Sie möchten, dass die Kabelbinder enthalten und nicht kaputt sind, tauschen Sie sie aus ROW_NUMBER() für RANK(). RANK() weist gebundenen Reihen die gleiche Nummer zu und überspringt die nächste Reihe (1, 1, 3), whereas DENSE_RANK() macht dasselbe, ohne (1, 1, 2) zu überspringen.

# 4. Segmentierung der Kunden nach Ausgaben mit NTILE(n)

NTILE(n) unterteilt geordnete Zeilen in n ungefähr gleiche Buckets und weist jeder Zeile eine Bucket-Nummer zu. Es ist das richtige Device für die Kundenstaffelung, Ausgabenquartile oder den Aufbau von Kohorten für A/B-Analysen ohne feste Schwellenwerte.

Aufgabe: Ordnen Sie Kunden anhand ihres gesamten abgeschlossenen Transaktionswerts in Ausgabenquartile ein.

WITH customer_spend AS (
    SELECT
        customer_id,
        SUM(quantity) AS total_spend,
        COUNT(*) AS total_transactions
    FROM transactions
    WHERE standing="accomplished"
    GROUP BY customer_id
)
SELECT
    customer_id,
    total_spend,
    total_transactions,
    NTILE(4) OVER (ORDER BY total_spend) AS spend_quartile
FROM customer_spend
ORDER BY total_spend DESC;

Ausgabe:

customer_id | total_spend | total_transactions | spend_quartile
-------------+-------------+--------------------+----------------
        5901 |     1495.00 |                  5 |              4
        6103 |      835.00 |                  5 |              3
        4482 |      653.00 |                  7 |              3
        8810 |      237.00 |                  3 |              2
        6204 |      177.00 |                  3 |              2
        7891 |      177.00 |                  3 |              1
        3317 |       57.00 |                  3 |              1
(7 rows)

Quartil 4 ist Ihr höchstes Ausgabevolumen; Quartil 1 ist Ihr niedrigstes. NTILE() Kodiert die Ausgabenschwellenwerte nicht fest, sodass die Buckets automatisch neu kalibriert werden, wenn neue Kunden hinzugefügt werden. Dies macht es robuster als statische Cutoffs CASE WHEN total_spend > 500.

# 5. Glätten verrauschter Daten mit einem rollenden Fenster

Ein gleitender (oder gleitender) Durchschnitt glättet die monatliche Volatilität und macht Traits in Zeitreihendaten viel einfacher zu lesen. Fensterfunktionen mit einem expliziten ROWS BETWEEN Mit dem Body können Sie genau steuern, wie viele Perioden einbezogen werden sollen.

Aufgabe: Berechnen Sie einen gleitenden 3-Monats-Durchschnitt des monatlichen Umsatzes, um Lärm auszugleichen.

WITH month-to-month AS (
    SELECT
        DATE_TRUNC('month', created_at)::DATE AS month,
        SUM(quantity) AS monthly_revenue
    FROM transactions
    WHERE standing="accomplished"
    GROUP BY DATE_TRUNC('month', created_at)
)
SELECT
    month,
    monthly_revenue,
    ROUND(AVG(monthly_revenue) OVER (
        ORDER BY month
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2) AS revenue_3mo_avg
FROM month-to-month
ORDER BY month;

Ausgabe:

month    | monthly_revenue | revenue_3mo_avg
-------------+-----------------+-----------------
 2023-09-01  |           19.00 |           19.00
 2023-10-01  |           19.00 |           19.00
 2023-11-01  |           79.00 |           39.00
 2024-01-01  |          275.00 |          124.33
 2024-02-01  |          476.00 |          276.67
 2024-03-01  |          555.00 |          435.33
 2024-04-01  |          835.00 |          622.00
 2024-05-01  |          775.00 |          721.67
 2024-06-01  |          598.00 |          736.00
(9 rows)

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW weist die Fensterfunktion an, sich die aktuelle Zeile und die beiden Zeilen davor anzusehen. In den ersten beiden Zeilen werden weniger Eingaben benötigt, da kein Vorverlauf vorhanden ist. Daher dienen sie als 1-Monats- bzw. 2-Monats-Durchschnitt.

Tauschen ROWS für RANGE wenn Sie alle Zeilen mit demselben einschließen möchten ORDER BY Wert (nützlich, wenn mehrere Zeilen einen Zeitstempel teilen). Für eine längere Glättung wechseln 2 PRECEDING Zu 5 PRECEDING für ein 6-Monats-Fenster.

# 6. Bedingtes Aggregieren mit FILTER

FILTER Hier können Sie a anwenden WHERE Bedingung einem bestimmten Aggregat zuordnen, ohne die Abfrage in mehrere Unterabfragen aufzuteilen. Das Ergebnis sind mehrere bedingte Aggregationen in einem einzigen Durchgang über die Daten.

Aufgabe: Erhalten Sie den Gesamtumsatz, die Rückerstattungen und die Anzahl fehlgeschlagener Transaktionen, aufgeschlüsselt nach Monat – alles in einer Zeile professional Monat.

SELECT
    DATE_TRUNC('month', created_at) AS month,
    SUM(quantity) FILTER (WHERE standing="accomplished") AS revenue_completed,
    SUM(quantity) FILTER (WHERE standing="refunded")  AS revenue_refunded,
    COUNT(*)    FILTER (WHERE standing="failed")    AS failed_count
FROM transactions
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;

Ausgabe:

month             | revenue_completed | revenue_refunded | failed_count
------------------------+-------------------+------------------+--------------
 2023-09-01 00:00:00+00 |             19.00 |                  |            0
 2023-10-01 00:00:00+00 |             19.00 |                  |            0
 2023-11-01 00:00:00+00 |             79.00 |                  |            0
 2024-01-01 00:00:00+00 |            275.00 |                  |            0
 2024-02-01 00:00:00+00 |            476.00 |            79.00 |            1
 2024-03-01 00:00:00+00 |            555.00 |            79.00 |            0
 2024-04-01 00:00:00+00 |            835.00 |           299.00 |            0
 2024-05-01 00:00:00+00 |            775.00 |                  |            1
 2024-06-01 00:00:00+00 |            598.00 |                  |            2
(9 rows)

Die Different zu FILTER besteht aus drei separaten Unterabfragen, die miteinander verbunden sind – mehr Code, schwerer zu lesen und oft langsamer. Beachten Sie, dass SUM mit FILTER kehrt zurück NULL (nicht Null), wenn in einem bestimmten Monat keine Zeilen übereinstimmen, was genau ist: In diesen Monaten gab es tatsächlich keine Rückerstattungen. Einpacken COALESCE(..., 0) wenn Sie Nullen bevorzugen.

FILTER ist Commonplace-SQL und funktioniert in PostgreSQL und BigQuery. Verwenden Sie in Snowflake und einigen anderen SUM(CASE WHEN standing="accomplished" THEN quantity END) stattdessen.

# 7. Erkennen aufeinanderfolgender Aktivitätsstreifen mit Fensterfunktionen

Das Finden ununterbrochener Sequenzen – aktive Monate ohne Lücke, aufeinanderfolgende Tage mit Transaktionen, Abonnementsstreaks – ist eines der schwierigeren SQL-Probleme. Die klassische Lösung verwendet eine Fensterfunktion, um Zeilen ohne rekursiven CTE in Streifen zu gruppieren.

Die Technik: Weisen Sie jedem aktiven Monat eine fortlaufende Zeilennummer innerhalb seiner Kundenpartition zu. Wenn die Monate wirklich aufeinanderfolgend sind, ergibt das Subtrahieren dieser Zeilennummer vom Monatsdatum für jeden Monat im Streifen denselben konstanten Wert. Eine Lücke durchbricht die Konstante.

Aufgabe: Finden Sie die aufeinanderfolgenden aktiven Monate jedes Kunden (Monate mit mindestens einer abgeschlossenen Transaktion).

WITH monthly_activity AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', created_at)::DATE AS active_month
    FROM transactions
    WHERE standing="accomplished"
    GROUP BY customer_id, DATE_TRUNC('month', created_at)
),
with_prev AS (
    SELECT
        customer_id,
        active_month,
        LAG(active_month) OVER (
            PARTITION BY customer_id
            ORDER BY active_month
        ) AS prev_month
    FROM monthly_activity
),
streak_groups AS (
    SELECT
        customer_id,
        active_month,
        SUM(CASE WHEN active_month = prev_month + INTERVAL '1 month' THEN 0 ELSE 1 END)
            OVER (PARTITION BY customer_id ORDER BY active_month) AS streak_id
    FROM with_prev
),
streaks AS (
    SELECT
        customer_id,
        streak_id,
        MIN(active_month) AS streak_start,
        MAX(active_month) AS streak_end,
        COUNT(*) AS streak_length_months
    FROM streak_groups
    GROUP BY customer_id, streak_id
)
SELECT customer_id, streak_start, streak_end, streak_length_months
FROM streaks
ORDER BY customer_id, streak_start;

Ausgabe:

customer_id | streak_start | streak_end | streak_length_months
-------------+--------------+------------+----------------------
        3317 | 2024-01-01   | 2024-01-01 |                    1
        3317 | 2024-03-01   | 2024-03-01 |                    1
        3317 | 2024-05-01   | 2024-05-01 |                    1
        4482 | 2023-09-01   | 2023-11-01 |                    3
        4482 | 2024-01-01   | 2024-01-01 |                    1
        4482 | 2024-03-01   | 2024-05-01 |                    3
        5901 | 2024-02-01   | 2024-06-01 |                    5
        6103 | 2024-01-01   | 2024-04-01 |                    4
        6103 | 2024-06-01   | 2024-06-01 |                    1
        6204 | 2024-01-01   | 2024-01-01 |                    1
        6204 | 2024-03-01   | 2024-03-01 |                    1
        6204 | 2024-05-01   | 2024-05-01 |                    1
        7891 | 2024-02-01   | 2024-02-01 |                    1
        7891 | 2024-04-01   | 2024-05-01 |                    2
        8810 | 2024-01-01   | 2024-02-01 |                    2
        8810 | 2024-04-01   | 2024-04-01 |                    1
(16 rows)

# Kurzreferenz

Diese Muster funktionieren in Commonplace-SQL, ohne auf datenbankspezifische Funktionen angewiesen zu sein, und sie erscheinen häufig in analytischen Arbeitsabläufen wie Aufbewahrungsanalysen, Improve-Trichterverfolgung und Umsatzberichten.

Tipp Wann man es verwendet
LAG() / LEAD()

Zeit zwischen Ereignissen, Vorher/Nachher-Vergleiche professional Entität

Selbst beitreten

Übergänge zwischen Zuständen erkennen (Upgrades, Reaktivierungen)

ROW_NUMBER()

Prime-N Zeilen professional Gruppe, Deduplizierung

NTILE(n)

Kundensegmentierung in Ausgaben-/Aktivitätsstufen

Rollfenster (ROWS BETWEEN)

Glatte, verrauschte Zeitreihen, gleitende Durchschnitte

FILTER

Mehrere bedingte Aggregationen in einem Abfragedurchlauf

Aufeinanderfolgende Streifenerkennung

Abonnementstreaks, Retention-Analyse, Sitzungslücken

Sobald Sie damit vertraut sind, können viele mehrstufige Datentransformationen, die häufig in Python durchgeführt werden, sauberer und effizienter in einer einzigen SQL-Abfrage ausgedrückt werden.

Bala Priya C ist ein Entwickler und technischer Redakteur aus Indien. Sie arbeitet gerne an der Schnittstelle von Mathematik, Programmierung, Datenwissenschaft und Inhaltserstellung. Zu ihren Interessen- und Fachgebieten gehören DevOps, Datenwissenschaft und Verarbeitung natürlicher Sprache. Sie liebt es zu lesen, zu schreiben, zu programmieren und Kaffee zu trinken! Derzeit arbeitet sie daran, zu lernen und ihr Wissen mit der Entwickler-Group zu teilen, indem sie Tutorials, Anleitungen, Meinungsbeiträge und mehr verfasst. Bala erstellt außerdem ansprechende Ressourcenübersichten und Programmier-Tutorials.



Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert