[mysql] joinen op postcode range

Pagina: 1
Acties:

  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
Ik heb een tabel met user-data en een tabel met postcode-ranges die per range de streeknaam en (zelfverzonnen) regio aangeven.

de tabel met user-data bestaat o.a. uit een primary key `id` en een postcode veld die overal bestaat uit 4cijfers gevolgd door een spatie en twee letters (of is helemaal leeg).
dump van de tabel met postcodes:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE postcodes (
    postcode_id tinyint(3) unsigned NOT NULL auto_increment,
    range_from smallint(5) unsigned default NULL,
    range_till smallint(5) unsigned default NULL,
    regio tinyint(3) unsigned default NULL,
    streek char(25) default NULL,
    PRIMARY KEY (postcode_id),
    UNIQUE KEY range (range_from,range_till),
    UNIQUE KEY postcode_id (postcode_id),
    KEY rf (range_from),
    KEY rt (range_till),
    KEY regio (regio)
) TYPE=MyISAM;


De bedoeling is om nu aan de hand van de postcode de regio te bepalen van een gebruiker. Dat is opzich natuurlijk een eitje, maar wat niet wil lukken is om dit een beetje efficient te doen.
Het meest efficiente wat ik tot nu toe heb is dit:

MySQL:
1
2
3
4
5
6
SELECT
    COUNT(*)
FROM data_main AS dm, postcodes AS p
#WHERE SUBSTRING(dm.postcode, 1, 4) BETWEEN p.range_from AND p.range_till
#WHERE dm.range BETWEEN p.range_from AND p.range_till
WHERE dm.range >= p.range_from AND dm.range <= p.range_till

(range is een kolom die ik extra heb aangemaakt om alleen de cijfers van de postcode in op te slaan, en van het type SMALLINT)

Die laatste WHERE is het snelste, die 'between'-where lijkt ietsje langzamer en die substring-where is echt een stuk langzamer.

Maar, die laatste WHERE is nog steeds te langzaam.
Als ik een explain doe dan blijkt ook dat ie alle 164 rows van de postcodes tabel beschouwd
explain waarbij er een index op de kolom dm.range staat:
MySQL:
1
2
3
4
5
6
7
explain SELECT COUNT(*)
FROM data_main AS dm, postcodes AS p
WHERE dm.range >= p.range_from AND dm.range <= p.range_till

table   type    possible_keys   key key_len ref rows    Extra
p   index   range,rf,rt range   6   NULL    164 Using index
dm  ALL range       NULL    NULL    NULL    13943   range checked for each record (index map: 32)

(wat overigens gek is, is dat als ik op dm.range een index zet, dit een positief effect heeft op de 'between'-where, maar een negatief effect op de laatste where :S)

explain zonder die index en met een BETWEEN:
code:
1
2
3
4
5
6
7
explain SELECT COUNT(*)
FROM data_main AS dm, postcodes AS p
WHERE dm.range BETWEEN p.range_from AND p.range_till

table   type    possible_keys   key key_len ref rows    Extra
p   index   NULL    range   6   NULL        164 Using index
dm  ALL NULL    NULL    NULL    NULL        13943   where used


En hij duurt ongeveer 2 seconden. Dat is echt onacceptabel, zeker gezien dit princiepe in veel ingewikkelder queries gebruikt moet gaan worden en het verschil dus alleen maar toe zal nemen.
Hij mag eigenlijk maar een paar honderste van een seconde duren ... en dat zal waarschijnlijk ook wel lukken als mysql in de gaten heeft dat die ranges uniek zijn en er dus maar 1 row in die postcode tabel op gejoined hoeft te worden. Hij heeft volgens de explain ook wel in de gaten dat ie die range-key moet nemen in de postcode tabel, maar heeft vervolgens niet in de gaten dat ie niet alle rows hoeft te checken

Iemand enig idee hoe ik dit mysql duidelijk maak?

