Category: Uncategorized

This is my process to improve the performance of my PostgreSQL database and queries

Every now and then I’ll run the queries below to help me figure out ways to optimize the queries my application is using, and to figure out if certain tables and views (relations) need to be optimized.

Updated January 18, 2020, to add a query to alter the autovacuum and autoanalyze factors on a per table basis because I have a “cache” table that is updated quite frequently and generates many dead tuples every minute. The table would balloon from 250 MB to 15 GB in a few hours. I hope that reducing these factors to just a threshold of 100 dead tuples will prevent the inflation. I also need to monitor this that it doesn’t make my RDS database work too hard.

/* Show the last time at which every table and view was vacuumed and analyzed */
SELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze  
FROM pg_stat_all_tables  d_d
WHERE schemaname = 'public';  

/* See which queries are most popular */
SELECT * FROM pg_stat_statements where calls >=1000; /* replace 1000 with any integer, depending on how active your database is queried */
select pg_stat_statements_reset();

/* Kill a specific query */
select pg_terminate_backend(INTEGER); /* replace INTEGER with the process ID of a specific query */

/* Get the size of the database */
select pg_size_pretty(pg_database_size('DATABASE_NAME')); /* replace DATABASE_NAME with the name of your database */

/* Kill queries that are taking too long; replace DATABASE_NAME with the name of your database */
SELECT query, pg_terminate_backend(pid) 
    FROM pg_stat_activity 
    WHERE datname = 'DATABASE_NAME' 
      AND pid <> pg_backend_pid() 
      AND (state = 'active' 
      OR state = 'idle') 
      AND state_change < current_timestamp - INTERVAL '15' MINUTE; /* change 15 to any integer that you think means a query is taking too long */

/* See which queries are running */
select * from pg_stat_activity;
SELECT * FROM pg_stat_activity WHERE wait_event IS NOT NULL AND backend_type = 'client backend';

/* Find very large indexes and indexes that aren't used much */
WITH table_scans as ( 
    SELECT relid, 
        tables.idx_scan + tables.seq_scan as all_scans, 
        ( tables.n_tup_ins + tables.n_tup_upd + tables.n_tup_del ) as writes, 
                pg_relation_size(relid) as table_size 
        FROM pg_stat_user_tables as tables 
),
all_writes as ( 
    SELECT sum(writes) as total_writes 
    FROM table_scans 
),
indexes as ( 
    SELECT idx_stat.relid, idx_stat.indexrelid, 
        idx_stat.schemaname, idx_stat.relname as tablename, 
        idx_stat.indexrelname as indexname, 
        idx_stat.idx_scan,
        pg_relation_size(idx_stat.indexrelid) as index_bytes, 
        indexdef ~* 'USING btree' AS idx_is_btree 
    FROM pg_stat_user_indexes as idx_stat 
        JOIN pg_index 
            USING (indexrelid) 
        JOIN pg_indexes as indexes 
            ON idx_stat.schemaname = indexes.schemaname 
                AND idx_stat.relname = indexes.tablename 
                AND idx_stat.indexrelname = indexes.indexname 
    WHERE pg_index.indisunique = FALSE 
),
index_ratios AS ( 
SELECT schemaname, tablename, indexname, 
    idx_scan, all_scans,
    round(( CASE WHEN all_scans = 0 THEN 0.0::NUMERIC 
        ELSE idx_scan::NUMERIC/all_scans * 100 END),2) as index_scan_pct, 
    writes,
    round((CASE WHEN writes = 0 THEN idx_scan::NUMERIC ELSE idx_scan::NUMERIC/writes END),2) 
        as scans_per_write, 
    pg_size_pretty(index_bytes) as index_size, 
    pg_size_pretty(table_size) as table_size, 
    idx_is_btree, index_bytes
    FROM indexes 
    JOIN table_scans 
    USING (relid) 
),
index_groups AS ( 
SELECT 'Never Used Indexes' as reason, *, 1 as grp 
FROM index_ratios 
WHERE
    idx_scan = 0
    and idx_is_btree 
UNION ALL 
SELECT 'Low Scans, High Writes' as reason, *, 2 as grp 
FROM index_ratios 
WHERE
    scans_per_write <= 1
    and index_scan_pct < 10 
    and idx_scan > 0 
    and writes > 100 
    and idx_is_btree 
UNION ALL 
SELECT 'Seldom Used Large Indexes' as reason, *, 3 as grp 
FROM index_ratios 
WHERE
    index_scan_pct < 5
    and scans_per_write > 1 
    and idx_scan > 0 
    and idx_is_btree 
    and index_bytes > 100000000 
UNION ALL 
SELECT 'High-Write Large Non-Btree' as reason, index_ratios.*, 4 as grp 
FROM index_ratios, all_writes 
WHERE
    ( writes::NUMERIC / ( total_writes + 1 ) ) > 0.02 
    AND NOT idx_is_btree 
    AND index_bytes > 100000000 
ORDER BY grp, index_bytes DESC ) 
SELECT reason, schemaname, tablename, indexname, 
    index_scan_pct, scans_per_write, index_size, table_size
