Zurück zur Artikelliste Artikel
20 Leseminuten

SQL-Projekt für Portfolio: Northwind Store

SQL-Portfolio-Projekte sind ein wichtiger Bestandteil der Ausbildung von Datenanalysten. Wie fängt man ein Projekt an, und was macht man mit den gewonnenen Erkenntnissen? Wir verwenden die Northwind-Datenbank, um diese Frage zu beantworten.

Die Durchführung eines soliden SQL-Projekts ist ein wesentliches Element im Lernprozess eines Datenanalysten. Ein SQL-Projekt ist ein großartiges Lernwerkzeug, da es Sie dazu bringt, SQL auf einen realen Datensatz anzuwenden. Dies ist besonders wichtig, wenn Sie ein Anfänger sind und nicht die Möglichkeit haben, mit realen Datensätzen zu arbeiten. Durch die Arbeit mit mehreren realen Datensätzen lernen Sie, auf welche Probleme Sie in der realen Welt stoßen können. Als Bonus ist die Durchführung mehrerer SQL-Projekte für Ihr Portfolio immer eine schöne Sache, die in einem Lebenslauf zu sehen ist.

Die Frage ist, wie Sie ein SQL-Projektportfolio erstellen. Wenn Sie einen kostenlosen Online-Datensatz gefunden haben, den Sie analysieren möchten, was tun Sie dann? Wir werden diese Fragen anhand der Northwind-Store-Datenbank beantworten.

Sie finden diesen Datensatz im Kurs SQL-Datenbanken für die Praxis. Er ist einer von sechs Datensätzen, die Daten von einer Universität, Blogverkehr, Sportergebnissen, einem Musikgeschäft und dem Museum of Modern Art (MoMA) enthalten. Dieser Kurs ist Teil des SQL-Praxis -Tracks, in dem Sie Aggregation, JOIN, Subqueries, CTEs, CASE WHEN und andere wichtige SQL-Themen üben können. Wenn Sie Ihre Kenntnisse in einigen dieser für die Berichterstattung wichtigen Bereiche auffrischen möchten, sollten Sie unseren Erstellen einfacher SQL-Berichte Kurs.

Nehmen wir nun den Northwind-Store-Datensatz und verwenden ihn für ein SQL-Projekt für ein Datenanalysten-Portfolio. Ich werde dieses Projekt in PostgreSQL durchführen, aber alles, was ich tue, lässt sich mit geringfügigen Änderungen der Syntax auf andere Datenbanken übertragen.

Sie können dabei auch unser kostenloses Spickzettel für SQL zur Datenanalyse zu Rate ziehen.

Der Northwind Store-Datensatz

Die einzige Information, die ich Ihnen über diese Datenbank geben werde, ist, dass sie aus sechs Tabellen besteht:

  1. categories - Eine Liste der Produktkategorien.
  2. channels - Eine Liste der Quellen, über die das Geschäft Kunden akquiriert.
  3. customers - Eine Liste der Kunden des Ladens.
  4. order_items - Eine Liste der Produkte, die in jeder Bestellung enthalten sind.
  5. orders - Eine Liste der von Kunden aufgegebenen Bestellungen.
  6. products - Eine Liste der Produkte, die das Geschäft anbietet.

Den Rest der Informationen werden wir im Rahmen unseres Projekts sammeln. Diese Datenbankinspektion sollte eigentlich am Anfang eines jeden Projekts stehen, bevor man mit der Analyse beginnt.

1. Inspektion der Datenbank

In dieser Phase der Vorbereitung eines SQL-Projekts geht es darum, Ihre Daten kennenzulernen, z. B. Tabellen- und Spaltennamen, Primär- und Fremdschlüssel, die Beziehungen zwischen den Tabellen und die Datentypen in jeder Tabelle.

Inspektion der Tabellennamen

Eine primitive Methode, um etwas über die Tabellen im Dataset zu erfahren, besteht darin, sie im Browser des RDBMS zu suchen, in das Sie das Dataset importiert haben, z. B. PostgreSQL, SQL Server oder MySQL.

In PostgreSQL können Sie diese Abfrage schreiben, um eine Liste aller Tabellen in einer Datenbank zu erhalten:

SELECT table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('information_schema', 'pg_catalog') AND table_type = 'BASE TABLE'
ORDER BY table_schema, table_name;

Sie sucht nach Daten in der information_schemawo Sie die Metadaten der Datenbank finden können. Da ich den Namen jeder Tabelle haben möchte, verwende ich die Ansicht tables nachdem ich das Schema referenziert habe, und füge table_name in SELECT ein.

Die erste Bedingung in WHERE filtert Systemschemata heraus und lässt nur benutzerdefinierte Tabellen übrig. Die zweite Bedingung stellt sicher, dass nur Basistabellen aufgelistet werden, ohne Views und andere Tabellen.

Hier ist die Liste der Tabellen in der Northwind-Datenbank:

table_name
categories
channels
customers
order_items
orders
products

