Stránkování

Školení, která pořádám

Pokud je v databázi více záznamů, než je únosné zobrazit na jedné stránce, je potřeba nějak vyřešit stránkování. Osobně mám raději delší stránky – pokud se data čitelně zobrazují už během načítání (tedy pokud není použit tabulkový design, obrázky mají uvedené rozměry a tak dále), tak delší stránky ničemu nevadí, ale určitá hranice se v mnoha situacích udělat musí.

O tom, jak zjistit celkový počet řádek na stránkách se stránkováním jsem už psal, teď bych rád rozebral, jaké parametry se pro stránkování dají předávat. Řešení, které se nabízí samo, je předávat počet přeskočených záznamů nebo číslo stránky:

<?php
// předávání počtu přeskočených záznamů
$result = mysql_query("SELECT SQL_CALC_FOUND_ROWS * FROM tabulka ORDER BY datum DESC, id DESC LIMIT $limit OFFSET " . intval($_GET["offset"]));
$pocet = mysql_result(mysql_query(" SELECT FOUND_ROWS()"), 0);
if ($_GET["offset"]) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . ($_GET["offset"] != $limit ? "?offset=" . ($_GET["offset"] - $limit) : "") . '">zpět</a>';
}
if ($pocet > $_GET["offset"] + $limit) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . "?offset=" . ($_GET["offset"] + $limit) . '">vpřed</a>';
}

// předávání čísla stránky
$result = mysql_query("SELECT SQL_CALC_FOUND_ROWS * FROM tabulka ORDER BY datum DESC, id DESC LIMIT $limit OFFSET " . ($limit * $_GET["strana"]));
$pocet = mysql_result(mysql_query(" SELECT FOUND_ROWS()"), 0);
if ($_GET["strana"]) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . ($_GET["strana"] != 1 ? "?strana=" . ($_GET["strana"] - 1) : "") . '">zpět</a>';
}
if ($pocet > $limit * ($_GET["strana"] + 1)) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . "?strana=" . ($_GET["strana"] + 1) . '">vpřed</a>';
}
?>

Způsoby jsou si podobné jako vejce vejci, druhý mi je ale o něco sympatičtější, protože se předávají nižší hodnoty a uživatel nám nemůže podstrčit neočekávaný offset (pokud by s tím počítala jiná část skriptu). Všimněte si, že záznamy se třídí nejprve podle data a potom ještě podle id. Pokud totiž datum není unikátní (např. vychází dva články denně), nemusely by se některé záznamy zobrazit, zatímco jiné by se zobrazily na dvou stránkách. Databáze totiž nezaručuje, že řádky bude vracet v nějakém konkrétním pořadí (např. v pořadí vložení, jak by se někdo mohl domnívat). Také si všimněte toho, že u listování zpět se parametr předává jen tehdy, pokud je nenulový – to je důležité kvůli vyhledávačům a cachím – stejný obsah by neměl být dostupný na různých URL (na které navíc vede odkaz).

Chování vyhledávačů a cachí mě ostatně vedlo i k napsání tohoto článku. Jisté je, že tyto tradiční způsoby nejsou vhodné pro cache – když přidáme nový záznam, obsah stránek na původních URL se změní. Způsoby nejsou vhodné ani pro uživatele – nemůžou si konkrétní stránku uložit do oblíbených položek, protože už za týden na ní bude něco jiného. Chování vyhledávačů je jako vždy spekulace – vyhledávače sice mají rády, když se stránky mění – častěji na ně pak chodí, ale pokud se všechen čas vyhrazený pro náš web spotřebuje na tyto pseudozměny, nemusí zbýt čas na skutečně nově přidaný obsah.

Ze všech úhlů pohledu tedy tradiční řešení představují problém, který je potřeba řešit. Asi nejjednodušší bude, když offset nebudeme předávat od začátku, ale od konce:

