4 Minutes read

C’est à la suite de la conférence SQL vs les préjugés de Lætitia Lavrot, au Forum PHP 2025 que j’ai décidé de rédiger cet article.

Constat

Aujourd’hui SQL est un allié sous-estimé par les créateurs d’applications modernes PHP. Celui-ci est souvent associé à des adjectifs peu flatteur tels que : complexe, verbeux, obsolète et lent. Voici un exemple pouvant illustrer ces mots.

Si jamais en voyant cette requête vous prenez peur c’est normal.

La peur n’évite pas le danger

La peur

Bien que le SQL ait évolué, notre apprentissage de celui-ci est resté dans le même état que lors de nos années d’études pour deux raisons :

  • L’apprentissage que l’on contourne par l’usage d’autres outils ou langages.
  • La peur du lâcher-prise et de faire confiance à celui-ci.

Le danger

Malgré vos formidables capacités de développeurs, il est des cas où les implémentations de vos besoins sont difficiles en SQL (encore une fois selon vous).

Pour illustrer mes propos, voici 4 exemples assez claires que l’on a déjà certainement déjà tous fait au moins une fois dans notre vie.

Problème de l’update de masse

<?php
// Récupération des produits
$stmt = $pdo->query("SELECT id, price FROM products");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Boucle sur chaque produit pour mettre à jour le prix
foreach ($products as $product) {
$newPrice = $product['price'] * 1.05; // Augmentation de 5%

// Préparation de la requête de mise à jour
$updateStmt = $pdo->prepare(
"UPDATE products
SET price = :price
WHERE id = :id"
);

// Exécution de la requête avec les nouvelles valeurs
$updateStmt->execute([
'price' => $newPrice,
'id' => $product['id']
]);
}
?>

Problème de requêtes N+1

<?php
// 1 requête pour récupérer tous les articles
$articles = $pdo->query("SELECT * FROM articles")->fetchAll(PDO::FETCH_ASSOC);

foreach ($articles as $article) {
echo "<h2>{$article['titre']}</h2>";

// 1 requête par article pour récupérer les commentaires (N requêtes)
$stmt = $pdo->prepare("SELECT * FROM commentaires WHERE article_id = ? ORDER BY id DESC LIMIT 3");
$stmt->execute([$article['id']]);
$commentaires = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($commentaires as $commentaire) {
echo "<p>{$commentaire['contenu']}</p>";
}
}
?>

Problème des 3 requêtes pour une insertion

<?php
// Étape 1 : Création d'un ticket de support
$stmt = $pdo->prepare(
"INSERT INTO support_ticket (user_id, subject, priority)
VALUES (:user_id, :subject, :priority)"
);

$stmt->execute([
'user_id' => $userId,
'subject' => $ticketSubject,
'priority' => $ticketPriority
]);

// Étape 2 : Récupération de l'ID du nouveau ticket
$ticketId = $pdo->lastInsertId();

// Étape 3 : Récupération des détails du ticket
$stmt = $pdo->prepare(
"SELECT id, ticket_number, created_at, status
FROM support_ticket
WHERE id = :id"
);

$stmt->execute(['id' => $ticketId]);
$ticket = $stmt->fetch();

// Affichage des détails du ticket
echo "Ticket créé avec succès !n";
echo "Numéro : " . $ticket['ticket_number'] . "n";
echo "Date : " . $ticket['created_at'] . "n";
echo "Statut : " . $ticket['status'] . "n";
?>

Problème de boucles en chaine

<?php
// Récupérer tous les employés, triés par département puis par score de performance décroissant
$query = "SELECT * FROM employees ORDER BY department_id, performance_score DESC";
$employees = $pdo->query($query)->fetchAll();

// Regrouper les employés par département
$groupedByDepartment = [];
foreach ($employees as $employee) {
$groupedByDepartment[$employee['department_id']][] = $employee;
}

// Extraire les 3 meilleurs employés par département
$topEmployees = [];
foreach ($groupedByDepartment as $departmentId => $members) {
$topEmployees[$departmentId] = array_slice($members, 0, 3);
}

// Préparer les résultats pour l'affichage
$finalResult = array_merge(...array_values($topEmployees));
?>

Dans ces quatre exemples, le code écrit peut devenir une source d’instabilité pour votre projet.

