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...