<?php
// předávání počtu záznamů zbývajících do konce
$pocet = mysql_result(mysql_query("SELECT COUNT(*) FROM tabulka"), 0);
$offset = ($_GET["offset"] ? $_GET["offset"] : $pocet); // offset se předává od konce, aby stránky zůstaly trvale platné
$result = mysql_query("SELECT * FROM tabulka ORDER BY datum DESC, id DESC LIMIT $limit OFFSET " . ($pocet - $offset));
if ($offset < $pocet) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . ($offset + $limit < $pocet ? "?offset=" . ($offset + $limit) : "") . '">zpět</a>';
}
if ($offset > $limit) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . "?offset=" . ($offset - $limit) . '">vpřed</a>';
}
?>

Kvůli vícerému přičítání a odčítání kód vyžaduje pro pochopení trochu více soustředění, ale nepřehledný myslím není (pokud si jsme vědomi faktu, že offset počítáme od konce). Spolu se stránkováním tento způsob pochopitelně použít nelze – pokud je v databázi např. 55 záznamů a $limit je 10, musely by se na stránce č. 5 (počítáno od konce) zobrazit záznamy 41-50 (opět počítáno od konce), ale uživatel tam očekává záznamy 36-45 (protože na úvodní stránce jsou záznamy 46-55).

Tento způsob má ještě jednu výhodu – pokud do databáze přibude záznam v době mezi načtením stránky a přechodem na další stránku (typické u diskusních fór, kde příspěvky přibývají rychle), nezobrazí se již zobrazený záznam znovu. (Pokud je v době načtení stránky v databázi 55 záznamů a $limit je 10, následně do databáze přibude 56. záznam a uživatel prvním popsaným způsobem přejde na ?offset=10, zobrazí se záznamy 37-46 – záznam č. 46 ale uživatel už viděl. U třetího způsobu tento problém nevznikne.) Daní za popsané výhody je to, že do cachí a vyhledávačů se postupně dostanou stránky se všemi offsety (1, 2, 3, …) a ne jen násobky limitu.

V mnoha situacích je lepší se stránkování úplně vyhnout a data rozdělit po logických celcích (např. dnech nebo měsících), jindy to ale možné není.

Přijďte si o tomto tématu popovídat na školení Návrh a používání MySQL databáze.

Jakub Vrána, Řešení problému, 2.11.2005, diskuse: 43 (nové: 0)

Diskuse

ia:

Ja by som len doplnil, že pri stránkovaní je dobré uviesť aj odkazy na jednotlivé stránky a jednoznačne aj počet stránok.
Aspoň mne osobne veľmi vadí, keď mám len odkaz "ďalej" a "nazad" a nemám poňatie kde som a koľko strán mám pred sebou :)

Ondrej:

Ahoj Jakube,
muzu se zeptat jaka je vyhoda v pouzivani SQL_CALC_FOUND_ROWS oproti klasickemu prepocitani zaznamu na jeden dotaz COUNT(*)? Koukam do dokumentace MySQL a neni mi to jasny.
Dik Ondrej

ikona Jakub Vrána OpenID:

http://php.vrana.cz/ziskani-poctu-radek.php

johno:

Osobne v tom tiež nevidím nejaký význam. Jediné čo ma napadá, je to, že je to jeden SELECT a tak je to thread-safe, ale to sa dá pomocou jednoduchej transakcie dosiahnuť taktiež.

V tom článku čo linkujete sa píše, že je to neefektívne, ale nejako mi chýba zdôvodnenie. Môžete to prosím rozviesť alebo ma nakopnúť správnym smerom?

ikona Jakub Vrána OpenID:

Pokud je dotaz složitý, případně pomalý (spojuje tabulky, má složité podmínky, provádí seskupování, ...), tak se v případě dvou dotazů musí vyhodnotit dvakrát, u jednoho dotazu jenom jednou.

Vzhledem k tomu, že je dotaz na jednom místě, tak se také nemůže stát, že omylem změníme jen dotaz pro získání dat a zapomeneme na dotaz pro získání počtu řádek. Samozřejmě se to dá řešit např. uložením společné části dotazů do proměnné, ale já to vidím tak, že používání starého způsobu je ve všech ohledech krkolomnější.

johno:

