5 handige SQL queries
Dat SQL handige features heeft, konden we hier al terug lezen. Af en toe heb je queries nodig waarbij enige creativiteit nodig is of waarvan je verdere bewerkingen niet wilt doen je in scripttaal zoals Perl of PHP. Gelukkig kan SQL meeste functionaliteit voor je verschaffen en scheelt het weer script werk en performance. We zullen hier vijf handige SQL queries laten zien.
SQL dump
Voor het uitvoeren van de queries hebben we natuurlijk data nodig. Hieronder vind je een voorbeeld tabel met gegevens erin en de SQL structuur. We zullen voornamelijk gebruik maken van MySQL. Het moet echter geen probleem zijn om de queries te herschrijven zodat deze ook werken op PostgreSQL of Oracle.
-
CREATE TABLE `geeks` (
-
`id` int(1) NOT NULL auto_increment,
-
`naam` varchar(20) NOT NULL,
-
`gebdatum` date NOT NULL,
-
`geek` tinyint(4) NOT NULL,
-
`posts_count` int(1) NOT NULL,
-
PRIMARY KEY (`id`)
-
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-
INSERT INTO `geeks` VALUES (1, 'Mathieu Kooiman', '2000-01-01', 1, 1337);
-
INSERT INTO `geeks` VALUES (2, 'Mathieu Kooiman', '2000-01-01', 1, 1337);
-
INSERT INTO `geeks` VALUES (3, 'Mathieu Kooiman', '2000-01-01', 1, 1337);
-
INSERT INTO `geeks` VALUES (4, 'Tri Pham', '1337-01-01', 1, 7);
-
INSERT INTO `geeks` VALUES (5, 'Steve Jobs', '2002-06-06', 0, 0);
-
INSERT INTO `geeks` VALUES (6, 'Bill Gates', '2006-08-21', 0, 120);
-
INSERT INTO `geeks` VALUES (7, 'Linus Torvalds', '2006-08-25', 1, 2000);
-
mysql> SELECT * FROM geeks;
-
+----+-----------------+------------+------+-------------+
-
| id | naam | gebdatum | geek | posts_count |
-
+----+-----------------+------------+------+-------------+
-
| 1 | Mathieu Kooiman | 2000-01-01 | 1 | 1337 |
-
| 2 | Mathieu Kooiman | 2000-01-01 | 1 | 1337 |
-
| 3 | Mathieu Kooiman | 2000-01-01 | 1 | 1337 |
-
| 4 | Tri Pham | 1337-01-01 | 1 | 7 |
-
| 5 | Steve Jobs | 2002-06-06 | 0 | 0 |
-
| 6 | Bill Gates | 2006-08-21 | 0 | 120 |
-
| 7 | Linus Torvalds | 2006-08-25 | 1 | 2000 |
-
+----+-----------------+------------+------+-------------+
-
7 rows in set (0.00 sec)
Zoals je misschien al opvalt, zitten er dubbele rijen tussen. Dat is met opzet gedaan, waarom lees je later. Laten we beginnen met onze eerste query.
Een datum formaat geven
Meestal wordt een datum opgeslagen in een DATE of DATETIME type, niet in een varchar type!
Als je data (meervoud van datum) in varchar type hebt opgeslagen, probeer dat zo snel mogelijk om te zetten in DATE of DATETIME. Doordat je niet een datum datatype gebruikt beperk je jezelf enorm in de functionaliteit die SQL je kan bieden!
De datum wordt altijd opgeslagen in deze vorm: YYYY-MM-DD (DATE) en YYYY-MM-DD HH:MM:SS (DATETIME). Wanneer je deze datums wilt weergeven, dan wil je dat natuurlijk in een leesbaar formaat doen. Dus in het Nederlandse datum formaat 22-09-2006 in plaats van 2006-09-22. Veel beginnende web developers kennen SQL nog niet genoeg en weten niet hoe zij dit probleem moeten aanpakken.
Een oplossing is de gegevens in je scripttaal verwerken. PHP biedt bijvoorbeeld de nodige functies om datums netjes weer te geven. Dit is zonde, SQL kan dit namelijk voor je doen en nog veel meer.
De 'magische' functie voor MySQL is DATE_FORMAT().
DATE_FORMAT(date,format)
Formats the date value according to the format string.
Als eerste parameter verwacht het een datum in DATE of DATETIME formaat. Daarna kun je aangeven hoe je het gepresenteerd wilt hebben.
Wil je 2006-09-22 weergeven als 22-09-2006? Dan doe je dat zo:
-
mysql> SELECT id, gebdatum, DATE_FORMAT(gebdatum, '%d-%m-%Y') AS gebdatum2 FROM geeks;
-
+----+------------+------------+
-
| id | gebdatum | gebdatum2 |
-
+----+------------+------------+
-
| 1 | 2000-01-01 | 01-01-2000 |
-
| 2 | 2000-01-01 | 01-01-2000 |
-
| 3 | 2000-01-01 | 01-01-2000 |
-
| 4 | 1337-01-01 | 01-01-1337 |
-
| 5 | 2002-06-06 | 06-06-2002 |
-
| 6 | 2006-08-21 | 21-08-2006 |
-
| 7 | 2006-08-25 | 25-08-2006 |
-
+----+------------+------------+
-
7 rows in set (0.00 sec)
Kolom gebdatum2 kun je nu gebruiken voor het weergeven van je datum. Je kunt nog veel meer formaten doorvoeren, in de handleiding vind je een overzicht hiervan.
Belangrijk: Gebruik altijd een alias voor je nieuwe datum kolom zodat je deze makkelijk kunt benaderen. Doe je dat niet, dan zal de kolomnaam de functie aanroep zijn en dat is natuurlijk niet handig. Verder is het afgeraden je nieuwe kolom te gebruiken in een WHERE clausule. Doordat het resultaat van DATE_FORMAT() een string is en niet meer een datum-type zal het sorteren hierop ongewenste resultaten leveren.
Het kan wel eens voorkomen dat je Nederlandse woorden wilt gebruiken voor data. Zoals 22 augustus 2006. Dit kun je ook met SQL doen, alleen wordt het wel een lange query. Vincent Kleijendorst heeft een mooie oplossing bedacht. Hij combineert PHP en MySQL om zo dynamisch tot het goede resultaat te komen. Er wordt uitveorig gebruik gemaakt van MySQL's ELT() functie, die we vanaf versie 3.20.17 kennen, en speelt bij deze snippet een belangrijke rol.
Voorbeeld van ELT() en wat deze doet:
-
mysql> SELECT ELT(1, 'ej', 'Heja', 'hej', 'foo');
-
-> 'ej'
-
mysql> SELECT ELT(4, 'ej', 'Heja', 'hej', 'foo');
-
-> 'foo'
De functie dateFormat() gebruikt dezelfde argumenten als de PHP functie date() en geeft de MySQL DATE_FORMAT() functie terug.
-
<?php
-
-
function dateFormat($sDateFormat, $sDateField, $sTaal = 'nl'){
-
-
// format een datetime veld uit mysql met de argumenten van de functie date()
-
-
$aDate['d'] = 'DATE_FORMAT(' . $sDateField . ", '%d')";
-
$aDate['g'] = 'DATE_FORMAT(' . $sDateField . ", '%l')";
-
$aDate['G'] = 'DATE_FORMAT(' . $sDateField . ", '%k')";
-
$aDate['h'] = 'DATE_FORMAT(' . $sDateField . ", '%h')";
-
$aDate['H'] = 'DATE_FORMAT(' . $sDateField . ", '%H')";
-
$aDate['i'] = 'DATE_FORMAT(' . $sDateField . ", '%i')";
-
$aDate['j'] = 'DATE_FORMAT(' . $sDateField . ", '%e')";
-
$aDate['m'] = 'DATE_FORMAT(' . $sDateField . ", '%m')";
-
$aDate['n'] = 'DATE_FORMAT(' . $sDateField . ", '%c')";
-
$aDate['s'] = 'DATE_FORMAT(' . $sDateField . ", '%S')";
-
$aDate['Y'] = 'DATE_FORMAT(' . $sDateField . ", '%Y')";
-
$aDate['y'] = 'DATE_FORMAT(' . $sDateField . ", '%y')";
-
$aDate['z'] = 'DATE_FORMAT(' . $sDateField . ", '%j')";
-
$aDate['w'] = 'DATE_FORMAT(' . $sDateField . ", '%w')";
-
$aDate['%'] = '%%';
-
-
switch ($sTaal){
-
case 'nl':
-
$aDate['D'] = 'ELT((WEEKDAY(' . $sDateField . ")+1), 'ma','di','wo','do','vr','za','zo')";
-
$aDate['F'] = 'ELT(MONTH(' . $sDateField . "), 'januari','februari','maart','april','mei','juni','juli','augustus','september','oktober','november','december')";
-
$aDate['l'] = 'ELT((WEEKDAY(' . $sDateField . ")+1), 'maandag','dinsdag','woensdag','donderdag','vrijdag','zaterdag','zondag')";
-
$aDate['m'] = 'ELT(MONTH(' . $sDateField . "), 'jan','feb','mrt','apr','mei','jun','jul','aug','sep','okt','nov','dec')";
-
break;
-
-
case 'en':
-
$aDate['D'] = 'DATE_FORMAT(' . $sDateField . ", '%a')";
-
$aDate['F'] = 'DATE_FORMAT(' . $sDateField . ", '%M')";
-
$aDate['l'] = 'DATE_FORMAT(' . $sDateField . ", '%W')";
-
$aDate['m'] = 'DATE_FORMAT(' . $sDateField . ", '%b')";
-
break;
-
}
-
-
for($i = 0; $i <$iFormatLength; $i++) {
-
// replace argumenten per karakter
-
$aReturn[] = $aDate[$sChar];
-
} else {
-
$aReturn[] = "'" . $sChar . "'";
-
}
-
}
-
}
-
?>
Hieronder staat een voorbeeld hoe je het moet gebruiken.
-
<?php
-
-
$sql = "SELECT naam, " . dateFormat('l j F, Y', 'datum') . "
-
AS nl_datum FROM tabel";
-
?>
Handige helper, bedankt Vincent.
Dubbele rijen verwijderen
Naarmate je tabel groter wordt kan het zijn dat je dubbele gegevens krijgt. Nu wil je nog een query om alle dubbele gegevens te verwijderen. Dit is gelukkig niet zo moeilijk. De eerste stap is de dubbele data opzoeken en dat kan met een simpele SELECT samen met een subquery.
-
mysql> SELECT * FROM geeks WHERE id NOT IN (SELECT MIN(id) FROM geeks GROUP BY naam);
-
+----+-----------------+------------+------+-------------+
-
| id | naam | gebdatum | geek | posts_count |
-
+----+-----------------+------------+------+-------------+
-
| 2 | Mathieu Kooiman | 2000-01-01 | 1 | 1337 |
-
| 3 | Mathieu Kooiman | 2000-01-01 | 1 | 1337 |
-
+----+-----------------+------------+------+-------------+
-
2 rows in set (0.01 sec)
Nu hebben wel alleen gegroepeerd op naam, met andere woorden alle dubbele gegevens gebaseerd op kolom 'naam'. Wil je echter op alle kolommen groeperen, dan voeg je die simpelweg toe aan je GROUP BY clausule. We gaan er in dit geval ervan uit dat we de rij met de kleinste id (vaak de eerst ingevoegde rij) willen behouden, vandaar de MIN() in de subquery.
Nu is het verwijderen van de rijen een fluitje van een cent, vervang de SELECT met een DELETE en klaar is klara!
-
DELETE FROM geeks WHERE id NOT IN (SELECT MIN(id) FROM geeks GROUP BY naam);
Condities in queries
Een veel voorkomende situatie is dat je een bepaalde tekst wilt weergeven aan de hand van een waarde. De dateFormat() functie van Vincent van zojuist was daar al een voorbeeld voor.
In onze tabel geeks hebben we de kolom geek. Is de waarde 0, dan is de persoon geen geek. Is de waarde 1, dan is diegene wel een geek. Je wilt dus in je query op een of andere manier een conditie gebruiken. Nu kan je dit oplossen in je favoriete scripttaal, maar SQL kan dit ook. Bijna elke taal kent wel een if-else constructie en in SQL kan dit ook. Je kunt je afvragen of zo'n constructie wel in SQL thuis hoort. Het kan soms handiger zijn om dit te verwerken in je scripttaal.
In SQL bestaat een CASE statement. Deze gebruik je als volgt:
-
mysql> SELECT id, geek, CASE WHEN geek=0 THEN 'Nee' WHEN geek=1 THEN 'Ja' END AS geek_status FROM geeks;
-
+----+------+-------------+
-
| id | geek | geek_status |
-
+----+------+-------------+
-
| 1 | 1 | Ja |
-
| 2 | 1 | Ja |
-
| 3 | 1 | Ja |
-
| 4 | 1 | Ja |
-
| 5 | 0 | Nee |
-
| 6 | 0 | Nee |
-
| 7 | 1 | Ja |
-
+----+------+-------------+
-
7 rows in set (0.00 sec)
Nu kan je zoveel WHEN statements gebruiken in een CASE. In dit geval is het simpel: 0 = 'Nee' en 1 = 'Ja'. Deze query kunnen we versimpelen aangezien we weten dat alleen 0 en 1 kunnen. Daarom kunnen we een ELSE statement gebruiken. De nieuwe query wordt:
-
SELECT *, CASE WHEN geek=0 THEN 'Nee' ELSE 'Ja' END AS geek_status FROM geeks;
Vergeet niet de END aan het einde van je CASE statement!
Officieel kent MySQL ook een IF statement. Aangezien dit een MySQL hersenspinsel is en over het algemeen dus niet in andere database servers bestaat is het aan te raden om deze functie niet te gebruiken
Berekenen van aantal dagen verschil tussen data
Je hebt de registratiedatum van een van je forum gebruikers. Stel nu dat je wilt weten hoeveel dagen deze gebruiker al lid is. Simpel! In MySQL kennen we DATEDIFF().
DATEDIFF(expr1,expr2)
DATEDIFF() returns expr1 – expr2 expressed as a value in days from one date to the other. expr1 and expr2 are date or date-and-time expressions. Only the date parts of the values are used in the calculation.
Let in dit geval goed op want de parameter volgorde is van belang. Als je niet oplet krijg je straks nog een negatief aantal dagen. Je oudste datum moet als eerste parameter!
LET OP:DATEDIFF() is geen standaard SQL functie, deze werkt alleen op MySQL en SQL server.
-
mysql> SELECT CURRENT_DATE();
-
+--------------+
-
| CURRENT_DATE |
-
+--------------+
-
| 2006-08-22 |
-
+--------------+
-
1 row in set (0.00 sec)
-
mysql> SELECT id, CURRENT_DATE(), gebdatum, DATEDIFF(CURRENT_DATE(), gebdatum) AS diff FROM geeks;
-
+----+----------------+------------+--------+
-
| id | CURRENT_DATE() | gebdatum | diff |
-
+----+----------------+------------+--------+
-
| 1 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 2 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 3 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 4 | 2006-08-22 | 1337-01-01 | 244580 |
-
| 5 | 2006-08-22 | 2002-06-06 | 1538 |
-
| 6 | 2006-08-22 | 2006-08-21 | 1 |
-
| 7 | 2006-08-22 | 2006-08-25 | -3 |
-
+----+----------------+------------+--------+
-
7 rows in set (0.01 sec)
Als eerste halen we de huidige datum, dat doen we door de functie CURRENT_DATE() te gebruiken. Daarna zie je query in actie met diff waarin het aantal dagen verschil zit met CURRENT_DATE (op het huidige moment van schrijven). CURRENT_DATE() gebruiken hier slechts als voorbeeld, je kunt natuurlijk ook kolomnamen gebruiken.
Een andere manier is met TO_DAYS(), hiermee kun je een datum omzetten in het aantal dagen vanaf 0000-00-00 (makkelijker is met DATEDIFF()). Hiermee kun je dus ook mee rekenen. Deze query geeft hetzelfde resultaat als hierboven.
-
mysql> SELECT TO_DAYS(CURRENT_DATE());
-
+-------------------------+
-
| TO_DAYS(CURRENT_DATE()) |
-
+-------------------------+
-
| 732910 |
-
+-------------------------+
-
1 row in set (0.00 sec)
-
mysql> SELECT id, CURRENT_DATE(), gebdatum, TO_DAYS(CURRENT_DATE()) - TO_DAYS(gebdatum) AS diff FROM geeks;
-
+----+----------------+------------+--------+
-
| id | CURRENT_DATE() | gebdatum | diff |
-
+----+----------------+------------+--------+
-
| 1 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 2 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 3 | 2006-08-22 | 2000-01-01 | 2425 |
-
| 4 | 2006-08-22 | 1337-01-01 | 244580 |
-
| 5 | 2006-08-22 | 2002-06-06 | 1538 |
-
| 6 | 2006-08-22 | 2006-08-21 | 1 |
-
| 7 | 2006-08-22 | 2006-08-25 | -3 |
-
+----+----------------+------------+--------+
-
7 rows in set (0.00 sec)
LET OP: TO_DAYS() is een MySQL only functie.
Running total berekenen
Voor de volgende query willen we een andere berekening uitvoeren. Dit keer je wil je weten wat het totaal aantal posts is voorafgaande rijen plus het huidige rij. Voorbeeld: de eerste drie rijen zijn identiek en hebben allemaal een posts_count van 1337. Nu wil je weten hoeveel posts er zijn bij gebruiker 'Tri Pham' plus alle voorafgaande posts_count bij elkaar opgeteld? In dit geval is dat 1337 + 1337 + 1337 + 7 = 4018. Dit wordt een 'running total' genoemd. Dit is de query met resultaat.
-
mysql> SELECT a.id, a.posts_count, (SELECT SUM(b.posts_count) FROM geeks b WHERE
-
b.id <= a.id) AS totaal FROM geeks a ORDER BY totaal;
-
+----+-------------+--------+
-
| id | posts_count | totaal |
-
+----+-------------+--------+
-
| 1 | 1337 | 1337 |
-
| 2 | 1337 | 2674 |
-
| 3 | 1337 | 4011 |
-
| 4 | 7 | 4018 |
-
| 5 | 0 | 4018 |
-
| 6 | 120 | 4138 |
-
| 7 | 2000 | 6138 |
-
+----+-------------+--------+
-
7 rows in set (0.01 sec)
De sleutel tot deze query is de join tussen b.id en a.id. Voor elke rij die een kleinere of gelijke id heeft wordt de posts_count opgenomen in de SUM(). Nu werken we met een id veld, maar dit kun je natuurlijk ook toepassen voor andere numerieke data.
Wil je het alleen weten tot en met gebruikter 'Tri Pham'? Dan voeg je simpelweg een WHERE clausule toe.
-
mysql> SELECT a.id, a.posts_count, (SELECT SUM(b.posts_count) FROM geeks b WHERE
-
b.id <= a.id) AS totaal FROM geeks a WHERE naam = 'Tri Pham';
-
+----+-------------+--------+
-
| id | posts_count | totaal |
-
+----+-------------+--------+
-
| 4 | 7 | 4018 |
-
+----+-------------+--------+
-
1 row in set (0.05 sec)
In dit geval is de sorteer volgorde niet meer belangrijk, dus die is weggehaald uit de query.
We hebben 5 handige queries laten zien die laten zien waar SQL wel allemaal tot toe in staat is. Heb je zelf nog een handige query die je wilt delen? Deel het met ons!
Volg Scriptorama via RSS!
Reageer ook!
"Heb je zelf nog een handige query die je wilt delen?"
Je kunt een SELECT query gebruiken als het VALUES gedeelte van een insert-query:
INSERT INTO
tabel (
veld1,
veld2
)
(
SELECT
MAX(waarde1) + 1,
"hallo"
FROM
tabel
WHERE
condities
)
Door Ruud
op 08.23.06 @ 6:55 am | Permalink
Die query om dubbele records uit een db te verwijderen is wel leuk maar wat als je nou de nieuwste van de dubbele records wilt behouden?
Jij doet er nu een GROUP BY op en dan krijg je een willekeurige terug.
Door Peter r
op 09.06.06 @ 9:11 am | Permalink
@Peter r: Als je de nieuwste wilt behouden dan gebruik je MAX() in de subquery.
Ik geloof dat je niet helemaal snapt wat GROUP BY doet in de subquery, je groepeert kolommen op dezelfde data. Er is dus niks 'willekeurigs' aan, door die NOT IN behoud je de eerste (MIN) of de nieuwste (MAX) want die selecteer je.
Door Tri Pham
op 09.06.06 @ 11:14 pm | Permalink
Jawel ik snap wel wat GROUP BY doet en ik weet dan ook wel dat mijn query als volgt wordt:
SELECT * FROM article WHERE id NOT IN (SELECT MIN(date) FROM article GROUP BY id)
maar dan krijg je zo'n ontzettend zware query als je dit op 250 000 records gaat doen.
Door Peter r
op 09.07.06 @ 9:35 am | Permalink
Het laatste voorbeeld met TriPham moet toch zijn:
WHERE a.naam = 'Tri Pham'
en niet
WHERE naam = 'Tri Pham'
Door Youp
op 09.30.06 @ 6:47 pm | Permalink
@Peter R:
> maar dan krijg je zo'n ontzettend zware query
> als je dit op 250 000 records gaat doen.
Als je op een database met 250000 records een dergelijke query moet doen, dan is het maar de vraag of je niet al eerder een foutje hebt gemaakt :)
Door berry__
op 10.09.06 @ 10:57 am | Permalink
@berry__
Ja klopt, maar soms moet je puin ruimen van anderen ;-)
Sorry btw voor erg.... late reactie ;-)
Door Peter R
op 09.26.07 @ 7:55 am | Permalink
Ik snap nog steeds amper wat van werken met data's zekers als je veel gegevens wilt gebruiken voor statistieken :{
Door Eric
op 03.02.08 @ 12:59 pm | Permalink
Bij het onderdeel: Een datum formaat geven. Stel je hebt de select-query in een functie staan. Hoe kun je dan de functie dateFormat gebruiken binnen je eigen functie??
Door Fausto
op 08.06.08 @ 11:02 am | Permalink
@Fausto: Ik begrijp niet helemaal wat je bedoeld? Je kunt in die functie en in jouw SELECT gewoon de MySQL functie DATE_FORMAT gebruiken.
Door Mathieu Kooiman
op 08.06.08 @ 12:36 pm | Permalink
@Mathieu: Ik heb de volgende functie in een klasse:
function getActiviteitenLijst() {
$query = "SELECT ap.agendapunt_id, ap.activiteit, en hier de datum (ap.datum)...
}
met daaronder de dateFormat klasse van Vincent. Hoe verwerk je dat dan in mijn functie?
Door Fausto
op 08.06.08 @ 1:51 pm | Permalink
@Fausto:
dateFormat() retourneert dus een stuk SQL. Overigens zou ik zelf dergelijke formatting niet in SQL doen, maar juist in je applicatie, bijvoorbeeld middels de DateTime extensie ( http://www.scriptorama.nl/php/php-53-date-extensie-revisited )
Door Mathieu Kooiman
op 08.07.08 @ 9:52 am | Permalink
Bestaat er ook een omgekeerde versie van date_format, dus het omvormen van bijvoorbeeld 22-09-2006 naar 'DATE'-formaat 2006-09-22 ? Dit zou handig zijn als een gebruiker een datum uit een db moet bijwerken, die vervolgens als date-formaat in de db moet worden teruggeschreven.
Door Ed van Andel
op 05.18.09 @ 11:46 am | Permalink
http://dev.mysql.com/doc/refman/5.0/en/date-and-time-functions.html#function_str-to-date
Door Richard van Velzen
op 05.22.09 @ 11:26 am | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>