Untersuchung der Spalteninformationen

Nun wollen wir die Details der einzelnen Tabellen besser verstehen. Die Kenntnis ihrer Spalten ist ein guter Anfang.

Wir können wieder abfragen information_schema um wichtige Informationen über die Spalten zu erhalten:

SELECT table_name,
       column_name,
       data_type,
       is_nullable,
       column_default
FROM information_schema.columns
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
ORDER BY table_name, ordinal_position;

Wir können Tabellennamen, Spaltennamen und den Datentyp jeder Spalte finden, indem wir table_name, column_name und data_type in SELECT auflisten.

Die Spalte is_nullable liefert uns Informationen darüber, ob die Spalte NULL Werte akzeptiert.

table_namecolumn_namedata_typeis_nullable
categoriescategory_idintegerNO
categoriescategory_namecharacter varyingNO
categoriesdescriptiontextYES
channelsidintegerNO
channelschannel_namecharacter varyingNO
customerscustomer_idintegerNO
customersemailcharacter varyingNO
customersfull_namecharacter varyingNO
customersaddresscharacter varyingYES
customerscitycharacter varyingYES
customersregioncharacter varyingYES
customerspostal_codecharacter varyingYES
customerscountrycharacter varyingYES
customersphonecharacter varyingYES
customersregistration_datetimestamp without time zoneNO
customerschannel_idintegerNO
customersfirst_order_idintegerYES
customersfirst_order_datetimestamp without time zoneYES
customerslast_order_idintegerYES
customerslast_order_datetimestamp without time zoneYES
order_itemsorder_idintegerNO
order_itemsproduct_idintegerNO
order_itemsunit_pricenumericNO
order_itemsquantitysmallintNO
order_itemsdiscountnumericNO
ordersorder_idintegerNO
orderscustomer_idintegerNO
ordersorder_datetimestamp without time zoneYES
orderstotal_amountnumericNO
ordersship_namecharacter varyingYES
ordersship_addresscharacter varyingYES
ordersship_citycharacter varyingYES
ordersship_regioncharacter varyingYES
ordersship_postalcodecharacter varyingYES
ordersship_countrycharacter varyingYES
ordersshipped_datetimestamp without time zoneYES
productsproduct_idintegerNO
productsproduct_namecharacter varyingNO
productscategory_idintegerNO
productsunit_pricenumericYES
productsdiscontinuedbooleanNO

Auf diese Weise haben wir alle Informationen an einem Ort, was die Untersuchung erleichtert.

Zunächst können wir anhand der Spaltennamen erkennen, welche Daten jede Tabelle enthält. Die Datentypen erscheinen alle logisch. Es gibt zum Beispiel keine IDs, die als character varying definiert sind. Es besteht also keine Notwendigkeit, die Daten in diesem Stadium in ein geeignetes Format zu konvertieren.

Unter NULLerhalten wir einige wertvolle Informationen über die Tabellen:

  • Die Tabelle categories erlaubt es, dass die Kategoriebeschreibung NULL lautet.
  • Die Tabelle channels lässt überhaupt keine NULLs zu.
  • Die Tabelle customers erlaubt NULLs in vielen Spalten, einschließlich Land. Wenn wir eine geografische Segmentierung der Kunden erstellen wollen, könnten diese NULLs uns Probleme bereiten.
  • Die obige Ausgabe zeigt, dass keine Spalten in der Tabelle order_items löschbar sind.
  • Für die products ist es interessant zu sehen, dass der Stückpreis NULL sein kann.
  • Es scheint ein wenig seltsam, dass die Tabelle orders so viele löschbare Spalten hat. Im Grunde genommen ist es damit möglich, eine Auftrags-ID und keine anderen Informationen über den Auftrag zu haben. Das müssen wir bei der Analyse dieser Tabelle im Auge behalten.

Auffinden von Primär- und Fremdschlüsseln

Der nächste Schritt der Datenprüfung besteht darin zu verstehen, wie die Tabellen zusammenarbeiten. Dazu müssen wir zunächst ihre Primärschlüssel (PKs) und Fremdschlüssel (FKs) finden.

Die PKs zeigen uns, welche Spalte(n) eine Tabelle zur eindeutigen Identifizierung von Daten verwendet. Auf diese Weise können Sie die Struktur der Tabelle besser verstehen.

FKs zeigen uns, welche Spalte mit einem Primärschlüssel einer anderen Tabelle verknüpft ist. Dies ist die Grundlage für das Verständnis der Beziehungen zwischen den Tabellen.

Sie können sowohl PKs als auch FKs mit der folgenden Abfrage auflisten:

SELECT kcu.table_name,
       kcu.column_name,
       tc.constraint_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu 
ON kcu.constraint_name = tc.constraint_name
AND kcu.constraint_schema = tc.constraint_schema
WHERE tc.constraint_type IN ('PRIMARY KEY', 'FOREIGN KEY')
ORDER BY kcu.table_name ASC, tc.constraint_type DESC;

