Dans ce tutoriel, nous explorerons l’utilisation de la fonction COALESCE dans les requêtes SQL de Data Explorer.
COALESCE vous permet de gérer les valeurs NULL dans les résultats de vos requêtes. Si vos données contiennent des valeurs NULL, vous pouvez utiliser COALESCE pour fournir une valeur par défaut (comme 0) à la place de ces valeurs NULL.
COALESCE est particulièrement utile lorsque vous devez effectuer ultérieurement des calculs ou des analyses de données sur les résultats de votre requête, où des valeurs NULL pourraient causer des problèmes ou des interprétations erronées.
Syntaxe
La fonction COALESCE prend deux arguments ou plus et renvoie la première valeur non NULL rencontrée de gauche à droite dans la liste. Si tous les arguments sont NULL, COALESCE renvoie NULL.
La syntaxe de base pour COALESCE est la suivante :
COALESCE(valeur1, valeur2, ..., valeurN)
Par exemple, COALESCE(NULL, 1, 2) renverra 1 car 1 est le premier argument non NULL.
Requêtes d’exemple
Examinons quelques requêtes d’exemple pour comprendre comment COALESCE est utilisé dans les requêtes de Data Explorer.
Messages créés, J’aime reçus et Signets reçus
Niveau de complexité : Débutant
Cette requête récupère le nombre total de messages créés, de J’aime reçus et de signets reçus sur les messages pour chaque utilisateur d’un site. Si un utilisateur n’a aucun message, J’aime ou signet reçu, la fonction COALESCE renverra 0 au lieu de NULL.
SELECT
users.id AS user_id,
users.username,
COALESCE(COUNT(posts.id), 0) AS post_count,
COALESCE(SUM(posts.like_count), 0) AS likes_received,
COALESCE(SUM(posts.bookmark_count), 0) AS bookmarks_received
FROM
users
LEFT JOIN
posts ON users.id = posts.user_id
GROUP BY
users.id, users.username
ORDER BY
post_count DESC, likes_received DESC, bookmarks_received DESC
Résultats d’exemple :
| user | username | post_count | likes_received | bookmarks_received |
|---|---|---|---|---|
| 1 | alice | 345 | 6 | 9 |
| 2 | bella | 278 | 5 | 6 |
| 3 | charlie | 37 | 3 | 3 |
| 4 | dave | 0 | 0 | 0 |
Dans cette requête, nous joignons la table users à la table posts sur le champ user_id. Nous utilisons ensuite la fonction COALESCE pour nous assurer que si un utilisateur n’a aucun message, J’aime reçu ou signet reçu, nous renvoyons 0 au lieu de NULL. Les résultats sont regroupés par ID d’utilisateur et nom d’utilisateur, puis triés par nombre de messages, J’aime et signets dans l’ordre décroissant.
Sujets et Réponses par Utilisateur
Niveau de complexité : Intermédiaire
Cette requête obtient le nombre de sujets et de réponses publiés par chaque utilisateur entre deux dates. Si un utilisateur n’a aucun sujet ou réponse, COALESCE renverra 0 au lieu de NULL.
-- [params]
-- date :start_date
-- date :end_date
-- string NULL:username
WITH qtt_topics AS (
SELECT
t.user_id,
COUNT(*) AS topics
FROM topics t
WHERE
t.user_id > 0
AND t.deleted_at ISNULL
AND t.archetype = 'regular'
AND t.created_at::date BETWEEN :start_date AND :end_date
GROUP BY t.user_id
),
qtt_replies AS (
SELECT
p.user_id,
COUNT(*) AS replies
FROM posts p
WHERE
p.user_id > 0
AND p.deleted_at ISNULL
AND p.post_number != 1
AND p.created_at::date BETWEEN :start_date AND :end_date
GROUP BY p.user_id
),
total AS (
SELECT
COALESCE(qr.user_id, qt.user_id) user_id,
COALESCE(topics,0) qtt_topics,
COALESCE(replies,0) qtt_replies
FROM qtt_topics qt
FULL JOIN qtt_replies qr ON qt.user_id = qr.user_id
ORDER BY user_id)
SELECT
username,
qtt_topics,
qtt_replies
FROM total
INNER JOIN users u ON u.id = user_id
WHERE :username ISNULL OR LOWER(username) LIKE '%'||LOWER(:username)||'%'
Résultats d’exemple:
| username | qtt_topics | qtt_replies |
|---|---|---|
| Alice | 10 | 50 |
| Bella | 15 | 45 |
| Charlie | 12 | 30 |
Dans cette requête, COALESCE est utilisé dans l’expression de table commune (CTE) total. Il garantit que si l’user_id est NULL dans qtt_topics ou qtt_replies, l’autre valeur est utilisée. Ceci est important car une FULL JOIN est utilisée pour combiner qtt_topics et qtt_replies, et si un utilisateur n’a que des sujets mais aucune réponse (ou inversement), son user_id serait NULL dans l’une des tables. COALESCE empêche cela de se produire.
Explication détaillée avec commentaires en ligne
-- [params]
-- date :start_date
-- date :end_date
-- string NULL:username
-- Définir une CTE (Expression de table commune) pour compter les sujets par utilisateur
WITH qtt_topics AS (
SELECT
t.user_id, -- identifiant de l'utilisateur
COUNT(*) AS topics -- nombre de sujets
FROM topics t -- depuis la table topics
WHERE
t.user_id > 0 -- ne considérer que les identifiants d'utilisateurs non nuls
AND t.deleted_at ISNULL -- ne considérer que les sujets non supprimés
AND t.archetype = 'regular' -- ne considérer que les sujets réguliers
AND t.created_at::date BETWEEN :start_date AND :end_date -- ne considérer que les sujets créés entre start_date et end_date
GROUP BY t.user_id -- regrouper par identifiant d'utilisateur pour obtenir le nombre de sujets par utilisateur
),
-- Définir une CTE pour compter les réponses par utilisateur
qtt_replies AS (
SELECT
p.user_id, -- identifiant de l'utilisateur
COUNT(*) AS replies -- nombre de réponses
FROM posts p -- depuis la table posts
WHERE
p.user_id > 0 -- ne considérer que les identifiants d'utilisateurs non nuls
AND p.deleted_at ISNULL -- ne considérer que les messages non supprimés
AND p.post_number != 1 -- ne considérer que les messages qui ne sont pas le premier message d'un sujet (c.-à-d. les réponses)
AND p.created_at::date BETWEEN :start_date AND :end_date -- ne considérer que les messages créés entre start_date et end_date
GROUP BY p.user_id -- regrouper par identifiant d'utilisateur pour obtenir le nombre de réponses par utilisateur
),
-- Définir une CTE pour combiner les comptes de sujets et de réponses par utilisateur
total AS (
SELECT
COALESCE(qr.user_id, qt.user_id) user_id, -- identifiant de l'utilisateur (provenant soit de qtt_replies soit de qtt_topics)
COALESCE(topics,0) qtt_topics, -- nombre de sujets (si NULL, retourner 0)
COALESCE(replies,0) qtt_replies -- nombre de réponses (si NULL, retourner 0)
FROM qtt_topics qt -- depuis la CTE qtt_topics
FULL JOIN qtt_replies qr ON qt.user_id = qr.user_id -- jointure avec la CTE qtt_replies sur l'identifiant de l'utilisateur
ORDER BY user_id -- trier par identifiant de l'utilisateur
)
-- Requête principale pour obtenir le jeu de résultats final
SELECT
username, -- nom d'utilisateur
qtt_topics, -- nombre de sujets
qtt_replies -- nombre de réponses
FROM total -- depuis la CTE total
INNER JOIN users u ON u.id = user_id -- jointure avec la table users sur l'identifiant de l'utilisateur
WHERE :username ISNULL OR LOWER(username) LIKE '%'||LOWER(:username)||'%' -- filtrer par nom d'utilisateur (si fourni)
Données sur les questions résolues
Niveau de complexité : Avancé / Nécessite le plugin Discourse Solved
Cette requête est utilisée pour obtenir des détails sur les sujets, y compris s’ils sont résolus ou non, le temps écoulé avant la première réponse, le temps écoulé avant la solution, et d’autres statistiques connexes.
Cette requête suppose que tous les sujets d’un site peuvent être résolus.
--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all
WITH valid_topics AS (
SELECT
t.id,
t.user_id,
t.title,
t.views,
posts_count-1 AS "posts_count",
t.created_at,
(CURRENT_DATE::date-t.created_at::date) AS "total_days",
string_agg(tags.name, ', ') AS tag_names
FROM topics t
LEFT JOIN topic_tags tt ON tt.topic_id = t.id
LEFT JOIN tags ON tags.id = tt.tag_id
LEFT JOIN tag_group_memberships tgm ON tgm.tag_id = tt.tag_id
LEFT JOIN (SELECT topic_id, MIN(created_at) AS "created_at"
FROM posts
WHERE deleted_at ISNULL
AND post_type = 1
AND post_number > 1
GROUP BY topic_id) date_first_reply ON date_first_reply.topic_id = t.id
WHERE t.deleted_at ISNULL
AND t.created_at::date BETWEEN :start_date AND :end_date
AND t.archetype = 'regular'
GROUP BY t.id, t.user_id, t.title, t.views, posts_count, t.created_at
),
solved_topics AS (
SELECT
vt.id,
tcf.created_at
FROM topic_custom_fields tcf
INNER JOIN valid_topics vt ON vt.id = tcf.topic_id
WHERE tcf.name = 'accepted_answer_post_id'
),
last_reply AS (
SELECT p.topic_id, p.user_id FROM posts p
INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p
WHERE deleted_at ISNULL
AND post_type = 1
GROUP BY topic_id) x ON x.post = p.id
),
first_reply AS (
SELECT p.topic_id, p.user_id, p.created_at FROM posts p
INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p
WHERE deleted_at ISNULL
AND post_type = 1
AND post_number > 1
GROUP BY topic_id) x ON x.post = p.id
)
SELECT
CASE
WHEN st.id IS NOT NULL THEN 'solved'
ELSE 'unsolved'
END AS status,
vt.tag_names,
vt.id AS topic_id,
vt.user_id topic_user_id,
ue.email,
vt.title,
vt.views,
lr.user_id AS last_reply_user_id,
ue2.email AS last_reply_user_email,
vt.created_at::date topic_create,
COALESCE(TO_CHAR(fr.created_at,'YYYY-MM-DD'),'') AS first_reply_create,
COALESCE(TO_CHAR(st.created_at,'YYYY-MM-DD'),'') AS solution_create,
COALESCE(fr.created_at::date - vt.created_at::date,0) AS "time_first_reply(days)",
COALESCE(CEIL(extract(epoch from (fr.created_at - vt.created_at))/3600.00),0) as "time_first_reply(hours)",
COALESCE(st.created_at::date - vt.created_at::date,0) AS "time_solution(days)",
COALESCE(CEIL(extract(epoch from (st.created_at - vt.created_at))/3600.00),0) as "time_solution(hours)",
posts_count AS number_of_replies,
total_days AS total_days_without_solution
FROM valid_topics vt
LEFT JOIN last_reply lr ON lr.topic_id = vt.id
LEFT JOIN first_reply fr ON fr.topic_id = vt.id
LEFT JOIN solved_topics st ON st.id = vt.id
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true
WHERE (:tag_name = 'all' OR LOWER(vt.tag_names) LIKE '%' || LOWER(:tag_name) || '%')
ORDER BY tag_names, total_days DESC
Résultats d’exemple:
| status | tag_names | topic | topic_user | title | views | last_reply_user | last_reply_user_email | topic_create | first_reply_create | solution_create | time_first_reply(days) | time_first_reply(hours) | time_solution(days) | time_solution(hours) | number_of_replies | total_days_without_solution | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| solved | a, c, b | A Topic Title (7) | alice | alice@example.com | A Topic Title | 58 | bella | bella@example.com | 2023-08-25 | 2023-08-25 | 2023-08-29 | 0 | 1 | 1 | 24 | 9 | 4 |
| unsolved | tag1 | Welcome to the Lounge (3) | system | no_email | Welcome to the Lounge | 3 | system | no_email | 2023-05-01 | 0 | 0 | 0 | 0 | 2 | 134 |
Dans cette requête, COALESCE est utilisé dans les lignes suivantes :
COALESCE(TO_CHAR(fr.created_at,'YYYY-MM-DD'),'') AS first_reply_create: Cette ligne convertit la datecreated_atde la première réponse en chaîne de caractères. Si la première réponse n’existe pas (c.-à-d.fr.created_atest null), elle renverra une chaîne vide (``).COALESCE(TO_CHAR(st.created_at,'YYYY-MM-DD'),'') AS solution_create: De même, cette ligne convertit la datecreated_atde la solution en chaîne de caractères. Si la solution n’existe pas (c.-à-d.st.created_atest null), elle renverra une chaîne vide (``).COALESCE(fr.created_at::date - vt.created_at::date,0) AS "time_first_reply(days)": Cette ligne calcule la différence de temps en jours entre la création du sujet et la première réponse. Si la première réponse n’existe pas (c.-à-d.fr.created_atest null), elle renverra 0.COALESCE(CEIL(extract(epoch from (fr.created_at - vt.created_at))/3600.00),0) as "time_first_reply(hours)": Cette ligne calcule la différence de temps en heures entre la création du sujet et la première réponse. Si la première réponse n’existe pas (c.-à-d.fr.created_atest null), elle renverra 0.COALESCE(st.created_at::date - vt.created_at::date,0) AS "time_solution(days)": Cette ligne calcule la différence de temps en jours entre la création du sujet et la solution. Si la solution n’existe pas (c.-à-d.st.created_atest null), elle renverra 0.COALESCE(CEIL(extract(epoch from (st.created_at - vt.created_at))/3600.00),0) as "time_solution(hours)": Cette ligne calcule la différence de temps en heures entre la création du sujet et la solution. Si la solution n’existe pas (c.-à-d.st.created_atest null), elle renverra 0.
Dans tous ces cas, COALESCE est utilisé pour empêcher l’apparition de valeurs NULL dans les résultats finaux, ce qui améliore la lisibilité de la requête résultante et peut être utile pour un traitement ou une analyse ultérieure des données.
Explication détaillée avec commentaires en ligne
--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all
-- Définir une CTE pour les sujets valides
WITH valid_topics AS (
-- Sélectionner les champs nécessaires
SELECT
t.id, -- identifiant du sujet
t.user_id, -- identifiant de l'utilisateur
t.title, -- titre du sujet
t.views, -- nombre de vues
posts_count-1 AS "posts_count", -- nombre de messages dans le sujet
t.created_at, -- date de création du sujet
(CURRENT_DATE::date-t.created_at::date) AS "total_days", -- nombre total de jours depuis la création du sujet
string_agg(tags.name, ', ') AS tag_names -- agréger tous les tags associés au sujet
FROM topics t -- depuis la table topics
-- Joindre les tables nécessaires pour obtenir les noms des tags
LEFT JOIN topic_tags tt ON tt.topic_id = t.id
LEFT JOIN tags ON tags.id = tt.tag_id
LEFT JOIN tag_group_memberships tgm ON tgm.tag_id = tt.tag_id
-- Sous-requête pour obtenir la date de la première réponse pour chaque sujet
LEFT JOIN (SELECT topic_id, MIN(created_at) AS "created_at"
FROM posts
WHERE deleted_at ISNULL
AND post_type = 1
AND post_number > 1
GROUP BY topic_id) date_first_reply ON date_first_reply.topic_id = t.id
WHERE t.deleted_at ISNULL -- ne considérer que les sujets non supprimés
AND t.created_at::date BETWEEN :start_date AND :end_date -- ne considérer que les sujets créés entre start_date et end_date
AND t.archetype = 'regular' -- ne considérer que les sujets réguliers
GROUP BY t.id, t.user_id, t.title, t.views, posts_count, t.created_at -- regrouper par les champs nécessaires pour obtenir le bon comptage
),
-- Définir une CTE pour les sujets résolus
solved_topics AS (
-- Sélectionner l'identifiant du sujet et la date de création de la solution
SELECT
vt.id,
tcf.created_at
FROM topic_custom_fields tcf -- depuis la table topic_custom_fields
INNER JOIN valid_topics vt ON vt.id = tcf.topic_id -- jointure avec la CTE valid_topics
WHERE tcf.name = 'accepted_answer_post_id' -- ne considérer que les sujets ayant une réponse acceptée
),
-- Définir une CTE pour la dernière réponse de chaque sujet
last_reply AS (
-- Sélectionner l'identifiant du sujet et l'identifiant de l'utilisateur de la dernière réponse
SELECT p.topic_id, p.user_id FROM posts p
INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p -- sous-requête pour obtenir l'identifiant du dernier message pour chaque sujet
WHERE deleted_at ISNULL
AND post_type = 1
GROUP BY topic_id) x ON x.post = p.id -- jointure avec la table posts pour obtenir l'identifiant de l'utilisateur de la dernière réponse
),
-- Définir une CTE pour la première réponse de chaque sujet
first_reply AS (
-- Sélectionner l'identifiant du sujet, l'identifiant de l'utilisateur et la date de création de la première réponse
SELECT p.topic_id, p.user_id, p.created_at FROM posts p
INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p -- sous-requête pour obtenir l'identifiant de la première réponse pour chaque sujet
WHERE deleted_at ISNULL
AND post_type = 1
AND post_number > 1
GROUP BY topic_id) x ON x.post = p.id -- jointure avec la table posts pour obtenir l'identifiant de l'utilisateur et la date de création de la première réponse
)
-- Requête principale pour obtenir le jeu de résultats final
SELECT
CASE
WHEN st.id IS NOT NULL THEN 'solved' -- si l'identifiant du sujet est dans la CTE solved_topics, alors le statut est « résolu »
ELSE 'unsolved' -- sinon, le statut est « non résolu »
END AS status,
vt.tag_names, -- noms des tags
vt.id AS topic_id, -- identifiant du sujet
vt.user_id topic_user_id, -- identifiant de l'utilisateur
ue.email, -- adresse e-mail de l'utilisateur
vt.title, -- titre du sujet
vt.views, -- nombre de vues
lr.user_id AS last_reply_user_id, -- identifiant de l'utilisateur de la dernière réponse
ue2.email AS last_reply_user_email, -- adresse e-mail de l'utilisateur ayant fait la dernière réponse
vt.created_at::date topic_create, -- date de création du sujet
COALESCE(TO_CHAR(fr.created_at,'YYYY-MM-DD'),'') AS first_reply_create, -- date de création de la première réponse, si elle existe, sinon chaîne vide
COALESCE(TO_CHAR(st.created_at,'YYYY-MM-DD'),'') AS solution_create, -- date de création de la solution, si elle existe, sinon chaîne vide
COALESCE(fr.created_at::date - vt.created_at::date,0) AS "time_first_reply(days)", -- temps écoulé avant la première réponse en jours
COALESCE(CEIL(extract(epoch from (fr.created_at - vt.created_at))/3600.00),0) as "time_first_reply(hours)", -- temps écoulé avant la première réponse en heures
COALESCE(st.created_at::date - vt.created_at::date,0) AS "time_solution(days)", -- temps écoulé avant la solution en jours
COALESCE(CEIL(extract(epoch from (st.created_at - vt.created_at))/3600.00),0) as "time_solution(hours)", -- temps écoulé avant la solution en heures
posts_count AS number_of_replies, -- nombre de réponses
total_days AS total_days_without_solution -- nombre total de jours sans solution
FROM valid_topics vt -- depuis la CTE valid_topics
LEFT JOIN last_reply lr ON lr.topic_id = vt.id -- jointure avec la CTE last_reply
LEFT JOIN first_reply fr ON fr.topic_id = vt.id -- jointure avec la CTE first_reply
LEFT JOIN solved_topics st ON st.id = vt.id -- jointure avec la CTE solved_topics
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true -- jointure avec la table user_emails pour obtenir l'adresse e-mail de l'utilisateur
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true -- jointure avec la table user_emails pour obtenir l'adresse e-mail de l'utilisateur ayant fait la dernière réponse
WHERE (:tag_name = 'all' OR LOWER(vt.tag_names) LIKE '%' || LOWER(:tag_name) || '%') -- filtrer par nom de tag
ORDER BY tag_names, total_days DESC -- trier par noms de tags et nombre total de jours dans l'ordre décroissant
Si vous avez des questions ou des exemples sur la façon dont vous avez utilisé COALESCE dans vos requêtes Data Explorer, n’hésitez pas à les partager ci-dessous. ![]()