FROM index_groups; 

/* Modify the autovacuum and autoanalyze factors for a single table 
https://klotzandrew.com/blog/posgres-per-table-autovacuum-management */
ALTER TABLE cache SET (autovacuum_analyze_scale_factor = 0, autovacuum_analyze_threshold = 100);

WITH raw_data AS (
  SELECT
    pg_namespace.nspname,
    pg_class.relname,
    pg_class.oid AS relid,
    pg_class.reltuples,
    pg_stat_all_tables.n_dead_tup,
    pg_stat_all_tables.n_mod_since_analyze,
    (SELECT split_part(x, '=', 2) FROM unnest(pg_class.reloptions) q (x) WHERE x ~ '^autovacuum_analyze_scale_factor=' ) as c_analyze_factor,
    (SELECT split_part(x, '=', 2) FROM unnest(pg_class.reloptions) q (x) WHERE x ~ '^autovacuum_analyze_threshold=' ) as c_analyze_threshold,
    (SELECT split_part(x, '=', 2) FROM unnest(pg_class.reloptions) q (x) WHERE x ~ '^autovacuum_vacuum_scale_factor=' ) as c_vacuum_factor,
    (SELECT split_part(x, '=', 2) FROM unnest(pg_class.reloptions) q (x) WHERE x ~ '^autovacuum_vacuum_threshold=' ) as c_vacuum_threshold,
    to_char(pg_stat_all_tables.last_vacuum, 'YYYY-MM-DD HH24:MI:SS') as last_vacuum,
    to_char(pg_stat_all_tables.last_autovacuum, 'YYYY-MM-DD HH24:MI:SS') as last_autovacuum
  FROM
    pg_class
  JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
    LEFT OUTER JOIN pg_stat_all_tables ON pg_class.oid = pg_stat_all_tables.relid
  WHERE
    n_dead_tup IS NOT NULL
    AND nspname NOT IN ('information_schema', 'pg_catalog')
    AND nspname NOT LIKE 'pg_toast%'
    AND pg_class.relkind = 'r'
), data AS (
  SELECT
    *,
    COALESCE(raw_data.c_analyze_factor, current_setting('autovacuum_analyze_scale_factor'))::float8 AS analyze_factor,
    COALESCE(raw_data.c_analyze_threshold, current_setting('autovacuum_analyze_threshold'))::float8 AS analyze_threshold,
    COALESCE(raw_data.c_vacuum_factor, current_setting('autovacuum_vacuum_scale_factor'))::float8 AS vacuum_factor,
    COALESCE(raw_data.c_vacuum_threshold, current_setting('autovacuum_vacuum_threshold'))::float8 AS vacuum_threshold
  FROM raw_data
)
SELECT
  relid,
  nspname,
  relname,
  reltuples,
  n_dead_tup,
  ROUND(reltuples * vacuum_factor + vacuum_threshold) AS v_threshold,
  n_mod_since_analyze,
  ROUND(reltuples * analyze_factor + analyze_threshold) AS a_threshold,
  c_analyze_factor as caf,
  c_analyze_threshold as cat,
  c_vacuum_factor as cvf,
  c_vacuum_threshold as cvt,
  analyze_factor as af,
  analyze_threshold as at,
  vacuum_factor as vf,
  vacuum_threshold as vt,
  last_vacuum,
  last_autovacuum
