URL: https://linuxfr.org/users/gilcot/journaux/encore-du-sql-noir Title: encore du SQL noir 🎭 Authors: Gil Cot ✔ Date: 2026ćčŽ04月06æ—„T21:29:00+02:00 License: CC By-SA Tags: sql, retour_d_expĂ©rience et jeu_libre Score: 8 On s’était laissĂ© prĂ©cĂ©demment avec la promesse d’examiner plus d’enquĂȘtes s’il y a de la demande.[^unandeja] [^unandeja]: Un petit coup de `stat` m’indique que mon fichier a Ă©tĂ© Ă©ditĂ© le 22 avril 2025 pour la derniĂšre fois, et je le reprends le 6 avril 2026 (on va dire encore deux semaines et ça fera un an...) Du coup, il se peut que le contenu du site ait Ă©voluĂ© depuis ; mais je n’ai pas pris la peine de vĂ©rifier et valider mon Ă©crit ressorti du carton oĂč il prenait la poussiĂšre. # 4^th Case Le dossier est prĂ©sentĂ© en commençant par sa date prĂ©cise, mais ce serait trop facile ```sql SELECT COUNT(*) FROM crime_scene WHERE date = 19871031; -- 51 SELECT COUNT(*) FROM crime_scene WHERE location LIKE '%Coconut%'; -- 2 SELECT COUNT(*) FROM crime_scene WHERE description LIKE '%Leonard%'; -- 0 ``` On peut associer le lieu, mais ce n’est pas `'Coconut Grove'` comme je l’aurais cru. ## 1^st step Un fois le dossier ouvert, on nous rĂ©sume les deux lignes de `witness_statements.clue` Ă  trouver. ```sql -- imbrication SELECT * FROM witness_statements WHERE crime_scene_id = ( SELECT id FROM crime_scene WHERE date = 19871031 AND location LIKE '%Coconut %' ); -- jointure SELECT w.* FROM witness_statements w INNER JOIN crime_scene c ON w.crime_scene_id = c.id WHERE c.date = 19871031 AND c.location LIKE '%Coconut %'; ``` Petite interrogation ici : que reprĂ©sente ce champ `witness_id` ? Poursuivons. Les deux confessions renvoient Ă  une rĂ©servation de chambre. ```sql SELECT COUNT(*) FROM hotel_checkins; -- 200 SELECT COUNT(*) FROM hotel_checkins WHERE hotel_name LIKE '% regency'; -- 16 SELECT COUNT(*) FROM hotel_checkins WHERE hotel_name LIKE 'grand %'; -- 1 SELECT COUNT(*) FROM hotel_checkins WHERE check_in_date = 19871030; -- 65 SELECT COUNT(*) FROM hotel_checkins WHERE room_number = 707; -- 14 SELECT COUNT(*) FROM hotel_checkins WHERE (room_number = 707) AND (check_in_date = 19871030); -- 10 ``` Étrange toutes les rĂ©servations faites au mĂȘme hĂŽtel, mĂȘme jour et mĂȘme chambre. Est-ce que le journal des observations va nous apprendre quelque chose ? ```sql -- imbrication SELECT id, note FROM surveillance_records WHERE hotel_checkin_id IN ( SELECT id FROM hotel_checkins WHERE (room_number = 707) AND (check_in_date = 19871030) ); -- jointure SELECT s.id, s.note FROM surveillance_records s INNER JOIN hotel_checkins h ON s.hotel_checkin_id = h.id WHERE (h.room_number = 707) AND (h.check_in_date = 19871030) ; ``` Quatre lignes sans note (valeur `NULL`) et une grosse comme un nez de clown...> Subject was overheard yelling on a phone: "Did you kill him?" Mais si l’on tente de remonter naĂŻvement la personne qui a fait cette rĂ©servation, cela ne marche pas... ```sql --- requĂȘtes imbriquĂ©es (en remontant le puzzle) SELECT * FROM person WHERE id IN ( SELECT person_id FROM hotel_checkins WHERE (room_number = 707) AND (check_in_date = 19871030) AND (id =( SELECT hotel_checkin_id FROM surveillance_records WHERE note LIKE '%Did you kill him?%' ) -- surveillance_records.hotel_checkin_id ) -- hotel_checkins.person_id ); -- jointure SELECT p.* FROM surveillance_records s INNER JOIN hotel_checkins h ON s.hotel_checkin_id = h.id INNER JOIN person p ON h.person_id = p.id WHERE (h.room_number = 707) AND (h.check_in_date = 19871030) AND (s.note LIKE '%Did you kill him?%') ; ``` En fait, n’allons pas croire que le coupable se trouve parmi les sept personnes ayant rĂ©servĂ©e... ```sql --- imbrication SELECT * FROM person WHERE id IN ( SELECT person_id FROM hotel_checkins WHERE (room_number = 707) AND (check_in_date = 19871030) ); -- jointure SELECT p.* FROM hotel_checkins h INNER JOIN person p ON h.person_id = p.id WHERE (h.room_number = 707) AND (h.check_in_date = 19871030) ; ``` Au passage, je note l’égalitĂ© entre `surveillance_records.id` et `surveillance_records.hotel_checkin_id` sans savoir si cela a quelque signification. ## 2^nd step Bien, au stade actuel, on sait qui a causĂ© de meurtre mais on n’a pas encore d’assassin, tout au plus un commanditaire ou une personne qui en sait trop ? Bonne nouvelle, comme une certaine [police secrĂšte d’époque](https://www.historyhit.com/the-stasi-the-most-terrifying-secret-police-in-history/) ou une certaine [agence gouvernementale actuelle](https://www.techtarget.com/searchsecurity/definition/National-Security-Agency), nous surveillons les conversations [afin de prĂ©server les libertĂ©s](https://www.vie-publique.fr/loi/277157-loi-pour-une-securite-globale-preservant-les-libertes)... ```sql SELECT COUNT(*) FROM phone_records; -- 200 SELECT COUNT(DISTINCT call_date) FROM phone_records; -- 3 SELECT DISTINCT call_date FROM phone_records; -- 19871028 19871029 19871030 SELECT COUNT(*) FROM phone_records WHERE call_date = 19871030 -- 67 SELECT COUNT(*) FROM phone_records WHERE call_date = 19871028 -- 66 SELECT COUNT(*) FROM phone_records WHERE call_date = 19871028 -- 67 ``` Ça va, que septante-sept lignes Ă  scruter... Ou on peut ruser en cherchant directement les appels depuis ou vers la personne ayant rĂ©servĂ©... ```sql SELECT * FROM phone_records WHERE (call_date = 19871030) AND ( ( caller_id IN (11, 34, 78, 123, 178, 156, 198) ) OR ( recipient_id IN (11, 34, 78, 123, 178, 156, 198) ) ); ``` Ça nous fait deux personnes Ă  entendre... ```sql SELECT id, confession FROM final_interviews WHERE person_id = (SELECT caller_id FROM phone_records WHERE note LIKE '%kill %') OR person_id = (SELECT recipient_id FROM phone_records WHERE note LIKE '%kill %') ; ``` Noter qu’ici aussi on a `final_interviews.id` qui a la mĂȘme valeur que `final_interviews.person_id` pour une raison que j’ignore. Mais on n’est pas plus avancĂ©... Mais la conversion tĂ©lĂ©phonique contient un indice que l’on peut tenter d’exploiter.> You should have left the carpenter do it himself! On va donc s’intĂ©resser aux menuisiers... ```sql -- imbrication SELECT id, confession FROM final_interviews WHERE WHERE person_id IN ( SELECT id FROM person WHERE occupation LIKE 'carpenter' ); -- jointure SELECT i.id, i.confession FROM final_interviews i INNER JOIN person p ON i.person_id = p.id WHERE p.occupation LIKE 'carpenter'; ``` ...et il y en a bien un qui avoue la commandite. Dans le lot, il y a deux tables qui n’ont pas Ă©tĂ© utilisĂ©es : `vehicle_registry` et `catering_orders`. Dites moi si vous savez quelle est leur utilitĂ©... # 5^th Case Pour finir en beautĂ©, temporairement (puisqu’on espĂšre d’autres enquĂȘtes), on va appliquer notre [forensic](https://en.wikipedia.org/wiki/Forensic_science) Ă  l’industrie des [ÎŒprocs](https://en.wikipedia.org/wiki/Microprocessor). ## access logs check Plus de table `crime_scene` et aucune idĂ©e de laquelle par laquelle commencer. Mais le `incident_reports` me semble bien... ```sql SELECT * FROM incident_reports LIMIT 9; -- confirmation SELECT * FROM incident_reports WHERE location LIKE '%quantum%'; -- bingo ``` Comme les tables sont donnĂ©es pratiquement dans l’ordre d’exploration conseillĂ©, la seconde Ă©tape va ĂȘtre la consultations des tĂ©moignages comme prĂ©cĂ©demment. ```sql SELECT * FROM witness_statements WHERE incident_id = (SELECT id FROM incident_reports WHERE location LIKE 'Quantum%'); ``` Le premier tĂ©moignage mentionne un serveur ```sql SELECT DISTINCT server_location FROM computer_access_logs LIMIT 9; -- check SELECT * FROM computer_access_logs WHERE server_location = 'Helsinki'; -- 14 SELECT * FROM computer_access_logs WHERE server_location = 'Helsinki' AND access_date = (SELECT date FROM incident_reports WHERE location LIKE 'Quantum%'); -- 4 ``` Le second tĂ©moignage mentionne une carte d’accĂšs ```sql SELECT COUNT(*) FROM keycard_access_logs; -- 200 SELECT COUNT(*) FROM keycard_access_logs WHERE access_date = (SELECT date FROM incident_reports WHERE location LIKE 'Quantum%'); -- 200 SELECT DISTINCT keycard_code FROM keycard_access_logs LIMIT 9; -- overview SELECT COUNT(*) FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%'; -- 70 SELECT * FROM keycard_access_logs WHERE REGEX_LIKE(keycard_code,'QX-0[0-9][13579]'); -- [Error in query] no such function: REGEX_LIKE SELECT * FROM keycard_access_logs WHERE keycard_code REGEX 'QX-0[0-9][13579]'; -- [Error in query] near "REGEX": syntax error SELECT * FROM keycard_access_logs WHERE keycard_code SIMILAR TO 'QX-0[0-9][13579]'; -- [Error in query] near "SIMILAR": syntax error SELECT * FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%' --AND (SUBSTR(keycard_code,5,1) BETWEEN 0 AND 9) AND SUBSTR(keycard_code,6,1) IN (1,3,5,7,9); -- 0 SELECT * FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%' --AND (SUBSTR(keycard_code,5,1) BETWEEN '0' AND '9') AND SUBSTR(keycard_code,6,1) IN ('1','3','5','7','9'); -- 36 SELECT * FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%' --AND (SUBSTRING(keycard_code,5,1) BETWEEN '0' AND '9') AND SUBSTRING(keycard_code,6,1) IN ('1','3','5','7','9'); -- 36 SELECT * FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%' --AND (SUBSTRING(keycard_code FROM 5 FOR 1) BETWEEN '0' AND '9') AND SUBSTRING(keycard_code FROM 6 FOR 1) IN ('1','3','5','7','9'); -- 36 -- [Error in query] near "from": syntax error ``` Ici, tous les accĂšs sont datĂ©s du mĂȘme jour ; donc ce critĂšre ne sera pas utile/pertinent. J’ai pensĂ© Ă  utiliser l’information d’[imparitĂ©](https://fr.wiktionary.org/wiki/imparit%C3%A9) en prenant en compte le fait que tous les badges ici commencent par « `QX-` » avec trois chiffres (et non deux... Ă  moins que ce ne soient deux impaires et que j’ai mal compris ?) Je pense que l’usage des expressions rationnelles est fort utile (on rĂ©duit de 70 Ă  36 entrĂ©es, ce qui est encore beaucoup.) - Sauf erreur, la fonction `REGEX_LIKE(column,'POSIX ERE'[,opt-flags])` connue de plusieurs Ă©diteurs (en tout cas DB2, My/Maria, Oracle, Pg, T-SQL, et certainement d’autres), est dans le standard (1999 ?) Mais ce n’est pas reconnu ici... - On retrouve aussi la syntaxe `column REGEX 'some RE'` chez certains Ă©diteurs (dont Sybase/SAP, [SQLite via une fonction personnalisĂ©e](https://www.slingacademy.com/article/how-to-use-regular-expressions-regex-in-sqlite/), MySQL et MariaDB qui ont aussi l’alias `RLIKE`, etc.) Ce n’est pas reconnu non plus... - Il y a la syntaxe `column SIMILAR TO 'SQL RE'` qui est dans SQL:1999 et fonctionne comme `column LIKE 'pattern'` (les mĂ©ta caractĂšres `_` et `%` ont la mĂȘme signification et c’est tout le champ qui est validĂ©), mais avec des extensions des expressions rationnelles POSIX (il n’y a pas le mĂ©ta caractĂšre `.` ni les ancres `^` et `$`.) C’est un excellent entre deux supportĂ© par certains Ă©diteurs (dont PostgreSQL et SQLanywhere) mais pas encore par tous (notamment pas My/Maria et SQLite), ce qui est le cas (non support) ici. - Moins connu (et ne fonctionne pas ici...) SQL:2003 a Ă©galement spĂ©cifiĂ© `SUBSTRING(column SIMILAR 'pattern' [ESCAPE escape])` comme Ă©criture alternative. Pour l’instant, ça ne semble reconnu que par PostgreSQL... Tout espoir n’est pas perdu car l’expression rationnelle peut ĂȘtre traduite facilement par des comparaisons sur des sous-chaĂźnes... Mais j’y vais avec un peu d’apprĂ©hension car c’est un peu le zoo... - Dans le sillage d’Oracle, beaucoup de SGBD (dont SQLite) utilisent `SUBSTR(column,start[,length])` avec le comptage Ă  partir de `1` et optionnellement le nombre de caractĂšres Ă  extraire (tout le reste si rien n’est prĂ©cisĂ©.) Beaucoup d’implĂ©mentations permettent de compter Ă  partir de la fin en utilisant un nombre nĂ©gatif (avec `-1` donc pour le dernier caractĂšre.) Ça marche ici... - D’autres Ă©diteurs (dont SQL Anywhere), dans le sillage de T-SQL, utilisent plutĂŽt `SUBSTRING(column,start[,length])` pareillement. Avec des signatures similaires, on retrouvera les deux formes (c’est le cas ici...), `SUBSTRING` Ă©tant alors un alias de `SUBSTR` (cas par exemple de SQLite≄3.34) ou l’inverse (cas par exemple de Sybase, mais aussi My/Maria qui ajoutent aussi l’alias `MID` en clin d’Ɠil aux BASIC.) - On peut retrouver d’autres fonctions (comme `MID(column,start[,length])` dĂ©jĂ  Ă©voquĂ© et qui n’est pas reconnu ici...) et SQL:2008 a finalement retenu `SUBSTRING(column [FROM start] [FOR length])` (pas reconnu ici non plus, mais pris en charge uniquement dans My/Maria, Ingres, Pg, aucunement par les grosses solutions propriĂ©taires au moment oĂč j’écris...) Ă©videmment (le standard privilĂ©gie toujours une formulation explicite et un peu verbeuse.) - Parmi les fonctions d’extraction de chaĂźne, SQL Server et d’autres dans son sillage proposent les raccourcis (pas exactement mais on va dire cela pour faire simple) `LEFT(column,length)` et `RIGHT(column,length)` pour `SUBSTRING(column,1,length)` (ou `SUBSTRING(column FOR length)`...) et `SUBSTRING(column,-1,length)` respectivement. On a eu du bol... Mais il reste quand mĂȘme beaucoup de lignes Ă  parcourir (14 et 36) Puisque nos deux tables ont des champs communs, on peut faire faire la comparaison au serveur... ```sql SELECT employee_id, access_date, access_time FROM computer_access_logs WHERE server_location = 'Helsinki' AND access_date = (SELECT date FROM incident_reports WHERE location LIKE 'Quantum%') INTERSECT SELECT employee_id, access_date, access_time FROM keycard_access_logs WHERE keycard_code LIKE 'QX-0%' AND SUBSTRING(keycard_code,6,1) IN ('1','3','5','7','9') ; -- 0/1 avec/sans access_time ``` Non, cette personne n’est pas notre coupable comme il en ressort de sa confession... Il est normal que nous soyons passĂ© Ă  cĂŽtĂ© vu que cette entrĂ©e (de `witness_statements`) n’est associĂ©e Ă  aucun incident... 😉 ## mails logs check Bien, voyons donc les mails reçus par cette personne ce jour lĂ . ```sql SELECT * FROM email_logs WHERE (recipient_employee_id = 99) -- mettre la valeur trouvĂ©e AND (email_date = (SELECT date FROM incident_reports WHERE location LIKE 'Quantum%') ); -- le premier critĂšre seul suffit en fait ``` La source du message n’a rien notĂ© d’anormal (i.e. pas d’entrĂ©e dans `witness_statements`) et n’a envoyĂ© que ce message. Par contre cette source a reçu deux autres messages anonymes... ```sql SELECT * FROM email_logs WHERE recipient_employee_id = (SELECT sender_employee_id FROM email_logs WHERE recipient_employee_id = 99); ``` On connaĂźt deux Ă©lĂ©ments de la chaĂźne d’évĂ©nements de l’affaire. ```sql SELECT * FROM employee_records WHERE id = 99 OR id = (SELECT sender_employee_id FROM email_logs WHERE recipient_employee_id = 99); ``` Il nous manque une troisiĂšme personne mentionnĂ©e dans l’un des mails> [...] but we need her inside F18 before 9. [...]> Unlock 18 quietly by 9. He’ll use his own credentials to access it shortly after L leaves. [...] La lumiĂšre est, quand on comprend Ă  quoi il est fait rĂ©fĂ©rence... ```sql SELECT COUNT(*) FROM facility_access_logs; -- 100 SELECT COUNT(*) FROM facility_access_logs WHERE access_date = (SELECT date FROM incident_reports WHERE location LIKE 'Quantum%'); -- 100 SELECT COUNT(DISTINCT facility_name) FROM facility_access_logs; -- 79 SELECT DISTINCT facility_name FROM facility_access_logs LIMIT 9; -- check... SELECT COUNT(*) FROM facility_access_logs WHERE facility_name LIKE '%18'; -- 3 -- jointure SELECT e.*, f.access_time FROM facility_access_logs f INNER JOIN employee_records e ON e.id = f.employee_id WHERE f.facility_name LIKE '%18' ORDER BY f.access_time; -- imbrication SELECT * FROM employee_records WHERE id IN ( SELECT employee_id FROM facility_access_logs WHERE facility_name LIKE '%18' ORDER BY access_time ); ``` Et voilĂ ... # Ă  suivre Six affaires rĂ©solues et 1900 points d’expĂ©rience engrangĂ©s. Il y a des requĂȘtes que j’aurais voulu refaire pour tester le support des CTE et autres constructions un peu sympathiques. Mais la longue pause (et je ne sais mĂȘme plus pourquoi j’avais oubliĂ© ce [*writeup*](https://www.merriam-webster.com/dictionary/write-up) en cours) me rend l’exercice peu attrayant (en plus j’ai peu de bande passante.) Entre temps, le site s’est dotĂ© d’un blog que je viens de dĂ©couvrir : [www.sqlnoir.com/blog/](https://www.sqlnoir.com/blog/page/4). Le [premier post](https://www.sqlnoir.com/blog/games-to-learn-sql) prĂ©sente d’autres jeux similaires qui occuperont probablement mes prochains congĂ©s. Les deux posts suivants donnent les clĂ©s pour SQL Noir et le quotidien des analystes...

AltStyle ă«ă‚ˆăŁăŠć€‰æ›ă•ă‚ŒăŸăƒšăƒŒă‚ž (->ă‚ȘăƒȘă‚žăƒŠăƒ«) /