Sú dostupné nejaké porovnávacie testy výkonu pre spôsob bez použitia SQL_CALC_FOUND_ROWS a s ním? Ja som na nyx.cz dávnejšie čítal, že to s ním dokonca bolo niekedy aj pomalšie. Možno to už optimalizovali.

Hrubeš:

Anebo <asp:GridView ID="GridView1" AllowPaging="true" runat="server"> :)

ikona llook:

Jo a jak to souvisí?

Havran:

A nejaka alternativa ako dosiahnut celkovy pocet vratenych riadkov pri LIMIT v SELECTE na PostgreSQL? :-)

Pavel Z.:

Tohle řešení víceméně používám, ale nevím jak elgantně zistit, na které stránce se článek nachází, když znám pouze jeho id. Proměnnou v GETu předávat nemůžu kvůli keším a vyhledávačům, cookies se mi v tomto zdají krkolomné. Zkrátka chci dostat z dotazu pořadí, na kterém by se nacházel záznam při použití ORDER BY. Díky

ikona Jakub Vrána OpenID:

To je zajímavý problém, taky už jsem ho jednou nebo dvakrát řešil. Na elegantní řešení jsem bohužel nepřišel a tak zbývá pouze krkolomné:

Pokud je původní dotaz "SELECT ... ORDER BY a, id", zjistí pořadí záznamu "SELECT COUNT(*) ... WHERE a < $a OR (a = $a AND id < $id)".

ia:

Možno krkolomné, ale hlavne že to funguje.
Aj ja som na tento problém narazil, ale toto ma nenapadlo...

ikona Jakub Vrána OpenID:

Další možnost: http://php.blog.cz/0508/jak-v-mysql-zjist…-poradi-vypisu

Je ovšem ještě krkolomnější a pomalejší. Hodit se ale může v případě, kdy je ORDER BY tak složitý (třeba dynamický), že postavit z něj podmínku není triviální.

Pavel Prostřední:

Ta první verze by šla upravit takto ... aby se nekonečně negenerovaly stránky.
<?php
if ( ($limit * $HTTP_GET_VARS["strana"])+$limit < $pocet) {

if (
$pocet > $limit * ($_GET["strana"] + 1)) {
    echo "<a href='$_SERVER[PHP_SELF]?strana=" . ($_GET["strana"] + 1) . "'>vpřed</a>\n";
}

}
?>

ikona Jakub Vrána OpenID:

Nerozumím. Proč jsou dvě stejné podmínky vnořené do sebe? A za jakých okolností se nekonečně generují stránky?

me2d:

ahoj mam prozbycku potreboval bych osetrit strankovani bez db jde o to ze na strance vypisuji soubory z urcite slozky a potreboval bych ji po urcitem poctu treba 20 aby mi to nabidlo novou stranku kde bude dalsich 20 jde to nebo ty veci musim nasoupat do db

David:

Zdarec mam problem použil sem jedno z řešení na své stránce moc tomu nerozumim, ale v jednom případě to chodí bezvadně to stránkování ale potřebuju to použít na více místech jako třeba fotogalerie a kniha navštěv a když sem to dal do knihy návštěv tak mě to háže do galerie mě připadá že ten offset si správně předává hodnoty ale z fotogalerie mam tu třetí verzi stránkování předávání počtu záznamů zbývajících do konce. Asi dělám něco špatně chtěl sem to přejmenovat na něco jiného ale nejde to a offfset sem nikde nenašel tak nevim co to je jestli to tam bejt musí v tom tvaru nebo ne. Potřebuju poradit co stim .

nuninek:

Zdravicko, prave jsem na internetu hledal clanky o strankovani v PHP a narazil jsem na tento, ale bohuzel nic mi nefunguje. Pouzivam MySQLi a nemohu si poradit s mysql_result. Muzete prosim poradit jak to cele upravit?

Golem:

Ako by sa dalo do funkcie pridat predavanie parametrov, napriklad ak sme triedili zaznamy koli vyhladavaniu?

ikona Jakub Vrána OpenID:

Pozornosti doporučuji funkci http_build_query.

Petr:

