Category: Uncategorized

The high rises of the Bronzeville lakefront

3600 S King Dr

I shot this aerial photo yesterday from the 3600 block of South Dr. Martin Luther King Jr. Drive.

The “towers in the park” are three groups of apartment buildings (none are public housing).

From north (background) to south (foreground):

  • Prairie Shores (5 identical buildings that are very hard to see)
  • Lake Meadows (4 identical buildings behind the associated one-story shopping center plus multiple buildings to the east, closer to the lakefront)
  • T.K. Lawless Gardens (3 identical buildings, that appear the tallest because they’re the closest, 746 units, and 54 townhouses not seen)

John Warren Moutoussamy (an architecture graduate of Illinois Institute of Technology who studied under Ludwig Mies van der Rohe) designed Lawless Gardens, according to this IIT Magazine article.

Two-flat owner journal 1: Peoples Gas charges a lot of money for no gas

One of the first things I did after I bought a two-flat in July was contact Peoples Gas and Comed to ensure utilities were in my name, and that the utility connections would not be interrupted.

A few days later I decided that I wasn’t going to move in, because I wanted to make a good amount of changes and the best time to do that would be when nobody is living there. “Good amount of changes” turned into “gut rehab”. One of my friends is an architect and we (mostly him) are drawing permit plans right now.

It wasn’t until a week ago (8 weeks since I bought the house) that I realized there’s no reason to be paying for Peoples Gas to maintain a connection when I’m not using natural gas.

I’m writing this journal entry to exclaim how expensive it is to just “leave the gas line connected”.

It costs $50 per month per unit to have the privilege of possibly purchasing the delivery of natural gas through a pipe. Both units used 0 therms in the longest-period bill I received. (I received three bills, only one of which was for 30 days.)

The bill for the only 30-day period Peoples Gas served one of the units in my two-flat. Both units are unoccupied until after the gut rehab is completed. See the “customer charge”, which is the charge just to have an account open and for the potential to use gas.

I’ve got to pay $50 per unit for no gas.

I visited a three-flat under construction in Pilsen on Friday, and talked to the developer, Brent. He described how he’s following high-efficiency building wall standards to create a “tight envelope” (one in which very little air can leak) so that the tenants can “receive the comfort they’re paying for”. When it comes to setting the thermostat, the air delivered by HVAC machines should match that exactly, no more, no less. No oversized furnaces pushing too much heated air because so much of the air leaks through the walls and windows.

And, as a way to control costs, Brent will not connect a natural gas pipe to the building, mostly because of the expensive and default customer charge that persists even when no gas is used. A VRF (variable refrigerant flow) and heat pump machines will be entirely powered by electricity to serve the tenant’s heating and cooling needs.

Brent said that the tight building envelope coupled with the high-efficiency HVAC means that it’s more cost effective to use electricity to heat a house than natural gas.

After our meeting, I looked again at my final bill from Peoples Gas (I closed the account two days prior) and understood what Brent was saying about controlling costs. With an electric water heater and an electric range, there’s no need to have any gas connection.

I will probably have to keep the gas at my two-flat, to power the furnaces, because I don’t have the expertise or financial resources to renovate an existing building to have a tight enough envelope to make electrically-generated heat more cost effective than gas-generated heat.

Update January 13, 2021: I turned off the gas and closed the accounts so I don’t have to waste any money while I’m not living there (a gut rehab still needs to happen).

To keep water pipes from freezing and bursting I cleared the vast majority of water lines and added an electric pipe heating cable to keep the remaining sections warm.

The two water service pipes (I don’t know why there are two) have an electric pipe heating cable and are wrapped in foam insulation. The cable has a thermostat that touches the pipe and starts heating when the pipe drops to 38°F. The pipe is heated until the thermostat detects ~46°F.

What is this place? Des Plaines “park” edition

Screenshot shows the “park” in Des Plaines, Illinois.

I was methodically reviewing features on OpenStreetMap that could benefit from additional attributes, namely missing names and cities. The information on OpenStreetMap feeds into Chicago Cityscape so that the real estate information service I created can show people looking up addresses the locations of nearby amenities, including parks.

This particularly large park in Des Plaines had no name and no city, so I started investigating. From the overhead imagery view, it looked to me like a landfill.

Since there was a forest preserve to the east, I looked in the Forest Preserve District of Cook County’s nice interactive map for any properties near Beck Lake. Nothing found.

Overhead imagery of the so-called “park”. Source: Mapbox

I looked on Google Maps Street View for some insight and there was no name. (Note that data from Google Maps cannot be used to amend OpenStreetMap because of Google Maps having a data license that isn’t compatible with OSM’s openness requirements).

Next I looked at the Cook County parcel map that Chicago Cityscape has to identify its “Property Index Number” (PIN). All of it fits within 04-31-300-003-0000 – the database pointed to “Catholic Cemeteries” as the property owner.

I looked up that PIN on the Cook County Recorder of Deeds to try and find more information. Lo and behold, the most recent document that was recorded against the PIN was for a “Memorandum of Option Agreement” that identified the Catholic Bishop of Chicago as the landlord and Patriot Acres, LLC, as the tenant.

Patriot Acres has a website that describes a new commercial composting facility being built at this location.

I re-tagged the feature on OSM with “amenity=waste_disposal” and “waste=organic” to properly describe it as a composting facility, and not a park. Case closed.

Chicago bonfire rules

I believe that more people are hosting more backyard bonfires in Chicago in order to spend time with friends outdoors, because it’s one of the lowest risk ways to still enjoy life during the COVID-19 pandemic that Trump is prolonging.

The City of Chicago has rules on what can be burned. Be sensitive to neighbors who rely in keeping their windows open to cool and ventilate their homes.

The summary version is this: You can burn manufactured firewood that you purchased, and you can burn wood logs from any trees someone manually cut. All logs have to be untreated and two feet long or shorter.

You can’t burn scrap wood, lumber, plywood, or particle board. You also cannot burn garbage. Avoid having bonfires when the wind speed is 15 miles per hour or faster.

Read all the rules in this PDF. The rules aren’t in the municipal code; the code in section 11-4-740 says that the environment commissioner shall make rules (Chicago doesn’t currently have a department of environment).

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;