Retour à la liste des articles Articles
22 minutes de lecture

SQL Project pour le portefeuille : Magasin Northwind

Les projets SQL pour un portefeuille sont une partie importante de la formation d'un analyste de données. Comment démarrez-vous un projet, et où allez-vous avec les informations que vous découvrez ? Nous utiliserons la base de données du magasin Northwind pour répondre à ces questions.

La réalisation d'un projet SQL solide est un élément essentiel du processus d'apprentissage de tout analyste de données. Un projet SQL est un excellent outil d'apprentissage, car il vous oblige à utiliser SQL sur un ensemble de données réelles. C'est particulièrement important si vous êtes un débutant et que vous n'avez pas l'occasion de travailler avec des ensembles de données réels. En travaillant avec plusieurs ensembles de données réels, vous apprenez les problèmes que vous pouvez rencontrer dans le monde réel. En outre, la réalisation de plusieurs projets SQL dans le cadre de votre portefeuille est toujours une bonne chose à voir dans un CV.

La question est de savoir comment préparer un portefeuille de projets SQL. Une fois que vous avez trouvé un jeu de données en ligne gratuit que vous souhaitez analyser, que faites-vous ensuite ? Nous allons répondre à ces questions en utilisant la base de données du magasin Northwind.

Vous trouverez cet ensemble de données dans le cours Bases de données SQL pour la pratique. Il s'agit de l'un des six ensembles de données qui comprennent des données provenant d'une université, du trafic d'un blog, de résultats sportifs, d'un magasin de musique et du Museum of Modern Art (MoMA). Ce cours fait partie de la pisteLa pratique du SQL , où vous pouvez pratiquer l'agrégation, JOINs, les sous-requêtes, les CTE, CASE WHEN, et d'autres sujets SQL importants. Si vous avez besoin de rafraîchir vos connaissances sur certains de ces domaines importants pour la création de rapports, essayez notre cours de Création de rapports basiques en SQL cours.

Prenons maintenant l'ensemble de données du magasin Northwind et utilisons-le pour un projet SQL destiné à un portefeuille d'analystes de données. Je vais réaliser ce projet avec PostgreSQL, mais tout ce que je fais est transférable à d'autres bases de données avec des modifications mineures de la syntaxe.

N'hésitez pas à consulter notre aide-mémoire SQL pour l'analyse de données.

Le jeu de données du magasin Northwind

La seule information que je vous donnerai sur cette base de données est qu'elle se compose de six tables :

  1. categories - Une liste de catégories de produits.
  2. channels - Une liste des sources par lesquelles le magasin acquiert des clients.
  3. customers - Une liste des clients du magasin.
  4. order_items - Une liste des produits inclus dans chaque commande.
  5. orders - La liste des commandes passées par les clients.
  6. products - la liste des produits proposés par le magasin.

Nous recueillerons le reste des informations en réalisant notre projet. En fait, cette inspection de la base de données devrait constituer l'étape initiale de tout projet, celle qui précède le début de l'analyse.

1. Inspection de la base de données

Cette étape de la préparation d'un projet SQL consiste à connaître vos données, telles que les noms des tables et des colonnes, les clés primaires et étrangères, les relations entre les tables et les types de données de chaque table.

Inspection des noms de tables

Une première façon de connaître les tables de l'ensemble de données est de les trouver dans le navigateur du SGBDR dans lequel vous avez importé l'ensemble de données, par exemple PostgreSQL, SQL Server ou MySQL.

Dans PostgreSQL, vous pouvez écrire cette requête pour obtenir une liste de toutes les tables d'une base de données :

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;

Elle recherche les données dans la table information_schemaCette requête recherche les données dans la base de données "tables", où vous pouvez trouver les métadonnées de la base de données. Comme je souhaite obtenir le nom de chaque table, j'utilise la vue tables après avoir référencé le schéma et placé table_name dans SELECT.

La première condition dans WHERE filtre les schémas système et ne laisse que les tables définies par l'utilisateur. La deuxième condition garantit que seules les tables de base sont répertoriées, sans les vues et les autres tables.

Voici la liste des tables de la base de données Northwind :