Pokouším se script o stránkování použít na localhostu a nefunguje. Nemůžu najít důvod, DB funguje:
1) připojení do MySQL je v pořádku, script pro config chybu nehlásí
2) select opravdu hledá v DB, ale tváří se, jako by nic nenašel, vůbec nereaguje
3) na obrazovce mám pořád původní počet článků, pouze odkazy Předchozí a Další zde již jsou, bohužel nefungují, přestože se v adresovém řádku posune hodnota o 1
4) nějak nemůžu přijít na to, jak script pozná, kolik chci mít na výpisu článků.
5) globální proměnné mám off
Dotaz je triviální, ale script se mi zdál tak nádherně jednoduchý, že jsem ho chtěl použít, nicméně nerozběhl.
Děkuji za radu,
Petr.

Vigez:

Mna napada pozriet sa v dotaze na hodnotu limit

ikona Jakub Vrána OpenID:

4. Určuje to proměnná $limit, kterou si dříve v kódu musíš nastavit.

Petr:

Děkuji za radu.

Je to $limit = 5;
nebo $_REQUEST["limit"] = 5;?
A před co to mám vložit?

Petr:

Nastavil jsem $limit = 5;
hned po načtení databáze. Script ale vůbec nenačte z databáze to, co má. Cesta do správné tabulky je v pořádku, tady nic nehlásí. Při stránkování správně reaguje na počet stran (při 31 článcích skočí na 1 - 6 stránku a správně reaguje odkazy Předchozí - Další, v obou řazení skáče offset po 5, resp. stránka 1 - 6), na stránce jsou ale pořád všechny články. Kde dělám chybu?
Děkuji, Petr.

Petr:

Omlouvám se, že nedám pokoj, ale mně ten script fakt nefunguje. Na stránce běží, komunikace s DB je, počet článků načítá, ale na stránce jsou stále všechny články. Využil jsem druhou část (skok po stránkách, nikoli offset). $limit jsem nastavil v obou výše uvedených možnostech, ale script vůbec info o limitu počtu článků na stránku nebere. Opravdu nevím, kde mohu dělat chybu?
Děkuji, Petr.

Marty:

Seznam stránek (resp. čísla stránek) většinou nic neřeknou, pokud jde o nějaký výpis jmen řazený podle abecedy. Šlo by udělat nějaký sql dotaz, který by vybíral jen každy x-tý záznam? Pokud by to šlo, tak by se dalo jednoduše zjistit na jaké písmeno začína první jméno na každé stránce a vypsat ho do stránkování. Výsledek by měl být zhruba takovýhle: A1, A2, A3,A4-B1, B2, B3,B4, ...

Martin2007:

Používám na svých stránkách stránkování typu

<< Předchozí - 1 - 2 - 3 - 4 - |5| - 6 - 7 - 8 - 9 - Další >>

ale při 50 stránkách by byl řádek moooc dlouhý a rozházel by mi strukturu
stránky

Můžete mi někdo poradit, jak naprogramovat, aby se zobrazovaly pouze 2 stránky
před a za aktuální stránkou?

např.

<< Předchozí - 3 - 4 - |5| - 6 - 7 - Další >>

určitě proto bude nějáká jednoduchá podmínka nebo vlastně 2

<?php
if ($strana > 2)
echo
$strana;

if (
$strana < 2)
echo
$strana;
?>

něco v tom smyslu...

budu rád za každou rozumnou radu a jestli na to příjdu sám, tak sem dam určitě
vědět, protože jak jsem tak prohledával internet a fóra, tak by to zajímalo
nejen mě...

ikona Jakub Vrána OpenID:

http://phpminadmin.svn.sourceforge.net/viewvc/…=markup#l_176

Martin2007:

díky moc za radu nad zlato

už jsem na to přišel, vše funguje, tak jak má ;))

Adlier:

Vím, že už jsi na to přišel a je to dlouho, ale zkusil jsem si jednoduché stránkování s počtem stran i zkracováním zápisu.Doufám, že se někomu bude hodit a že mě neukamenujete... :D