Dieses Mal müssen wir zwei Ansichten aus der Tabelle information_schemaverwenden, nämlich table_constraints und key_column_usage.

Die Ansicht table_constraints Ansicht enthält Informationen über die Beschränkungen für jede Tabelle. Die Ansicht key_column_usage dient dazu, Informationen über die Spalten mit den Schlüsseleinschränkungen zu erhalten.

Diese beiden Ansichten sind durch zwei gemeinsame Spalten verbunden: constraint_name (der Name der Einschränkung) und constraint_schema (der Name des Schemas, das die Einschränkung enthält).

Ich verwende die WHERE Klausel, um nur die Primärschlüssel- und Fremdschlüssel-Beschränkungen auszugeben.

In einem letzten Schritt ordne ich die Ausgabe alphabetisch nach dem Tabellennamen und dann umgekehrt alphabetisch nach dem Einschränkungstyp.

Der Code gibt diese Tabelle aus:

table_namecolumn_nameconstraint_type
categoriescategory_idPRIMARY KEY
channelsidPRIMARY KEY
customerscustomer_idPRIMARY KEY
customerslast_order_idFOREIGN KEY
customersfirst_order_idFOREIGN KEY
customerschannel_idFOREIGN KEY
order_itemsorder_idPRIMARY KEY
order_itemsproduct_idPRIMARY KEY
order_itemsorder_idFOREIGN KEY
order_itemsproduct_idFOREIGN KEY
ordersorder_idPRIMARY KEY
orderscustomer_idFOREIGN KEY
productsproduct_idPRIMARY KEY
productscategory_idFOREIGN KEY

Die Tabellen categories und channels sind die einzigen Tabellen, die PK, aber keine FK haben.

Alle anderen Tabellen haben eine PK und mindestens eine FK. Die einzige Ausnahme ist die Tabelle order_itemsdie die PK-Beschränkung auf zwei Spalten hat: order_id und product_id. Mit anderen Worten, die Tabelle hat einen zusammengesetzten Primärschlüssel, was bedeutet, dass der eindeutige Bezeichner der Daten die eindeutige Kombination von zwei Spalten ist. In diesem Fall geht es darum, nur eine Produkt-ID für eine einzelne Bestellung anzuzeigen.

Bei näherer Betrachtung zeigt sich, dass dieselben Spalten auch ein Fremdschlüssel für dieselbe Tabelle sind. Dies bedeutet, dass die Primärschlüssel ebenfalls Fremdschlüssel sind, da sie aus anderen Tabellen stammen.

Verstehen der Beziehungen zwischen den Tabellen

Die Auflistung der PKs und FKs gibt uns bereits einen Eindruck von den Beziehungen zwischen den Tabellen. Wir können sie jedoch noch besser verstehen, wenn wir die über- und untergeordneten Tabellen und Spalten der Fremdschlüssel anzeigen.

Warum sollten wir das tun? Ein Fremdschlüssel ist immer der Primärschlüssel einer anderen Tabelle. Die Auflistung aller über- und untergeordneten Tabellen und Spalten ist eine Verbesserung gegenüber der vorherigen Abfrage, da die gemeinsamen Spalten zwischen den Tabellen leicht zu erkennen sind.

Diese Informationen sind nützlich, um die Beziehungen zwischen den Tabellen zu verstehen, um zu wissen, welche Spalten Sie verwenden können, um Tabellen zu verbinden, und ob sie direkt oder über eine andere Tabelle verbunden werden können.

Hier ist der Code:

SELECT ccu.table_name AS parent_table,
       ccu.column_name AS parent_column,
       kcu.table_name AS child_table,
       kcu.column_name AS child_column      
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY kcu.table_name;

In der Abfrage verwenden wir Informationen aus der Tabelle table_constraints, key_column_usage, und constraint_column_usage Ansichten.

Die Ansicht constraint_column_usage Ansicht zeigt die Spalten, die von einer Einschränkung verwendet werden. Wir verwenden sie, um die übergeordnete Tabelle und Spalte der FK anzuzeigen, d.h. die Tabelle und Spalte, in der diese FK tatsächlich eine PK ist.

Die Daten von key_column_usage zeigen die Tabelle und die Spalte eines FKs.

parent_tableparent_columnchild_tablechild_column
ordersorder_idcustomerslast_order_id
ordersorder_idcustomersfirst_order_id
channelsidcustomerschannel_id
ordersorder_idorder_itemsorder_id
productsproduct_idorder_itemsproduct_id
customerscustomer_idorderscustomer_id
categoriescategory_idproductscategory_id

Aus der obigen Ausgabe geht hervor, dass die Tabelle orders direkt mit der Tabelle verbunden ist customers über die Spalten order_id (PK) und customer_id (FK) verbunden ist. Sie ist mit der Tabelle verbunden order_items über order_id (PK).

Die Tabelle channels ist direkt nur mit der Tabelle customers.