table_name
categories
channels
customers
order_items
orders
products

Inspection des informations sur les colonnes

Nous voulons maintenant mieux comprendre les détails de chaque table. Connaître leurs colonnes est un bon début.

Nous pouvons à nouveau interroger information_schema pour obtenir des informations importantes sur les colonnes :

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;

Nous pouvons trouver les noms des tables, les noms des colonnes et le type de données de chaque colonne en listant table_name, column_name, et data_type dans SELECT.

La colonne is_nullable nous indiquera si la colonne accepte les valeurs NULL.

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

De cette manière, nous disposons de toutes les informations en un seul endroit, ce qui facilite les recherches.

Tout d'abord, nous pouvons comprendre quelles sont les données de chaque tableau en voyant les noms des colonnes. Les types de données semblent tous logiques. Par exemple, aucun ID n'est défini comme character varying. Il n'est donc pas nécessaire de convertir les données dans un format approprié à ce stade.

En ce qui concerne les NULL, nous obtenons des informations précieuses sur les tables :

  • Le tableau categories permet à la description de la catégorie d'être NULL.
  • Le tableau channels n'autorise pas du tout NULLs.
  • Le tableau customers autorise les NULLdans de nombreuses colonnes, y compris le pays. Si nous voulons créer une segmentation géographique des clients, ces NULLpourraient nous poser des problèmes.
  • La sortie ci-dessus montre qu'aucune colonne de la table order_items n'est nullable.
  • Pour la table products il est intéressant de noter que le prix unitaire peut être NULL.
  • Il semble un peu étrange que la table orders ait autant de colonnes nullables. En effet, il est possible d'avoir un numéro d'identification de l'ordre et aucune autre information sur l'ordre. Nous devons garder cela à l'esprit lors de l'analyse de cette table.

Recherche des clés primaires et étrangères

L'étape suivante de l'inspection des données consiste à comprendre comment les tables fonctionnent ensemble. Pour ce faire, nous devons d'abord trouver les clés primaires (PK) et les clés étrangères (FK).

Les clés primaires nous indiquent quelle(s) colonne(s) une table utilise(nt) pour identifier les données de manière unique. Ce faisant, vous comprendrez mieux la structure de la table.

Les clés primaires nous indiquent quelle colonne est liée à une clé primaire d'une autre table. C'est la base pour comprendre les relations entre les tables.

Vous pouvez dresser la liste des PK et des FK à l'aide de la requête ci-dessous :

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;

Cette fois, nous devons utiliser deux vues de la table information_schemaà savoir table_constraints et key_column_usage.

La vue table_constraints contient des informations sur les contraintes de chaque table. La vue key_column_usage permet d'obtenir des informations sur les colonnes ayant des contraintes de clé.

Ces deux vues sont reliées par deux colonnes communes : constraint_name (le nom de la contrainte) et constraint_schema (le nom du schéma contenant la contrainte).

J'utilise la clause WHERE pour ne sortir que les contraintes de clé primaire et de clé étrangère.

Pour finir, j'ordonne la sortie par ordre alphabétique du nom de la table, puis par ordre alphabétique inverse du type de contrainte.

Le code produit cette table :

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

Les tables categories et channels sont les seules tables qui ont une PK mais pas de FK.

Toutes les autres tables ont une PK et au moins une FK. La seule exception est la table order_itemsorder_id product_idEn d'autres termes, la table possède une clé primaire composite, ce qui signifie que l'identifiant unique des données est la combinaison unique de deux colonnes. Dans ce cas, il s'agit d'afficher un seul numéro de produit par commande individuelle.

Un examen plus approfondi révèle que les mêmes colonnes sont également une clé étrangère pour la même table. Cela signifie que les clés primaires sont également étrangères, puisqu'elles proviennent d'autres tables.

Comprendre les relations entre les tables

L'énumération des clés primaires et étrangères nous donne déjà une idée des relations entre les tables. Cependant, nous pouvons mieux les comprendre en montrant les tables et colonnes mères et filles des clés étrangères.

Pourquoi faire cela ? Une clé étrangère est toujours la clé primaire d'une autre table. L'énumération de toutes les tables et colonnes mères et filles constitue une amélioration par rapport à la requête précédente, car elle permet de voir facilement les colonnes partagées entre les tables.