<?php
//NASTAVENI PROMENNYCH
$stranek= 10;
// $_GET[strana]
//SAMOTNY SCRIPT
If(!$_GET[strana]){ //Pro pripad ze by promenna nebyla definována
$_GET[strana]=1;
}
For (
$i=1;$i<=$stranek;$i++)  //Bude provádět cyklus dokud se nedostane na posledni cislo
{
        If ($i >=($_GET[strana] - 4) AND $i < $_GET[strana] || $i > $_GET[strana] AND  $i <=($_GET[strana] + 4))
        { //Pokud je cislo do ctyr poli pred a za aktualni pozici, vypise jejich hodnoty
          $help_prom=$i; //Promenna ktera podminuje zobrazeni prvniho (č.1) a posledního (v tomto případě 10)  čísla
          echo " $i";
        }
        If ($i == $_GET[strana]){ //Pokud je $i stejná jako naše aktuální pozice, zvýrazníme ji...
        $help_prom=$i;  //Promenna ktera podminuje zobrazeni prvniho (č.1) a posledního (v tomto případě 10)  čísla
        echo " <b>$i</b>";
        }
        If ($i == 1 AND !$help_prom==1){  //Pokud je $i rovno 1 ale nebylo jeste zobrazeno, Pridame za cislo tri tecky
        echo "1...";
        }
        If ($i == $stranek AND $help_prom < $stranek){ //Pokud je $i rovno $stranek(posledni stranka) ale nebylo jeste zobrazeno, Pridame pred cislo tri tecky
        echo "...$stranek";
        }

}
?>

tommm:

zdravim,
-používam na svojich strankach jednoduchu knihu navstev, teda iba nacitanie a zobrazenie prispevkov z mysql databazy.
-chcel by som ju ale rozsirit o strankovanie, trebars po 20 prispevkov na stranku. nevie niekto mi jednoducho poradit/vysvetlit,ako to vyriesit? v php i mysql som zaciatocnik, nepoznam vsetky funkcie..

Maši:

všechny skriptiky jsem zkusila, ale v všech mám problém v tom že když jdu dopředu tak vše běhá jak má, ale jakmile se chci vrátit hodí mi to někam do.... nevíte možný důvod?

0yeb :

mr. vrana, vy jste php borec ! diky

peter:

joj.ja som tento kod hodil do svojho skriptu.strankovanie mi to ukazalo, ale mi to nestrankuje.čo mam zle?
<?php if ($action=='uloz'):
$soubor = "kniha.body";
@
$ext = fopen($soubor, "r");
@
$obsah = fread($ext, filesize($soubor));
@
FClose($ext);
if ((
$vzkaz=="") or ($jmeno=="")):
echo
"<h6>Nejsou vyplněny požadované údaje</h6>";
else:
$ext = fopen("kniha.body", "w");
$mail = HTMLSpecialCHars($mail);
if(
$mail == "")
$mail = "";
else
$mail = "<a href=\"mailto:$mail\">".$mail."</a>";
$jmeno = HTMLSpecialCHars($jmeno);
$vzkaz = HTMLSpecialCHars($vzkaz);
$tab = "<tr><td width=\"300\" >".$jmeno."</td><td align=\"right\">".Date(" d/m/Y H:i:s")."</td></tr><tr><td colspan=\"2\">".$vzkaz."</td></tr><tr><td width=\"150\">".$mail."</td></tr><tr><td colspan=\"2\"><hr size=\"1\"></td></tr>";
fputs($ext, "$tab");
fputs ($ext, "$obsah");
FClose($ext);
@
$ext = fopen("kniha.body", "r");
@
fpassThru($ext);
endif;
else: @
$ext = fopen("kniha.body", "r");
@
fpassThru($ext); endif;
//NASTAVENI PROMENNYCH
$stranek= 10;
// $_GET[strana]
//SAMOTNY SCRIPT
If(!$_GET[strana]){ //Pro pripad ze by promenna nebyla definována
$_GET[strana]=1;
}
For (
$i=1;$i<=$stranek;$i++) //Bude provádět cyklus dokud se nedostane na posledni cislo
{
If (
$i >=($_GET[strana] - 4) AND $i < $_GET[strana] || $i > $_GET[strana] AND $i <=($_GET[strana] + 4))
{
//Pokud je cislo do ctyr poli pred a za aktualni pozici, vypise jejich hodnoty
$help_prom=$i; //Promenna ktera podminuje zobrazeni prvniho (č.1) a posledního (v tomto případě 10) čísla
echo " $i";
}
If (
$i == $_GET[strana]){ //Pokud je $i stejná jako naše aktuální pozice, zvýrazníme ji...
$help_prom=$i; //Promenna ktera podminuje zobrazeni prvniho (č.1) a posledního (v tomto případě 10) čísla
echo " <b>$i</b>";
}
If (
$i == 1 AND !$help_prom==1){ //Pokud je $i rovno 1 ale nebylo jeste zobrazeno, Pridame za cislo tri tecky
echo "1...";
}
If (
$i == $stranek AND $help_prom < $stranek){ //Pokud je $i rovno $stranek(posledni stranka) ale nebylo jeste zobrazeno, Pridame pred cislo tri tecky
echo "...$stranek";
}
}
?>