FROM
  data
ORDER BY n_dead_tup DESC;

My investment in crowdfunded projects hasn’t gone well

A second project that I invested in through the crowdfunding portal, Small Change, has “failed”. It’s a project for a worker-owned bed and breakfast. According to the project owner, the project needed more funds than what they were able to obtain from Small Change investors and other investors. The owner found a new investor, who required that all Small Change investors be removed from the project. They announced that to us earlier this year, and said we would receive our initial investment + 3%.

The Small Change leadership didn’t think this plan was good enough, starting with disagreeing the removal of Small Change investors.

Eventually, the project owner returned our initial investment + 8%. I don’t know if the project owner had the right to cancel and return our investments, but I’m happy to have my money back + 8%.

I’m disappointed in the ultimate outcome of not being able to participate in 1476 Magazine because this is the second project I invested in via Small Change that has not succeeded. The first was for a group of three single-family houses built to a high energy efficiency standard that have not yet sold.

I have an investment in a third project (in Philadelphia) that is converting a disused industrial building into mixed-use with residential and I hope that one succeeds, but I haven’t received an update since it was funded. Update: I inquired and the project owner sent me the update they sent in the last couple of months, which I seemed to have missed.

Golaski Labs in Philadelphia. A Small Change poster advertises the project. The photo was taken in 2018, and significant progress has been made since then.


I believe in the Small Change mission, and crowdfunding broadly, but once I made my third investment I paused so that I could wait to see what happens with all of the projects.

Chicago’s Best Landlords, Inc.

3901 W Jackson Blvd / Chicago’s Best Landlord, Inc.
A 12-flat in West Garfield Park, Chicago.

I’m halfway done reading the book, “Family Properties: How the Struggle Over Race and Real Estate Transformed Chicago and Urban America“, by Beryl Satter, and I’ve started keeping a list of all the places she mentions.

The book is about her father, and the history of racist housing policies and displacement in Chicago’s West Side. Mark Satter, who died of heart problems when he was 49, owned several properties in Lawndale and Garfield Park.

One of them was a 12-flat in West Garfield Park at 3901 W Jackson Blvd, one block west of the eponymous park. Mark died in 1965, and the properties reverted to a trust he had set up, controlled by his wife, Clarice. Mired in debt to contractors and the mortgage, Clarice had family friend, Irving Block, sell the four properties.

3901 W Jackson Blvd eventually became the property of L.J. Epstein, a known slumlord, in 1967. (It’s unclear in the book, and online deed records don’t go that far back, if it was owned by someone else between Clarice selling it and L.J. acquiring it.)

Less than two years later, though, because of L.J.’s neglect of the building, Judge Kral placed it in receivership because of the activism of Ruby Kirk, a Black tenant from Tennessee.

The judge made Ruby the receiver, because the judge perceived that she cared about the building more than the owner. Ruby created a non-profit corporation on Monday, September 22, 1969, called “Chicago’s Best Landlord, Inc.” (It was involuntarily dissolved on February 1, 2003.)

Beryl wrote that during the remodel the corporation’s name was engraved into the stone above the front door. Along with the initials of the tenants at the time. As I was reading it I thought that stone had to still be there, and since I was biking over to Garfield Park for some hot air balloon training with a couple friends, I would stop by.

Ruby was also successful in pressing First National Bank of Chicago to issue a loan to her, a Black woman, when banks refused to lend to Black people as the Federal Housing Administration refused to insure the loans. If you’re from around here, you may remember that First National Bank merged with Banc One to become Bank One, which then became part of Chase. Chase is the second largest loan issuer in Chicago, and still has a “hard time” lending to Black people. It was also then, however, that the federal rules changed and FHA would insure mortgages in Black neighborhoods.