Ces informations sont utiles pour comprendre les relations entre les tables, savoir quelles colonnes vous pouvez utiliser pour joindre les tables et si elles peuvent être jointes directement ou par l'intermédiaire d'une autre table.

Voici le 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;

Dans la requête, nous utilisons des informations provenant des tables table_constraints, key_column_usage, et constraint_column_usage dans la requête.

La vue constraint_column_usage montre les colonnes utilisées par une contrainte. Nous l'utiliserons pour montrer la table et la colonne parentes du FK, c'est-à-dire la table et la colonne où ce FK est en fait un PK.

Les données de key_column_usage affichent la table et la colonne d'un FK.

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

D'après la sortie ci-dessus, nous voyons que la table orders est directement connectée à la table customers via les colonnes order_id (PK) et customer_id (FK). Il est connecté à la table order_items via order_id (PK).

La table channels n'est directement connectée qu'à la table customers.

Nous avons déjà établi que les clés primaires de la table order_items sont également des clés étrangères. D'après le tableau ci-dessus, nous pouvons voir qu'elles proviennent des tables orders et products.

La table categories n'est reliée qu'à la table products.

2. Exploration des données

Lorsque vous commencez à travailler avec une nouvelle base de données, la première étape consiste à explorer vos données afin d'obtenir des informations de base sur chaque table de la base de données. C'est une bonne pratique que d'examiner les éléments suivants :

  • Compter les lignes.
  • Trouver les valeurs minimales et maximales (en particulier pour les dates).
  • Dresser la liste des catégories distinctes.
  • Compter les valeurs distinctes dans chaque catégorie.
  • compter les lignes par valeur de catégorie
  • Sommes et moyennes pour les valeurs numériques clés.

Cette liste n'est pas exhaustive et peut changer en fonction du tableau que vous explorez. Il s'agit toutefois d'un minimum qui devrait fonctionner pour la plupart des tableaux.

En général, cette étape se résume à l'agrégation et au regroupement des données. Je vais vous montrer comment cela fonctionne sur une table de notre base de données, puis vous pourrez explorer d'autres tables de la même manière.

Exploration du tableau des commandes

Nombre de lignes

Comme prévu, nous utilisons ici la fonction d'agrégation COUNT():

SELECT COUNT(*) AS number_of_rows
FROM orders;

J'utilise la fonction COUNT() avec un astérisque ; cela comptera toutes les lignes, y compris les NULL.

number_of_rows
11,618

La table orders compte 11 618 lignes. En d'autres termes, il y a 11 618 commandes.

Valeurs minimales et maximales

Voyons quelles colonnes peuvent être utilisées avec les fonctions MIN() et MAX(). Parfait - nous pouvons trouver la date la plus ancienne et la plus récente à laquelle une commande a été passée et expédiée :

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;

Le résultat montre que la première commande a été passée à l'adresse 2017-01-01 et la dernière à l'adresse 2024-06-24. En ce qui concerne les dates d'expédition, la première date est 2017-01-01 et la plus récente est 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

Lister les catégories

Nous n'avons pas besoin de fonctions d'agrégation pour ce faire :

SELECT DISTINCT ship_country
FROM orders
ORDER BY ship_country;

Dans la requête, nous utilisons la clause DISTINCT pour n'afficher chaque pays qu'une seule fois.

Voici la liste des pays figurant dans ce tableau :

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

Il sera également utile de voir une liste de villes :

SELECT DISTINCT ship_city
FROM orders
ORDER BY ship_city;

Comme il y a beaucoup de villes, voici une liste partielle :

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

Comptage des valeurs distinctes dans chaque catégorie

Voyons maintenant le nombre de pays dans lesquels Northwind a effectué des livraisons :

SELECT COUNT(DISTINCT ship_country) AS number_of_countries
FROM orders;

Nous utilisons à nouveau COUNT(), mais cette fois nous ajoutons DISTINCT et nous référençons la colonne que nous voulons compter.

Le résultat montre qu'il y a 21 pays différents où l'entreprise livre :