Wir haben bereits festgestellt, dass die Primärschlüssel der Tabelle order_items auch Fremdschlüssel sind. Aus der obigen Tabelle können wir sehen, dass sie aus den Tabellen orders und . products.

Die Tabelle categories ist nur mit der Tabelle products.

2. Erkundung der Daten

Wenn Sie mit einer neuen Datenbank zu arbeiten beginnen, sollte der erste Schritt darin bestehen, Ihre Daten zu untersuchen, um einige grundlegende Informationen über jede Tabelle in einer Datenbank zu erhalten. Es ist eine gute Praxis, sich die folgenden Punkte anzusehen:

  • Zählen von Zeilen.
  • Minimal- und Maximalwerte ermitteln (besonders bei Datumsangaben).
  • Auflistung verschiedener Kategorien.
  • Zählen der unterschiedlichen Werte in jeder Kategorie.
  • Zählen von Zeilen pro Kategoriewert.
  • Summen und Durchschnittswerte für wichtige numerische Werte.

Diese Liste ist nicht erschöpfend und kann sich mit der zu untersuchenden Tabelle ändern. Sie ist jedoch ein Minimum und sollte für die meisten Tabellen funktionieren.

Im Allgemeinen läuft dieser Schritt auf eine grundlegende Datenaggregation und Gruppierung hinaus. Ich zeige Ihnen, wie dies bei einer Tabelle aus unserer Datenbank funktioniert, und Sie können dann andere Tabellen auf die gleiche Weise untersuchen.

Untersuchen der Tabelle Bestellungen

Anzahl der Zeilen

Wie erwartet, verwenden wir hier die Aggregatfunktion COUNT():

SELECT COUNT(*) AS number_of_rows
FROM orders;

Ich verwende die Funktion COUNT() mit einem Sternchen; damit werden alle Zeilen gezählt, einschließlich der NULL-Zeilen.

number_of_rows
11,618

Die Tabelle orders hat 11.618 Zeilen. Mit anderen Worten, es gibt 11.618 Bestellungen.

Mindest- und Höchstwerte

Schauen wir uns an, welche Spalten sich für die Verwendung der Funktionen MIN() und MAX() eignen. Perfekt - wir können das älteste und das jüngste Datum finden, an dem eine Bestellung aufgegeben und versandt wurde:

SELECT MIN(order_date) AS oldest_order_date, 
	 MAX(order_date) AS latest_order_date,
	 MIN(shipped_date) AS oldest_shipped_date,
	 MAX(shipped_date) AS latest_shipped_date
FROM orders;

Aus der Ausgabe geht hervor, dass die erste Bestellung am 2017-01-01 und die letzte am 2024-06-24 aufgegeben wurde. Was die Versanddaten betrifft, so ist das erste Datum auf 2017-01-01 und das jüngste auf 2024-06-23.

oldest_order_datelatest_order_dateoldest_shipped_datelatest_shipped_date
2017-01-01 0:00:002024-06-24 0:00:002017-01-06 0:00:002024-06-23 0:00:00

Auflistung von Kategorien

Hierfür brauchen wir keine Aggregatfunktionen:

SELECT DISTINCT ship_country
FROM orders
ORDER BY ship_country;

In der Abfrage verwenden wir die DISTINCT Klausel, um jedes Land nur einmal anzuzeigen.

Hier ist eine Liste der Länder in dieser Tabelle:

ship_country
Argentina
Austria
Belgium
Brazil
Canada
Denmark
Finland
France
Germany
Ireland
Italy
Mexico
Norway
Poland
Portugal
Spain
Sweden
Switzerland
UK
USA
Venezuela

Es ist auch nützlich, eine Liste der Städte zu sehen:

SELECT DISTINCT ship_city
FROM orders
ORDER BY ship_city;

Da es viele Städte gibt, ist hier eine Teilliste:

ship_city
Aachen
Abilene
Achille
Adelphi
Adrian
Akron
Albany
Alberton
Albuquerque
Yucca Valley

Zählen der unterschiedlichen Werte in jeder Kategorie

Sehen wir uns nun die Anzahl der Länder an, in die Northwind Sendungen geliefert hat:

SELECT COUNT(DISTINCT ship_country) AS number_of_countries
FROM orders;

Wir verwenden wieder COUNT(), aber diesmal fügen wir DISTINCT hinzu und verweisen auf die Spalte, die wir zählen wollen.

Die Ausgabe zeigt, dass es 21 verschiedene Länder gibt, in die das Unternehmen liefert:

number_of_countries
21

Wir könnten dasselbe für Städte tun:

SELECT COUNT(DISTINCT ship_city) AS number_of_cities
FROM orders;

Es gibt 494 eindeutige Städte:

number_of_cities
494

Auch wenn wir sie nicht als eigene Kategorie aufgeführt haben, ist es nützlich zu wissen, wie viele Kunden Bestellungen aufgegeben haben:

SELECT COUNT(DISTINCT customer_id) AS number_of_customers_with_orders
FROM orders;