overigens zoek ik eigenlijk uiteindelijk naar een oplossing waarbij ik die dm.range kolom niet nodig heb en het dus met een SUBSTRING op de postcodes af kan.
En een referentie naar de primary key van de postcodes-tabel opslaan in de user-tabel vind ik ook geen oplossing, omdat ik dan na iedere update query nog eens een aparte update query moet gaan doen om die pk-referentie bij te werken (mysql-versie laat geen updates over meerdere kolommen toe)

  • Nielsz
  • Registratie: Maart 2001
  • Niet online
Zowiezo moet je de lettercombinatie scheiden van de 4cijferige postcode.

  • lier
  • Registratie: Januari 2004
  • Laatst online: 16:59

lier

MikroTik nerd

Volgens mij zijn de letters van de postcode niet zo interessant voor de lokatie.
Waarom splits je daarom de postcode kolom niet op in cijfers en letters ?

Edit: Bugger, weer te laat...

[ Voor 10% gewijzigd door lier op 06-04-2005 11:02 ]

Eerst het probleem, dan de oplossing


  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
Nielsz schreef op woensdag 06 april 2005 @ 11:01:
Zowiezo moet je de lettercombinatie scheiden van de 4cijferige postcode.
moet? Ik heb er (tot nu toe) nog geen enkele reden voor gehad. Dat was alleen maar lastig geweest omdat ik het dan iedere keer dat ik het wilde gebruiken weer moest concatten. Maar ok, als het anders niet gaat werken, dan doe ik dat uiteraard.

anyways, deze opmerkingen helpen me niet veel verder, want ik heb die cijfers dus al apart opgeslagen en nog steeds is die query niet optimaal

  • lier
  • Registratie: Januari 2004
  • Laatst online: 16:59

lier

MikroTik nerd

Uit je verhaal bleek niet echt duidelijk dat je dat al gedaan had...
Inmiddels zie ik het (ook) wel staan.

Heb je een (samengestelde) index staan op de twee range velden ?

Eerst het probleem, dan de oplossing


  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
lier schreef op woensdag 06 april 2005 @ 11:36:
Uit je verhaal bleek niet echt duidelijk dat je dat al gedaan had...
Inmiddels zie ik het (ook) wel staan.
tja, toch stond het er dus
Heb je een (samengestelde) index staan op de twee range velden ?
Ook dat staat in m'n openingspost hoor. Daar heb ik een volledige structure dump van de tabel neergezet.
Die heb ik dus en heb het ook zonder die samengestelde index geprobeerd (wat geen qua tijd op hetzelfde neerkwam)

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

Je hebt 13943 records in die data_main en je wilt voor alle records weten welke ranges eromheen zitten en vervolgens tel je al die combinaties. En dan ben je verbaasd dat het 2 seconden duurt?

Tenzij bovenstaande query-beschrijving precies is wat je wilt, denk ik niet dat je met deze query bereikt wat je in je openingspost beschrijft. Als je wilt bepalen in welke regio een gebruiker zich bevindt moet je ook die query testen en niet een query die iets heel anders doet, lijkt me.
Bij de query waar je test waar de gebruiker zich bevindt moet er natuurlijk nog een dm.userid = X bij je where...

  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