number_of_countries
21

Nous pourrions faire la même chose pour les villes :

SELECT COUNT(DISTINCT ship_city) AS number_of_cities
FROM orders;

Il y a 494 villes uniques :

number_of_cities
494

Même si nous ne les avons pas répertoriées dans une catégorie distincte, il sera utile de savoir combien de clients ont passé des commandes :

SELECT COUNT(DISTINCT customer_id) AS number_of_customers_with_orders
FROM orders;

Nous utilisons COUNT() pour compter les identifiants des clients. Bien entendu, nous avons besoin de DISTINCT, car on peut s'attendre à ce que certains clients apparaissent plusieurs fois, c'est-à-dire chaque fois qu'ils passent une commande.

Il y a 761 clients uniques qui ont commandé au moins une fois à l'entreprise.

number_of_customers_with_orders
761

Comptage des lignes par valeur de catégorie

Pour ce faire, nous devons énumérer chaque valeur de catégorie (nous l'avons fait précédemment) et compter le nombre de lignes pour chaque valeur.

Par exemple, nous obtiendrons le nombre de commandes par pays :

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

Je sélectionne la colonne ship_country et j'utilise ensuite COUNT(*) pour compter toutes les lignes. Pour que le comptage se fasse par pays, je dois introduire la clause GROUP BY ship_country.

Nous obtenons ainsi le nombre de commandes par pays. Le résultat montre que les États-Unis sont le marché le plus important, avec 9 789 commandes :

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

Somme et valeurs moyennes

Si vos données sont appropriées, vous devez trouver des moyennes et des totaux de valeurs numériques. Nous pouvons le faire pour la colonne total_amount, qui représente la valeur d'une commande :

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

Pour afficher ces deux calculs, utilisez les fonctions d'agrégation SUM() et AVG().

Nous pouvons voir que le chiffre d'affaires total du magasin est d'un peu plus de 19 millions. La valeur moyenne d'une commande est de 1 636,15 :

orders_total_valueaverage_order_value
19,008,819.691,636.15

Exploration des données en prime

Comme je l'ai mentionné, les agrégations précédentes devraient être le minimum que vous fassiez pour chaque table de la base de données.

Cette exploration des données va un peu plus loin (mais pas trop loin) que les simples agrégations et GROUP BY. Bien qu'il s'agisse toujours de la base, vous pouvez également utiliser d'autres concepts tels que le filtrage des données (à l'aide de WHERE et/ou HAVING), l'extraction de parties de dates ou de l'heure, l'utilisation de CASE WHEN pour étiqueter les données, et ainsi de suite.

Examinons quelques exemples.

Nombre de produits

Nous pouvons utiliser la fonction COUNT() pour trouver le nombre total de produits vendus par le magasin.

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

La clause WHERE contient une condition pour n'afficher que les produits qui ne sont pas en fin de série, c'est-à-dire les produits que le magasin vend actuellement.

D'après la section précédente, nous savons que la colonne discontinued est de type booléen. Nous devons donc utiliser l'opérateur IS FALSE pour n'inclure dans le décompte que les produits qui ne sont pas en rupture de stock.

Le résultat est 69 :

number_of_active_products
69

Recettes par année

Un rapport simple indiquant les recettes par année peut être créé à l'aide de SUM():

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

Nous utilisons la fonction EXTRACT() pour obtenir uniquement les années à partir des dates de commande. Ensuite, nous additionnons les montants totaux de toutes les commandes et nous les regroupons par année pour afficher les valeurs de chaque année séparément.

Les résultats montrent que l'année 2018 a été l'année de pointe pour l'entreprise. L'année où les recettes sont les plus faibles est 2024, mais cela peut s'expliquer par le fait que l'année n'est pas terminée (au moment de l'analyse).

Un autre point intéressant est qu'il n'y a pas de données sur les revenus pour les années 2019-2022. Il convient de vérifier si les données sont manquantes pour une raison précise ou s'il s'agit d'une erreur.

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

Segmentation des clients par pays

Nous disposons de données sur les pays des clients, de sorte qu'un aperçu du nombre de clients dans chaque pays serait instructif.