Wir verwenden COUNT(), um die Kunden-IDs zu zählen. Natürlich benötigen wir DISTINCT, da zu erwarten ist, dass einige Kunden mehrmals auftauchen, d. h. jedes Mal, wenn sie eine Bestellung aufgeben.

Es gibt 761 eindeutige Kunden, die mindestens einmal bei diesem Unternehmen bestellt haben.

number_of_customers_with_orders
761

Zählen der Zeilen pro Kategoriewert

Hierfür wird jeder Kategoriewert aufgelistet (das haben wir bereits getan) und die Anzahl der Zeilen für jeden Wert gezählt.

So erhalten wir zum Beispiel die Anzahl der Bestellungen pro Land:

SELECT ship_country,
	 COUNT(*) AS number_of_orders
FROM orders
GROUP BY ship_country
ORDER BY number_of_orders DESC;

Ich wähle die Spalte ship_country aus und verwende dann COUNT(*), um alle Zeilen zu zählen. Damit die Zählung für jedes Land erfolgt, muss ich die Klausel GROUP BY ship_country einfügen.

Auf diese Weise erhalten wir die Anzahl der Bestellungen nach Land. Die Ausgabe zeigt, dass die USA mit 9.789 Bestellungen der größte Markt sind:

ship_countrynumber_of_orders
USA9,789
France236
Brazil233
Germany230
UK144
Spain123
Mexico118
Venezuela103
Argentina87
Canada82
Italy64
Austria58
Portugal58
Belgium56
Denmark56
Finland47
Norway30
Sweden28
Poland27
Ireland26
Switzerland23

Summe und Durchschnittswerte

Wenn Ihre Daten geeignet sind, sollten Sie Durchschnittswerte und Summen von numerischen Werten ermitteln. Das können wir für die Spalte total_amount tun, die den Wert einer Bestellung darstellt:

SELECT SUM(total_amount) AS orders_total_value,
       AVG(total_amount) AS average_order_value
FROM orders;

Um diese beiden Berechnungen darzustellen, verwenden Sie die Aggregatfunktionen SUM() und AVG().

Wir sehen, dass der Gesamtumsatz des Ladens etwas über 19 Millionen beträgt. Der durchschnittliche Bestellwert beträgt 1.636,15:

orders_total_valueaverage_order_value
19,008,819.691,636.15

Zusätzliche Datenexploration

Wie ich bereits erwähnt habe, sollten die vorherigen Aggregationen das Minimum sein, das Sie für jede Tabelle in der Datenbank durchführen.

Diese Datenexploration geht ein wenig weiter (aber nicht zu weit) als einfache Aggregationen und GROUP BY. Dies ist zwar immer noch die Grundlage, aber Sie können auch andere Konzepte anwenden, wie z. B. das Filtern von Daten (unter Verwendung von WHERE und/oder HAVING), das Extrahieren von Datumsteilen oder der Uhrzeit, die Verwendung von CASE WHEN zur Kennzeichnung von Daten und so weiter.

Schauen wir uns nun einige Beispiele an.

Anzahl der Produkte

Wir können die Funktion COUNT() verwenden, um die Gesamtzahl der Produkte zu ermitteln, die das Geschäft verkauft.

SELECT COUNT(*) AS number_of_active_products
FROM products
WHERE discontinued IS FALSE; 

Die WHERE -Klausel enthält eine Bedingung, damit nur Produkte angezeigt werden, die nicht auslaufen, d. h., es handelt sich um die Produkte, die das Geschäft derzeit verkauft.

Aus dem vorherigen Abschnitt wissen wir, dass die Spalte discontinued ein boolescher Typ ist. Daher müssen wir den Operator IS FALSE verwenden, um nur nicht eingestellte Produkte in die Zählung aufzunehmen.

Die Anzahl ist 69:

number_of_active_products
69

Umsatz nach Jahr

Ein einfacher Bericht, der den Umsatz nach Jahr zeigt, kann mit SUM() erstellt werden:

SELECT EXTRACT(YEAR FROM order_date) AS revenue_year,
       SUM(total_amount) AS revenue  
FROM orders
GROUP BY revenue_year;

Wir verwenden die Funktion EXTRACT(), um nur die Jahre ab den Bestelldaten zu erhalten. Dann summieren wir die Gesamtbeträge aller Bestellungen und gruppieren sie nach Jahr, um die Werte für jedes Jahr separat anzuzeigen.

Aus den Ergebnissen ist ersichtlich, dass das Jahr 2018 das umsatzstärkste Jahr des Unternehmens war. Das Jahr mit den niedrigsten Einnahmen ist 2024, aber das könnte daran liegen, dass das Jahr noch nicht zu Ende ist (zum Zeitpunkt der Analyse).

Interessant ist auch, dass es keine Umsatzdaten für die Jahre 2019-2022 gibt. Dies sollte überprüft werden, um festzustellen, ob die Daten aus einem bestimmten Grund fehlen oder ob es sich um einen Fehler handelt.