3901 W Jackson Blvd / Chicago’s Best Landlord, Inc.
Enjoy this little piece of Chicago history -> book -> real-life-right-now photo. Ruby Kirk’s initials are in the center.

Chicago’s Best Landlord, Inc. owned the 12-flat from 1969 until 2001, when the U.S. Department of Housing and Urban Development (HUD) foreclosed the property. First National Bank had assigned the mortgage to HUD in 1976.

Peter Gritzanis purchased the property for $270,000, according to the deed recorded on June 28, 2001, at the Cook County Recorder of Deeds office. It looks like Peter stopped owning it in 2012.

My travel in 2019: North America and a quick hop over to Oceania

I visited new-to-me cities in the United States, went to Mexico for the first time ever, and returned to Canada for the first time in over 10 years. I also rode a lot of new transit – including bike share – systems, which are denoted.

Oh, and I [brag] flew in United Polaris business class* [/brag] to New Zealand with my mom, after which we also visited Australia. Sadly, it’s now been over 2.5 years since I’ve traveled in Europe. 

The ferry to Manly is bidirectional

Two ferries in front of Sydney Opera House at Circular Quay in Sydney, Australia.

Aside: Transit advocates already know this: Every city I visited outside the United States had bus lanes. And not just a few here and there, but everywhere. On every route, in every city and suburb.

Here’s where I went in 2019 (in chronological order) (links go to photo albums on my Flickr):

  • Miami, Miami Beach, and Ft. Lauderdale, Florida
    • I rode Brightline (now Virgin Trains); to Ft. Lauderdale in economy and from Ft. Lauderdale in first class. That meant use of the lounge, unlimited snacks and alcoholic beverages.
    • Metrorail (elevated/subway)
    • Metrobus – The bus is the best way to get your cute butt over to sunning on South Beach. Miami-Dade Transit also uses an app to sell QR-code based tickets that you show to a bus driver and scan to board Metrorail.
    • Metromover (the free people mover in downtown Miami)
    • I also rode a scooter for the first time, and I rode a bike for a few hours.
  • Toronto, Canada
    • I first visited Toronto over a decade ago with my family.
    • I rode the subway, the UP Express (airport train), bike share, and the trams and buses.
  • Mexico City, Mexico
    • Métro, the ultra cheap and frequent subway, of course! I rode it out of the airport to a station 100 meters from my hotel.
    • Métrobus – my first BRT, and it was stupendous.
    • Bike share – A tourist from the U.K. asked me what the conditions were like cycling around the city. I said that depends on your comfort riding a bike in a busy big city, which is something I’m used to. After you observe and adapt to the different driving conditions and customs, it’s really no different than biking in Chicago or New York City.
    • Jitney bus. I forget what it’s called locally. This particular bus driver was plying a main street in a CDMX-branded purple bus that he apparently owned, looking for fares and once the bus was full (no more standing room), he pushed down on the accelerator to the nearest subway station.
  • Sacramento and Napa, California
    • I didn’t take any transit here, not even the light rail. I didn’t have any time.
    • I pedaled a Jump electric dockless bike for the first time.
  • Oakland and Berkeley (2nd time I’ve been)
    • I took Flixbus for the first time, from Sacramento to West Oakland. This is a German-based company that’s a network operator rather than a vehicle operator. Local companies contract with Flixbus to use Flixbus’s branding and ticketing systems.
    • Bay Wheels bike share (it used to be called Ford GoBike)
  • Charleston, South Carolina (no transit here, but I rented a bike for two days)

