Sommaire
On sâĂ©tait laissĂ© prĂ©cĂ©demment avec la promesse dâexaminer plus dâenquĂȘtes sâil y a de la demande.1
4th Case
Le dossier est présenté en commençant par sa date précise, mais ce serait trop facile
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.
1st step
Un fois le dossier ouvert, on nous résume les deux lignes de witness_statements.clue à trouver.
-- 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.
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 ?
-- 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âŠ
--- 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âŠ
--- 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.
2nd 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 ou une certaine agence gouvernementale actuelle, nous surveillons les conversations afin de prĂ©server les libertĂ©sâŠ
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Ă©âŠ
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âŠ
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âŠ
-- 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Ă©âŠ
5th Case
Pour finir en beautĂ©, temporairement (puisquâon espĂšre dâautres enquĂȘtes), on va appliquer notre forensic Ă lâindustrie des ”procs.
access logs check
Plus de table crime_scene et aucune idĂ©e de laquelle par laquelle commencer. Mais le incident_reports me semble bienâŠ
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.
SELECT * FROM witness_statements WHERE incident_id =
(SELECT id FROM incident_reports WHERE location LIKE 'Quantum%');
Le premier témoignage mentionne un serveur
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
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Ă© 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, MySQL et MariaDB qui ont aussi lâaliasRLIKE, 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 commecolumn 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 de1et 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-1donc 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 deSUBSTR(cas par exemple de SQLiteâ„3.34) ou lâinverse (cas par exemple de Sybase, mais aussi My/Maria qui ajoutent aussi lâaliasMIDen 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 retenuSUBSTRING(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)etRIGHT(column,length)pourSUBSTRING(column,1,length)(ouSUBSTRING(column FOR length)âŠ) etSUBSTRING(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âŠ
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à .
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âŠ
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.
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âŠ
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 pose (et je ne sais mĂȘme plus pourquoi jâavais oubliĂ© ce writeup 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/. Le premier post 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âŠ
-
Un petit coup de
statmâindique que mon fichier a Ă©tĂ© Ă©ditĂ© le 22 avril 20255 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. â©

# Typo
Posté par Snark . Ăvalué à  1 (+0/-0).
s/pose/pause/
Envoyer un commentaire
Suivre le flux des commentaires
Note : les commentaires appartiennent Ă celles et ceux qui les ont postĂ©s. Nous nâen sommes pas responsables.