Nous ne sommes pas non plus intéressés par les clients qui n'ont pas passé de commande, car cela peut gonfler artificiellement le nombre de clients. Non, nous ne voulons que les clients qui commandent chez nous. Voici la requête :

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;

Nous sélectionnons et regroupons par pays et utilisons la fonction d'agrégation COUNT() pour trouver le nombre de clients. Si les données de la colonne first_order ne sont pas nulles, alors ce client a passé au moins une commande ; c'est la condition que nous devons utiliser dans WHERE.

Les données sont classées du plus grand au plus petit nombre de clients.

Le résultat montre que le marché le plus important de Northwind en termes de clients est celui des États-Unis. On peut également en conclure qu'il s'agit du marché le plus important en termes de chiffre d'affaires.

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. Analyse avancée des données et informations exploitables

Ce que nous avons fait jusqu'à présent est un bon début. Cependant, l'analyse des données dans un projet SQL pour un portefeuille ne doit pas s'arrêter à une simple agrégation et à une exploration pour chaque table.

Nous allons maintenant aller plus loin et écrire des requêtes plus complexes qui nous permettront de prendre des mesures et d'améliorer les activités de Northwind. Par exemple, nous pourrions vouloir voir comment les ventes changent en fonction du canal. Voyons ce que nous obtenons et décidons ensuite des prochaines étapes.

Ventes par canal

Pour chaque canal, nous voulons afficher les ventes totales, la valeur moyenne des commandes, le nombre total de commandes et le nombre de clients uniques. Nous voulons également classer les canaux en fonction des ventes totales. Voici la requête :

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;

Cette requête utilise plusieurs agrégations : SUM() pour calculer les ventes totales, AVG() pour la valeur moyenne des commandes, et COUNT() pour le nombre total de commandes et (avec DISTINCT) pour les clients uniques.

Ensuite, nous utilisons la fonction de fenêtre RANK() pour classer les chaînes en fonction de leurs ventes totales. Étant donné que les fonctions de fenêtre sont exécutées avant l'agrégation, nous ne pouvons pas simplement utiliser la colonne total_sales dans la fonction de fenêtre. Au lieu de cela, je dois reproduire l'ensemble du calcul - une tâche simple qui s'effectue par copier-coller.

Nous utilisons les données des tableaux channels et orders. Cependant, je ne peux pas joindre directement ces deux tables car elles n'ont pas de colonne commune. Au lieu de cela, nous devons les joindre par l'intermédiaire de la table customers.

Voici le résultat :

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

Nous constatons que la plupart des ventes de Northwind proviennent de la recherche organique. Il n'y a pas de contradiction dans les données : le classement par chiffre d'affaires total reflète également le nombre de commandes et de clients uniques.

Il est intéressant de noter que le canal de recherche organique n'a pas la valeur moyenne de commande la plus élevée. Cela mérite d'être amélioré !

Envisageons une stratégie d'augmentation du chiffre d'affaires qui se concentre sur l'augmentation de la valeur moyenne des commandes de tous les canaux plutôt que sur de nouvelles acquisitions.

Nous ne pourrons probablement pas adopter une approche uniforme basée uniquement sur le canal de commercialisation. Dans chaque segment, il peut y avoir des clients dont les habitudes de consommation sont diamétralement opposées. Nous pouvons supposer que cela s'applique particulièrement à la recherche organique, qui est un canal très important.

Nous devons donc en savoir plus sur la répartition de la valeur des commandes pour chaque canal.

Distribution de la valeur des commandes

Calculons la valeur moyenne des commandes, la médiane, le quartile supérieur, le quartile inférieur et l'écart-type pour chaque canal :

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;

La valeur moyenne des commandes est calculée par AVG().

La médiane (ou 50e centile), le quartile supérieur et le quartile inférieur sont calculés à l'aide de la fonction d'agrégation d'ensembles ordonnés PERCENTILE_CONT(), le centile étant spécifié entre parenthèses.

Pour le calcul du percentile, les données doivent être triées par ordre croissant, ce que nous faisons à l'aide de la clause WITHIN GROUP.

Après le calcul du percentile, STDDEV() est utilisé pour calculer l'écart type.