bla bla bla:

to strankovanie bolo riesene pre data, ktore "prichadzaju" z databaze a nie pre data zo suboru...tam treba pouzit iny algoritmus

vasava:

Stránkovanie takmer totožné s google:

<?php
function paging($count, $items, $src)
{
    $min = floor($_GET['from'] / $items);

    if ($min + 10 <= $count) $max = $min + 9;
    else $max = $count;
    if ($min <= 10) $min = 0;
    else $min -= 10;

    if ($_GET['from'])
    echo "<a href='",$src,"&amp;from=0'>«</a>",
    "<a href='",$src,"&amp;from=",$_GET['from'] - $items,"'>predchádzajúca</a>";

    for ($i = $min; $i <= $max; ++$i)
    {
        echo "<a href='",$src,"&amp;from=",$i * $items,"'";
        if ($i == $_GET['from'] / $items) echo " class='selected'";
        echo ">",$i + 1,"</a>";
    }

    if ($_GET['from'] != $count * $items)
    echo "<a href='",$src,"&amp;from=",$_GET['from'] + $items,"'>ďalšia</a>",
    "<a href='",$src,"&amp;from=",$count * $items,"'>»</a>";
}

// e.g.:
echo "<p class='paging'>",paging(32,8,'?section=comments'),"</p>";
?>

ikona Jirka:

Díky za tip na stránkování, nejsem si ale jist, zda funguje při položkách >1000 - zde začne zlobit maximální stránka...

ciprys:

jsem začáteční, ale místo:
echo "<a href='$_SERVER[PHP_SELF]"
by mělo být:
echo "<a href='$_SERVER[PHP_SELF]?"

ikona Jakub Vrána OpenID:

To ne, otazník se doplní v ternárním operátoru. Ale chybělo tam escapování.

Ondra:

<?php
if ($pocet > $limit * ($_GET["strana"] + 1)) {
    echo ' <a href="' . htmlspecialchars($_SERVER["PHP_SELF"]) . "?strana=" . ($_GET["strana"] + 1) . '">vpřed</a>';
}
?>

Nevim, jestli to nekdo jeste po takove dobe resi, ale v podmince u druheho prikladu je zrejme mensi chybka - $pocet > $limit * ($_GET["strana"] + 1) by melo byt vetsi nebo rovno

$pocet >= $limit * ($_GET["strana"] + 1) protoze pokud by se nasobek limitu a stranky rovnal poctu zaznamu v DB (napr. 30 = 15 * 2) tak to nehodi odkaz na stranu 2 a uzivatel tak lze uvidi jen prvni stranku bez moznosti skocit na druhych (poslednich) 15 zaznamu ;)

ikona Jakub Vrána OpenID:

Nikoliv. Číslo strany začíná od nuly.

Dominik:

Zdravím, co takové stránkování, kde obsah nejen přidávám, ale i mažu/obměňuji.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.