[{"data":1,"prerenderedAt":2024},["ShallowReactive",2],{"page-\u002Fsql\u002F03-sorting-and-pagination":3},{"id":4,"title":5,"body":6,"description":43,"extension":2018,"meta":2019,"navigation":315,"path":2020,"seo":2021,"stem":2022,"__hash__":2023},"content\u002Fsql\u002F03-sorting-and-pagination.md","03 — Sorting, Pagination & LIMIT",{"type":7,"value":8,"toc":1992},"minimark",[9,13,18,34,191,196,232,238,242,419,423,426,510,514,589,679,683,809,828,835,869,876,930,934,938,948,958,962,968,1177,1192,1198,1211,1215,1226,1230,1446,1450,1552,1556,1611,1615,1770,1774,1777,1856,1971,1974,1988],[10,11,5],"h1",{"id":12},"_03-sorting-pagination-limit",[14,15,17],"h2",{"id":16},"order-by","ORDER BY",[19,20,21,24,25,29,30,33],"p",{},[22,23,17],"code",{}," sorts the final result set. It runs ",[26,27,28],"em",{},"after"," ",[22,31,32],{},"SELECT"," (so column aliases are visible here).",[35,36,38],"code-wrapper",{"language":37},"sql",[39,40,44],"pre",{"className":41,"code":42,"language":37,"meta":43,"style":43},"language-sql shiki shiki-themes github-light github-dark","SELECT name, city FROM customers ORDER BY name;               -- ascending (default)\nSELECT name, city FROM customers ORDER BY name ASC;           -- explicit ascending\nSELECT name, city FROM customers ORDER BY name DESC;          -- descending\nSELECT name, city FROM customers ORDER BY city, name;         -- sort by city, then name within city\nSELECT name, city FROM customers ORDER BY city DESC, name ASC; -- mixed directions per column\n","",[22,45,46,78,104,130,157],{"__ignoreMap":43},[47,48,51,54,57,61,64,67,69,71,74],"span",{"class":49,"line":50},"line",1,[47,52,32],{"class":53},"svdQ7",[47,55,56],{"class":53}," name",[47,58,60],{"class":59},"ssxIu",", city ",[47,62,63],{"class":53},"FROM",[47,65,66],{"class":59}," customers ",[47,68,17],{"class":53},[47,70,56],{"class":53},[47,72,73],{"class":59},";               ",[47,75,77],{"class":76},"sdCPZ","-- ascending (default)\n",[47,79,81,83,85,87,89,91,93,95,98,101],{"class":49,"line":80},2,[47,82,32],{"class":53},[47,84,56],{"class":53},[47,86,60],{"class":59},[47,88,63],{"class":53},[47,90,66],{"class":59},[47,92,17],{"class":53},[47,94,56],{"class":53},[47,96,97],{"class":53}," ASC",[47,99,100],{"class":59},";           ",[47,102,103],{"class":76},"-- explicit ascending\n",[47,105,107,109,111,113,115,117,119,121,124,127],{"class":49,"line":106},3,[47,108,32],{"class":53},[47,110,56],{"class":53},[47,112,60],{"class":59},[47,114,63],{"class":53},[47,116,66],{"class":59},[47,118,17],{"class":53},[47,120,56],{"class":53},[47,122,123],{"class":53}," DESC",[47,125,126],{"class":59},";          ",[47,128,129],{"class":76},"-- descending\n",[47,131,133,135,137,139,141,143,145,148,151,154],{"class":49,"line":132},4,[47,134,32],{"class":53},[47,136,56],{"class":53},[47,138,60],{"class":59},[47,140,63],{"class":53},[47,142,66],{"class":59},[47,144,17],{"class":53},[47,146,147],{"class":59}," city, ",[47,149,150],{"class":53},"name",[47,152,153],{"class":59},";         ",[47,155,156],{"class":76},"-- sort by city, then name within city\n",[47,158,160,162,164,166,168,170,172,175,178,181,183,185,188],{"class":49,"line":159},5,[47,161,32],{"class":53},[47,163,56],{"class":53},[47,165,60],{"class":59},[47,167,63],{"class":53},[47,169,66],{"class":59},[47,171,17],{"class":53},[47,173,174],{"class":59}," city ",[47,176,177],{"class":53},"DESC",[47,179,180],{"class":59},", ",[47,182,150],{"class":53},[47,184,97],{"class":53},[47,186,187],{"class":59},"; ",[47,189,190],{"class":76},"-- mixed directions per column\n",[192,193,195],"h3",{"id":194},"sorting-by-column-position-fragile-avoid","Sorting by Column Position (fragile, avoid)",[35,197,198],{"language":37},[39,199,201],{"className":41,"code":200,"language":37,"meta":43,"style":43},"-- ORDER BY 2 means \"the second column in the SELECT list\"\nSELECT name, city FROM customers ORDER BY 2;    -- fragile: reordering SELECT columns silently changes the sort\n",[22,202,203,208],{"__ignoreMap":43},[47,204,205],{"class":49,"line":50},[47,206,207],{"class":76},"-- ORDER BY 2 means \"the second column in the SELECT list\"\n",[47,209,210,212,214,216,218,220,222,226,229],{"class":49,"line":80},[47,211,32],{"class":53},[47,213,56],{"class":53},[47,215,60],{"class":59},[47,217,63],{"class":53},[47,219,66],{"class":59},[47,221,17],{"class":53},[47,223,225],{"class":224},"snvgF"," 2",[47,227,228],{"class":59},";    ",[47,230,231],{"class":76},"-- fragile: reordering SELECT columns silently changes the sort\n",[19,233,234,235,237],{},"Positional ",[22,236,17],{}," is legal SQL but discouraged — it's fragile and harder to read. Always use column names or aliases.",[192,239,241],{"id":240},"sorting-by-expression","Sorting by Expression",[35,243,244],{"language":37},[39,245,247],{"className":41,"code":246,"language":37,"meta":43,"style":43},"-- Sort by a computed value — alias is visible in ORDER BY (runs after SELECT)\nSELECT name, length(name) AS name_len\nFROM customers\nORDER BY length(name) DESC, name;              -- sort by name length desc, then name asc as tiebreaker\n\n-- Sort by a CASE expression — custom ordering not derivable from column values\nSELECT name, city FROM customers\nORDER BY\n  CASE city\n    WHEN 'NYC' THEN 1                           -- NYC first\n    WHEN 'LA'  THEN 2                           -- then LA\n    ELSE 3                                      -- then everything else\n  END,\n  name;                                         -- alphabetical within each city tier\n",[22,248,249,254,279,286,311,317,323,336,342,351,370,386,398,407],{"__ignoreMap":43},[47,250,251],{"class":49,"line":50},[47,252,253],{"class":76},"-- Sort by a computed value — alias is visible in ORDER BY (runs after SELECT)\n",[47,255,256,258,260,262,265,268,270,273,276],{"class":49,"line":80},[47,257,32],{"class":53},[47,259,56],{"class":53},[47,261,180],{"class":59},[47,263,264],{"class":53},"length",[47,266,267],{"class":59},"(",[47,269,150],{"class":53},[47,271,272],{"class":59},") ",[47,274,275],{"class":53},"AS",[47,277,278],{"class":59}," name_len\n",[47,280,281,283],{"class":49,"line":106},[47,282,63],{"class":53},[47,284,285],{"class":59}," customers\n",[47,287,288,290,293,295,297,299,301,303,305,308],{"class":49,"line":132},[47,289,17],{"class":53},[47,291,292],{"class":53}," length",[47,294,267],{"class":59},[47,296,150],{"class":53},[47,298,272],{"class":59},[47,300,177],{"class":53},[47,302,180],{"class":59},[47,304,150],{"class":53},[47,306,307],{"class":59},";              ",[47,309,310],{"class":76},"-- sort by name length desc, then name asc as tiebreaker\n",[47,312,313],{"class":49,"line":159},[47,314,316],{"emptyLinePlaceholder":315},true,"\n",[47,318,320],{"class":49,"line":319},6,[47,321,322],{"class":76},"-- Sort by a CASE expression — custom ordering not derivable from column values\n",[47,324,326,328,330,332,334],{"class":49,"line":325},7,[47,327,32],{"class":53},[47,329,56],{"class":53},[47,331,60],{"class":59},[47,333,63],{"class":53},[47,335,285],{"class":59},[47,337,339],{"class":49,"line":338},8,[47,340,341],{"class":53},"ORDER BY\n",[47,343,345,348],{"class":49,"line":344},9,[47,346,347],{"class":53},"  CASE",[47,349,350],{"class":59}," city\n",[47,352,354,357,361,364,367],{"class":49,"line":353},10,[47,355,356],{"class":53},"    WHEN",[47,358,360],{"class":359},"sJ6F3"," 'NYC'",[47,362,363],{"class":53}," THEN",[47,365,366],{"class":224}," 1",[47,368,369],{"class":76},"                           -- NYC first\n",[47,371,373,375,378,381,383],{"class":49,"line":372},11,[47,374,356],{"class":53},[47,376,377],{"class":359}," 'LA'",[47,379,380],{"class":53},"  THEN",[47,382,225],{"class":224},[47,384,385],{"class":76},"                           -- then LA\n",[47,387,389,392,395],{"class":49,"line":388},12,[47,390,391],{"class":53},"    ELSE",[47,393,394],{"class":224}," 3",[47,396,397],{"class":76},"                                      -- then everything else\n",[47,399,401,404],{"class":49,"line":400},13,[47,402,403],{"class":53},"  END",[47,405,406],{"class":59},",\n",[47,408,410,413,416],{"class":49,"line":409},14,[47,411,412],{"class":53},"  name",[47,414,415],{"class":59},";                                         ",[47,417,418],{"class":76},"-- alphabetical within each city tier\n",[14,420,422],{"id":421},"nulls-in-order-by","NULLs in ORDER BY",[19,424,425],{},"NULLs sort either first or last depending on the engine and direction. The SQL standard leaves this implementation-defined — a portability hazard.",[427,428,429,449],"table",{},[430,431,432],"thead",{},[433,434,435,439,445],"tr",{},[436,437,438],"th",{},"Engine",[436,440,441,444],{},[22,442,443],{},"ASC"," default",[436,446,447,444],{},[22,448,177],{},[450,451,452,470,483,496],"tbody",{},[433,453,454,458,465],{},[455,456,457],"td",{},"PostgreSQL",[455,459,460,461],{},"NULLs ",[462,463,464],"strong",{},"last",[455,466,460,467],{},[462,468,469],{},"first",[433,471,472,475,479],{},[455,473,474],{},"MySQL \u002F SQLite",[455,476,460,477],{},[462,478,469],{},[455,480,460,481],{},[462,482,464],{},[433,484,485,488,492],{},[455,486,487],{},"Oracle",[455,489,460,490],{},[462,491,464],{},[455,493,460,494],{},[462,495,469],{},[433,497,498,501,506],{},[455,499,500],{},"SQL Server",[455,502,460,503,505],{},[462,504,469],{}," (smallest)",[455,507,460,508],{},[462,509,464],{},[192,511,513],{"id":512},"forcing-null-placement","Forcing NULL Placement",[35,515,516],{"language":37},[39,517,519],{"className":41,"code":518,"language":37,"meta":43,"style":43},"-- PostgreSQL: explicit NULLS FIRST \u002F NULLS LAST\nSELECT id, ordered_on FROM orders\nORDER BY ordered_on DESC NULLS LAST;           -- NULLs always last, regardless of ASC\u002FDESC\n\nSELECT id, ordered_on FROM orders\nORDER BY ordered_on NULLS FIRST;               -- NULLs always first\n",[22,520,521,526,538,558,562,572],{"__ignoreMap":43},[47,522,523],{"class":49,"line":50},[47,524,525],{"class":76},"-- PostgreSQL: explicit NULLS FIRST \u002F NULLS LAST\n",[47,527,528,530,533,535],{"class":49,"line":80},[47,529,32],{"class":53},[47,531,532],{"class":59}," id, ordered_on ",[47,534,63],{"class":53},[47,536,537],{"class":59}," orders\n",[47,539,540,542,545,547,550,553,555],{"class":49,"line":106},[47,541,17],{"class":53},[47,543,544],{"class":59}," ordered_on ",[47,546,177],{"class":53},[47,548,549],{"class":53}," NULLS",[47,551,552],{"class":53}," LAST",[47,554,100],{"class":59},[47,556,557],{"class":76},"-- NULLs always last, regardless of ASC\u002FDESC\n",[47,559,560],{"class":49,"line":132},[47,561,316],{"emptyLinePlaceholder":315},[47,563,564,566,568,570],{"class":49,"line":159},[47,565,32],{"class":53},[47,567,532],{"class":59},[47,569,63],{"class":53},[47,571,537],{"class":59},[47,573,574,576,578,581,584,586],{"class":49,"line":319},[47,575,17],{"class":53},[47,577,544],{"class":59},[47,579,580],{"class":53},"NULLS",[47,582,583],{"class":53}," FIRST",[47,585,73],{"class":59},[47,587,588],{"class":76},"-- NULLs always first\n",[35,590,591],{"language":37},[39,592,594],{"className":41,"code":593,"language":37,"meta":43,"style":43},"-- Portable: ORDER BY (col IS NULL) trick — works on all engines\n-- (col IS NULL) returns FALSE (0) for non-NULLs, TRUE (1) for NULLs\n-- FALSE sorts before TRUE, so non-NULLs come first, NULLs last — in ascending order\nSELECT id, ordered_on FROM orders\nORDER BY (ordered_on IS NULL), ordered_on;     -- NULLs last in ASC, portable across engines\n\n-- For NULLs last in DESC, adjust:\nSELECT id, ordered_on FROM orders\nORDER BY (ordered_on IS NULL), ordered_on DESC; -- NULLs still last (boolean sort first), then DESC within non-NULLs\n",[22,595,596,601,606,611,621,640,644,649,659],{"__ignoreMap":43},[47,597,598],{"class":49,"line":50},[47,599,600],{"class":76},"-- Portable: ORDER BY (col IS NULL) trick — works on all engines\n",[47,602,603],{"class":49,"line":80},[47,604,605],{"class":76},"-- (col IS NULL) returns FALSE (0) for non-NULLs, TRUE (1) for NULLs\n",[47,607,608],{"class":49,"line":106},[47,609,610],{"class":76},"-- FALSE sorts before TRUE, so non-NULLs come first, NULLs last — in ascending order\n",[47,612,613,615,617,619],{"class":49,"line":132},[47,614,32],{"class":53},[47,616,532],{"class":59},[47,618,63],{"class":53},[47,620,537],{"class":59},[47,622,623,625,628,631,634,637],{"class":49,"line":159},[47,624,17],{"class":53},[47,626,627],{"class":59}," (ordered_on ",[47,629,630],{"class":53},"IS",[47,632,633],{"class":53}," NULL",[47,635,636],{"class":59},"), ordered_on;     ",[47,638,639],{"class":76},"-- NULLs last in ASC, portable across engines\n",[47,641,642],{"class":49,"line":319},[47,643,316],{"emptyLinePlaceholder":315},[47,645,646],{"class":49,"line":325},[47,647,648],{"class":76},"-- For NULLs last in DESC, adjust:\n",[47,650,651,653,655,657],{"class":49,"line":338},[47,652,32],{"class":53},[47,654,532],{"class":59},[47,656,63],{"class":53},[47,658,537],{"class":59},[47,660,661,663,665,667,669,672,674,676],{"class":49,"line":344},[47,662,17],{"class":53},[47,664,627],{"class":59},[47,666,630],{"class":53},[47,668,633],{"class":53},[47,670,671],{"class":59},"), ordered_on ",[47,673,177],{"class":53},[47,675,187],{"class":59},[47,677,678],{"class":76},"-- NULLs still last (boolean sort first), then DESC within non-NULLs\n",[14,680,682],{"id":681},"limit-and-offset","LIMIT and OFFSET",[35,684,685],{"language":37},[39,686,688],{"className":41,"code":687,"language":37,"meta":43,"style":43},"-- First 10 rows (must pair with ORDER BY for deterministic results)\nSELECT * FROM orders ORDER BY ordered_on DESC LIMIT 10;\n\n-- Skip 20, take 10 (page 3 of a 10-per-page listing)\nSELECT * FROM orders ORDER BY ordered_on DESC LIMIT 10 OFFSET 20;\n\n-- FETCH syntax (ANSI standard, equivalent to LIMIT\u002FOFFSET)\nSELECT * FROM orders ORDER BY ordered_on DESC\nOFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;\n",[22,689,690,695,723,727,732,760,764,769,786],{"__ignoreMap":43},[47,691,692],{"class":49,"line":50},[47,693,694],{"class":76},"-- First 10 rows (must pair with ORDER BY for deterministic results)\n",[47,696,697,699,702,705,708,710,712,714,717,720],{"class":49,"line":80},[47,698,32],{"class":53},[47,700,701],{"class":53}," *",[47,703,704],{"class":53}," FROM",[47,706,707],{"class":59}," orders ",[47,709,17],{"class":53},[47,711,544],{"class":59},[47,713,177],{"class":53},[47,715,716],{"class":53}," LIMIT",[47,718,719],{"class":224}," 10",[47,721,722],{"class":59},";\n",[47,724,725],{"class":49,"line":106},[47,726,316],{"emptyLinePlaceholder":315},[47,728,729],{"class":49,"line":132},[47,730,731],{"class":76},"-- Skip 20, take 10 (page 3 of a 10-per-page listing)\n",[47,733,734,736,738,740,742,744,746,748,750,752,755,758],{"class":49,"line":159},[47,735,32],{"class":53},[47,737,701],{"class":53},[47,739,704],{"class":53},[47,741,707],{"class":59},[47,743,17],{"class":53},[47,745,544],{"class":59},[47,747,177],{"class":53},[47,749,716],{"class":53},[47,751,719],{"class":224},[47,753,754],{"class":59}," OFFSET ",[47,756,757],{"class":224},"20",[47,759,722],{"class":59},[47,761,762],{"class":49,"line":319},[47,763,316],{"emptyLinePlaceholder":315},[47,765,766],{"class":49,"line":325},[47,767,768],{"class":76},"-- FETCH syntax (ANSI standard, equivalent to LIMIT\u002FOFFSET)\n",[47,770,771,773,775,777,779,781,783],{"class":49,"line":338},[47,772,32],{"class":53},[47,774,701],{"class":53},[47,776,704],{"class":53},[47,778,707],{"class":59},[47,780,17],{"class":53},[47,782,544],{"class":59},[47,784,785],{"class":53},"DESC\n",[47,787,788,791,793,796,799,802,804,806],{"class":49,"line":344},[47,789,790],{"class":59},"OFFSET ",[47,792,757],{"class":224},[47,794,795],{"class":53}," ROWS",[47,797,798],{"class":53}," FETCH",[47,800,801],{"class":53}," NEXT",[47,803,719],{"class":224},[47,805,795],{"class":53},[47,807,808],{"class":59}," ONLY;\n",[19,810,811,814,815,817,818,821,822,824,825,827],{},[22,812,813],{},"LIMIT"," without ",[22,816,17],{}," returns an ",[462,819,820],{},"arbitrary"," set of rows — the planner is free to return any rows in any order. Always pair ",[22,823,813],{}," with an ",[22,826,17],{}," on a unique column (or a tie-breaker) for deterministic results.",[192,829,831,834],{"id":830},"limit-0-a-schema-probe",[22,832,833],{},"LIMIT 0"," — a schema probe",[35,836,837],{"language":37},[39,838,840],{"className":41,"code":839,"language":37,"meta":43,"style":43},"-- Get column names\u002Ftypes without fetching any data — useful for introspection\n-- The planner short-circuits: zero rows fetched, only metadata returned\nSELECT * FROM orders LIMIT 0;\n",[22,841,842,847,852],{"__ignoreMap":43},[47,843,844],{"class":49,"line":50},[47,845,846],{"class":76},"-- Get column names\u002Ftypes without fetching any data — useful for introspection\n",[47,848,849],{"class":49,"line":80},[47,850,851],{"class":76},"-- The planner short-circuits: zero rows fetched, only metadata returned\n",[47,853,854,856,858,860,862,864,867],{"class":49,"line":106},[47,855,32],{"class":53},[47,857,701],{"class":53},[47,859,704],{"class":53},[47,861,707],{"class":59},[47,863,813],{"class":53},[47,865,866],{"class":224}," 0",[47,868,722],{"class":59},[192,870,872,875],{"id":871},"fetch-with-ties-postgresql-13-sql-server",[22,873,874],{},"FETCH ... WITH TIES"," (PostgreSQL 13+, SQL Server)",[35,877,878],{"language":37},[39,879,881],{"className":41,"code":880,"language":37,"meta":43,"style":43},"-- Top 3 amounts, but include any other orders with the same amount as the 3rd row.\n-- WITH TIES can return MORE than 3 rows if there are ties at the boundary.\nSELECT id, amount FROM orders\nORDER BY amount DESC\nFETCH FIRST 3 ROWS WITH TIES;\n",[22,882,883,888,893,904,913],{"__ignoreMap":43},[47,884,885],{"class":49,"line":50},[47,886,887],{"class":76},"-- Top 3 amounts, but include any other orders with the same amount as the 3rd row.\n",[47,889,890],{"class":49,"line":80},[47,891,892],{"class":76},"-- WITH TIES can return MORE than 3 rows if there are ties at the boundary.\n",[47,894,895,897,900,902],{"class":49,"line":106},[47,896,32],{"class":53},[47,898,899],{"class":59}," id, amount ",[47,901,63],{"class":53},[47,903,537],{"class":59},[47,905,906,908,911],{"class":49,"line":132},[47,907,17],{"class":53},[47,909,910],{"class":59}," amount ",[47,912,785],{"class":53},[47,914,915,918,920,922,924,927],{"class":49,"line":159},[47,916,917],{"class":53},"FETCH",[47,919,583],{"class":53},[47,921,394],{"class":224},[47,923,795],{"class":53},[47,925,926],{"class":53}," WITH",[47,928,929],{"class":59}," TIES;\n",[14,931,933],{"id":932},"pagination-patterns","Pagination Patterns",[192,935,937],{"id":936},"offset-pagination-simple-but-slow-at-scale","OFFSET Pagination (simple but slow at scale)",[35,939,941],{"language":940},"text",[39,942,946],{"className":943,"code":945,"language":940,"meta":43},[944],"language-text","Page 1: LIMIT 10 OFFSET 0\nPage 2: LIMIT 10 OFFSET 10\nPage 3: LIMIT 10 OFFSET 20\n...\nPage N: LIMIT 10 OFFSET (N-1)*10\n",[22,947,945],{"__ignoreMap":43},[19,949,950,953,954,957],{},[462,951,952],{},"The O(N) problem",": ",[22,955,956],{},"OFFSET 100000"," still scans and discards 100000 rows before returning 10. On a large table, deep pages become progressively slower — O(offset + page_size) per page. Page 10000 is as slow as scanning 100000 rows.",[192,959,961],{"id":960},"keyset-seek-pagination-the-fast-alternative","Keyset (Seek) Pagination — the fast alternative",[19,963,964,965,967],{},"Instead of skipping rows, remember the last row's sort value and ask for rows ",[26,966,28],{}," it:",[35,969,970],{"language":37},[39,971,973],{"className":41,"code":972,"language":37,"meta":43,"style":43},"-- Page 1: fetch the first 10 rows\nSELECT id, name, ordered_on\nFROM orders\nORDER BY ordered_on DESC, id DESC   -- id as unique tie-breaker (critical!)\nLIMIT 10;\n\n-- Suppose the last row was (id=42, ordered_on='2024-03-01').\n-- Page 2: fetch rows strictly \"before\" that row in the sort order.\n-- The row-value comparator (ordered_on, id) \u003C (date, id) is lexicographic:\n-- compare ordered_on first; if equal, compare id.\nSELECT id, name, ordered_on\nFROM orders\nWHERE (ordered_on, id) \u003C ('2024-03-01', 42)   -- row-value comparison: lexicographic \"before\"\nORDER BY ordered_on DESC, id DESC\nLIMIT 10;\n\n-- Page N: use the last row from page N-1 as the cursor — O(1) seek regardless of depth\nSELECT id, name, ordered_on\nFROM orders\nWHERE (ordered_on, id) \u003C ($last_ordered_on, $last_id)   -- parameterized cursor\nORDER BY ordered_on DESC, id DESC\nLIMIT 10;\n",[22,974,975,980,992,998,1014,1022,1026,1031,1036,1041,1046,1056,1062,1090,1102,1111,1116,1122,1133,1140,1155,1168],{"__ignoreMap":43},[47,976,977],{"class":49,"line":50},[47,978,979],{"class":76},"-- Page 1: fetch the first 10 rows\n",[47,981,982,984,987,989],{"class":49,"line":80},[47,983,32],{"class":53},[47,985,986],{"class":59}," id, ",[47,988,150],{"class":53},[47,990,991],{"class":59},", ordered_on\n",[47,993,994,996],{"class":49,"line":106},[47,995,63],{"class":53},[47,997,537],{"class":59},[47,999,1000,1002,1004,1006,1009,1011],{"class":49,"line":132},[47,1001,17],{"class":53},[47,1003,544],{"class":59},[47,1005,177],{"class":53},[47,1007,1008],{"class":59},", id ",[47,1010,177],{"class":53},[47,1012,1013],{"class":76},"   -- id as unique tie-breaker (critical!)\n",[47,1015,1016,1018,1020],{"class":49,"line":159},[47,1017,813],{"class":53},[47,1019,719],{"class":224},[47,1021,722],{"class":59},[47,1023,1024],{"class":49,"line":319},[47,1025,316],{"emptyLinePlaceholder":315},[47,1027,1028],{"class":49,"line":325},[47,1029,1030],{"class":76},"-- Suppose the last row was (id=42, ordered_on='2024-03-01').\n",[47,1032,1033],{"class":49,"line":338},[47,1034,1035],{"class":76},"-- Page 2: fetch rows strictly \"before\" that row in the sort order.\n",[47,1037,1038],{"class":49,"line":344},[47,1039,1040],{"class":76},"-- The row-value comparator (ordered_on, id) \u003C (date, id) is lexicographic:\n",[47,1042,1043],{"class":49,"line":353},[47,1044,1045],{"class":76},"-- compare ordered_on first; if equal, compare id.\n",[47,1047,1048,1050,1052,1054],{"class":49,"line":372},[47,1049,32],{"class":53},[47,1051,986],{"class":59},[47,1053,150],{"class":53},[47,1055,991],{"class":59},[47,1057,1058,1060],{"class":49,"line":388},[47,1059,63],{"class":53},[47,1061,537],{"class":59},[47,1063,1064,1067,1070,1073,1076,1079,1081,1084,1087],{"class":49,"line":400},[47,1065,1066],{"class":53},"WHERE",[47,1068,1069],{"class":59}," (ordered_on, id) ",[47,1071,1072],{"class":53},"\u003C",[47,1074,1075],{"class":59}," (",[47,1077,1078],{"class":359},"'2024-03-01'",[47,1080,180],{"class":59},[47,1082,1083],{"class":224},"42",[47,1085,1086],{"class":59},")   ",[47,1088,1089],{"class":76},"-- row-value comparison: lexicographic \"before\"\n",[47,1091,1092,1094,1096,1098,1100],{"class":49,"line":409},[47,1093,17],{"class":53},[47,1095,544],{"class":59},[47,1097,177],{"class":53},[47,1099,1008],{"class":59},[47,1101,785],{"class":53},[47,1103,1105,1107,1109],{"class":49,"line":1104},15,[47,1106,813],{"class":53},[47,1108,719],{"class":224},[47,1110,722],{"class":59},[47,1112,1114],{"class":49,"line":1113},16,[47,1115,316],{"emptyLinePlaceholder":315},[47,1117,1119],{"class":49,"line":1118},17,[47,1120,1121],{"class":76},"-- Page N: use the last row from page N-1 as the cursor — O(1) seek regardless of depth\n",[47,1123,1125,1127,1129,1131],{"class":49,"line":1124},18,[47,1126,32],{"class":53},[47,1128,986],{"class":59},[47,1130,150],{"class":53},[47,1132,991],{"class":59},[47,1134,1136,1138],{"class":49,"line":1135},19,[47,1137,63],{"class":53},[47,1139,537],{"class":59},[47,1141,1143,1145,1147,1149,1152],{"class":49,"line":1142},20,[47,1144,1066],{"class":53},[47,1146,1069],{"class":59},[47,1148,1072],{"class":53},[47,1150,1151],{"class":59}," ($last_ordered_on, $last_id)   ",[47,1153,1154],{"class":76},"-- parameterized cursor\n",[47,1156,1158,1160,1162,1164,1166],{"class":49,"line":1157},21,[47,1159,17],{"class":53},[47,1161,544],{"class":59},[47,1163,177],{"class":53},[47,1165,1008],{"class":59},[47,1167,785],{"class":53},[47,1169,1171,1173,1175],{"class":49,"line":1170},22,[47,1172,813],{"class":53},[47,1174,719],{"class":224},[47,1176,722],{"class":59},[19,1178,1179,1180,1183,1184,1187,1188,1191],{},"The row-value comparison ",[22,1181,1182],{},"(ordered_on, id) \u003C (date, id)"," is a clean way to express \"lexicographically before.\" It uses an index on ",[22,1185,1186],{},"(ordered_on DESC, id DESC)"," and is ",[462,1189,1190],{},"O(log N) to seek + O(page_size) to fetch"," regardless of depth — page 10000 is as fast as page 1.",[19,1193,1194,1197],{},[462,1195,1196],{},"Keyset pagination requirements",":",[1199,1200,1201,1205,1208],"ul",{},[1202,1203,1204],"li",{},"A stable, unique sort key (or a composite key with a unique tie-breaker).",[1202,1206,1207],{},"An index covering the sort key(s).",[1202,1209,1210],{},"The client must remember the last row's key, not just a page number.",[192,1212,1214],{"id":1213},"when-offset-is-fine","When OFFSET is fine",[1199,1216,1217,1220,1223],{},[1202,1218,1219],{},"Small result sets (a few thousand rows).",[1202,1221,1222],{},"Admin\u002Finternal tools where performance doesn't matter.",[1202,1224,1225],{},"Random-access pages (\"jump to page 50\") where keyset doesn't apply (keyset only goes forward\u002Fbackward from a known position).",[14,1227,1229],{"id":1228},"complex-implementation-production-keyset-pagination","Complex Implementation: Production Keyset Pagination",[35,1231,1232],{"language":37},[39,1233,1235],{"className":41,"code":1234,"language":37,"meta":43,"style":43},"-- A production-grade keyset pagination query with a composite cursor.\n-- Schema: orders(id, customer_id, status, placed_at, total)\n-- Index: CREATE INDEX idx_orders_placed_id ON orders (placed_at DESC, id DESC);\n\n-- Page 1 — no cursor, just take the first page\nSELECT id, customer_id, status, placed_at, total\nFROM orders\nORDER BY placed_at DESC, id DESC\nLIMIT 20;\n\n-- Page N — cursor from the previous page's last row\n-- $cursor_placed_at and $cursor_id are the last row's values from page N-1\nSELECT id, customer_id, status, placed_at, total\nFROM orders\nWHERE (placed_at, id) \u003C ($cursor_placed_at, $cursor_id)   -- lexicographic \"strictly before\"\nORDER BY placed_at DESC, id DESC\nLIMIT 20;\n\n-- With a WHERE filter on status — the filter applies before the cursor comparison\nSELECT id, customer_id, status, placed_at, total\nFROM orders\nWHERE status = 'shipped'\n  AND (placed_at, id) \u003C ($cursor_placed_at, $cursor_id)\nORDER BY placed_at DESC, id DESC\nLIMIT 20;\n-- Requires a partial index: CREATE INDEX idx_orders_shipped_placed ON orders (placed_at DESC, id DESC) WHERE status = 'shipped';\n",[22,1236,1237,1242,1247,1252,1256,1261,1274,1280,1293,1302,1306,1311,1316,1326,1332,1347,1359,1367,1371,1376,1386,1392,1405,1418,1431,1440],{"__ignoreMap":43},[47,1238,1239],{"class":49,"line":50},[47,1240,1241],{"class":76},"-- A production-grade keyset pagination query with a composite cursor.\n",[47,1243,1244],{"class":49,"line":80},[47,1245,1246],{"class":76},"-- Schema: orders(id, customer_id, status, placed_at, total)\n",[47,1248,1249],{"class":49,"line":106},[47,1250,1251],{"class":76},"-- Index: CREATE INDEX idx_orders_placed_id ON orders (placed_at DESC, id DESC);\n",[47,1253,1254],{"class":49,"line":132},[47,1255,316],{"emptyLinePlaceholder":315},[47,1257,1258],{"class":49,"line":159},[47,1259,1260],{"class":76},"-- Page 1 — no cursor, just take the first page\n",[47,1262,1263,1265,1268,1271],{"class":49,"line":319},[47,1264,32],{"class":53},[47,1266,1267],{"class":59}," id, customer_id, ",[47,1269,1270],{"class":53},"status",[47,1272,1273],{"class":59},", placed_at, total\n",[47,1275,1276,1278],{"class":49,"line":325},[47,1277,63],{"class":53},[47,1279,537],{"class":59},[47,1281,1282,1284,1287,1289,1291],{"class":49,"line":338},[47,1283,17],{"class":53},[47,1285,1286],{"class":59}," placed_at ",[47,1288,177],{"class":53},[47,1290,1008],{"class":59},[47,1292,785],{"class":53},[47,1294,1295,1297,1300],{"class":49,"line":344},[47,1296,813],{"class":53},[47,1298,1299],{"class":224}," 20",[47,1301,722],{"class":59},[47,1303,1304],{"class":49,"line":353},[47,1305,316],{"emptyLinePlaceholder":315},[47,1307,1308],{"class":49,"line":372},[47,1309,1310],{"class":76},"-- Page N — cursor from the previous page's last row\n",[47,1312,1313],{"class":49,"line":388},[47,1314,1315],{"class":76},"-- $cursor_placed_at and $cursor_id are the last row's values from page N-1\n",[47,1317,1318,1320,1322,1324],{"class":49,"line":400},[47,1319,32],{"class":53},[47,1321,1267],{"class":59},[47,1323,1270],{"class":53},[47,1325,1273],{"class":59},[47,1327,1328,1330],{"class":49,"line":409},[47,1329,63],{"class":53},[47,1331,537],{"class":59},[47,1333,1334,1336,1339,1341,1344],{"class":49,"line":1104},[47,1335,1066],{"class":53},[47,1337,1338],{"class":59}," (placed_at, id) ",[47,1340,1072],{"class":53},[47,1342,1343],{"class":59}," ($cursor_placed_at, $cursor_id)   ",[47,1345,1346],{"class":76},"-- lexicographic \"strictly before\"\n",[47,1348,1349,1351,1353,1355,1357],{"class":49,"line":1113},[47,1350,17],{"class":53},[47,1352,1286],{"class":59},[47,1354,177],{"class":53},[47,1356,1008],{"class":59},[47,1358,785],{"class":53},[47,1360,1361,1363,1365],{"class":49,"line":1118},[47,1362,813],{"class":53},[47,1364,1299],{"class":224},[47,1366,722],{"class":59},[47,1368,1369],{"class":49,"line":1124},[47,1370,316],{"emptyLinePlaceholder":315},[47,1372,1373],{"class":49,"line":1135},[47,1374,1375],{"class":76},"-- With a WHERE filter on status — the filter applies before the cursor comparison\n",[47,1377,1378,1380,1382,1384],{"class":49,"line":1142},[47,1379,32],{"class":53},[47,1381,1267],{"class":59},[47,1383,1270],{"class":53},[47,1385,1273],{"class":59},[47,1387,1388,1390],{"class":49,"line":1157},[47,1389,63],{"class":53},[47,1391,537],{"class":59},[47,1393,1394,1396,1399,1402],{"class":49,"line":1170},[47,1395,1066],{"class":53},[47,1397,1398],{"class":53}," status",[47,1400,1401],{"class":53}," =",[47,1403,1404],{"class":359}," 'shipped'\n",[47,1406,1408,1411,1413,1415],{"class":49,"line":1407},23,[47,1409,1410],{"class":53},"  AND",[47,1412,1338],{"class":59},[47,1414,1072],{"class":53},[47,1416,1417],{"class":59}," ($cursor_placed_at, $cursor_id)\n",[47,1419,1421,1423,1425,1427,1429],{"class":49,"line":1420},24,[47,1422,17],{"class":53},[47,1424,1286],{"class":59},[47,1426,177],{"class":53},[47,1428,1008],{"class":59},[47,1430,785],{"class":53},[47,1432,1434,1436,1438],{"class":49,"line":1433},25,[47,1435,813],{"class":53},[47,1437,1299],{"class":224},[47,1439,722],{"class":59},[47,1441,1443],{"class":49,"line":1442},26,[47,1444,1445],{"class":76},"-- Requires a partial index: CREATE INDEX idx_orders_shipped_placed ON orders (placed_at DESC, id DESC) WHERE status = 'shipped';\n",[14,1447,1449],{"id":1448},"anti-pattern-offset-pagination-at-scale","Anti-Pattern: OFFSET Pagination at Scale",[35,1451,1452],{"language":37},[39,1453,1455],{"className":41,"code":1454,"language":37,"meta":43,"style":43},"-- ❌ WRONG: OFFSET pagination on a large table — deep pages are O(N)\nSELECT id, name FROM orders ORDER BY placed_at DESC LIMIT 20 OFFSET 1000000;\n-- The planner must produce and discard 1,000,000 rows before returning 20.\n-- On a 2M-row table, this can take seconds — and gets worse every page.\n\n-- ✅ RIGHT: keyset pagination — O(log N) seek, constant time per page\nSELECT id, name FROM orders\nWHERE (placed_at, id) \u003C ($cursor_placed_at, $cursor_id)\nORDER BY placed_at DESC, id DESC\nLIMIT 20;\n",[22,1456,1457,1462,1491,1496,1501,1505,1510,1522,1532,1544],{"__ignoreMap":43},[47,1458,1459],{"class":49,"line":50},[47,1460,1461],{"class":76},"-- ❌ WRONG: OFFSET pagination on a large table — deep pages are O(N)\n",[47,1463,1464,1466,1468,1470,1472,1474,1476,1478,1480,1482,1484,1486,1489],{"class":49,"line":80},[47,1465,32],{"class":53},[47,1467,986],{"class":59},[47,1469,150],{"class":53},[47,1471,704],{"class":53},[47,1473,707],{"class":59},[47,1475,17],{"class":53},[47,1477,1286],{"class":59},[47,1479,177],{"class":53},[47,1481,716],{"class":53},[47,1483,1299],{"class":224},[47,1485,754],{"class":59},[47,1487,1488],{"class":224},"1000000",[47,1490,722],{"class":59},[47,1492,1493],{"class":49,"line":106},[47,1494,1495],{"class":76},"-- The planner must produce and discard 1,000,000 rows before returning 20.\n",[47,1497,1498],{"class":49,"line":132},[47,1499,1500],{"class":76},"-- On a 2M-row table, this can take seconds — and gets worse every page.\n",[47,1502,1503],{"class":49,"line":159},[47,1504,316],{"emptyLinePlaceholder":315},[47,1506,1507],{"class":49,"line":319},[47,1508,1509],{"class":76},"-- ✅ RIGHT: keyset pagination — O(log N) seek, constant time per page\n",[47,1511,1512,1514,1516,1518,1520],{"class":49,"line":325},[47,1513,32],{"class":53},[47,1515,986],{"class":59},[47,1517,150],{"class":53},[47,1519,704],{"class":53},[47,1521,537],{"class":59},[47,1523,1524,1526,1528,1530],{"class":49,"line":338},[47,1525,1066],{"class":53},[47,1527,1338],{"class":59},[47,1529,1072],{"class":53},[47,1531,1417],{"class":59},[47,1533,1534,1536,1538,1540,1542],{"class":49,"line":344},[47,1535,17],{"class":53},[47,1537,1286],{"class":59},[47,1539,177],{"class":53},[47,1541,1008],{"class":59},[47,1543,785],{"class":53},[47,1545,1546,1548,1550],{"class":49,"line":353},[47,1547,813],{"class":53},[47,1549,1299],{"class":224},[47,1551,722],{"class":59},[14,1553,1555],{"id":1554},"tips-tricks","💡 Tips & Tricks",[1199,1557,1558,1568,1581,1593],{},[1202,1559,1560,1563,1564,1567],{},[462,1561,1562],{},"Performance",": for user-facing paginated lists on large tables, ",[462,1565,1566],{},"always prefer keyset over OFFSET"," — keyset is O(log N) to seek plus O(page_size) to fetch, while OFFSET is O(offset + page_size). The difference is invisible at page 5 and dramatic at page 5000.",[1202,1569,1570,1573,1574,1576,1577,1580],{},[462,1571,1572],{},"Idiom",": when using ",[22,1575,17],{}," for pagination, always include a ",[462,1578,1579],{},"unique tie-breaker column"," (typically the primary key) as the final sort key — without it, rows with equal sort values can appear in arbitrary order across pages, causing duplicates or skips when data changes between page loads.",[1202,1582,1583,953,1585,1588,1589,1592],{},[462,1584,1572],{},[22,1586,1587],{},"ORDER BY (col IS NULL), col"," is a portable way to push NULLs to the end of an ascending sort without relying on ",[22,1590,1591],{},"NULLS LAST"," — the boolean expression sorts FALSE (0) before TRUE (1), so non-NULL rows come first, then NULLs, then within each group the actual column values sort normally.",[1202,1594,1595,953,1597,814,1599,1601,1602,29,1605,29,1608,1610],{},[462,1596,1562],{},[22,1598,813],{},[22,1600,17],{}," is cheap (the planner just stops early), but ",[22,1603,1604],{},"LIMIT n",[26,1606,1607],{},"with",[22,1609,17],{}," on an unindexed column still sorts the entire result set before taking the top n — add an index on the sort column for \"top N\" queries to avoid a full sort.",[14,1612,1614],{"id":1613},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[1199,1616,1617,1641,1667,1685,1709,1718,1734,1753],{},[1202,1618,1619,1626,1627,1630,1631,1633,1634,1637,1638,1640],{},[462,1620,1621,814,1623,1625],{},[22,1622,813],{},[22,1624,17],{}," is non-deterministic",": the database may return ",[26,1628,1629],{},"any"," rows that match the ",[22,1632,1066],{},", in any order, and the set can differ between runs (especially after ",[22,1635,1636],{},"VACUUM"," or index changes). Never rely on \"the first 10 rows\" being meaningful without an ",[22,1639,17],{},".",[1202,1642,1643,1658,1659,1662,1663,1666],{},[462,1644,1645,1648,1649,1651,1652,1651,1655],{},[22,1646,1647],{},"OFFSET"," counts from the result after ",[22,1650,1066],{},"\u002F",[22,1653,1654],{},"GROUP BY",[22,1656,1657],{},"HAVING",", not from the table. A query with ",[22,1660,1661],{},"WHERE amount > 100 LIMIT 10 OFFSET 20"," skips 20 rows ",[26,1664,1665],{},"that pass the WHERE",", not 20 rows of the table.",[1202,1668,1669,1672,1673,1676,1677,1680,1681,1684],{},[462,1670,1671],{},"NULL sort order is engine-specific",": the same ",[22,1674,1675],{},"ORDER BY col ASC"," can put NULLs first (MySQL, SQLite) or last (PostgreSQL, Oracle). If the order matters, use ",[22,1678,1679],{},"NULLS FIRST\u002FLAST"," or the ",[22,1682,1683],{},"IS NULL"," trick — don't assume.",[1202,1686,1687,1695,1696,1698,1699,1702,1703,1705,1706,1708],{},[462,1688,1689,1692,1693],{},[22,1690,1691],{},"FETCH FIRST n ROWS ONLY"," vs ",[22,1694,1604],{},": they're equivalent in PostgreSQL, but ",[22,1697,917],{}," is ANSI standard and supports ",[22,1700,1701],{},"WITH TIES",", while ",[22,1704,813],{}," is a PostgreSQL\u002FMySQL extension. SQLite uses ",[22,1707,813],{}," only.",[1202,1710,1711,1717],{},[462,1712,1713,1716],{},[22,1714,1715],{},"OFFSET 0"," is legal but pointless",": it's a no-op. Some ORMs generate it; ignore it.",[1202,1719,1720,953,1726,1729,1730,1733],{},[462,1721,1722,1723,1725],{},"Large ",[22,1724,1647],{}," can be slower than a full scan",[22,1727,1728],{},"OFFSET 1000000 LIMIT 10"," on a 2M-row table may take longer than ",[22,1731,1732],{},"SELECT count(*)"," because the planner has to produce and discard a million rows. Keyset pagination avoids this entirely.",[1202,1735,1736,953,1741,1744,1745,1748,1749,1752],{},[462,1737,1738,1740],{},[22,1739,1701],{}," returns variable-size results",[22,1742,1743],{},"FETCH FIRST 3 ROWS WITH TIES"," can return ",[26,1746,1747],{},"more"," than 3 rows if there are ties at the boundary — this is correct behavior (you asked for the top 3 ",[26,1750,1751],{},"values","), but callers expecting exactly 3 rows must handle variable result sizes.",[1202,1754,1755,1760,1761,1763,1764,1766,1767,1769],{},[462,1756,1757,1759],{},[22,1758,17],{}," in subqueries is ignored by the standard",": the SQL standard does not guarantee that ",[22,1762,17],{}," inside a subquery or view is preserved in the outer query — the outer query's ",[22,1765,17],{}," is what matters. PostgreSQL mostly preserves it as an optimization fence, but don't rely on it; move ",[22,1768,17],{}," to the outermost query.",[14,1771,1773],{"id":1772},"spot-the-bug","🧠 Spot the Bug",[19,1775,1776],{},"A developer implements pagination like this and reports that \"page 2 sometimes shows rows that were already on page 1.\" What's wrong?",[35,1778,1779],{"language":37},[39,1780,1782],{"className":41,"code":1781,"language":37,"meta":43,"style":43},"-- Page 1\nSELECT id, name, ordered_on FROM orders ORDER BY ordered_on DESC LIMIT 10;\n\n-- Page 2\nSELECT id, name, ordered_on FROM orders ORDER BY ordered_on DESC LIMIT 10 OFFSET 10;\n",[22,1783,1784,1789,1816,1820,1825],{"__ignoreMap":43},[47,1785,1786],{"class":49,"line":50},[47,1787,1788],{"class":76},"-- Page 1\n",[47,1790,1791,1793,1795,1797,1800,1802,1804,1806,1808,1810,1812,1814],{"class":49,"line":80},[47,1792,32],{"class":53},[47,1794,986],{"class":59},[47,1796,150],{"class":53},[47,1798,1799],{"class":59},", ordered_on ",[47,1801,63],{"class":53},[47,1803,707],{"class":59},[47,1805,17],{"class":53},[47,1807,544],{"class":59},[47,1809,177],{"class":53},[47,1811,716],{"class":53},[47,1813,719],{"class":224},[47,1815,722],{"class":59},[47,1817,1818],{"class":49,"line":106},[47,1819,316],{"emptyLinePlaceholder":315},[47,1821,1822],{"class":49,"line":132},[47,1823,1824],{"class":76},"-- Page 2\n",[47,1826,1827,1829,1831,1833,1835,1837,1839,1841,1843,1845,1847,1849,1851,1854],{"class":49,"line":159},[47,1828,32],{"class":53},[47,1830,986],{"class":59},[47,1832,150],{"class":53},[47,1834,1799],{"class":59},[47,1836,63],{"class":53},[47,1838,707],{"class":59},[47,1840,17],{"class":53},[47,1842,544],{"class":59},[47,1844,177],{"class":53},[47,1846,716],{"class":53},[47,1848,719],{"class":224},[47,1850,754],{"class":59},[47,1852,1853],{"class":224},"10",[47,1855,722],{"class":59},[1857,1858,1859,1863,1891,1898,1948,1959],"details",{},[1860,1861,1862],"summary",{},"Answer",[19,1864,1865,1866,1869,1870,1872,1873,1876,1877,1879,1880,1882,1883,1886,1887,1890],{},"The sort key is ",[22,1867,1868],{},"ordered_on"," alone, and ",[22,1871,1868],{}," is ",[462,1874,1875],{},"not unique"," — many orders can share the same date. When multiple rows have the same ",[22,1878,1868],{}," value, their relative order is unspecified, and the planner is free to return them in ",[26,1881,1629],{}," order on each execution. Between the page-1 and page-2 queries, the planner may choose a different order for the tied rows, causing a row to appear in the ",[22,1884,1885],{},"LIMIT 10"," window of page 1 and again in the ",[22,1888,1889],{},"OFFSET 10 LIMIT 10"," window of page 2 (or to be skipped entirely).",[19,1892,1893,1894,1897],{},"The fix: add a ",[462,1895,1896],{},"unique tie-breaker"," — typically the primary key — as the final sort key:",[35,1899,1900],{"language":37},[39,1901,1903],{"className":41,"code":1902,"language":37,"meta":43,"style":43},"SELECT id, name, ordered_on\nFROM orders\nORDER BY ordered_on DESC, id DESC    -- id as unique tie-breaker → total order\nLIMIT 10 OFFSET 10;\n",[22,1904,1905,1915,1921,1936],{"__ignoreMap":43},[47,1906,1907,1909,1911,1913],{"class":49,"line":50},[47,1908,32],{"class":53},[47,1910,986],{"class":59},[47,1912,150],{"class":53},[47,1914,991],{"class":59},[47,1916,1917,1919],{"class":49,"line":80},[47,1918,63],{"class":53},[47,1920,537],{"class":59},[47,1922,1923,1925,1927,1929,1931,1933],{"class":49,"line":106},[47,1924,17],{"class":53},[47,1926,544],{"class":59},[47,1928,177],{"class":53},[47,1930,1008],{"class":59},[47,1932,177],{"class":53},[47,1934,1935],{"class":76},"    -- id as unique tie-breaker → total order\n",[47,1937,1938,1940,1942,1944,1946],{"class":49,"line":132},[47,1939,813],{"class":53},[47,1941,719],{"class":224},[47,1943,754],{"class":59},[47,1945,1853],{"class":224},[47,1947,722],{"class":59},[19,1949,1950,1951,1954,1955,1958],{},"With a total order (",[22,1952,1953],{},"ordered_on, id","), every row has a deterministic position, and pagination is stable. Even better, use keyset pagination on ",[22,1956,1957],{},"(ordered_on, id)"," for O(1) page seeks.",[19,1960,1961,953,1964,1966,1967,1970],{},[462,1962,1963],{},"The lesson",[22,1965,17],{}," on a non-unique column produces a ",[26,1968,1969],{},"partial"," order — tied rows are in arbitrary order and can drift between queries. Always add a unique tie-breaker for pagination.",[14,1972,1973],{"id":1860},"Summary",[19,1975,1976,1977,1979,1980,1651,1982,1984,1985,1987],{},"You can now sort with ",[22,1978,17],{}," (including NULL placement and custom orders), limit results with ",[22,1981,813],{},[22,1983,917],{},", and paginate — knowing why keyset pagination beats ",[22,1986,1647],{}," at scale. Next: combining tables with joins, the most powerful feature of relational databases.",[1989,1990,1991],"style",{},"html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}",{"title":43,"searchDepth":80,"depth":80,"links":1993},[1994,1998,2001,2007,2012,2013,2014,2015,2016,2017],{"id":16,"depth":80,"text":17,"children":1995},[1996,1997],{"id":194,"depth":106,"text":195},{"id":240,"depth":106,"text":241},{"id":421,"depth":80,"text":422,"children":1999},[2000],{"id":512,"depth":106,"text":513},{"id":681,"depth":80,"text":682,"children":2002},[2003,2005],{"id":830,"depth":106,"text":2004},"LIMIT 0 — a schema probe",{"id":871,"depth":106,"text":2006},"FETCH ... WITH TIES (PostgreSQL 13+, SQL Server)",{"id":932,"depth":80,"text":933,"children":2008},[2009,2010,2011],{"id":936,"depth":106,"text":937},{"id":960,"depth":106,"text":961},{"id":1213,"depth":106,"text":1214},{"id":1228,"depth":80,"text":1229},{"id":1448,"depth":80,"text":1449},{"id":1554,"depth":80,"text":1555},{"id":1613,"depth":80,"text":1614},{"id":1772,"depth":80,"text":1773},{"id":1860,"depth":80,"text":1973},"md",{},"\u002Fsql\u002F03-sorting-and-pagination",{"title":5,"description":43},"sql\u002F03-sorting-and-pagination","CKHxQ7rmxFbtpRTSQc8AYGiI4YA2lI6HNmQzGSdO_bU",1789924654284]