revenue_yearrevenue
20173,088,759.84
20189,368,330.91
20234,646,048.11
20241,905,680.83

Kundensegmentierung nach Land

Wir haben Daten über die Länder der Kunden, daher wäre eine Übersicht über die Anzahl der Kunden in jedem Land informativ.

Außerdem sind wir nicht an Kunden interessiert, die keine Bestellung aufgegeben haben, da dies die Zahl der Kunden künstlich aufblähen kann. Nein, wir wollen nur Kunden, die bei uns bestellen. Hier ist die Abfrage:

SELECT country,
	 COUNT(*) AS number_of_customers
FROM customers
WHERE first_order_id IS NOT NULL
GROUP BY country
ORDER BY number_of_customers DESC;

Wir wählen und gruppieren nach Land und verwenden die Funktion COUNT() aggregate, um die Anzahl der Kunden zu ermitteln. Wenn die Daten in der Spalte first_order nicht null sind, dann hat dieser Kunde mindestens eine Bestellung aufgegeben; dies ist die Bedingung, die wir in WHERE verwenden müssen.

Die Daten sind von der höchsten bis zur niedrigsten Anzahl von Kunden geordnet.

Die Ausgabe zeigt, dass der größte Markt für Northwind, gemessen an der Kundenzahl, die USA sind. Daraus kann man auch schließen, dass dies der größte Markt nach Umsatz ist.

countrynumber_of_customers
USA697
Germany8
France8
Brazil8
UK5
Venezuela4
Spain4
Mexico4
Argentina3
Canada3
Belgium2
Denmark2
Portugal2
Finland2
Italy2
Austria2
Sweden1
Poland1
Ireland1
Switzerland1
Norway1

3. Fortgeschrittene Datenanalyse und umsetzbare Einblicke

Was wir bis jetzt getan haben, ist ein guter Anfang. Die Datenanalyse in einem SQL-Projekt für ein Portfolio sollte sich jedoch nicht auf eine einfache Aggregation und Untersuchung für jede Tabelle beschränken.

Wir werden jetzt darüber hinausgehen und komplexere Abfragen schreiben, die es uns ermöglichen, Maßnahmen zu ergreifen und das Geschäft von Northwind zu verbessern. Wir möchten zum Beispiel sehen, wie sich die Umsätze je nach Vertriebskanal verändern. Sehen wir uns an, was wir erhalten, und entscheiden wir dann, was unsere nächsten Schritte sein werden.

Umsatz nach Kanal

Für jeden Kanal möchten wir den Gesamtumsatz, den durchschnittlichen Auftragswert, die Gesamtzahl der Aufträge und die Anzahl der einzelnen Kunden anzeigen. Außerdem möchten wir die Kanäle nach dem Gesamtumsatz ordnen. Hier ist die Abfrage:

SELECT ch.channel_name,
       SUM(o.total_amount) AS total_sales,
       AVG(o.total_amount) AS average_order_value,
       COUNT(o.order_id) AS total_orders,
       COUNT(DISTINCT o.customer_id) AS unique_customers,
       RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS sales_rank
FROM channels ch
LEFT JOIN customers c
ON ch.id = c.channel_id
LEFT JOIN orders o
ON c.customer_id = o.customer_id
GROUP BY ch.channel_name
ORDER BY sales_rank;

Diese Abfrage verwendet mehrere Aggregationen: SUM() zur Berechnung des Gesamtumsatzes, AVG() für den durchschnittlichen Bestellwert und COUNT() für die Gesamtzahl der Bestellungen und (mit DISTINCT) für die eindeutigen Kunden.

Anschließend wird die Fensterfunktion RANK() verwendet, um die Kanäle nach dem Gesamtumsatz zu ordnen. Da Fensterfunktionen vor der Aggregation ausgeführt werden, können wir nicht einfach die Spalte total_sales in der Fensterfunktion verwenden. Stattdessen muss ich die gesamte Berechnung replizieren - eine einfache Aufgabe, die durch Kopieren und Einfügen erledigt wird.

Wir verwenden Daten aus den Tabellen channels und orders. Ich kann diese beiden Tabellen jedoch nicht direkt verbinden, da sie keine gemeinsame Spalte haben. Stattdessen müssen wir sie über die Tabelle customers.

Hier ist die Ausgabe:

channel_nametotal_salesaverage_order_valuetotal_ordersunique_customerssales_rank
Organic Search14,003,046.951,603.108,7355651
Direct2,436,649.061,799.591,354912
Referral1,034,734.451,645.05629463
Social837,378.771,824.35459324
Paid Search483,824.241,645.66294195
Email213,186.221,450.2514786

Wir sehen, dass der Großteil der Verkäufe von Northwind über die organische Suche erfolgt. Es gibt keine Widersprüche in den Daten - der Rang nach Gesamtumsatz spiegelt auch die Anzahl der Bestellungen und der einzelnen Kunden wider.

Interessant ist, dass der Kanal "Organische Suche" nicht den höchsten durchschnittlichen Bestellwert aufweist. Hier besteht Verbesserungsbedarf!

