Mogelijke onschuld
Tegenstanders van de doodstraf hebben aangevoerd dat het risico om per ongeluk een onschuldig persoon te executeren een te hoge prijs is. In dit hoofdstuk proberen we bij benadering vast te stellen hoeveel onschuldige mensen er mogelijk zijn geëxecuteerd.
Het belangrijkste voorbehoud is dat een bewering van onschuld, zelfs als deze op iemands sterfbed wordt gedaan, nog geen onschuld bewijst. Bovendien, zelfs als de gevangene de waarheid spreekt, zijn er vele interpretaties van onschuld: de gevangene kan beschuldigd zijn van de moord op twee mensen, maar slechts onschuldig zijn aan de moord op één van hen; of hij kan de omstander hebben gedood maar niet de agent. Dit zijn echter niet alleen maar spitsvondigheden: in Texas rechtvaardigt moord alleen niet de doodstraf. De gevangene moet een halsmisdaad hebben begaan, zoals het doden van een politieagent of van meerdere mensen. De gevangene kan dus onschuldig zijn in strikt juridische zin, maar misschien niet volgens de gangbare morele normen.
Toch blijft er iets verontrustends aan beweringen van onschuld die volgehouden worden tot vlak voor de executie, wanneer er weinig meer te winnen valt. Onze taak hier is om uit te zoeken hoe vaak dit gebeurt, door te berekenen in welk deel van de laatste verklaringen onschuld wordt geclaimd.
Aggregatiefuncties
We hebben twee getallen nodig om de verhouding te berekenen:
Teller: het aantal executies met een bewering van onschuld.
Noemer: het totale aantal executies.
Tot nu toe kwam elke rij in de uitvoer voort uit een enkele rij van de invoer. Hier hebben we echter voor zowel de teller als de noemer informatie nodig uit meerdere invoerrijen. Dit vertelt ons dat we een aggregatiefunctie moeten gebruiken. “Aggregeren” betekent meerdere elementen samenvoegen tot een geheel. Op dezelfde manier nemen aggregatiefuncties meerdere rijen gegevens en combineren ze die tot één getal.
De COUNT-functie
COUNT is waarschijnlijk de meest gebruikte aggregatiefunctie. Zoals de naam al zegt, telt deze functie dingen! COUNT(<kolom>) geeft bijvoorbeeld het aantal rijen in de kolom terug die niet NULL zijn.
Zoals je ziet, is de COUNT-functie onlosmakelijk verbonden met het concept NULL. Laten we een kleine uitstap maken om meer te leren over NULL.
NULL-waarden
In SQL is NULL de waarde van een leeg veld. Dit is iets anders dan de lege tekenreeks '' en het gehele getal 0, die allebei niet als NULL worden beschouwd. Om te controleren of een veld NULL is, gebruik je IS en IS NOT in plaats van = en !=.
Hiermee kunnen we de noemer van onze verhouding vinden:
Variaties op COUNT
Tot zover gaat alles goed. Maar wat als we niet weten welke kolommen vrij zijn van NULL? Erger nog, wat als geen enkele kolom vrij is van NULL? Er moet toch een manier zijn om de lengte van de tabel te bepalen!
De oplossing is COUNT(*). Dit doet denken aan SELECT *, waarbij de * voor alle kolommen staat. In de praktijk telt COUNT(*) rijen zolang ten minste één van hun kolommen niet NULL is. Zo kunnen we de lengte van een tabel bepalen, want een tabel hoort geen rijen te bevatten die volledig NULL zijn.
Een andere veelvoorkomende variatie is het tellen van een deel van de tabel, bijvoorbeeld het aantal executies in Harris County. We zouden SELECT COUNT(*) FROM executions WHERE county='Harris' kunnen uitvoeren. Die query filtert de gegevens eerst tot een kleinere dataset met alleen de executies in Harris en telt vervolgens alle rijen. Maar wat als we tegelijkertijd het aantal executies in Bexar County willen bepalen?
De oplossing is een CASE WHEN-blok, dat werkt als één grote if-else-opdracht. Het heeft twee vormen, en degene die ik prettig vind is:
CASE
WHEN <clause> THEN <result>
WHEN <clause> THEN <result>
...
ELSE <result>
END
Dit is toegegeven een van de wat onhandigere onderdelen van SQL. Een veelgemaakte fout is het vergeten van het END-commando en van de ELSE-voorwaarde, die als vangnet dient voor het geval alle voorgaande voorwaarden onwaar zijn. Bedenk ook uit het vorige hoofdstuk dat voorwaarden expressies zijn die als waar of onwaar geëvalueerd kunnen worden. Het is dus belangrijk om na te denken over de booleaanse waarde van alles wat je erin stopt.
Oefenen
Het is de moeite waard om even een stap terug te doen en na te denken over de verschillende manieren waarop de computer deze drie queries heeft afgehandeld. In de WHERE-versie filterde hij eerst tot een kleine tabel en aggregeerde hij daarna, terwijl hij in de andere twee de volledige tabel moest doorlopen. In de versie met COUNT + CASE WHEN hoefde hij de tabel maar één keer te doorlopen, terwijl de versie met twee keer COUNT hem twee keer liet doorlopen. Hoewel de uitvoer dus identiek was, waren de prestaties waarschijnlijk het best bij de eerste en het slechtst bij de derde versie.
Documentatie opzoeken
Dit boek is nooit bedoeld als een allesomvattend naslagwerk voor de SQL-taal. Daarvoor zul je andere online bronnen moeten raadplegen. Dat is een vaardigheid op zich, en een die het waard is om onder de knie te krijgen, want je zult nog jaren nadat je vertrouwd bent geraakt met de taal documentatie blijven opzoeken.
Het goede nieuws is dat opzoeken met de mentale modellen die je in dit boek leert snel en pijnloos zou moeten gaan, omdat je alleen nog details controleert, zoals of de functie AVERAGE of AVG heet, in plaats van uit te zoeken welke aanpak je moet kiezen.
Om dingen op te zoeken gebruik ik vaak W3 Schools, Stack Overflow en de officiële SQLite-documentatie.
SELECT DISTINCT is eigenlijk geen aggregatiefunctie, omdat het geen enkel getal teruggeeft en omdat het werkt op de uitvoer van de query in plaats van op de onderliggende tabel. Toch heb ik het hier opgenomen, omdat het een gemeenschappelijk kenmerk deelt: het werkt op meerdere rijen.
Een vreemde query
Laten we, voordat we afsluiten, eens kijken naar deze query:
SELECT first_name, COUNT(*) FROM executions.
Ziet die er niet vreemd uit? Als je een goed mentaal model van aggregaties hebt, zou dat zo moeten zijn! COUNT(*) probeert één enkele waarde terug te geven: de lengte van de tabel executions. first_name probeert voor elke rij één waarde terug te geven. Moet de computer dan één of meerdere rijen teruggeven? Als hij er één teruggeeft, welke first_name moet hij dan kiezen? Als hij er meerdere teruggeeft, moet hij het resultaat van COUNT(*) dan over alle rijen herhalen? De vormen van de uitvoer passen gewoon niet bij elkaar!
In de praktijk proberen databases iets zinnigs terug te geven, zelfs als je er onzin in stopt. In dit geval kiest onze database de voornaam uit de laatste rij van onze tabel. Omdat onze tabel in omgekeerde chronologische volgorde staat, is de laatste rij die van Charlie Brooks Jr., de eerste persoon die werd geëxecuteerd nadat het Hooggerechtshof het verbod op de doodstraf had opgeheven. Verschillende databases gaan hier verschillend mee om, dus je kunt beter niet op hun standaardgedrag vertrouwen. Als je weet dat je de laatste rij wilt, moet je die expliciet opzoeken. Veel SQL-dialecten hebben een aggregatiefunctie LAST die dit eenvoudig maakt. Helaas heeft SQLite die niet, dus is er een omweg nodig.
SQL-dialecten en databases
Hoewel we dit een boek over SQL hebben genoemd, is het, als we muggenzifterig willen zijn, eigenlijk een boek over SQLite. Dit komt doordat SQL een denkbeeldig concept is, een platonisch ideaal. In werkelijkheid bestaan er alleen dialecten die proberen te voldoen aan de SQL-specificaties.
SQL is bovendien onvolledig gespecificeerd, wat betekent dat sommige functionaliteit niet door de standaarden wordt vastgelegd. De standaarden zeggen bijvoorbeeld niet of de functie die de lengte van een tekenreeks bepaalt LEN (SQL Server) of LENGTH (SQLite) moet heten, of hoe identifiers zoals tabel- of kolomnamen tussen aanhalingstekens moeten worden gezet (` in MySQL, " in SQLite).
Om het nog erger te maken, kan zelfs één enkele query in één enkel dialect verschillend worden verwerkt, omdat de onderliggende databases verschillende architecturen kunnen hebben. Het PostgreSQL-dialect kan bijvoorbeeld worden gebruikt op databases die verspreid zijn over veel verschillende fysieke machines, maar ook op databases die uit één enkel bestand bestaan. Dit betekent dat de mentale modellen die we hier ontwikkelen slechts een hulpmiddel zijn. Ze weerspiegelen misschien niet precies wat de database doet.
We hebben gekozen voor SQLite, dat zowel een dialect als een implementatie is, omdat het een van de meest gebruikte databases is. We hebben ook geprobeerd ons te richten op de kernfunctionaliteit en het mentale model van SQL in plaats van op specifieke eigenaardigheden van SQLite. Met een robuust mentaal model is het eenvoudig om te wisselen tussen SQL-dialecten en databases.
Conclusie en samenvatting
Laten we wat we tot nu toe hebben geleerd gebruiken om onze taak te voltooien:
Deze methode om beweringen van onschuld te vinden is toegegeven nogal onnauwkeurig, omdat onschuld ook in andere bewoordingen kan worden uitgedrukt, zoals “not guilty” (niet schuldig). Toch vermoed ik dat ze het werkelijke aantal eerder onderschat, en dat ze waarschijnlijk wel de juiste orde van grootte geeft. De vraag waarmee we dan blijven zitten, is of we bereid zijn de mogelijkheid te accepteren dat tot wel 5% van de mensen die we executeren in werkelijkheid onschuldig is. (Paul Graham is daar niet toe bereid.)
Samengevat zijn we overgestapt van bewerkingen op rijniveau in het vorige hoofdstuk naar het gebruik van aggregatiefuncties op meerdere rijen in de dataset. Dit heeft een nieuwe weg geopend om gedrag op systeemniveau te bestuderen. In het volgende hoofdstuk leren we hoe we aggregatiefuncties kunnen toepassen op meerdere subgroepen van de dataset met behulp van het GROUP BY-blok.