ACM schreef op woensdag 06 april 2005 @ 12:13:
Je hebt 13943 records in die data_main en je wilt voor alle records weten welke ranges eromheen zitten en vervolgens tel je al die combinaties.
Nee, niet helemaal. Iedere postcode valt maar binnen 1 range. Dat is de grap nou juist; het is dus een 1:1 relatie. En ik tel niet zozeer de combinaties maar deed gewoon COUNT(*) omdat ik alleen wil weten hoe lang het duurt.
En dan ben je verbaasd dat het 2 seconden duurt?
Als ik SELECT COUNT(*) FROM data_main doe, dan praten we over honderste van een seconde. En ik verbaas me er niet zozeer over dat het 2 seconde duurt, want volgens die explain gaat ie per row in de data_main tabel alle rows van de postcodes tabel af - maar ik wil er dus naar toe dat ie in de gaten heeft dat die ranges uniek zijn en dat ie dus per row in de data_main tabel maar 1 row (de juiste) in de postcodes tabel beschouwd. En dan verwacht ik vervolgens dat we weer over honderste van een seconde praten ja :)
Tenzij bovenstaande query-beschrijving precies is wat je wilt, denk ik niet dat je met deze query bereikt wat je in je openingspost beschrijft. Als je wilt bepalen in welke regio een gebruiker zich bevindt moet je ook die query testen en niet een query die iets heel anders doet, lijkt me.
wat doe ik dan anders volgens jou?
nogmaals: het voorbeeld is slechts een simpele query, die alleen de snelheid en de aard van de JOIN moet testen; vandaar ook het gebruik van COUNT(*). En als dat werkt ga ik uiteraard een andere query gebruiken, maar volgens hetzelfde princiepe.
Bij de query waar je test waar de gebruiker zich bevindt moet er natuurlijk nog een dm.userid = X bij je where...
dat lijkt me logisch :)

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

marty schreef op woensdag 06 april 2005 @ 12:28:
Nee, niet helemaal. Iedere postcode valt maar binnen 1 range. Dat is de grap nou juist; het is dus een 1:1 relatie. En ik tel niet zozeer de combinaties maar deed gewoon COUNT(*) omdat ik alleen wil weten hoe lang het duurt.
Maar je telt alsnog wel 13k relaties.
Als ik SELECT COUNT(*) FROM data_main doe, dan praten we over honderste van een seconde.
Dat komt door de manier waarop MyIsam is opgezet, als je er een niet-indexable check bij doet (where trim(postcode) != '' ofzo) zal je zien dat het gelijk wat langer duurt. Of het dan de 2 seconden haalt durf ik niet te zeggen.
En ik verbaas me er niet zozeer over dat het 2 seconde duurt, want volgens die explain gaat ie per row in de data_main tabel alle rows van de postcodes tabel af
Dat kan je eigenlijk niet afleiden uit mysql's gebrekkige explain. In dit geval komt het er op neer dat blijkbaar alle postcode-ranges een keer gebruikt worden, maar volgens mij staat er niet dat je per record uit data_main alle ranges afgaat.
wat doe ik dan anders volgens jou?
nogmaals: het voorbeeld is slechts een simpele query, die alleen de snelheid en de aard van de JOIN moet testen; vandaar ook het gebruik van COUNT(*). En als dat werkt ga ik uiteraard een andere query gebruiken, maar volgens hetzelfde princiepe.
Maar je joint nu alle records van data_main met een postcode, dat is niet wat je uiteindelijk wilt doen, dus niet wat je nu zou moeten testen.
dat lijkt me logisch :)
En van die uitbreiding, met dm.userid = X erbij, is het belangrijk om te weten hoe het performt.

  • Nielsz
  • Registratie: Maart 2001
  • Niet online
marty schreef op woensdag 06 april 2005 @ 11:17:
[...]


moet? Ik heb er (tot nu toe) nog geen enkele reden voor gehad. Dat was alleen maar lastig geweest omdat ik het dan iedere keer dat ik het wilde gebruiken weer moest concatten. Maar ok, als het anders niet gaat werken, dan doe ik dat uiteraard.

anyways, deze opmerkingen helpen me niet veel verder, want ik heb die cijfers dus al apart opgeslagen en nog steeds is die query niet optimaal
Als je alleen '1234' opslaat ipv '1234 AB', dan kan je van het veld een int maken..

  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
ACM schreef op woensdag 06 april 2005 @ 12:41:
[...]