Oceania (16 nights)

  • Auckland, New Zealand
    • Tip: You’re probably going hiking in New Zealand, and bringing hiking boots. Clean them very well before you go to prevent being delayed at your arrival airport because you’ve triggered a biosecurity check of your shoes.
    • Lots of buses, which came quite frequently.
    • Regional rail from Britomart to Mt. Eden (yeah, a very short ride)
    • An annoying thing about transit in Auckland is that the transit card “deposit” is pricey and non-refundable. Additionally, the stored value (e-purse) on the card can pay for a second rider, but at the full cash rate, not the card rate.
  • Tongariro National Park
    • There’s a thrice-weekly train between Auckland and Wellington that stops here, but I didn’t schedule my trip well to be able to ride it.
    • I took a shuttle bus from my wonderful hotel (The Park Hotel Ruepehu) to the Mangatepopo Carpark, which is the start of the amazing Tongariro Alpine Crossing, and then from the end of the hike at Ketetahi Carpark back to the hotel. It was $20 NZD and it meant that my mom wouldn’t have to drive 60 miles for the same trip.
  • Wellington, New Zealand
    • The only transit we took here was the historic cable car – a cable-pulled funicular – from the high street to Kelburn (which is a suburb, but the way cities and suburbs as governmental jurisdictions and distinct geographies work in New Zealand and Australia is different than the U.S.).
    • At the top of the hill in Kelburn is a free cable car museum with a gorgeous restored vintage cable car, and an entrance to the botanic gardens.
    • We were going to take SkyBus to the airport, but due to a Christmas festival in the CBD, I wasn’t sure where it was going to pick up and we were running late.
  • Sydney, Australia
    • I observed the high quality transit of Sydney (really, Transport NSW, because it’s all operated by the state of New South Wales) when departing the airport via the Airport Link. The T8 line is operated by Transport NSW’s subsidiary Sydney Trains, but the airport and other stations along the line are owned by a private organization that charges a station access fee (which is about $10 USD). Anyway, the first train we boarded was brand new, delivered this year! The train is called “B set” and has everything Metra doesn’t: Level boarding, passenger information displays, two wide doors per carriage, two full levels, and plenty of seating and standing space at the boarding level for people who need step-free access (this includes people with disabilities, people with strollers, luggage, bikes, etc.).
    • In addition to Sydney Trains, we rode NSW TrainLink to Katoomba in the Blue Mountains.
    • We rode the tram line (which they call L1, for light rail) from Chinatown to the Jubilee Park stop to visit Tramsheds, a redevelopment of a, get this, tram shed.
    • To get to Bondi Beach we rode Sydney Trains to Bondi Junction, a station that hooks into a shopping center and pedestrian shopping street. It’s odd…where we boarded bus route #333 for the final hop to the beach, and where we entered on the return from the shopping street, you can’t really see the station. It appears surrounded by offices and shopping. (I didn’t know this until later, but #333 runs from Circular Quay in the CBD to Bondi Beach and further on.)
    • I can’t forget the ferries…A public transport ferry, owned and operated by Transport NSW, carries up to 1,000 people from Circular Quay non-stop to Manly, a suburb at the eastern head of the Sydney Harbour. One of the boats on this route is featured in the photo above. At Manly we took a bus to the North Head reserve to get a look at the Pacific Ocean, the cliffs, Sydney Harbour National Park, and the city skyline.
Train going west from Flinders Railway Station
A Metro train departs Flinders Railway Station in Melbourne.
  • Melbourne, Australia
    • SkyBus to and from the airport. This company runs frequent coaches for about $13 USD every 20 minutes from Tullamarine (MEL) airport to Southern Cross station. Here I bought two transit cards so we could ride a “Metro” (regional rail) train to Flinders Street station and then a tram to our hotel in St. Kilda (which is a suburb of Melbourne, but you could walk from our hotel to the Melbourne CBD in 20 minutes).
    • Melbourne, Victoria, has the world’s largest tram network, and a lot of it is free, so what do you think I did?
    • I rented a bike and rode around for four hours, as well.
    • We also rode the Belgrave Metro line to Belgrave station in order to ride Puffing Billy, a steam engine (which I don’t recommend unless you really like steam engines).
    • To get over to “Welcome to Thornbury”, a food truck and beer garden in the Melbourne “suburb” of Northcote we rode the Mernda Metro line from Central Station and then walked a couple blocks from Croxton Station. To return, we rode the 86 tram (my mom said she prefers the trams to trains because she can see more of the city).