Stellen wir uns eine Strategie zur Umsatzsteigerung vor, die sich auf die Erhöhung des durchschnittlichen Bestellwerts aller Kanäle und nicht auf Neuakquisitionen konzentriert.

Wahrscheinlich wird es nicht möglich sein, einen einheitlichen Ansatz zu wählen, der nur auf dem Marketingkanal basiert. Innerhalb jedes Segments gibt es möglicherweise Kunden mit gegensätzlichen Kaufgewohnheiten. Wir können davon ausgehen, dass dies insbesondere für die organische Suche gilt, die ein so großer Kanal ist.

Daher müssen wir mehr über die Verteilung des Bestellwerts für jeden Kanal erfahren.

Verteilung des Bestellwerts

Berechnen wir nun den durchschnittlichen Bestellwert, den Median, das obere Quartil, das untere Quartil und die Standardabweichung für jeden Kanal:

SELECT ch.channel_name,
	 AVG(total_amount) AS average_order_value,
       PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_amount) AS median_order_value,
       PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY total_amount) AS upper_quartile_order_value,
       PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY total_amount) AS lower_quartile_order_value,
       STDDEV(total_amount) AS order_value_stddev
FROM channels ch
LEFT JOIN customers c
ON ch.id = c.channel_id
LEFT JOIN orders o
ON c.customer_id = o.customer_id
GROUP BY ch.channel_name;

Der durchschnittliche Bestellwert wird von AVG() berechnet.

Der Median (oder das 50. Perzentil), das obere Quartil und das untere Quartil werden mit der Funktion PERCENTILE_CONT() ordered-set aggregate berechnet, wobei das Perzentil in Klammern angegeben wird.

Für die Berechnung des Perzentils müssen die Daten in aufsteigender Reihenfolge sortiert werden; dies geschieht mit der Klausel WITHIN GROUP.

Nach der Perzentilberechnung wird STDDEV() zur Berechnung der Standardabweichung verwendet.

Hier ist die Ausgabe:

channel_nameaverage_order_valuemedian_order_valueupper_quartile_order_valuelower_quartile_order_valueorder_value_stddev
Direct1,799.591,005.902,166.80517.402,323.63
Email1,450.25960.001,970.41492.501,655.97
Organic Search1,603.101,007.702,018.20480.001,916.39
Paid Search1,645.661,083.002,104.40486.551,813.22
Referral1,645.051,064.002,034.50482.901,969.01
Social1,824.351,122.802,123.20559.902,319.10

Analysieren wir die Metriken für den ersten Kanal.

Die Standardabweichung des Kanals Direct beträgt 2.323,63 und ist damit höher als der Mittelwert oder Durchschnitt. Dies deutet auf eine hohe Variabilität hin, d. h. es gibt wahrscheinlich Ausreißer oder eine große Bandbreite an Auftragswerten.

Die unteren und oberen Quartile zeigen, dass 50 % der Aufträge zwischen 517,40 und 2.166,80 liegen. Eine hohe Standardabweichung bedeutet jedoch, dass viele Aufträge außerhalb dieses Bereichs liegen.

Der Median ist deutlich niedriger als der Mittelwert, was darauf hindeutet, dass die Verteilung rechtsschief ist, d. h. eine geringe Anzahl von hohen Auftragswerten, die den Mittelwert erhöhen.

Wir können jeden Kanal auf die gleiche Weise analysieren.

Das bringt uns auf eine weitere Idee. Um maßgeschneiderte Strategien zur Umsatzsteigerung zu entwickeln, können wir die Kunden weiter nach dem oberen und unteren Quartil segmentieren. Alle Kunden, die über dem oberen Quartil liegen, werden als "High Spenders" betrachtet, während die Kunden unterhalb des unteren Quartils als "Low Spenders" gelten.

Auf diese Weise können wir unsere Strategie an jeden Kanal und an die Ausgabengewohnheiten jeder Gruppe anpassen.

Kunden mit hohem und niedrigem Wert nach Kanal

Ich werde diese Berechnung nur für einen Marketingkanal durchführen. Sie können sie auch für alle anderen durchführen, da der Code derselbe bleibt; Sie ändern nur den Kanal in WHERE.

Die Kunden im oberen Quartil sind hochwertige Kunden, die in einer separaten Liste aufgeführt werden sollen:

WITH customer_order_totals AS (
    SELECT c.customer_id,
           c.full_name,
           SUM(o.total_amount) AS total_order_value,
           ch.channel_name
    FROM customers c
    JOIN orders o ON c.customer_id = o.customer_id
    JOIN channels ch ON c.channel_id = ch.id
    WHERE ch.channel_name = 'Direct'
    GROUP BY c.customer_id, c.full_name, ch.channel_name
),

upper_quartile_value AS (
    SELECT PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY total_order_value) AS upper_quartile
    FROM customer_order_totals
)

