Cours Python · Module 4: Données, API et bases de données
SQL - les entrepôts de données relationnels
Dans cette leçon16
Content de te revoir ! Ici Darwin, avec une leçon cruciale sur le stockage des données.
Jusqu'ici, tu as travaillé avec des données de façon temporaire - dans des variables, des fichiers JSON, des pages scrapées. Mais que faire si tu as des millions d'observations d'espèces ? Que faire si tu dois rechercher rapidement toutes les espèces menacées du Serengeti ?
Alors il te faut une base de données - un système spécialisé dans le stockage et la gestion de grands ensembles de données !
Analogie safari : Les fichiers JSON sont comme le carnet d'un biologiste - ça marche pour 10 observations. Une base de données est une grande bibliothèque à catalogue - des tiroirs organisés avec des fiches, des index, et une recherche rapide sur des millions d'enregistrements !
Qu'est-ce qu'une base de données ?
Une base de données est un ensemble organisé de données + un système de gestion (SGBD - Système de Gestion de Base de Données).
Pourquoi des bases de données ?
- Recherche rapide - index, optimisation des requêtes
- Sécurité - contrôle d'accès, permissions
- Intégrité - validation, relations, clés étrangères
- Scalabilité - des millions d'enregistrements sans problème
- Accès concurrent - plusieurs utilisateurs en même temps
- Transactions - opérations atomiques (tout ou rien)
Relationnel vs NoSQL
Bases de données relationnelles (SQL)
Structure : des tables avec des lignes et des colonnes (comme un tableur Excel)
Exemples : PostgreSQL, MySQL, SQLite, Oracle, SQL Server
Caractéristiques :
- Schéma rigide (colonnes définies à l'avance)
- Relations entre les tables (clés étrangères)
- ACID (Atomicité, Cohérence, Isolation, Durabilité)
- Langage SQL (Structured Query Language)
1TABLE : species
2+----+-----------------+------------+----------+
3| id | scientific_name | population | habitat |
4+----+-----------------+------------+----------+
5| 1 | Panthera leo | 120 | savanna |
6| 2 | Gorilla gorilla | 230 | forest |
7+----+-----------------+------------+----------+Bases de données NoSQL
Structure : documents, paires clé-valeur, graphes, colonnes
Exemples : MongoDB, Redis, Cassandra, Neo4j
Caractéristiques :
- Schéma flexible (de type JSON)
- Scalabilité horizontale
- Rapide pour des cas d'usage spécifiques
Dans cette leçon : nous nous concentrons sur SQL (relationnel) - le fondement de la plupart des applications !
SQLite - une base de données dans un seul fichier
SQLite est la base de données SQL la plus populaire au monde :
- Pas de serveur - toute la base dans un seul fichier .db
- Zéro configuration - prête à l'emploi
- Intégrée à Python - le module
sqlite3 - Idéale pour commencer - prototypes, petites/moyennes applications
Usage : applications mobiles, navigateurs, objets IoT, prototypes
SQL - le langage de requête
SQL (Structured Query Language) est le langage universel pour communiquer avec les bases de données relationnelles.
Opérations de base (CRUD)
- CREATE - créer des tables
- INSERT - ajouter des enregistrements
- SELECT - récupérer des données (Read)
- UPDATE - mettre à jour des enregistrements
- DELETE - supprimer des enregistrements
CREATE TABLE - créer une table
1-- Table des espèces
2CREATE TABLE species (
3 id INTEGER PRIMARY KEY AUTOINCREMENT,
4 scientific_name TEXT NOT NULL UNIQUE,
5 common_name TEXT NOT NULL,
6 population INTEGER DEFAULT 0,
7 habitat TEXT,
8 endangered BOOLEAN DEFAULT 0,
9 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
10);
11
12-- Table des observations
13CREATE TABLE observations (
14 id INTEGER PRIMARY KEY AUTOINCREMENT,
15 species_id INTEGER NOT NULL,
16 observation_date DATE NOT NULL,
17 location TEXT NOT NULL,
18 count INTEGER DEFAULT 0,
19 notes TEXT,
20 FOREIGN KEY (species_id) REFERENCES species(id)
21);Types de données SQLite :
INTEGER- nombre entierREAL- nombre à virgule flottanteTEXT- texteBLOB- données binairesNULL- absence de valeur
Contraintes :
PRIMARY KEY- identifiant unique de ligneAUTOINCREMENT- incrémentation automatiqueNOT NULL- champ requisUNIQUE- valeur uniqueDEFAULT- valeur par défautFOREIGN KEY- relation vers une autre table
INSERT - ajouter des données
1-- Ajouter un seul enregistrement
2INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
3VALUES ('Panthera leo', 'Lion', 120, 'savanna', 1);
4
5-- Ajouter plusieurs enregistrements
6INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
7VALUES
8 ('Loxodonta africana', 'African Elephant', 450, 'savanna', 1),
9 ('Gorilla gorilla', 'Gorilla', 230, 'tropical forest', 1),
10 ('Python regius', 'Ball Python', 85, 'jungle', 0);
11
12-- Ajouter une observation
13INSERT INTO observations (species_id, observation_date, location, count, notes)
14VALUES (1, '2024-01-15', 'Serengeti North', 12, 'Pride with 2 cubs');SELECT - récupérer des données
1-- Toutes les espèces
2SELECT * FROM species;
3
4-- Colonnes sélectionnées
5SELECT common_name, population FROM species;
6
7-- WHERE - filtrage
8SELECT * FROM species WHERE endangered = 1;
9SELECT * FROM species WHERE population > 100;
10SELECT * FROM species WHERE habitat = 'savanna' AND endangered = 1;
11
12-- ORDER BY - tri
13SELECT * FROM species ORDER BY population DESC; -- Décroissant
14SELECT * FROM species ORDER BY common_name ASC; -- Croissant
15
16-- LIMIT - limiter les résultats
17SELECT * FROM species LIMIT 10;
18SELECT * FROM species LIMIT 10 OFFSET 20; -- Page 3 (20-30)
19
20-- LIKE - correspondance de motif
21SELECT * FROM species WHERE scientific_name LIKE 'Panthera%'; -- Commence par Panthera
22SELECT * FROM species WHERE habitat LIKE '%forest%'; -- Contient "forest"
23
24-- COUNT, SUM, AVG, MIN, MAX - agrégations
25SELECT COUNT(*) FROM species;
26SELECT AVG(population) FROM species;
27SELECT SUM(population) FROM species WHERE endangered = 1;
28SELECT MIN(population), MAX(population) FROM species;
29
30-- GROUP BY - regroupement
31SELECT habitat, COUNT(*) as species_count
32FROM species
33GROUP BY habitat;
34
35SELECT habitat, AVG(population) as avg_population
36FROM species
37GROUP BY habitat
38HAVING AVG(population) > 100; -- HAVING = WHERE pour les groupesUPDATE - mettre à jour des données
1-- Mettre à jour un seul enregistrement
2UPDATE species
3SET population = 125
4WHERE id = 1;
5
6-- Mettre à jour plusieurs enregistrements
7UPDATE species
8SET endangered = 1
9WHERE population < 50;
10
11-- Mettre à jour plusieurs colonnes
12UPDATE species
13SET population = 130, habitat = 'savanna and steppe'
14WHERE scientific_name = 'Panthera leo';DELETE - supprimer des données
1-- Supprimer un seul enregistrement
2DELETE FROM species WHERE id = 10;
3
4-- Supprimer plusieurs enregistrements
5DELETE FROM species WHERE population = 0;
6
7-- ATTENTION : Tout supprimer (sans WHERE !)
8DELETE FROM species; -- Supprime TOUS les enregistrements !JOIN - combiner des tables
1-- INNER JOIN - seulement les enregistrements correspondants
2SELECT
3 species.common_name,
4 observations.observation_date,
5 observations.location,
6 observations.count
7FROM observations
8INNER JOIN species ON observations.species_id = species.id;
9
10-- LEFT JOIN - tout de gauche + les correspondances de droite
11SELECT
12 species.common_name,
13 COUNT(observations.id) as observation_count
14FROM species
15LEFT JOIN observations ON species.id = observations.species_id
16GROUP BY species.id, species.common_name;
17
18-- Alias de tables (s, o)
19SELECT
20 s.common_name,
21 o.location,
22 o.count
23FROM observations o
24INNER JOIN species s ON o.species_id = s.id
25WHERE s.endangered = 1;Python + SQLite - le module sqlite3
Python dispose d'un module sqlite3 intégré pour travailler avec SQLite.
Les bases
1import sqlite3
2
3# 1. Se connecter à la base (crée le fichier s'il n'existe pas)
4conn = sqlite3.connect("safari.db")
5
6# 2. Créer un curseur (objet pour exécuter les requêtes)
7cursor = conn.cursor()
8
9# 3. Exécuter une requête SQL
10cursor.execute("""
11 CREATE TABLE IF NOT EXISTS species (
12 id INTEGER PRIMARY KEY AUTOINCREMENT,
13 scientific_name TEXT NOT NULL UNIQUE,
14 common_name TEXT NOT NULL,
15 population INTEGER DEFAULT 0,
16 endangered BOOLEAN DEFAULT 0
17 )
18""")
19
20# 4. Valider les changements (IMPORTANT !)
21conn.commit()
22
23# 5. Fermer la connexion
24conn.close()INSERT - ajouter des données
1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# Méthode 1 : requête avec des valeurs (DANGEREUX - injection SQL !)
7# NE FAIS PAS ÇA :
8# name = "Lion'; DROP TABLE species; --" # Injection SQL !
9# cursor.execute(f"INSERT INTO species (common_name) VALUES ('{name}')")
10
11# Méthode 2 : requêtes paramétrées (SÛR)
12cursor.execute("""
13 INSERT INTO species (scientific_name, common_name, population, endangered)
14 VALUES (?, ?, ?, ?)
15""", ("Panthera leo", "Lion", 120, 1))
16
17# Plusieurs enregistrements à la fois
18species_data = [
19 ("Loxodonta africana", "African Elephant", 450, 1),
20 ("Gorilla gorilla", "Gorilla", 230, 1),
21 ("Python regius", "Ball Python", 85, 0)
22]
23
24cursor.executemany("""
25 INSERT INTO species (scientific_name, common_name, population, endangered)
26 VALUES (?, ?, ?, ?)
27""", species_data)
28
29conn.commit()
30
31# Récupérer l'ID du dernier enregistrement ajouté
32print(f"ID de l'espèce ajoutée : {cursor.lastrowid}")
33
34conn.close()SELECT - récupérer des données
1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# fetchall() - tous les résultats sous forme de liste de tuples
7cursor.execute("SELECT * FROM species")
8all_species = cursor.fetchall()
9
10for species in all_species:
11 print(species) # (1, 'Panthera leo', 'Lion', 120, 1)
12
13# fetchone() - un seul résultat
14cursor.execute("SELECT * FROM species WHERE id = ?", (1,))
15lion = cursor.fetchone()
16print(lion) # (1, 'Panthera leo', 'Lion', 120, 1)
17
18# fetchmany(n) - n résultats
19cursor.execute("SELECT * FROM species")
20first_five = cursor.fetchmany(5)
21
22# Row factory - résultats sous forme de dictionnaires
23conn.row_factory = sqlite3.Row
24cursor = conn.cursor()
25
26cursor.execute("SELECT * FROM species")
27for row in cursor.fetchall():
28 print(row["common_name"], row["population"])
29
30conn.close()UPDATE et DELETE
1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# UPDATE
7cursor.execute("""
8 UPDATE species
9 SET population = ?
10 WHERE id = ?
11""", (125, 1))
12
13print(f"{cursor.rowcount} enregistrements mis à jour")
14
15# DELETE
16cursor.execute("DELETE FROM species WHERE population < ?", (10,))
17print(f"{cursor.rowcount} enregistrements supprimés")
18
19conn.commit()
20conn.close()Gestionnaire de contexte - gestion sûre de la connexion
1import sqlite3
2
3# Commit et fermeture automatiques
4with sqlite3.connect("safari.db") as conn:
5 cursor = conn.cursor()
6
7 cursor.execute("""
8 INSERT INTO species (scientific_name, common_name, population)
9 VALUES (?, ?, ?)
10 """, ("Acinonyx jubatus", "Cheetah", 7100))
11
12 # commit() automatiquement à la sortie du bloc with
13# close() automatiquementExemple safari - système de base de données complet
1import sqlite3
2from typing import List, Dict, Optional
3from datetime import datetime
4
5class SafariDatabase:
6 """Système complet de gestion des données Safari"""
7
8 def __init__(self, db_path: str = "safari.db"):
9 self.db_path = db_path
10 self.init_database()
11
12 def get_connection(self):
13 """Créer une connexion à la base"""
14 conn = sqlite3.connect(self.db_path)
15 conn.row_factory = sqlite3.Row # Résultats sous forme de dictionnaires
16 return conn
17
18 def init_database(self):
19 """Initialiser le schéma de la base"""
20 with self.get_connection() as conn:
21 cursor = conn.cursor()
22
23 # Table des espèces
24 cursor.execute("""
25 CREATE TABLE IF NOT EXISTS species (
26 id INTEGER PRIMARY KEY AUTOINCREMENT,
27 scientific_name TEXT NOT NULL UNIQUE,
28 common_name TEXT NOT NULL,
29 population INTEGER DEFAULT 0,
30 habitat TEXT,
31 endangered BOOLEAN DEFAULT 0,
32 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
33 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34 )
35 """)
36
37 # Table des observations
38 cursor.execute("""
39 CREATE TABLE IF NOT EXISTS observations (
40 id INTEGER PRIMARY KEY AUTOINCREMENT,
41 species_id INTEGER NOT NULL,
42 observation_date DATE NOT NULL,
43 location TEXT NOT NULL,
44 count INTEGER DEFAULT 0,
45 notes TEXT,
46 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47 FOREIGN KEY (species_id) REFERENCES species(id) ON DELETE CASCADE
48 )
49 """)
50
51 # Index pour une recherche plus rapide
52 cursor.execute("""
53 CREATE INDEX IF NOT EXISTS idx_species_endangered
54 ON species(endangered)
55 """)
56
57 cursor.execute("""
58 CREATE INDEX IF NOT EXISTS idx_observations_species
59 ON observations(species_id)
60 """)
61
62 conn.commit()
63
64 # === CRUD ESPÈCES ===
65
66 def create_species(self, scientific_name: str, common_name: str,
67 population: int = 0, habitat: str = "",
68 endangered: bool = False) -> int:
69 """Ajouter une nouvelle espèce"""
70 with self.get_connection() as conn:
71 cursor = conn.cursor()
72 cursor.execute("""
73 INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
74 VALUES (?, ?, ?, ?, ?)
75 """, (scientific_name, common_name, population, habitat, int(endangered)))
76
77 conn.commit()
78 return cursor.lastrowid
79
80 def get_species(self, species_id: int) -> Optional[Dict]:
81 """Récupérer une espèce par ID"""
82 with self.get_connection() as conn:
83 cursor = conn.cursor()
84 cursor.execute("SELECT * FROM species WHERE id = ?", (species_id,))
85 row = cursor.fetchone()
86 return dict(row) if row else None
87
88 def list_species(self, endangered: Optional[bool] = None,
89 habitat: Optional[str] = None,
90 min_population: int = 0) -> List[Dict]:
91 """Lister les espèces avec des filtres"""
92 with self.get_connection() as conn:
93 cursor = conn.cursor()
94
95 query = "SELECT * FROM species WHERE population >= ?"
96 params = [min_population]
97
98 if endangered is not None:
99 query += " AND endangered = ?"
100 params.append(int(endangered))
101
102 if habitat:
103 query += " AND habitat = ?"
104 params.append(habitat)
105
106 query += " ORDER BY common_name"
107
108 cursor.execute(query, params)
109 return [dict(row) for row in cursor.fetchall()]
110
111 def update_species(self, species_id: int, **kwargs) -> bool:
112 """Mettre à jour une espèce"""
113 if not kwargs:
114 return False
115
116 # Construction dynamique du UPDATE
117 fields = ", ".join([f"{key} = ?" for key in kwargs.keys()])
118 values = list(kwargs.values())
119 values.append(species_id)
120
121 with self.get_connection() as conn:
122 cursor = conn.cursor()
123 cursor.execute(f"""
124 UPDATE species
125 SET {fields}, updated_at = CURRENT_TIMESTAMP
126 WHERE id = ?
127 """, values)
128
129 conn.commit()
130 return cursor.rowcount > 0
131
132 def delete_species(self, species_id: int) -> bool:
133 """Supprimer une espèce"""
134 with self.get_connection() as conn:
135 cursor = conn.cursor()
136 cursor.execute("DELETE FROM species WHERE id = ?", (species_id,))
137 conn.commit()
138 return cursor.rowcount > 0
139
140 # === OBSERVATIONS ===
141
142 def create_observation(self, species_id: int, observation_date: str,
143 location: str, count: int, notes: str = "") -> int:
144 """Ajouter une observation"""
145 with self.get_connection() as conn:
146 cursor = conn.cursor()
147 cursor.execute("""
148 INSERT INTO observations (species_id, observation_date, location, count, notes)
149 VALUES (?, ?, ?, ?, ?)
150 """, (species_id, observation_date, location, count, notes))
151
152 conn.commit()
153 return cursor.lastrowid
154
155 def get_observations_for_species(self, species_id: int) -> List[Dict]:
156 """Récupérer les observations d'une espèce"""
157 with self.get_connection() as conn:
158 cursor = conn.cursor()
159 cursor.execute("""
160 SELECT
161 o.*,
162 s.common_name as species_name
163 FROM observations o
164 JOIN species s ON o.species_id = s.id
165 WHERE o.species_id = ?
166 ORDER BY o.observation_date DESC
167 """, (species_id,))
168
169 return [dict(row) for row in cursor.fetchall()]
170
171 # === STATISTIQUES ===
172
173 def get_statistics(self) -> Dict:
174 """Récupérer les statistiques de la base"""
175 with self.get_connection() as conn:
176 cursor = conn.cursor()
177
178 # Nombre total d'espèces
179 cursor.execute("SELECT COUNT(*) as count FROM species")
180 total_species = cursor.fetchone()["count"]
181
182 # Espèces menacées
183 cursor.execute("SELECT COUNT(*) as count FROM species WHERE endangered = 1")
184 endangered_count = cursor.fetchone()["count"]
185
186 # Population totale
187 cursor.execute("SELECT SUM(population) as total FROM species")
188 total_population = cursor.fetchone()["total"] or 0
189
190 # Espèces par habitat
191 cursor.execute("""
192 SELECT habitat, COUNT(*) as count
193 FROM species
194 GROUP BY habitat
195 ORDER BY count DESC
196 """)
197 by_habitat = [dict(row) for row in cursor.fetchall()]
198
199 return {
200 "total_species": total_species,
201 "endangered_count": endangered_count,
202 "total_population": total_population,
203 "by_habitat": by_habitat
204 }
205
206
207# === DÉMONSTRATION ===
208
209print("=== SYSTÈME DE BASE DE DONNÉES SAFARI ===\n")
210
211db = SafariDatabase("safari_demo.db")
212
213# 1. Ajouter des espèces
214print("1. Ajout des espèces...")
215lion_id = db.create_species("Panthera leo", "Lion", 120, "savanna", True)
216elephant_id = db.create_species("Loxodonta africana", "African Elephant", 450, "savanna", True)
217gorilla_id = db.create_species("Gorilla gorilla", "Gorilla", 230, "tropical forest", True)
218python_id = db.create_species("Python regius", "Ball Python", 85, "jungle", False)
219
220print(f" {4} espèces ajoutées")
221
222# 2. Récupérer une espèce
223print("\n2. Récupération d'une espèce...")
224lion = db.get_species(lion_id)
225print(f" {lion['common_name']} ({lion['scientific_name']})")
226print(f" Population : {lion['population']}, Menacée : {'Oui' if lion['endangered'] else 'Non'}")
227
228# 3. Lister les espèces avec un filtre
229print("\n3. Liste des espèces menacées...")
230endangered = db.list_species(endangered=True)
231for species in endangered:
232 print(f" - {species['common_name']} : {species['population']} individus")
233
234# 4. Mise à jour
235print("\n4. Mise à jour de la population du lion...")
236db.update_species(lion_id, population=125)
237lion = db.get_species(lion_id)
238print(f" Nouvelle population : {lion['population']}")
239
240# 5. Ajouter des observations
241print("\n5. Ajout des observations...")
242db.create_observation(lion_id, "2024-01-15", "Serengeti North", 12, "Pride with 2 cubs")
243db.create_observation(lion_id, "2024-01-20", "Masai Mara", 8, "Male coalition")
244db.create_observation(elephant_id, "2024-01-16", "Amboseli", 35, "Large herd")
245
246print(" 3 observations ajoutées")
247
248# 6. Récupérer les observations
249print("\n6. Observations du lion...")
250lion_obs = db.get_observations_for_species(lion_id)
251for obs in lion_obs:
252 print(f" - {obs['observation_date']} : {obs['count']}x à {obs['location']}")
253
254# 7. Statistiques
255print("\n7. Statistiques de la base...")
256stats = db.get_statistics()
257print(f" Nombre total d'espèces : {stats['total_species']}")
258print(f" Menacées : {stats['endangered_count']}")
259print(f" Population totale : {stats['total_population']}")
260print(" Par habitat :")
261for habitat_stat in stats['by_habitat']:
262 print(f" - {habitat_stat['habitat']} : {habitat_stat['count']} espèces")
263
264print("\nDémonstration terminée")Transactions - atomicité des opérations
1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4
5try:
6 cursor = conn.cursor()
7
8 # Début de la transaction (par défaut)
9 cursor.execute("INSERT INTO species (...) VALUES (...)")
10 cursor.execute("UPDATE observations SET ...")
11
12 # Valider la transaction
13 conn.commit()
14 print("Transaction validée")
15
16except Exception as e:
17 # Annuler la transaction en cas d'erreur
18 conn.rollback()
19 print(f"Transaction annulée : {e}")
20
21finally:
22 conn.close()Résumé
Dans cette leçon, tu as appris :
- Ce que sont les bases de données et à quoi elles servent
- Relationnel (SQL) vs NoSQL
- SQLite - une base de données dans un seul fichier
- SQL : CREATE TABLE, INSERT, SELECT, UPDATE, DELETE
- WHERE, ORDER BY, LIMIT, JOIN
- Python sqlite3 : connect, cursor, execute, fetchall
- Les requêtes paramétrées (prévention des injections SQL)
- Les transactions et le rollback
- Un système CRUD complet avec des relations
Point de contrôle
Avant de continuer :
- Tu sais créer une table (CREATE TABLE)
- Tu sais ajouter des données (INSERT)
- Tu sais récupérer des données avec des filtres (SELECT WHERE)
- Tu comprends JOIN (combiner des tables)
- Tu sais utiliser sqlite3 en Python
Analogie safari : Une base de données SQL est une grande bibliothèque à catalogue avec des millions de fiches d'espèces - recherche rapide, relations, intégrité des données. JSON est un carnet - parfait pour 10 entrées, insuffisant pour des millions !
Dans la prochaine leçon, Darwin te montrera SQLAlchemy ORM - comment travailler avec les bases de données en utilisant des classes Python plutôt que du SQL brut !
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle requête SQL récupère toutes les données de la table 'animals' ?
2. Quelle instruction ajoute un nouvel enregistrement à une table ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Clique les éléments dans le bon ordre :
- Tri horizontal
Remets les éléments dans le bon ordre :
- Éditeur de code
Crée une connexion à la base 'safari.db' et crée une table 'animals'
- Éditeur de code
Insère l'enregistrement {'name': 'Tygrys', 'species': 'Panthera tigris'} dans la table 'animals'
- Tri vertical
Remets dans l'ordre les étapes de travail avec SQLite :
- Tri horizontal
Remets dans l'ordre : SELECT * FROM animals WHERE species = 'Tiger'