My relaxing flight to Oceania

I have wanted to fly long haul international in business class for quite some time. I thought about saving miles for it. I roughly calculated how long it would take to acquire enough miles: about 3-4 years. And in the meantime, I can’t use the miles for what I think are more valuable trips, like deciding a week before to visit my mom ($600, or 30,000 miles, a “value” of 2 cents per mile).

Anyway, I decided I would pay for business class at some point, when it made sense. I used Google’s ITA Matrix website to find the cheapest days on which to fly one-way in one fare class, and the return in a different fare class. I figured out how to use Google Flights to find those itineraries, which conveniently links to United’s website for final purchase. I couldn’t do it. The prices were too high. And I didn’t want to fly first class from Chicago to San Francisco. (Air New Zealand flies nonstop from Chicago to Auckland, but I wanted to meet my mom in San Francisco so we could be on the same flight to Auckland.)

Fast forward to San Francisco International Airport, and my mom and I walk up to the gate. My mom asked if they hadn’t any better seats in economy available. Keep in mind that boarding starts in 10 minutes, so the answer was no. The gate agent mentioned that there are still seats in business class. I was curious, so I asked how much they were. She quoted me the upgrade fee, clarifying “and that’s per seat”.

I spent two seconds calculating the upgrade fee and comparing it to the hours of research I had done at home finding the right itinerary that would put me in business class for the cheapest outlay. “I’ll do it”, I replied, to which she asked, “For how many?”, double checking if it was for me, or me and my mom. “Two”, I said.

Not only do I still believe that I got a great deal on United’s Polaris business class, I also satisfied a longtime curiosity, enjoyed the flight, and woke up feeling ready to have a full day exploring Auckland.

Before leaving home I felt anxious that we would arrive in Auckland after having spent 14 hours in a cramped seat and potentially waste the first day feeling exhausted. Being able to sleep on a lie-flat seat meant that we felt pretty good when we arrived in Auckland at 8 AM. We got to the hotel by 10 AM – taking SkyBus to the city center – to drop off luggage, because the room wouldn’t be available until 2 PM.

We were still tired, though – when the hotel finally gave us access to a room, we inadvertently took a three hour nap.

Chicago’s massive parking footprint – as measured on December 25, 2019

No, I didn’t spend my day outside measuring parking lots. I spent it inside measuring existing, available data from OpenStreetMap. The last time I measured the amount of parking area in Chicagoland was on September 16, 2018, using the same data source.

Using the footprints of parking lots and garages drawn into OpenStreetMap as a data source, the area of land in a portion of Chicagoland occupied by parking lots and garages is 254,429,609 square feet. This portion represents the “envelope” of the Chicago city limits.

This map shows the tan-colored “clipping boundary” envelope in which the parking lots were measured. The Chicago city limits are shown in pink.

Last year it was 247,539,968 square feet, so the measured area of this portion of Chicagoland’s parking lots and garages increased by a hair over 2.7 percent. This isn’t necessarily new parking areas, but it’s parking areas that have been documented and mapped.

254,429,609 square feet converts to:

  • 5,841 acres
  • 9.13 mi^2 (square miles)
  • 23.64 km^2 (square kilometers)

Looking at just the City of Chicago limits, though, the land area of Chicago occupied by already-mapped parking lots and garages is 163,995,621 square feet, or about 2.5 percent of Chicago’s area.

That converts to:

  • 3,765 acres
  • 6.56 mi^2 (square miles)
  • 14.76 km^2 (square kilometers)
  • 2.5% area of Chicago is parking (Chicago’s land area is ~589.56 km^2 )

Want to make your own analysis? Here’s the study area I used, formatted as GeoJSON:

{"type": "Polygon", "coordinates": [[[-87.94, 41.639999999999986], [-87.94, 42.02000000000001], [-87.52, 42.02000000000001], [-87.52, 41.639999999999986], [-87.94, 41.639999999999986]]]}