{
    "archive_path": "archive/1776557376.218045",
    "base_url": "sgerogia.github.io/Postgres-Index-And-Queries",
    "basename": "",
    "bookmarked_date": "2026-04-19 00:09",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/sgerogia.github.io/Postgres-Index-And-Queries",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=sgerogia.github.io",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": null
    },
    "domain": "sgerogia.github.io",
    "downloaded_at": "2026-04-19T00:09:38.518126+00:00",
    "downloaded_datestr": "2026-04-19 00:09",
    "extension": "",
    "hash": "14Z4AMV0FCVH2NRZ0NRJ",
    "history": {
        "archive_org": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://web.archive.org/save/https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-04-19T00:10:34.538610+00:00",
                "index_texts": null,
                "output": "https://web.archive.org/web/20260419001018/https://sgerogia.github.io/Postgres-Index-And-Queries/",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:10:14.421960+00:00",
                "status": "succeeded"
            }
        ],
        "dom": [
            {
                "cmd": [
                    "/usr/bin/chromium-browser",
                    "--proxy-server=socks5://tor-socks-proxy:9150",
                    "--disable-features=DarkMode",
                    "--run-all-compositor-stages-before-draw",
                    "--hide-scrollbars",
                    "--autoplay-policy=no-user-gesture-required",
                    "--no-first-run",
                    "--use-fake-ui-for-media-stream",
                    "--use-fake-device-for-media-stream",
                    "--simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT'",
                    "--headless=new",
                    "--no-sandbox",
                    "--no-zygote",
                    "--disable-dev-shm-usage",
                    "--disable-software-rasterizer",
                    "--disable-sync",
                    "--window-size=1440,2000",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "--user-data-dir=/data/personas/Default/chrome_profile",
                    "--profile-directory=Default",
                    "--dump-dom",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "131.0.6778",
                "end_ts": "2026-04-19T00:09:56.365941+00:00",
                "index_texts": null,
                "output": "output.html",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:42.365786+00:00",
                "status": "succeeded"
            }
        ],
        "favicon": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://www.google.com/s2/favicons?domain=sgerogia.github.io"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-04-19T00:09:41.883958+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:38.531266+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--head",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-04-19T00:09:42.132479+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:41.983131+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [
            {
                "cmd": [
                    "(internal) archivebox.extractors.htmltotext",
                    "./{singlefile,dom}.html"
                ],
                "cmd_version": "0.8.5rc51",
                "end_ts": "2026-04-19T00:10:07.940618+00:00",
                "index_texts": [
                    "(/assets/icon/apple-icon-57x57.png) (/assets/icon/apple-icon-60x60.png) (/assets/icon/apple-icon-72x72.png) (/assets/icon/apple-icon-76x76.png) (/assets/icon/apple-icon-114x114.png) (/assets/icon/apple-icon-120x120.png) (/assets/icon/apple-icon-144x144.png) (/assets/icon/apple-icon-152x152.png) (/assets/icon/apple-icon-180x180.png) (/assets/icon/android-icon-192x192.png) (/assets/icon/favicon-32x32.png) (/assets/icon/favicon-96x96.png) (/assets/icon/favicon-16x16.png) (/assets/icon/manifest.json) Postgres Index stats and Query Optimization | Stelios Gerogiannakis Postgres Index stats and Query Optimization | Stelios Gerogiannakis (https://sgerogia.github.io/Postgres-Index-And-Queries/) (https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css) (/assets/css/screen.css) (/assets/css/main.css)  (/) (Stelios Gerogiannakis)    (/index.html) Home  () (Type and enter...)        Stelios Gerogiannakis My thoughts on software, tech, the future and everything in between   Share  (https://twitter.com/intent/tweet?text=Postgres Index stats and Query Optimization&url=https://sgerogia.github.io/Postgres-Index-And-Queries/)    (https://facebook.com/sharer.php?u=https://sgerogia.github.io/Postgres-Index-And-Queries/)    (https://www.linkedin.com/shareArticle?mini=true&url=https://sgerogia.github.io/Postgres-Index-And-Queries/)      2 Comments     (Stelios)  (https://sgerogia.github.io) Stelios () Follow Life-long learner, happy father, trying to do some software engineering on the side.   Postgres Index stats and Query Optimization  (Postgres Index stats and Query Optimization) (https://www.postgresql.org/) PostgreSQL is an extremely performant database. We are using it heavily and to great effect in my current place of work.\nHowever the internal design choices of Postgres mean that you may be faced (https://eng.uber.com/mysql-migration/) with performance degradation \nif not careful . From an application developer\u2019s point-of-view there is an easily accessible treasure trove \nof optimisation hints: the (https://www.postgresql.org/docs/9.2/monitoring-stats.html) pg_stat_user_indexes view. Some background info (Information)  Photo by Laurentiu Morariu on Unsplash  Postgres stores database rows on disk as a whole \u201cthing\u201d, called \u2018tuple\u2019.A tuple (i.e. a row) is read from disk (http://rachbelaid.com/introduction-to-postgres-physical-storage/) into memory as a whole unit , rather than individual column values. \nSimilarly, updating even a single column, results in the insertion of a new tuple; essentially a new version\nof the row. Because of this fundamental Postgres feature, there are 2 key effects: SQL UPDATE s have essentially the same disk overhead as INSERT s (see the (https://eng.uber.com/mysql-migration/) great Uber blog post ) indexes, if not carefully chosen, can kill performance in a write-heavy application.  Luckily, Postgres provides a view of index statistics pg_stat_user_indexes , which gives a nice overview of \nhow indexes are read/used. To get stats from this view you issue a query like thisSELECT * FROM pg_stat_user_indexes WHERE relname = 'apples' ORDER BY idx_scan; replacing \u2018apples\u2019 for your table\u2019s name. This returns results looking like this 1\n2\n3\n4\n5   relid | indexrelid | schemaname | relname |                   indexrelname           | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+------------------------------------------+-----------+--------------+---------------\n 29702 |    1199183 | public     | apples  | index_apples_on_created_at               |  12022742 |  40376724985 |   12152032527\n 29702 |    3132985 | public     | apples  | index_apples_on_farm_id_and_variety_type |  22060865 |   4310241377 |    2529610394\n 29702 |      29787 | public     | apples  | apples_pkey                              | 318104791 |    195366165 |     181310810          The columns in the above table are relname : Name of the DB table  indexrelname : Name of the user index these stats are for  relid : Identifier of the \u2018apples\u2019 table inside Postgres  indexrelid : Identifier of each index for Postgres. This is an (https://www.postgresql.org/docs/10/datatype-oid.html) OID and, (https://wiki.postgresql.org/wiki/Disk_Usage) if you have not tampered with the defaults only used for system objects. What this means in plain English is that it gives you a sense of an index\u2019s \u201cage\u201d: the \nhigher the id, the newer the index. This can help put the rest of the numbers into perspective.For example, in the results above we can deduce that the primary key apples_pkey was created almost at the same time as the \u2018apples\u2019 table (as one would expect) next to be created was index_apples_on_created_at  last one was index_apples_on_farm_id_and_variety_type    idx_scan : How many times the index has been scanned (used).This can be either directly by a application query (e.g. SELECT * FROM apples WHERE id = 1 ) or indirectly due to a JOIN. \nFor example, the primary key index apples_pkey has been scanned over 318 million times.  idx_tup_read : This is the number of index entries returned as a result of an index scan.An easy-to-understand example is the primary key (e.g. SELECT * FROM apples WHERE id = 1 ). If there is an apple with \nid = 1, then idx_tup_read will increase by 1. Modifying slightly the query SELECT * FROM apples WHERE id IN (1, 2) idx_tup_read will increase by 2 (if both ids exist). In both of these queries, idx_scan will increase by 1.  idx_tup_fetch : These are the number of rows fetched from the table as a result of an index scan.This is increased as a result of both positive and false positive results.For example, if both ids exist, the query SELECT * FROM apples WHERE id IN (1, 2) will increase idx_tup_fetch by 2, returning the rows to the client. Even if the query is modified to SELECT * FROM apples WHERE id IN (1, 2) AND color = 'purple' the counter would still be increased by 2.\nThe reason is that the tuples will need to be loaded from disk to examine the value of \u2018color\u2019.Even if \u2018purple\u2019 is unknown and the query returns 0 rows, the counter will still be increased.  Forensics (Investigation)  Photo by Helloquence on Unsplash  The true power of pg_stat_user_indexes lies when you examine how it changes over time. If your application has a \nsemi-predictable usage pattern (e.g. user behaviour is roughly the same on a daily basis), then taking regular snapshots\ncan provide some very valuable insights into how your application\u2019s queries behave under the hood. Let\u2019s take 2 snapshots from our imaginary tables and examine some possible scenarios. Initial 1\n2\n3\n4\n5\n6\n7   relid | indexrelid | schemaname | relname |           indexrelname                       | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+----------------------------------------------+-----------+--------------+---------------\n 29667 |    3403877 | public     | apples  | index_apples_farmed_not_yet_in_truck         |        37 |     73003975 |             0\n 29667 |    1848177 | public     | apples  | index_apples_on_to_farm                      |       377 |    200021437 |             0\n 29667 |   43112037 | public     | apples  | index_apples_on_to_truck                     |      5750 |    100021437 |     100014483\n 29667 |      29775 | public     | apples  | apples_pkey                                  |    708143 |       707811 |        707811\n 29667 |      29793 | public     | apples  | index_apples_on_farm_id                      | 351574733 |   1141817612 |    1140110029          Some time later 1\n2\n3\n4\n5\n6\n7   relid | indexrelid | schemaname | relname |           indexrelname                       | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+----------------------------------------------+-----------+--------------+---------------\n 29667 |    3403877 | public     | apples  | index_apples_farmed_not_yet_in_truck         |        37 |     73003975 |             0\n 29667 |    1848177 | public     | apples  | index_apples_on_to_farm                      |       410 |    274158086 |             0\n 29667 |   43112037 | public     | apples  | index_apples_on_to_truck                     |      5763 |    174153032 |     174133019\n 29667 |      29775 | public     | apples  | apples_pkey                                  |    809930 |       809565 |        809565\n 29667 |      29793 | public     | apples  | index_apples_on_farm_id                      | 390472396 |   1263259359 |    1261333830          1. Unused index (Abandoned)  Photo by v2osk on Unsplash  Indexes do not come for free.They impose 2 types of \u201ctax\u201d which we need to take into account: (https://en.wikipedia.org/wiki/Write_amplification) performance overhead for write-heavy applications (https://wiki.postgresql.org/wiki/Disk_Usage) disk size consumption for large tables  Therefore, the worst kind of index is the unused one. In the above example, the index_apples_farmed_not_yet_in_truck index\u2019s counters have not moved between the 2 snapshots.Assuming that all of our application\u2019s use cases have been executed between the 2 snapshotscrucial assumption! , it is safe to say that\nthis index needs to go and soon! It may be that the index was created at some point in the application\u2019s life and then things moved on. \nEither new code was added or the query using the index was deprecated. Comparing the 2 snapshots (rather than (https://www.cybertec-postgresql.com/en/get-rid-of-your-unused-indexes/) looking only for a zero counter ) \nwill reveal this and allow you to confidently remove the index. 2. Too broad index (Too big)  Photo by sutirta budiman on Unsplash  Looking to (https://www.postgresql.org/docs/current/row-estimation-examples.html) Wikipedia for the definition of an index A database index is a data structure that improves the speed of data retrieval operations on a database table at the \ncost of additional writes and storage space to maintain the index data structure.  An ideal index is an efficient filter which allows us to cherry-pick the few rows which match our query.This operation is much faster than scanning the entire table. \nOr so it is meant to be. Let\u2019s take index_apples_on_to_farm as an example.Between the 2 snapshots the index was used 33 times (410 - 377). \nIn those scans it returned a little over 74 million index entries. That is over 2.2 million index entries per scan. \nThis number needs to be put into perspective. If the apples table has, say, 3 billion rows, then the index_apples_on_to_farm is working beautifully. \nEach scan brings back a tiny fraction of the rows. But if it has, say, 4 million rows, then it is a completely different story! \nMaintaining an index simply to filter out only, say, half the rows might add of questionable gain, especially in a \nwrite-heavy table. In this fictitious example (the query planner would probably (https://www.postgresql.org/docs/current/row-estimation-examples.html) not let this \nhappen in real life ), we see that the index_apples_on_to_farm index\u2019s \nscans result in idx_tup_fetch remaining at zero. \nWe are looking at millions of entries in the index but do not go to the table \nto fetch the rows, in other words only a single disk read per row. \nThis might not be that bad from a performance perspective. The index_apples_on_to_truck index is a different story. 13 index scans,\neach resulting in over 5.7 million index entries and rows fetched from\nthe table. This dual disk access (filter by index, then fetch row) may be \nhinting at (https://www.postgresql.org/docs/current/planner-stats.html) incorrect table statistics  a completely unoptimized query  Putting these figures into perspective with regards to the overall table \nsize will show you if you have a potential problem in your hands. 3. Abused index (On fire)  Photo by raquel raclette on Unsplash  Sometimes you may have an index, optimized and well-functioning per se, \nthe statistics of which hint at some unoptimized query. Let\u2019s take a look at index_apples_on_farm_id .In this time period it has had close to 39 million scans. Each of these \nscans, on average, resulted in roughly 3 index entries and 3 rows returned. \nI.e. 39 million x 6 disk reads. The index per se seems highly selective and optimized; an index scan \nreturns 3 entries. However it has been called millions and millions of \ntimes. This may be hinting at a sub-optimal join with a much larger table. \nEither due to an unfiltered join or bad query planner statistics, our index_apples_on_farm_id ends up hammered inside a (https://malisper.me/postgres-nested-loop-joins/) nested loop . Again putting the statistics figures into perspective (magnitude of counters\nvs size of tables vs user load) will help you focus your efforts. 4. Well-functioning index (probably) (Fast cheetah)  Photo by Deven Wesolowski on Unsplash  To contrast a bit with the above, let\u2019s look at the primary key index apples_pkey .It has been scanned ~100,000 times resulting in roughly the same number \nof index entries and rows returned. In other words, each scan returns a \nsingle row.In addition, the number of scans is not out of proportion as that of index_apples_on_farm_id . If it is used in JOINS, it is not in an \nuncontrolled way. In other words, if we are looking for things to improve, this should not be the first place to look. Parting thought (Doctor)  Photo by Arvin Chingcuangco on Unsplash  You may have noticed that I have not written a single word about the underlying \nDB model of this example. What does the application do?How many users/load does it have?How many tables are there?How do they relate with each other?How many rows does table X contain? These (and more) are questions to start asking after you have established \nthat something does not look right. The index statistics allow you to \ntake a look at the database tables as an opaque black box, even with \nlittle initial domain knowledge. Same way that a doctor starts asking questions and probing deeper after something abnormal shows up during the regular check-up.  11 May 2019    (/categories#Databases) Databases  (/categories#Software-Development) Software Development    (/tags#database) #database  (/tags#indexes) #indexes  (/tags#optimization) #optimization  (/tags#postgres) #postgres    (//What-does-my-phone-do/) \u00ab What does my smartphone really do? (//Searching-Slack/) Slack as a searchable chat-ops sink \u00bb       (Disqus)   (Disqus)   Please enable JavaScript to view the (http://disqus.com/?ref_noscript) comments powered by Disqus.        Explore \u2192    (/categories#System-Design) System Design (3) (/categories#Architecture) Architecture (3) (/categories#Big-Data) Big Data (1) (/categories#DevOps) DevOps (2) (/categories#Software-Development) Software Development (4) (/categories#Engineering) Engineering (6) (/categories#Leadership) Leadership (7) (/categories#Hiring) Hiring (3) (/categories#Career) Career (1) (/categories#Network) Network (1) (/categories#Smartphones) Smartphones (1) (/categories#Databases) Databases (1) (/categories#Investing) Investing (4) (/categories#Personal-Development) Personal Development (1) (/categories#Payments) Payments (7) (/categories#Fintec) Fintec (5) (/categories#Blockchain) Blockchain (9) (/categories#General-Knowledge) General Knowledge (4) (/categories#Angel-Investing) Angel Investing (3) (/categories#Ethereum) Ethereum (5) (/categories#NFT) NFT (1) (/categories#Cosmos-SDK) Cosmos SDK (2) (/categories#Chainlink) Chainlink (1) (/categories#Soft-Skills) Soft Skills (2) (/categories#Microservices) Microservices (2) (/categories#LLM) LLM (1) (/categories#AI) AI (1)    Copyright \u00a9 2024 Stelios Gerogiannakis      (https://fonts.googleapis.com/css?family=Righteous%7CMerriweather:300,300i,400,400i,700,700i) (https://use.fontawesome.com/releases/v5.0.13/css/all.css)      "
                ],
                "output": "htmltotext.txt",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:10:07.884453+00:00",
                "status": "succeeded"
            }
        ],
        "media": [
            {
                "cmd": [
                    "/usr/local/bin/yt-dlp",
                    "--restrict-filenames",
                    "--trim-filenames",
                    "128",
                    "--write-description",
                    "--write-info-json",
                    "--write-annotations",
                    "--write-thumbnail",
                    "--no-call-home",
                    "--write-sub",
                    "--write-auto-subs",
                    "--convert-subs=srt",
                    "--yes-playlist",
                    "--continue",
                    "--no-abort-on-error",
                    "--ignore-errors",
                    "--geo-bypass",
                    "--add-metadata",
                    "--format=(bv*+ba/b)[filesize<=750m][filesize_approx<=?750m]/(bv*+ba/b)",
                    "--skip-download",
                    "--cache-dir=/data/yt-dlp-cache/",
                    "--cookies=/data/yt-dlp-cache/cookies.txt",
                    "--proxy=socks5://tor-socks-proxy:9150",
                    "--no-playlist",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "2024.10.7",
                "end_ts": "2026-04-19T00:10:14.348409+00:00",
                "index_texts": [],
                "output": "media/",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:10:08.205635+00:00",
                "status": "succeeded"
            }
        ],
        "mercury": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/postlight-parser",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "2.2.3",
                "end_ts": "2026-04-19T00:10:07.817793+00:00",
                "index_texts": null,
                "output": "mercury/",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:59.982063+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/home/archivebox/.npm/bin/readability-extractor",
                    "/tmp/tmpsv5l_cna",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-04-19T00:09:59.890189+00:00",
                "index_texts": [
                    "PostgreSQL is an extremely performant database. \nWe are using it heavily and to great effect in my current place of work.\nHowever the internal design choices of Postgres mean that you may be faced with performance degradation \nif not careful.\n\nFrom an application developer\u2019s point-of-view there is an easily accessible treasure trove \nof optimisation hints: the pg_stat_user_indexes view.\n\nSome background info\n\n\n\n  Photo by Laurentiu Morariu on Unsplash\n\n\nPostgres stores database rows on disk as a whole \u201cthing\u201d, called \u2018tuple\u2019.\nA tuple (i.e. a row) is read from disk into memory as a whole unit, rather than individual column values. \nSimilarly, updating even a single column, results in the insertion of a new tuple; essentially a new version\nof the row.\n\nBecause of this fundamental Postgres feature, there are 2 key effects:\n\n  SQL UPDATEs have essentially the same disk overhead as INSERTs (see the great Uber blog post)\n  indexes, if not carefully chosen, can kill performance in a write-heavy application.\n\n\nLuckily, Postgres provides a view of index statistics pg_stat_user_indexes, which gives a nice overview of \nhow indexes are read/used.\n\nTo get stats from this view you issue a query like this\nSELECT * FROM pg_stat_user_indexes WHERE relname = 'apples' ORDER BY idx_scan;\nreplacing \u2018apples\u2019 for your table\u2019s name.\n\nThis returns results looking like this\n1\n2\n3\n4\n5\n relid | indexrelid | schemaname | relname |                   indexrelname           | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+------------------------------------------+-----------+--------------+---------------\n 29702 |    1199183 | public     | apples  | index_apples_on_created_at               |  12022742 |  40376724985 |   12152032527\n 29702 |    3132985 | public     | apples  | index_apples_on_farm_id_and_variety_type |  22060865 |   4310241377 |    2529610394\n 29702 |      29787 | public     | apples  | apples_pkey                              | 318104791 |    195366165 |     181310810\n\n\nThe columns in the above table are\n\n  \n    relname: Name of the DB table\n  \n  \n    indexrelname: Name of the user index these stats are for\n  \n  \n    relid: Identifier of the \u2018apples\u2019 table inside Postgres\n  \n  indexrelid: Identifier of each index for Postgres. This is an OID and, if you have not tampered with the defaults \nonly used for system objects. What this means in plain English is that it gives you a sense of an index\u2019s \u201cage\u201d: the \nhigher the id, the newer the index. This can help put the rest of the numbers into perspective.\nFor example, in the results above we can deduce that\n    \n      the primary key apples_pkey was created almost at the same time as the \u2018apples\u2019 table (as one would expect)\n      next to be created was index_apples_on_created_at\n      last one was index_apples_on_farm_id_and_variety_type\n    \n  \n  \n    idx_scan: How many times the index has been scanned (used).\nThis can be either directly by a application query (e.g. SELECT * FROM apples WHERE id = 1) or indirectly due to a JOIN. \nFor example, the primary key index apples_pkey has been scanned over 318 million times.\n  \n  \n    idx_tup_read: This is the number of index entries returned as a result of an index scan.\nAn easy-to-understand example is the primary key (e.g. SELECT * FROM apples WHERE id = 1). If there is an apple with \nid = 1, then idx_tup_read will increase by 1. Modifying slightly the query SELECT * FROM apples WHERE id IN (1, 2) \nidx_tup_read will increase by 2 (if both ids exist). In both of these queries, idx_scan will increase by 1.\n  \n  idx_tup_fetch: These are the number of rows fetched from the table as a result of an index scan.\nThis is increased as a result of both positive and false positive results.\nFor example, if both ids exist, the query SELECT * FROM apples WHERE id IN (1, 2) will increase\nidx_tup_fetch by 2, returning the rows to the client. Even if the query is modified to \nSELECT * FROM apples WHERE id IN (1, 2) AND color = 'purple' the counter would still be increased by 2.\nThe reason is that the tuples will need to be loaded from disk to examine the value of \u2018color\u2019.\nEven if \u2018purple\u2019 is unknown and the query returns 0 rows, the counter will still be increased.\n\n\nForensics\n\n\n\n  Photo by Helloquence on Unsplash\n\n\nThe true power of pg_stat_user_indexes lies when you examine how it changes over time. If your application has a \nsemi-predictable usage pattern (e.g. user behaviour is roughly the same on a daily basis), then taking regular snapshots\ncan provide some very valuable insights into how your application\u2019s queries behave under the hood.\n\nLet\u2019s take 2 snapshots from our imaginary tables and examine some possible scenarios.\n\nInitial\n1\n2\n3\n4\n5\n6\n7\n relid | indexrelid | schemaname | relname |           indexrelname                       | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+----------------------------------------------+-----------+--------------+---------------\n 29667 |    3403877 | public     | apples  | index_apples_farmed_not_yet_in_truck         |        37 |     73003975 |             0\n 29667 |    1848177 | public     | apples  | index_apples_on_to_farm                      |       377 |    200021437 |             0\n 29667 |   43112037 | public     | apples  | index_apples_on_to_truck                     |      5750 |    100021437 |     100014483\n 29667 |      29775 | public     | apples  | apples_pkey                                  |    708143 |       707811 |        707811\n 29667 |      29793 | public     | apples  | index_apples_on_farm_id                      | 351574733 |   1141817612 |    1140110029\n\n\nSome time later\n1\n2\n3\n4\n5\n6\n7\n relid | indexrelid | schemaname | relname |           indexrelname                       | idx_scan  | idx_tup_read | idx_tup_fetch\n-------+------------+------------+---------+----------------------------------------------+-----------+--------------+---------------\n 29667 |    3403877 | public     | apples  | index_apples_farmed_not_yet_in_truck         |        37 |     73003975 |             0\n 29667 |    1848177 | public     | apples  | index_apples_on_to_farm                      |       410 |    274158086 |             0\n 29667 |   43112037 | public     | apples  | index_apples_on_to_truck                     |      5763 |    174153032 |     174133019\n 29667 |      29775 | public     | apples  | apples_pkey                                  |    809930 |       809565 |        809565\n 29667 |      29793 | public     | apples  | index_apples_on_farm_id                      | 390472396 |   1263259359 |    1261333830\n\n\n1. Unused index\n\n\n\n  Photo by v2osk on Unsplash\n\n\nIndexes do not come for free.\nThey impose 2 types of \u201ctax\u201d which we need to take into account:\n\n  performance overhead for write-heavy applications\n  disk size consumption for large tables\n\n\nTherefore, the worst kind of index is the unused one.\n\nIn the above example, the index_apples_farmed_not_yet_in_truck index\u2019s counters have not moved between the 2 snapshots.\nAssuming that all of our application\u2019s use cases have been executed between the 2 snapshotscrucial assumption!, it is safe to say that\nthis index needs to go and soon!\n\nIt may be that the index was created at some point in the application\u2019s life and then things moved on. \nEither new code was added or the query using the index was deprecated.\n\nComparing the 2 snapshots (rather than looking only for a zero counter) \nwill reveal this and allow you to confidently remove the index.\n\n2. Too broad index\n\n\n\n  Photo by sutirta budiman on Unsplash\n\n\nLooking to Wikipedia for the definition of an index\n\n  A database index is a data structure that improves the speed of data retrieval operations on a database table at the \ncost of additional writes and storage space to maintain the index data structure.\n\n\nAn ideal index is an efficient filter which allows us to cherry-pick the few rows which match our query.\nThis operation is much faster than scanning the entire table. \nOr so it is meant to be.\n\nLet\u2019s take index_apples_on_to_farm as an example.\nBetween the 2 snapshots the index was used 33 times (410 - 377). \nIn those scans it returned a little over 74 million index entries. That is over 2.2 million index entries per scan. \nThis number needs to be put into perspective.\n\nIf the apples table has, say, 3 billion rows, then the index_apples_on_to_farm is working beautifully. \nEach scan brings back a tiny fraction of the rows.\n\nBut if it has, say, 4 million rows, then it is a completely different story! \nMaintaining an index simply to filter out only, say, half the rows might add of questionable gain, especially in a \nwrite-heavy table.\n\nIn this fictitious example (the query planner would probably not let this \nhappen in real life), we see that the index_apples_on_to_farm index\u2019s \nscans result in idx_tup_fetch remaining at zero. \nWe are looking at millions of entries in the index but do not go to the table \nto fetch the rows, in other words only a single disk read per row. \nThis might not be that bad from a performance perspective.\n\nThe index_apples_on_to_truck index is a different story. 13 index scans,\neach resulting in over 5.7 million index entries and rows fetched from\nthe table. This dual disk access (filter by index, then fetch row) may be \nhinting at\n\n  incorrect table statistics\n  a completely unoptimized query\n\n\nPutting these figures into perspective with regards to the overall table \nsize will show you if you have a potential problem in your hands.\n\n3. Abused index\n\n\n\n  Photo by raquel raclette on Unsplash\n\n\nSometimes you may have an index, optimized and well-functioning per se, \nthe statistics of which hint at some unoptimized query.\n\nLet\u2019s take a look at index_apples_on_farm_id.\nIn this time period it has had close to 39 million scans. Each of these \nscans, on average, resulted in roughly 3 index entries and 3 rows returned. \nI.e. 39 million x 6 disk reads.\n\nThe index per se seems highly selective and optimized; an index scan \nreturns 3 entries. However it has been called millions and millions of \ntimes.\n\nThis may be hinting at a sub-optimal join with a much larger table. \nEither due to an unfiltered join or bad query planner statistics, our \nindex_apples_on_farm_id ends up hammered inside a nested loop.\n\nAgain putting the statistics figures into perspective (magnitude of counters\nvs size of tables vs user load) will help you focus your efforts.\n\n4. Well-functioning index (probably)\n\n\n\n  Photo by Deven Wesolowski on Unsplash\n\n\nTo contrast a bit with the above, let\u2019s look at the primary key index apples_pkey.\nIt has been scanned ~100,000 times resulting in roughly the same number \nof index entries and rows returned. In other words, each scan returns a \nsingle row.\nIn addition, the number of scans is not out of proportion as that of \nindex_apples_on_farm_id. If it is used in JOINS, it is not in an \nuncontrolled way.\n\nIn other words, if we are looking for things to improve, this should not\nbe the first place to look.\n\nParting thought\n\n\n\n  Photo by Arvin Chingcuangco on Unsplash\n\n\nYou may have noticed that I have not written a single word about the underlying \nDB model of this example.\n\nWhat does the application do?\nHow many users/load does it have?\nHow many tables are there?\nHow do they relate with each other?\nHow many rows does table X contain?\n\nThese (and more) are questions to start asking after you have established \nthat something does not look right. The index statistics allow you to \ntake a look at the database tables as an opaque black box, even with \nlittle initial domain knowledge.\n\nSame way that a doctor starts asking questions and probing deeper after \nsomething abnormal shows up during the regular check-up."
                ],
                "output": "readability/",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:56.698607+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "/usr/bin/curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--proxy",
                    "socks5://tor-socks-proxy:9150",
                    "--max-time",
                    "60",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ArchiveBox/{VERSION} (+https://github.com/ArchiveBox/ArchiveBox/)",
                    "https://sgerogia.github.io/Postgres-Index-And-Queries/"
                ],
                "cmd_version": "8.10.1",
                "end_ts": "2026-04-19T00:09:56.539530+00:00",
                "index_texts": null,
                "output": "Postgres Index stats and Query Optimization | Stelios Gerogiannakis",
                "pwd": "/data/archive/1776557376.218045",
                "schema": "ArchiveResult",
                "start_ts": "2026-04-19T00:09:56.510353+00:00",
                "status": "succeeded"
            }
        ],
        "wget": []
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": "https://web.archive.org/web/20260419001018/https://sgerogia.github.io/Postgres-Index-And-Queries/",
        "dom": "output.html",
        "favicon": "favicon.ico",
        "git": null,
        "media": "media/",
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Postgres Index stats and Query Optimization | Stelios Gerogiannakis",
        "warc": null,
        "wget": null
    },
    "link_dir": "/data/archive/1776557376.218045",
    "newest_archive_date": "2026-04-19T00:10:14.421960+00:00",
    "num_failures": 0,
    "num_outputs": 9,
    "oldest_archive_date": "2026-04-19T00:09:38.531266+00:00",
    "path": "/Postgres-Index-And-Queries/",
    "schema": "Link",
    "scheme": "https",
    "snapshot_abid": "snp_01KPHH4GR1515C1EDE017PFW7Z",
    "snapshot_id": "e49fd470-310a-494c-ab6f-257d0f67f0ff",
    "sources": [
        "/data/sources/1776557374-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1776557376.218045",
    "title": "Postgres Index stats and Query Optimization | Stelios Gerogiannakis",
    "url": "https://sgerogia.github.io/Postgres-Index-And-Queries/"
}