SELECT cot.customer_id,
       cot.full_name,
       cot.total_order_value,
       cot.channel_name
FROM customer_order_totals cot
JOIN upper_quartile_value uqv 
ON cot.total_order_value >= uqv.upper_quartile
ORDER BY cot.total_order_value DESC;

Die erste CTE berechnet den Gesamtauftragswert jedes Kunden, indem sie SUM() verwendet und die Tabellen customers und Bestellungen. In WHERE filtern wir alle Kanäle außer Direkt heraus.

Die zweite CTE berechnet das obere Quartil auf die bekannte Weise.

CROSS JOINs In der dritten SELECT werden dann die beiden CTEs miteinander verknüpft, um alle Kunden zu finden, deren Bestellwert über dem oberen Quartil liegt.

Hier sind die ersten zehn Zeilen der Ausgabe:

customer_idfull_nametotal_order_valuechannel_name
134Barry Michael79,371.50Direct
152Carolann Williams64,365.21Direct
7Frédérique Citeaux61,865.74Direct
17Sven Ottlieb57,251.14Direct
64Sergio Gutiérrez55,140.75Direct
490Alice Blevins54,736.24Direct
8Martín Sommer54,499.55Direct
303Gregory Mack52,554.20Direct
316Jeff Heard51,976.31Direct
129Stephan Bufford50,868.70Direct

Insgesamt gibt es 23 Kunden in der Ausgabe. Wir können diese Informationen nutzen, um diese hochwertigen Kunden gezielt anzusprechen. So können wir beispielsweise Treueprogramme erstellen, bei denen diese Kunden besondere Rabatte, persönlichen Service, einlösbare Punkte für Einkäufe, VIP-Mitgliedschaftsprogramme usw. erhalten.

Auf ähnliche Weise können wir Kunden auflisten, deren Bestellungen unter dem unteren Quartil liegen:

WITH customer_order_totals AS (
    SELECT c.customer_id,
           c.full_name,
           SUM(o.total_amount) AS total_order_value,
	       ch.channel_name
    FROM customers c
    JOIN orders o 
    ON c.customer_id = o.customer_id
    JOIN channels ch
    ON c.channel_id = ch.id
	WHERE ch.channel_name = 'Direct'
    GROUP BY c.customer_id, c.full_name, ch.channel_name
),

lower_quartile_value AS (
    SELECT PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY total_order_value) AS lower_quartile
    FROM customer_order_totals
)

SELECT cot.customer_id,
       cot.full_name,
       cot.total_order_value,
       cot.channel_name
FROM customer_order_totals cot, lower_quartile_value lqv
WHERE cot.total_order_value <= lqv.lower_quartile
ORDER BY cot.total_order_value ASC;

Diese Abfrage ist fast die gleiche wie die vorherige, nur dass wir diesmal das untere Quartil berechnen. Außerdem suchen wir nach Kunden, deren Bestellungen unter diesem Wert liegen.

Auch hier liefert die Abfrage 191 Kunden. Hier sind die ersten zehn Zeilen:

customer_idfull_nametotal_order_valuechannel_name
939Shannon Aguilar98.40Direct
997Barbra Armstrong251.50Direct
687Andrew Scott452.90Direct
787Dennis Myer912.00Direct
917Les Allen991.40Direct
921Shelby Turgeon1,162.25Direct
560Nancy Wiggins1,425.80Direct
678Tracey Thomas2,555.20Direct
756Dora Rowlands2,713.50Direct
715George Scott2,906.50Direct

Diese Kunden müssen anders angesprochen werden, da sie zu höheren Ausgaben motiviert werden müssen. Zu den Strategien zur Steigerung des Umsatzes mit diesen Kunden können personalisierte Produktpakete, kostenloser Versand ab einem bestimmten Betrag, höhere Rabatte für höhere Bestellschwellen oder das Angebot eines kostenlosen Geschenks ab einem bestimmten Bestellwert gehören.

Führen Sie nun die gleiche Analyse für alle anderen Kanäle durch und überlegen Sie, welche Taktiken Sie anwenden könnten, um die Bestellwerte aus diesem Kanal zu erhöhen.

Sind Sie bereit für Ihr eigenes SQL-Portfolio-Projekt?

In diesem Artikel haben wir gezeigt, wie Sie ein SQL-Portfolio-Projekt mit der Northwind-Store-Datenbank vorbereiten können. Wenn Sie kreativ genug sind, können Sie natürlich auch Ihren eigenen Datensatz erstellen.

Die Erstellung eines SQL-Projekts für Ihr Portfolio ist ein wichtiger Teil der Vorbereitung auf den Einstellungsprozess. Portfolios sind eine hervorragende Möglichkeit, Ihre praktischen Fähigkeiten in der Datenanalyse mit SQL zu präsentieren. Das obige Projekt ist nur ein Beispiel. In unserem Kurs SQL-Datenbanken für die Praxis, den wir Ihnen wärmstens empfehlen, können Sie viele weitere Analyseideen ausprobieren!