Maar je telt alsnog wel 13k relaties.
Weet ik. Maar als ik de primary key van data_main op de primary key van postcodes join (met een LEFT JOIN) (btw, ik weet dat deze niets met elkaar te maken hebben, maar het gaat even om het idee - dat is namelijk ook een 1:1 relatie) dan duurt diezelfde query de verwachte honderste van een seconden.
Dat kan je eigenlijk niet afleiden uit mysql's gebrekkige explain. In dit geval komt het er op neer dat blijkbaar alle postcode-ranges een keer gebruikt worden, maar volgens mij staat er niet dat je per record uit data_main alle ranges afgaat.
jawel toch? mysql geeft toch aan dat ie alle 164 rows van die tabel beschouwd
anyways, als ik dus dit doe:
MySQL:
1
2
3
explain SELECT COUNT(*)
FROM data_main AS dm
    LEFT JOIN postcodes AS p ON p.postcode_id=dm.id

dan staat er bij rows 1 en duurt die query, zoals ik al zei, slechts hondersten van een seconden. Hetzelfde effect moet ook kunnen met die postcode ranges.
Maar je joint nu alle records van data_main met een postcode, dat is niet wat je uiteindelijk wilt doen, dus niet wat je nu zou moeten testen.
Dat is wel wat ik uiteindelijk wil doen. Ik ga straks grotere queries schrijven die moeten gaan zoeken in de user-data op niet geindexeerde velden. Dan heb ik dus wederom te maken met alle records van data_main.

En daarbij: het gaat me ook een beetje om het idee. Ik wil gewoon dat het lukt dat ie slechts 1 range pakt en ze niet allemaal af gaat :) dat moet toch mogelijk zijn?

  • lier
  • Registratie: Januari 2004
  • Laatst online: 16:59

lier

MikroTik nerd

Is het een idee om de postcode tabel in plaats van met ranges te laten werken met een kolom en alle voorkomende postcode 10^4 = 10000 records uit te schrijven ?

Lijkt me niet echt veel werk (met behulp van een query laten vullen) en hiermee bereik je volgens mij wel het gewenste resultaat.

Eerst het probleem, dan de oplossing


  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
Jah, er zijn heel veel halve oplossingen te verzinnen die er allemaal voor zorgen dat de query sneller gaat. Maar dat wil ik niet. Ik wil gewoon een goed genormaliseerde database die daarbij ook nog snel is.
Komop, ik kan toch niet de eerste zijn die een waarde op een unieke range wil joinen - op een dusdanige manier dat ie niet alle ranges af gaat en dus de query traag maakt? Daar moet een database toch ook gewoon in kunnen voorzien?
Pas als het (vrijwel) zeker is dat dat niet kan ga ik over de minst erge halve oplossing nadenken

  • lier
  • Registratie: Januari 2004
  • Laatst online: 16:59

lier

MikroTik nerd

Marty,

Net als OO is het normaliseren van databases niet zaligmakend.

Je hebt nu zelf ondervonden dat deze oplossing niet goed werkt, waardoor je nu naar een andere oplossing op zoek moet. In de praktijk gebeurt het vaak dat (met in het achterhoofd de performance) voor dit soort halve (zeer foute opmerking) oplossingen gekozen moet worden.

Wat is er naast het normaliseren op tegen ?

Eerst het probleem, dan de oplossing


  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
lier schreef op woensdag 06 april 2005 @ 16:37:
Marty,

Net als OO is het normaliseren van databases niet zaligmakend.

Je hebt nu zelf ondervonden dat deze oplossing niet goed werkt, waardoor je nu naar een andere oplossing op zoek moet. In de praktijk gebeurt het vaak dat (met in het achterhoofd de performance) voor dit soort halve (zeer foute opmerking) oplossingen gekozen moet worden.

Wat is er naast het normaliseren op tegen ?
Lier,

Wat is er op tegen dat ik graag een oplossing voor mijn huidige probleem probeer te vinden? dat de 4 cijfers van een postcode op een dusdanige manier op een range wordt gekoppeld dat er slechts één range in beschouwing wordt genomen?
Inmiddels zijn 6 uur we al 13 posts verder en is er nog niemand inhoudelijk daar op ingegaan.