Voici le résultat :

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

Analysons les mesures pour le premier canal.

L'écart-type du canal Direct est de 2 323,63, ce qui est supérieur à la moyenne. Cela indique une grande variabilité, c'est-à-dire qu'il y a probablement des valeurs aberrantes ou un large éventail de valeurs de commande.

Les quartiles inférieur et supérieur montrent que 50 % des commandes se situent entre 517,40 et 2 166,80. Toutefois, un écart-type élevé signifie que de nombreux ordres se situent en dehors de cette fourchette.

La médiane est nettement inférieure à la moyenne, ce qui indique que la distribution est asymétrique, c'est-à-dire qu'un faible nombre de valeurs élevées augmente la moyenne.

Nous pouvons analyser chaque canal de la même manière.

Cela nous donne une autre idée. Pour créer des stratégies personnalisées d'augmentation du chiffre d'affaires, nous pouvons segmenter davantage les clients en fonction du quartile supérieur et du quartile inférieur. Nous considérerons tous les clients situés au-dessus du quartile supérieur comme des clients très dépensiers ; les clients situés en dessous du quartile inférieur seront des clients peu dépensiers.

Nous pourrons ainsi adapter notre stratégie à chaque canal et aux habitudes de consommation de chaque groupe.

Clients à forte et à faible valeur ajoutée par canal

Je vais effectuer ce calcul pour un seul canal de commercialisation. Vous pouvez le faire pour tous les autres, car le code reste le même ; vous changez seulement le canal dans WHERE.

Les clients situés dans le quartile supérieur sont des clients à forte valeur ajoutée, et nous voulons qu'ils figurent sur une liste distincte :

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;

Le premier CTE calcule la valeur totale de la commande de chaque client en utilisant SUM() et en joignant les tables customers et les commandes. Dans WHERE, nous filtrons tous les canaux à l'exception de Direct.

Le deuxième CTE calcule le quartile supérieur de la manière habituelle.

Le troisième SELECT CROSS JOINs les deux CTE pour trouver tous les clients dont la valeur de la commande est supérieure au quartile supérieur.

Voici les dix premières lignes de la sortie :

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

Au total, il y a 23 clients dans le résultat. Nous pouvons utiliser ces informations pour cibler ces clients de grande valeur. Par exemple, nous pouvons créer des programmes de fidélisation dans le cadre desquels ces clients peuvent bénéficier de remises spéciales, d'un service personnalisé, de points échangeables contre des achats, de programmes d'adhésion VIP, etc.

De même, nous pouvons dresser la liste des clients dont les commandes sont inférieures au quartile inférieur :

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;

Cette requête est presque identique à la précédente, sauf que nous calculons cette fois le quartile inférieur. Nous recherchons également les clients dont les commandes sont inférieures à ce quartile.

Une fois de plus, la requête renvoie 191 clients. Voici les dix premières lignes :

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

Ces clients doivent être abordés différemment, car ils doivent être incités à dépenser davantage. Les stratégies visant à augmenter les recettes provenant de ces clients peuvent inclure des offres groupées de produits personnalisées, la livraison gratuite à partir d'un certain montant, l'augmentation des remises pour des seuils de commande plus élevés ou l'offre d'un cadeau gratuit à partir d'un certain montant de commande.

Maintenant, faites la même analyse pour tous les autres canaux et réfléchissez aux tactiques que vous pourriez utiliser pour augmenter les valeurs de commande de ce canal.

Prêt pour votre propre projet de portefeuille SQL ?

Dans cet article, nous avons montré comment préparer un projet de portefeuille SQL en utilisant la base de données du magasin Northwind. Bien entendu, si vous êtes suffisamment créatif, vous pouvez également créer votre propre ensemble de données.

La réalisation d'un projet SQL pour votre portfolio est une partie importante de la préparation au processus d'embauche. Les portfolios sont une excellente vitrine de vos compétences pratiques en matière d'analyse de données en SQL. Le projet ci-dessus n'est qu'un exemple. Il y a beaucoup d'autres idées d'analyse que vous pouvez essayer dans notre cours Bases de données SQL pour la pratique, que nous recommandons vivement !