Dans tous ces cas, le débogage ne sera pas simple en cas de problèmes. Vous rencontrerez sûrement des problèmes difficiles comme la gestion de la concurrence, de la performance, de la perte de données potentielles etc.

La confiance

Maintenant je vais essayer de vous redonner confiance en SQL en reprenant les cas précédents.

Solution au problème de l’update de masse

<?php
// Connexion à la base de données avec PDO
$dsn = 'mysql:host=localhost;dbname=catalogue;charset=utf8';
$username = 'root';
$password = '';

try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Requête SQL
$sql = "UPDATE products SET price = price * 1.05";

// Exécution de la requête
$affectedRows = $pdo->exec($sql);

echo "$affectedRows produits mis à jour.";

} catch (PDOException $e) {
echo "Erreur : " . $e->getMessage();
}
?>

Solution au problème de requêtes N+1

<?php
$pdo = new PDO('pgsql:host=localhost;dbname=blog', 'user', 'password');

$sql = "
SELECT a.id AS article_id, a.titre, c.contenu
FROM articles a
LEFT JOIN LATERAL (
SELECT contenu
FROM commentaires
WHERE article_id = a.id
ORDER BY id DESC
LIMIT 3
) c ON true
ORDER BY a.id
";

$result = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

$currentArticleId = null;

foreach ($result as $row) {
if ($row['article_id'] !== $currentArticleId) {
// Nouveau titre d'article
$currentArticleId = $row['article_id'];
echo "<h2>{$row['titre']}</h2>";
}

// Afficher le commentaire s'il existe
if (!empty($row['contenu'])) {
echo "<p>{$row['contenu']}</p>";
}
}
?>

Solution au problème des 3 requêtes pour une insertion

<?php
// Connexion à la base de données PostgreSQL
// $pdo est supposé être déjà initialisé

// Requête unique : INSERT avec RETURNING
$stmt = $pdo->prepare(
"INSERT INTO support_ticket (user_id, subject, priority)
VALUES (:user_id, :subject, :priority)
RETURNING id, ticket_number, created_at, status"
);

$stmt->execute([
'user_id' => $userId,
'subject' => $ticketSubject,
'priority' => $ticketPriority
]);

$ticket = $stmt->fetch();

// Affichage des détails du ticket
echo "Ticket créé avec succès !n";
echo "Numéro : " . $ticket['ticket_number'] . "n";
echo "Date : " . $ticket['created_at'] . "n";
echo "Statut : " . $ticket['status'] . "n";
?>

Solution au problème de boucles en chaine

// Requête SQL avec window function pour récupérer les 3 meilleurs employés par département
$query = "
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY performance_score DESC) AS rank
FROM employees
) AS ranked_employees
WHERE rank <= 3
";

// Exécution de la requête
$topEmployees = $pdo->query($query)->fetchAll();

// Résultat prêt à être affiché ou utilisé
foreach ($topEmployees as $employee) {
// Exemple d'affichage
echo "{$employee['name']} - Département: {$employee['department_id']} - Score: {$employee['performance_score']}<br>";
}

Parmi toutes ces solutions, certaines ne seront pas disponibles en fonction de votre SGBD, comme l’usage de returning qui ne suit pas la norme ISO SQL.

Changement de postures

  • Faites confiance au langage et à l’optimiseur SQL
  • Déléguez les responsabilités de sélections, de calculs, de jointures, etc au SQL et réservez à votre langage l’orchestration et la logique métier.
  • Faites vous confiance car ce n’est pas trop dur, ni trop long, ou encore trop vieux et surtout pas trop lent.

Conclusion

Un usage mature de SQL, combiné à PHP, élimine boucles inutiles, allers‑retours réseau, incohérences concurrentielles et problèmes de requêtes N+1, tout en rendant le code plus lisible et robuste.
Finalement la difficulté n’est pas le langage, mais l’acceptation de son paradigme et de ses outils modernes.

À propos d’ekino

Le groupe ekino accompagne les grands groupes et les start-up dans leur transformation depuis plus de 10 ans, en les aidant à imaginer et réaliser leurs services numériques et en déployant de nouvelles méthodologies au sein des équipes projets. Pionnier dans son approche holistique, ekino s’appuie sur la synergie de ses expertises pour construire des solutions pérennes et cohérentes.

Pour en savoir plus, rendez-vous sur notre site — ekino.fr.


Comment j’ai appris à aimer SQL was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.