Dat wil ik graag omdat het de mooiste oplossing is en omdat ik wil weten hoe dat princiepe werkt want ik ga het vast nog wel eens een keer tegenkomen. En als dan blijkt dat dat écht niet mogelijk is, dan ga ik wel over andere dingen nadenken.

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

marty schreef op woensdag 06 april 2005 @ 16:50:
Wat is er op tegen dat ik graag een oplossing voor mijn huidige probleem probeer te vinden? dat de 4 cijfers van een postcode op een dusdanige manier op een range wordt gekoppeld dat er slechts één range in beschouwing wordt genomen?
Inmiddels zijn 6 uur we al 13 posts verder en is er nog niemand inhoudelijk daar op ingegaan.
Nou nog een keer dan. Dat is niet wat je bovenstaande query doet.
Dat je bovenstaande query wel sneller wilt zien te krijgen kan ik me iets bij voorstellen, wellicht doe je vaker een range-scan en wil je de postcode-regio erbij hebben.
Het lijkt er echter op dat MySQL (en postgresql weet het dit keer niet beter te doen) het niet anders kan dan bovenstaande. Als je slechts 1 record opvraagt uit die data_main, dan gebruikt mysql de index wel goed en wordt er maar 1 record uit de postcodes opgehaald. Maar zodra je er een paar meer uit data_main wilt hebben (met in(...)) dan vindt ie ineens dat ie een range scan moet uitvoeren per record dat je selecteert. En waarom ie dat ineens zoveel anders doet zou ik ook niet weten.

Je kan proberen om het als bug te rapporteren, PostgreSQL doet het op een vergelijkbare rare manier, dus ik ga daar op de mailinglist es vragen waarom het zo raar moet gaan.

  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
ACM schreef op woensdag 06 april 2005 @ 18:36:
[...]

Nou nog een keer dan. Dat is niet wat je bovenstaande query doet.
Volgens mij praten we langs elkaar heen. Ik doe gewoon een query waarbij ik verwacht dat op iedere row van data_main 1 row van postcodes wordt gejoined (dat gebeurt ook) en dat ie daarbij door het gebruik van indexen ook maar 1 row beschouwd (dat doet ie niet).
Dat je bovenstaande query wel sneller wilt zien te krijgen kan ik me iets bij voorstellen, wellicht doe je vaker een range-scan en wil je de postcode-regio erbij hebben.
klopt :) nu zitten we weer op 1 lijn :)
Het lijkt er echter op dat MySQL (en postgresql weet het dit keer niet beter te doen) het niet anders kan dan bovenstaande. Als je slechts 1 record opvraagt uit die data_main, dan gebruikt mysql de index wel goed en wordt er maar 1 record uit de postcodes opgehaald. Maar zodra je er een paar meer uit data_main wilt hebben (met in(...)) dan vindt ie ineens dat ie een range scan moet uitvoeren per record dat je selecteert. En waarom ie dat ineens zoveel anders doet zou ik ook niet weten.

Je kan proberen om het als bug te rapporteren, PostgreSQL doet het op een vergelijkbare rare manier, dus ik ga daar op de mailinglist es vragen waarom het zo raar moet gaan.
Hmm...zelfs dat doet ie niet bij mij. Welke mysql versie heb je?
als ik dit doe:
MySQL:
1
2
3
4
5
explain SELECT
    COUNT(dm.id)
FROM data_main AS dm, postcodes AS p
WHERE dm.id=2000
AND dm.range >= p.range_from AND dm.range <= p.range_till
krijg ik dit:
code:
1
2
3
table  type   possible_keys  key      key_len  ref    rows  Extra
dm     const  PRIMARY        PRIMARY  8        const  1 
p      range  range,rf,rt    range    3        NULL   164    where used; Using index

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

Ik heb 4.1.nogwat en gebruikte geloof ik BETWEEN ipv die vergelijkingen. Maar veel zou dat niet mogen schelen.

  • marty
  • Registratie: Augustus 2002
  • Laatst online: 27-03-2023
Erg typisch.
Ik was overigens vergeten m'n index op range terug te zetten die ik er om te testen even af had gehaald. Vandaar dat je die hierboven niet in de explain zag staan. Terugzetten maakte geen verschil
Dit is met mysql-versie 3.23.41

Heb het nog even op de bak van een vriend getest die versie 4.0.42 draait

als ik daar
MySQL:
1
2
3
4
5
6
explain SELECT
    COUNT(dt.id)
FROM data_test AS dt, postcodes AS p
WHERE
 dt.id=1 AND
dt.range >= p.range_from AND dt.range <= p.range_till
doe, dan komt daar idd een veel beter resultaat uit, hoewel nog niet zo goed als bij jouw 4.1 versie:
code:
1
2
3
4
5
6
+-------+-------+---------------+---------+---------+-------+------+--------------------------+
| table | type  | possible_keys | key     | key_len | ref   | rows | Extra                    |
+-------+-------+---------------+---------+---------+-------+------+--------------------------+
| dt    | const | PRIMARY,range | PRIMARY |       8 | const |    1 |                          |
| p     | range | range,rf,rt   | range   |       3 | NULL  |    4 | Using where; Using index |
+-------+-------+---------------+---------+---------+-------+------+--------------------------+

Maar waar ie die 4 nou vandaan haalt ... :S
Het lijkt wel alsof het stapje bij beetje beter wordt :-) moet denk ik gewoon tot versie 5 wachten ofzo :)

btw, en als ik daar
MySQL:
1
2
3
4
5
6
explain SELECT
    COUNT(dt.id)
FROM data_test AS dt, postcodes AS p
WHERE
 dt.id IN(1,100) AND
dt.range >= p.range_from AND dt.range <= p.range_till;
doe, dan krijg ik daar dit uit:
code:
1
2
3
4
5
6
+-------+-------+---------------+---------+---------+------+------+-----------------------------------------------+
| table | type  | possible_keys | key     | key_len | ref  | rows | Extra                                         |
+-------+-------+---------------+---------+---------+------+------+-----------------------------------------------+
| dt    | range | PRIMARY,range | PRIMARY |       8 | NULL |    2 | Using where                                   |
| p     | ALL   | range,rf,rt   | NULL    |    NULL | NULL |  164 | Range checked for each record (index map: 52) |
+-------+-------+---------------+---------+---------+------+------+-----------------------------------------------+

hetzelfde verschijnsel dus

  • crisp
  • Registratie: Februari 2000
  • Laatst online: 21:34

crisp

Devver

Pixelated

Dit is een classic en iets waar elk DBMS moeite mee zal hebben.
De methode met BETWEEN zal altijd veel meer records in beschouwing nemen aangezien MySQL er geen weet van heeft dat range_from altijd kleiner is dan range_till en dat de ranges elkaar niet overlappen (en dat laatste is ook de reden waarom ook met kleiner/groter dan er altijd meer records gechecked zullen worden).

Er is wel een truuk te bedenken in de volgende vorm:
code:
1
2
3
4
5
6
7
8
9
10
SELECT
    *
FROM
    postcodes FORCE INDEX(range_till)
WHERE
    range_till >= postcode
AND
    range_from <= postcode
LIMIT
    1

met een index op range_till dus. Het eerste record gevonden in de index (dus de laagste range_till die groter is dan of gelijk aan de te zoeken postcode) zal voldoen, en door de LIMIT 1 hoeft er daarna niet verder gezocht te worden. Helaas zal je dit in een combinatie-query als een subquery moeten gebruiken, en dus heb je minimaal MySQL 4.1 nodig.

Intentionally left blank

Pagina: 1