<?xml version="1.0" encoding="UTF-8" ?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" version="2.0"><channel><title>CrunchyData Blog</title>
<atom:link href="https://www.crunchydata.com/blog/topic/fun-with-postgres/rss.xml" rel="self" type="application/rss+xml" />
<link>https://www.crunchydata.com/blog/topic/fun-with-postgres</link>
<image><url>https://www.crunchydata.com/card.png</url>
<title>CrunchyData Blog</title>
<link>https://www.crunchydata.com/blog/topic/fun-with-postgres</link>
<width>800</width>
<height>419</height></image>
<description>PostgreSQL experts from Crunchy Data share advice, performance tips, and guides on successfully running PostgreSQL and Kubernetes solutions</description>
<language>en-us</language>
<pubDate>Fri, 15 Dec 2023 08:00:00 EST</pubDate>
<dc:date>2023-12-15T13:00:00.000Z</dc:date>
<dc:language>en-us</dc:language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<item><title><![CDATA[ The Postgres Playground: BYOS (Bring Your Own SQL) ]]></title>
<link>https://www.crunchydata.com/blog/the-postgres-playground-byos-bring-your-own-sql</link>
<description><![CDATA[ Craig has tips for using our Postgres playground with your own SQL. ]]></description>
<content:encoded><![CDATA[ <p>The Postgres playground started with a <a href=https://www.crunchydata.com/blog/crazy-idea-to-postgres-in-the-web-browser>fun idea</a> of "I wonder if I can put Postgres in a web browser". It evolved to a <a href=https://www.crunchydata.com/developers/tutorials>full set of guided tutorials</a> that you can follow along step by step with. We actually use Notion to power all of our tutorials and an in-house API. This means a new tutorial is as simple as some SQL and some markdown. But, did you know you can bring your own setup to the playground?<h2 id=just-postgres-in-a-browser><a href=#just-postgres-in-a-browser>Just Postgres in a browser</a></h2><p>All of our curated tutorials exist directly on the main <a href=https://www.crunchydata.com/developers>Crunchy Data Developer area</a>. However, you can visit an empty Postgres playground directly at: <a href=https://www.crunchydata.com/developers/playground>https://www.crunchydata.com/developers/playground</a>. This is a blank slate Postgres instance, utilizing compute and memory directly from your web browser. It is not persistent and not connected to a remote session.<p><img alt="empty postgres playground"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/dd4b3558-ae35-4afe-a842-10898de47500/public><h2 id=creating-your-own-sql-files><a href=#creating-your-own-sql-files>Creating your own SQL files</a></h2><p>If you pass in a parameter for a <code>sql</code> file it, the playground will automatically run that once the Postgres session is started.<p>We can take a very basic set of SQL, something like this:<pre><code class=language-sql>CREATE TABLE example (
  id serial,
  data text);

INSERT INTO example (data) VALUES ('hello');
INSERT INTO example (data) VALUES ('world');
</code></pre><p>You can build longer SQL files for full data inputs. If you run a <a href=https://www.crunchydata.com/blog/introduction-to-postgres-backups>pg_dump</a> from an existing database, that file will also work.<p>Like a restore file, you can also list SET statements for the session<pre><code class=language-sql>SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
</code></pre><h2 id=load-your-sql-file-into-a-url><a href=#load-your-sql-file-into-a-url>Load your SQL file into a url</a></h2><p>The easiest way to host a sql file online is to use the GitHub gist feature. You’ll create a new gist, give it a name. Then view the raw link. The raw view link is the URL we want for working with the playground.<p>My example my file is:<pre><code> https://gist.githubusercontent.com/craigkerstiens/62e00c80df31700302726f77595b7661/raw/3aa83426c148ac77bbed0f80ef31f433cf7e5053/example.sql
</code></pre><p><img alt="github gist"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/47479b97-7287-4860-0033-93378be14900/public><h2 id=build-your-own-playground-urls><a href=#build-your-own-playground-urls>Build your own playground URLs</a></h2><p>Now we can pass that into the playground and build a longer url with:<p>1 - The base playground URL: <code>https://www.crunchydata.com/developers/playground</code><p>2 - The sql parameter <code>?sql=</code><p>3 - Your full gist URL <code>https://gist.githubusercontent.com/craigkerstiens/62e00c80df31700302726f77595b7661/raw/3aa83426c148ac77bbed0f80ef31f433cf7e5053/example.sql</code><p><img alt="url with param"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/c0bc2a80-b8a7-4f4f-fa8c-44e972416300/public><p>Resulting in this:<pre><code>https://www.crunchydata.com/developers/playground?sql=https://gist.githubusercontent.com/craigkerstiens/62e00c80df31700302726f77595b7661/raw/3aa83426c148ac77bbed0f80ef31f433cf7e5053/example.sql
</code></pre><p>I’ve been really excited to see how many people are using the Postgres playground for education and training. Let us know how you’re using the playground on X <a href=https://twitter.com/crunchydata>@crunchydata</a>. ]]></content:encoded>
<category><![CDATA[ Fun with Postgres ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">94a3fc4862f5f58d72d67a639cbc519b4a34820351617ab500b4fc0235888fff</guid>
<pubDate>Fri, 15 Dec 2023 08:00:00 EST</pubDate>
<dc:date>2023-12-15T13:00:00.000Z</dc:date>
<atom:updated>2023-12-15T13:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Rolling the Dice with the PostgreSQL Random Functions ]]></title>
<link>https://www.crunchydata.com/blog/rolling-the-dice-with-postgres-random-function</link>
<description><![CDATA[ Paul shows examples of generating random numbers, random integers, random text values, and random groups.  He also shows off the new random_normal function which just came out with Postgres 16. ]]></description>
<content:encoded><![CDATA[ <p>Generating random numbers is a surprisingly common task in programs, whether it's to create test data or to provide a user with a random entry from a list of items.<p>PostgreSQL comes with <a href=https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE>just a few</a> simple foundational functions that can be used to fulfill most needs for randomness.<p>Almost all your random-ness needs will be met with the <code>random()</code> function.<h2 id=uniformity><a href=#uniformity>Uniformity</a></h2><p>The <code>random()</code> function returns a double precision float in a <a href=https://en.wikipedia.org/wiki/Continuous_uniform_distribution>continuous uniform distribution</a> between 0.0 and 1.0.<p>What does that mean? It means that you could get any value between 0.0 and 1.0, with equal probability, for each call of <code>random()</code>.<p><img alt="a numeric line representing all numbers between zero and 1."loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/7155a06a-c70f-447e-fdd1-7bcd12181400/public><p>Here's five uniform random numbers between 0.0 and 1.0.<pre><code class=language-pgsql>SELECT random() FROM generate_series(1, 5)
</code></pre><pre><code class=language-text>0.3978842227698167
0.7438732417540841
0.3875091442400458
0.4108009373061563
0.5524543763568912
</code></pre><p>Yep, those look pretty random! But, maybe not so useful?<h2 id=random-numbers><a href=#random-numbers>Random Numbers</a></h2><p>Most times when people are trying to generate random numbers, they are looking for random <strong>integers</strong> in a range, not random floats between 0.0 and 1.0.<p>Say you wanted random integers between 1 and 10, inclusive. How do you get that, starting from <code>random()</code>?<p>Start by scaling an ordinary <code>random()</code> number up be a factor of 10! Now you have a continuous distribution between 0 and 10.<p><img alt="a numeric line of all numbers represented by 10 * random()"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/00fab708-39c1-4468-7cb8-3c1386391100/public><pre><code class=language-pgsql>SELECT 10 * random() FROM generate_series(1, 5)
</code></pre><pre><code class=language-text>3.978842227698167
7.438732417540841
3.875091442400458
4.108009373061563
5.5245437635689125
</code></pre><p>Then, if you push every one of those numbers down to the nearest integer using <code>floor()</code> you'll end up with a random integer between 0 and 9.<p><img alt="a numeric line representing al descrete values from 0 to 9"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/5d351239-4f46-4e3a-0d9e-2aabf8ea9e00/public><pre><code class=language-pgsql>SELECT floor(10 * random()) FROM generate_series(1, 5)
</code></pre><pre><code class=language-text>4
8
4
5
6
</code></pre><p>If you wanted a random integer between 1 and 10, you just need to add 1 to the zero-base number.<p><img alt="a numeric line representing all descrete values from 1 to 10"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/06e4229b-5e34-46c5-a3a6-812d0542f500/public><pre><code class=language-pgsql>SELECT floor(10 * random()) + 1 FROM generate_series(1, 5)
</code></pre><pre><code class=language-text>3
7
3
4
5
</code></pre><h2 id=random-rows-and-values><a href=#random-rows-and-values>Random Rows and Values</a></h2><p>Sometimes the things you are trying to do randomly aren't numbers. How do you get a random entry out of a string? Or a random row from a table?<p>We already saw how to get one-based integers from <code>random()</code> and we can apply that technique to the problem of pulling an entry from an array.<pre><code class=language-pgsql>WITH f AS (
    SELECT ARRAY[
        'apple',
        'banana',
        'cherry',
        'pear',
        'peach'] AS fruits
)
SELECT fruits[ceil(array_length(fruits,1) * random())] AS snack
FROM f;
</code></pre><pre><code class=language-text> snack
-------
 peach
</code></pre><p>Getting a random row involves some tradeoffs and thinking. For a random value from a small table, the naive way to get a single random value is this.<pre><code class=language-pgsql>SELECT *
FROM fruits
ORDER BY random()
LIMIT 1
</code></pre><p><img alt="a visual representing of building a SQL query"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/6b7f8d8e-6bff-46ed-a422-da29774b9200/public><p>As you can imagine, this gets quite expensive if the <code>fruits</code> table gets too large, since it sorts the whole table every time.<p>If you only need a single random row, one way to achieve that is to add a random column to your table and index it.<pre><code class=language-pgsql>CREATE TABLE fruits (
  id SERIAL PRIMARY KEY,
  fruit TEXT NOT NULL,
  random FLOAT8 DEFAULT random()
  );

INSERT INTO fruits (fruit)
  VALUES ('apple'),('banana'),('cherry'),('pear'),('peach');

CREATE INDEX fruits_random_x ON fruits (random);
</code></pre><p><img alt="a visual respentation of building a SQL query with the underlying data visable"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/d8c5fcbc-3fd9-4437-3b72-fc1231f7b900/public><p>Then when it's time to search, use the random function to generate a starting search location and find the next highest value.<pre><code class=language-pgsql>SELECT *
FROM fruits
WHERE random > random()
ORDER BY random ASC
LIMIT 1;
</code></pre><pre><code class=language-text> id | fruit  |       random
----+--------+--------------------
  8 | banana | 0.1997961574379754
</code></pre><p>Be careful using this trick for more than one row though: since the values in the random column are fixed, the sequences of rows returned will be deterministic, even if the start row is random.<p>If you want to pull large portions of a table into a query (for random sampling, for example) look at the <code>TABLESAMPLE</code> clause of the <a href=https://www.postgresql.org/docs/current/sql-select.html><code>SELECT</code></a> command.<h2 id=random-groups><a href=#random-groups>Random Groups</a></h2><p>Suppose I wanted the entire contents of the fruits collection, but returned in two random groups? This is actually much like getting a single random value: order the whole set randomly, and then use that ordering to determine grouping.<pre><code class=language-pgsql>WITH random_fruits AS (
    SELECT id, fruit
    FROM fruits
    ORDER BY random()
)
SELECT row_number() over () % 2 AS group,
       id, fruit
FROM random_fruits
ORDER BY 1;
</code></pre><pre><code class=language-text> group | id | fruit
-------+----+--------
     0 | 11 | peach
     0 |  8 | banana
     1 | 10 | pear
     1 |  7 | apple
     1 |  9 | cherry
</code></pre><p>The '2' in the example above is the number of groups desired.<h2 id=random_normal><a href=#random_normal><code>random_normal</code></a></h2><p>So far we have just been looking at ways to permute the uniform distribution offered by the <code>random()</code> function. But there is in fact an infinite number of other probability distributions that random numbers could be a part of.<p>Of that infinite collection, by far the most frequently used in practice is the "<a href=https://en.wikipedia.org/wiki/Normal_distribution>normal distribution</a>" also known as the "Gaussian distribution" or "bell curve".<p>Rather than having a hard cut-off point, the normal distribution has a frequent center and then ever lower probability of values out to infinity in both directions.<p>The position of the center of the distribution is the "mean" and the rate of probability decay is controlled by the "standard deviation".<p><img alt="a bell curve reprensenting standard deviations"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/ac808a5d-1bc5-474e-e631-8a957dedbd00/public><p>To generate normally distributed data in PostgreSQL, use the <code>random_normal(mean, stddev)</code> function that was introduced in <a href=https://www.postgresql.org/docs/current/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE>version 16</a>.<pre><code class=language-pgsql>SELECT random_normal(0, 1)
FROM generate_series(1,10)
ORDER BY 1
</code></pre><pre><code class=language-text> -0.8147201382612904
 -0.5751449000210354
 -0.4643454485382744
 -0.0630592935151314
 0.26438942114339203
 0.39298889191244274
  0.4946046063256206
  0.8560911955145666
  1.3534309793797454
   1.664493506727331
</code></pre><p>It's kind of hard to appreciate that the data have a central tendency without generating a lot more of them and counting how many fall within each bin.<pre><code class=language-pgsql>SELECT random_normal()::integer,
       Count(*)
FROM generate_series(1,1000)
GROUP BY 1
ORDER BY 1
</code></pre><p>The cast to <code>integer</code> rounds the values towards the nearest integer, so you can see how the data are mostly between the first two standard deviations of the mean.<pre><code class=language-text> random_normal | count
---------------+-------
            -3 |     5
            -2 |    65
            -1 |   233
             0 |   378
             1 |   246
             2 |    67
             3 |     5
             4 |     1
</code></pre><h2 id=seeds-and-pseudo-randomness><a href=#seeds-and-pseudo-randomness>Seeds and Pseudo-randomness</a></h2><p>If you looked <strong>very</strong> closely at the examples in the first section you'll have noticed that they all started from the same, allegedly random values.<p>If <code>random()</code> truly is random, how did I get the same starting values four times in a row?<p>The answer, shockingly, is that <code>random()</code> is actually "<a href=https://en.wikipedia.org/wiki/Pseudorandomness>pseudo-random</a>".<p>A pseudorandom sequence of numbers is one that appears to be statistically random, despite having been produced by a completely deterministic and repeatable process.<p>With a pseudo-random number generator and a known starting point, I will always get the same sequence of numbers, at least on the same computer.<p>The reason most computer programs use pseudo-random number generators is that generating truly random numbers is actually quite an expensive operation (relatively speaking).<p>So programs instead generate one truly random number, and use that as a "seed" for a generator.<p>PostgreSQL uses the Blackman/Vigna "<a href=https://en.wikipedia.org/wiki/Xoroshiro128%2B>xoroshiro128 1.0</a>" pseudo-random number generator.<p>By default, on start-up PostgreSQL sets up a seed value by calling an external random number generator, using an appropriate method for the platform:<ul><li>Using OpenSSL <code>RAND_bytes()</code> if available, or<li>using Windows <code>CryptGenRandom()</code> on that platform, or<li>using the operating system <code>/dev/urandom</code> if necessary.</ul><p>So if you are interested in a random number, just calling <code>random()</code> will get you one every time.<p>But if you want to put your finger on the scales, you can use the <code>setseed()</code> function to cause your <code>random()</code> and <code>random_normal()</code> functions to generate a deterministic series of random numbers, starting from a seed value you specify. ]]></content:encoded>
<category><![CDATA[ Fun with Postgres ]]></category>
<author><![CDATA[ Paul.Ramsey@crunchydata.com (Paul Ramsey) ]]></author>
<dc:creator><![CDATA[ Paul Ramsey ]]></dc:creator>
<guid isPermalink="false">0b769e6091f1f43bae7e2cf18e86b756008f4fb621f9b308fb6c46f206282cb5</guid>
<pubDate>Fri, 22 Sep 2023 09:00:00 EDT</pubDate>
<dc:date>2023-09-22T13:00:00.000Z</dc:date>
<atom:updated>2023-09-22T13:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ XKCD Bad Map Projection with PostGIS ]]></title>
<link>https://www.crunchydata.com/blog/xkcd-bad-map-projection-with-postgis</link>
<description><![CDATA[ In honor of the beloved XKCD comic, Paul recreates some bad maps using PostGIS functions. Check out these funky cartographic contortions and learn a little spatial sql along the way. ]]></description>
<content:encoded><![CDATA[ <p>Last week, <a href=https://en.wikipedia.org/wiki/Randall_Munroe>Randall Munroe</a> dropped his latest <a href=https://xkcd.com>XKCD</a> "Bad Map Projection", number six, "<a href=https://xkcd.com/2807/>ABS(Longitude)</a>", which looks like this:<p><img alt=ABS(Longitude) loading=lazy src=https://imgs.xkcd.com/comics/bad_map_projection_abs_longitude.png><p>Truly this is a bad map projection, on a par with the previous five:<ul><li><a href=https://xkcd.com/1784/>Liquid Resize</a><li><a href=https://xkcd.com/1799/>Time Zones</a><li><a href=https://xkcd.com/2256/>South America</a><li><a href=https://xkcd.com/2489/>Greenland Special</a><li><a href=https://xkcd.com/2613/>Madagascator</a></ul><p>The last two are just applications of common map projections with very uncommon projection parameters that accentuate certain areas of the globe, a cartographic version of the classic <a href=https://en.wikipedia.org/wiki/View_of_the_World_from_9th_Avenue>"View of the World from 9th Avenue"</a>.<p><img alt="View of the World from 9th Avenue"loading=lazy src=https://upload.wikimedia.org/wikipedia/en/4/4d/Steinberg_New_Yorker_Cover.png><p>A colleague asked me if we could recreate <a href=https://xkcd.com/2807/>ABS(Longitude)</a> and I figured it was worth a try!<h2 id=getting-data><a href=#getting-data>Getting Data</a></h2><p>At a minimum, we want a countries layer and some independent place labels to provide context, which is available at the first stop for basic global data, <a href=https://www.naturalearthdata.com/downloads/110m-cultural-vectors/>Natural Earth</a>.<p>We have been playing with <a href=https://gdal.org/programs/ogr2ogr.html>ogr2ogr</a> and weird <a href=https://www.crunchydata.com/blog/remote-access-anything-from-postgres>remote access tricks</a> lately, and we can use <code>ogr2ogr</code> to load the data in one step.<pre><code class=language-bash># Load the countries and places directly from the remote
# zip file into the working PostgreSQL database

ogr2ogr \
 -f PostgreSQL \
 -nlt PROMOTE_TO_MULTI \
 -lco OVERWRITE=yes \
 -lco GEOMETRY_NAME=geom \
 postgresql://postgres@localhost/xkcd \
 /vsizip//vsicurl/https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip

ogr2ogr \
 -f PostgreSQL \
 -lco OVERWRITE=yes \
 -lco GEOMETRY_NAME=geom \
 postgresql://postgres@localhost/xkcd \
 /vsizip//vsicurl/https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_populated_places.zip
</code></pre><h2 id=preparing-the-data><a href=#preparing-the-data>Preparing the Data</a></h2><p>Now we have the data in the database, read to go!<p><img alt="Step 1"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/8d6d8164-a0f6-42df-5275-9f32c35c9d00/public><p>The process we are going to apply will be transforming the shapes of one polygon at a time, and the Natural Earth data models the countries with one <strong>MultiPolygon</strong> per country.<p><img alt=Canada loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/41bb6b7f-f573-415c-fa3b-0ffa90e5e200/public><p>Canada, for example, is one country, but 30 polygons.<p>We want a table with just one row for each polygon, so we "dump" all the multi-polygons using <a href=https://postgis.net/docs/ST_Dump.html><code>ST_Dump()</code></a>.<pre><code class=language-pgsql>CREATE SEQUENCE country_id;

CREATE TABLE countries AS
  SELECT nextval('country_id') AS id,
    name,
    (ST_Dump(geom)).geom::geometry(Polygon, 4326) AS geom
  FROM ne_110m_admin_0_countries;
</code></pre><p>Next, because we are going to be processing western shapes with negative longitude different from eastern shapes we have to solve the problem: what to do with shapes that straddle the <a href=https://en.wikipedia.org/wiki/Prime_meridian>prime meridian</a>?<p><img alt="Prime Meridian"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/f69c0c46-7d7e-4403-fa28-c43ab18faf00/public><p>The answer: <a href=https://postgis.net/docs/ST_Split.html><code>ST_Split()</code></a>!<p>First we create a prime meridian geometry to use as a "splitting blade".<pre><code class=language-pgsql>CREATE TABLE lon_0 AS
  SELECT ST_SetSrid(
    ST_MakeLine(
      ST_MakePoint(0,90),
      ST_MakePoint(0,-90)),
    4326)::geometry(LineString, 4326) AS geom;
</code></pre><p>Then we apply that blade to all the shapes that fall under it.<pre><code class=language-pgsql>CREATE TABLE split_at_0 AS
  SELECT id, name, ST_CollectionHomogenize(
    ST_Split(c.geom, lon_0.geom))::geometry(MultiPolygon, 4326) AS geom
  FROM countries c
  JOIN lon_0
  ON ST_Intersects(c.geom, lon_0.geom);
</code></pre><p>Surprisingly few countries end up chopped by the meridian.<p><img alt="Split countries"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/e4e5762d-46f1-4e49-51bb-7f9c1beff200/public><p>The output of the split is, for each input polygon, a multi-polygon of the components. But we want to operate on the shapes one polygon at a time, so again, we must dump the multi-polygon into its components.<p>A slightly longer query dumps the split shapes, and stores them in a table with the rest of the un-split polygons, labeling each shape depending on whether it is "west" or "east" of the prime meridian.<pre><code class=language-pgsql>CREATE TABLE countries_split AS
WITH split AS (
  SELECT id, name, (ST_Dump(geom)).geom::geometry(Polygon, 4326) AS geom
  FROM split_at_0
)
SELECT c.id, c.name, c.geom,
  CASE WHEN ST_X(ST_StartPoint(c.geom)) >= 0 THEN 'east' ELSE 'west' END AS side
FROM countries c
LEFT JOIN split s
USING (id)
WHERE s.id IS NULL
UNION ALL
SELECT s.id, s.name, s.geom,
  CASE WHEN ST_X(ST_StartPoint(s.geom)) >= 0 THEN 'east' ELSE 'west' END AS side
FROM split s;
</code></pre><p>We have divided the west from the east, and are ready for the final step.<p><img alt="Split countries"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/5f4bbd3f-ec50-4187-fec7-cbdc7f068f00/public><h2 id=flipping-the-west><a href=#flipping-the-west>Flipping the West</a></h2><p>Now we are ready to apply a transformation to all the "west" countries, to turn their negative longitudes into positive ones.<p>To do this, we will use the powerful <a href=https://postgis.net/docs/ST_Affine.html><code>ST_Affine()</code></a> function.<p>The two-dimensional form of the function looks like this:<pre><code class=language-text>ST_Affine(geom, a, b, d, e, xoff, yoff)
</code></pre><p>Where the parameters correspond to an affine transformation matrix:</p><mjx-container class=MathJax jax=SVG display=true><svg height=8.597ex viewBox="0 -2150 6463 3800"width=14.622ex focusable=false role=img style=vertical-align:-3.733ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M319 -645V1154H666V1070H403V-645H319Z"id=MJX-1-TEX-S4-23A1 /><path d="M319 -644V1155H403V-560H666V-644H319Z"id=MJX-1-TEX-S4-23A3 /><path d="M319 0V602H403V0H319Z"id=MJX-1-TEX-S4-23A2 /><path d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"id=MJX-1-TEX-I-1D44E /><path d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"id=MJX-1-TEX-I-1D44F /><path d="M35 393Q35 417 46 424T89 432Q95 432 112 432T141 431H223Q238 422 241 415T244 393Q244 389 244 383T237 367T216 355Q209 355 209 354L234 319Q259 286 260 286L308 354Q308 355 301 355Q285 356 278 365T270 384L271 393Q271 420 289 428Q295 431 376 431H459Q460 430 465 427T472 422T476 416T480 407T481 393Q481 368 470 362T434 355H425H392L344 290Q295 225 295 223Q294 223 309 203T350 149L405 77L439 76H453Q474 76 484 69T495 38Q495 10 473 1H303Q281 9 281 32V38Q281 49 282 54T290 67T313 76Q324 76 324 77L259 173L197 77Q202 76 209 76Q225 75 233 68T241 55T242 38Q242 28 242 24T236 12T221 1H51Q29 9 29 32V38Q29 48 29 51T31 59T38 67T51 76H117L171 149Q224 222 224 223L124 355H90H78Q54 355 45 361T35 393Z"id=MJX-1-TEX-M-1D6A1 /><path d="M52 216Q52 318 118 379T261 440Q343 440 407 378T472 216Q472 121 410 58T262 -6Q176 -6 114 58T52 216ZM388 225Q388 281 351 322T261 364Q213 364 175 325T136 225Q136 158 174 114T262 70T350 114T388 225Z"id=MJX-1-TEX-M-1D698 /><path d="M43 395Q44 405 44 408T47 416T53 423T66 431H176V461Q176 500 182 518Q201 570 252 593T353 617Q399 614 418 593T437 548Q437 528 424 514T387 499Q365 499 353 511T338 537V541H328Q275 536 261 494Q260 490 260 460V431H327Q334 431 346 431T364 432Q392 432 404 425T416 393T405 362T365 355H327H260V76H319Q375 76 388 71T401 38Q401 27 400 23T395 12T379 1H58Q47 6 42 12T36 23T35 38Q35 65 53 73Q59 76 117 76H176V355H121H93Q64 355 54 362T43 395Z"id=MJX-1-TEX-M-1D68F /><path d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"id=MJX-1-TEX-I-1D451 /><path d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"id=MJX-1-TEX-I-1D452 /><path d="M26 393Q26 417 37 424T80 431H134H217Q232 422 235 416T239 393Q239 379 236 371T226 360T214 356T197 355L179 354V353L188 330Q197 306 209 272T235 201T259 133T271 89V84L274 95Q279 122 298 185T335 300T352 354Q352 355 331 355Q312 355 304 358Q288 368 288 393Q288 408 291 415T310 431H478Q479 430 484 427T491 422T495 416T499 407T500 393Q500 376 493 367T479 357T458 355H452Q426 355 425 353Q420 337 351 124T280 -94Q240 -195 168 -220Q147 -228 125 -228Q89 -228 66 -201T42 -139Q42 -116 56 -102T93 -87Q117 -87 130 -102T144 -135V-138H126Q121 -148 121 -150T130 -152Q182 -147 207 -87Q211 -78 223 -40T236 1Q230 10 102 355H75L49 356Q26 363 26 393Z"id=MJX-1-TEX-M-1D6A2 /><path d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"id=MJX-1-TEX-N-30 /><path d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"id=MJX-1-TEX-N-31 /><path d="M0 1070V1154H347V-645H263V1070H0Z"id=MJX-1-TEX-S4-23A4 /><path d="M263 -560V1155H347V-644H0V-560H263Z"id=MJX-1-TEX-S4-23A6 /><path d="M263 0V602H347V0H263Z"id=MJX-1-TEX-S4-23A5 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=mrow><g data-mml-node=mo><use data-c=23A1 xlink:href=#MJX-1-TEX-S4-23A1 transform=translate(0,996) /><use data-c=23A3 xlink:href=#MJX-1-TEX-S4-23A3 transform=translate(0,-1006) /><svg height=402 viewBox="0 100.5 667 402"width=667 x=0 y=49><use data-c=23A2 xlink:href=#MJX-1-TEX-S4-23A2 transform=scale(1,1.002) /></svg></g><g data-mml-node=mtable transform=translate(667,0)><g data-mml-node=mtr transform=translate(0,1400)><g data-mml-node=mtd><g data-mml-node=mi><use data-c=1D44E xlink:href=#MJX-1-TEX-I-1D44E /></g></g><g data-mml-node=mtd transform=translate(1564.5,0)><g data-mml-node=mi><use data-c=1D44F xlink:href=#MJX-1-TEX-I-1D44F /></g></g><g data-mml-node=mtd transform=translate(3029,0)><g data-mml-node=TeXAtom data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A1 xlink:href=#MJX-1-TEX-M-1D6A1 /><use data-c=1D698 xlink:href=#MJX-1-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-1-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-1-TEX-M-1D68F transform=translate(1575,0) /></g></g></g></g><g data-mml-node=mtr><g data-mml-node=mtd transform=translate(4.5,0)><g data-mml-node=mi><use data-c=1D451 xlink:href=#MJX-1-TEX-I-1D451 /></g></g><g data-mml-node=mtd transform=translate(1546,0)><g data-mml-node=mi><use data-c=1D452 xlink:href=#MJX-1-TEX-I-1D452 /></g></g><g data-mml-node=mtd transform=translate(3029,0)><g data-mml-node=TeXAtom data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A2 xlink:href=#MJX-1-TEX-M-1D6A2 /><use data-c=1D698 xlink:href=#MJX-1-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-1-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-1-TEX-M-1D68F transform=translate(1575,0) /></g></g></g></g><g data-mml-node=mtr transform=translate(0,-1400)><g data-mml-node=mtd transform=translate(14.5,0)><g data-mml-node=mn><use data-c=30 xlink:href=#MJX-1-TEX-N-30 /></g></g><g data-mml-node=mtd transform=translate(1529,0)><g data-mml-node=mn><use data-c=30 xlink:href=#MJX-1-TEX-N-30 /></g></g><g data-mml-node=mtd transform=translate(3829,0)><g data-mml-node=mn><use data-c=31 xlink:href=#MJX-1-TEX-N-31 /></g></g></g></g><g data-mml-node=mo transform=translate(5796,0)><use data-c=23A4 xlink:href=#MJX-1-TEX-S4-23A4 transform=translate(0,996) /><use data-c=23A6 xlink:href=#MJX-1-TEX-S4-23A6 transform=translate(0,-1006) /><svg height=402 viewBox="0 100.5 667 402"width=667 x=0 y=49><use data-c=23A5 xlink:href=#MJX-1-TEX-S4-23A5 transform=scale(1,1.002) /></svg></g></g></g></g></svg></mjx-container><p>Or, in equation form:<ul><li><mjx-container class=MathJax jax=SVG><svg height=2.181ex viewBox="0 -759 8747.9 964"width=19.792ex focusable=false role=img style=vertical-align:-.464ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"id=MJX-2-TEX-I-1D465 /><path d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"id=MJX-2-TEX-V-2032 /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-2-TEX-N-3D /><path d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"id=MJX-2-TEX-I-1D44E /><path d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"id=MJX-2-TEX-N-2B /><path d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"id=MJX-2-TEX-I-1D44F /><path d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"id=MJX-2-TEX-I-1D466 /><path d="M35 393Q35 417 46 424T89 432Q95 432 112 432T141 431H223Q238 422 241 415T244 393Q244 389 244 383T237 367T216 355Q209 355 209 354L234 319Q259 286 260 286L308 354Q308 355 301 355Q285 356 278 365T270 384L271 393Q271 420 289 428Q295 431 376 431H459Q460 430 465 427T472 422T476 416T480 407T481 393Q481 368 470 362T434 355H425H392L344 290Q295 225 295 223Q294 223 309 203T350 149L405 77L439 76H453Q474 76 484 69T495 38Q495 10 473 1H303Q281 9 281 32V38Q281 49 282 54T290 67T313 76Q324 76 324 77L259 173L197 77Q202 76 209 76Q225 75 233 68T241 55T242 38Q242 28 242 24T236 12T221 1H51Q29 9 29 32V38Q29 48 29 51T31 59T38 67T51 76H117L171 149Q224 222 224 223L124 355H90H78Q54 355 45 361T35 393Z"id=MJX-2-TEX-M-1D6A1 /><path d="M52 216Q52 318 118 379T261 440Q343 440 407 378T472 216Q472 121 410 58T262 -6Q176 -6 114 58T52 216ZM388 225Q388 281 351 322T261 364Q213 364 175 325T136 225Q136 158 174 114T262 70T350 114T388 225Z"id=MJX-2-TEX-M-1D698 /><path d="M43 395Q44 405 44 408T47 416T53 423T66 431H176V461Q176 500 182 518Q201 570 252 593T353 617Q399 614 418 593T437 548Q437 528 424 514T387 499Q365 499 353 511T338 537V541H328Q275 536 261 494Q260 490 260 460V431H327Q334 431 346 431T364 432Q392 432 404 425T416 393T405 362T365 355H327H260V76H319Q375 76 388 71T401 38Q401 27 400 23T395 12T379 1H58Q47 6 42 12T36 23T35 38Q35 65 53 73Q59 76 117 76H176V355H121H93Q64 355 54 362T43 395Z"id=MJX-2-TEX-M-1D68F /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=msup><g data-mml-node=mi><use data-c=1D465 xlink:href=#MJX-2-TEX-I-1D465 /></g><g data-mml-node=mo transform="translate(605,363) scale(0.707)"><use data-c=2032 xlink:href=#MJX-2-TEX-V-2032 /></g></g><g data-mml-node=mo transform=translate(1127.2,0)><use data-c=3D xlink:href=#MJX-2-TEX-N-3D /></g><g data-mml-node=mi transform=translate(2183,0)><use data-c=1D44E xlink:href=#MJX-2-TEX-I-1D44E /></g><g data-mml-node=mi transform=translate(2712,0)><use data-c=1D465 xlink:href=#MJX-2-TEX-I-1D465 /></g><g data-mml-node=mo transform=translate(3506.2,0)><use data-c=2B xlink:href=#MJX-2-TEX-N-2B /></g><g data-mml-node=mi transform=translate(4506.5,0)><use data-c=1D44F xlink:href=#MJX-2-TEX-I-1D44F /></g><g data-mml-node=mi transform=translate(4935.5,0)><use data-c=1D466 xlink:href=#MJX-2-TEX-I-1D466 /></g><g data-mml-node=mo transform=translate(5647.7,0)><use data-c=2B xlink:href=#MJX-2-TEX-N-2B /></g><g data-mml-node=TeXAtom transform=translate(6647.9,0) data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A1 xlink:href=#MJX-2-TEX-M-1D6A1 /><use data-c=1D698 xlink:href=#MJX-2-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-2-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-2-TEX-M-1D68F transform=translate(1575,0) /></g></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=2.233ex viewBox="0 -759 8693.9 987"width=19.669ex focusable=false role=img style=vertical-align:-.516ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"id=MJX-3-TEX-I-1D466 /><path d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"id=MJX-3-TEX-V-2032 /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-3-TEX-N-3D /><path d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"id=MJX-3-TEX-I-1D451 /><path d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"id=MJX-3-TEX-I-1D465 /><path d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"id=MJX-3-TEX-N-2B /><path d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"id=MJX-3-TEX-I-1D452 /><path d="M26 393Q26 417 37 424T80 431H134H217Q232 422 235 416T239 393Q239 379 236 371T226 360T214 356T197 355L179 354V353L188 330Q197 306 209 272T235 201T259 133T271 89V84L274 95Q279 122 298 185T335 300T352 354Q352 355 331 355Q312 355 304 358Q288 368 288 393Q288 408 291 415T310 431H478Q479 430 484 427T491 422T495 416T499 407T500 393Q500 376 493 367T479 357T458 355H452Q426 355 425 353Q420 337 351 124T280 -94Q240 -195 168 -220Q147 -228 125 -228Q89 -228 66 -201T42 -139Q42 -116 56 -102T93 -87Q117 -87 130 -102T144 -135V-138H126Q121 -148 121 -150T130 -152Q182 -147 207 -87Q211 -78 223 -40T236 1Q230 10 102 355H75L49 356Q26 363 26 393Z"id=MJX-3-TEX-M-1D6A2 /><path d="M52 216Q52 318 118 379T261 440Q343 440 407 378T472 216Q472 121 410 58T262 -6Q176 -6 114 58T52 216ZM388 225Q388 281 351 322T261 364Q213 364 175 325T136 225Q136 158 174 114T262 70T350 114T388 225Z"id=MJX-3-TEX-M-1D698 /><path d="M43 395Q44 405 44 408T47 416T53 423T66 431H176V461Q176 500 182 518Q201 570 252 593T353 617Q399 614 418 593T437 548Q437 528 424 514T387 499Q365 499 353 511T338 537V541H328Q275 536 261 494Q260 490 260 460V431H327Q334 431 346 431T364 432Q392 432 404 425T416 393T405 362T365 355H327H260V76H319Q375 76 388 71T401 38Q401 27 400 23T395 12T379 1H58Q47 6 42 12T36 23T35 38Q35 65 53 73Q59 76 117 76H176V355H121H93Q64 355 54 362T43 395Z"id=MJX-3-TEX-M-1D68F /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=msup><g data-mml-node=mi><use data-c=1D466 xlink:href=#MJX-3-TEX-I-1D466 /></g><g data-mml-node=mo transform="translate(523,363) scale(0.707)"><use data-c=2032 xlink:href=#MJX-3-TEX-V-2032 /></g></g><g data-mml-node=mo transform=translate(1045.2,0)><use data-c=3D xlink:href=#MJX-3-TEX-N-3D /></g><g data-mml-node=mi transform=translate(2101,0)><use data-c=1D451 xlink:href=#MJX-3-TEX-I-1D451 /></g><g data-mml-node=mi transform=translate(2621,0)><use data-c=1D465 xlink:href=#MJX-3-TEX-I-1D465 /></g><g data-mml-node=mo transform=translate(3415.2,0)><use data-c=2B xlink:href=#MJX-3-TEX-N-2B /></g><g data-mml-node=mi transform=translate(4415.5,0)><use data-c=1D452 xlink:href=#MJX-3-TEX-I-1D452 /></g><g data-mml-node=mi transform=translate(4881.5,0)><use data-c=1D466 xlink:href=#MJX-3-TEX-I-1D466 /></g><g data-mml-node=mo transform=translate(5593.7,0)><use data-c=2B xlink:href=#MJX-3-TEX-N-2B /></g><g data-mml-node=TeXAtom transform=translate(6593.9,0) data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A2 xlink:href=#MJX-3-TEX-M-1D6A2 /><use data-c=1D698 xlink:href=#MJX-3-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-3-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-3-TEX-M-1D68F transform=translate(1575,0) /></g></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=1.903ex viewBox="0 -759 2541 841"width=5.749ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"id=MJX-4-TEX-I-1D467 /><path d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z"id=MJX-4-TEX-V-2032 /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-4-TEX-N-3D /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=msup><g data-mml-node=mi><use data-c=1D467 xlink:href=#MJX-4-TEX-I-1D467 /></g><g data-mml-node=mo transform="translate(498,363) scale(0.707)"><use data-c=2032 xlink:href=#MJX-4-TEX-V-2032 /></g></g><g data-mml-node=mo transform=translate(1020.2,0)><use data-c=3D xlink:href=#MJX-4-TEX-N-3D /></g><g data-mml-node=mi transform=translate(2076,0)><use data-c=1D467 xlink:href=#MJX-4-TEX-I-1D467 /></g></g></g></svg></mjx-container></ul><p>From the equation it is pretty clear, we want to negate the input <strong>x</strong> and leave everything else alone.<ul><li><mjx-container class=MathJax jax=SVG><svg height=1.692ex viewBox="0 -666 3140.6 748"width=7.105ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"id=MJX-5-TEX-I-1D44E /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-5-TEX-N-3D /><path d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"id=MJX-5-TEX-N-2212 /><path d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"id=MJX-5-TEX-N-31 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=mi><use data-c=1D44E xlink:href=#MJX-5-TEX-I-1D44E /></g><g data-mml-node=mo transform=translate(806.8,0)><use data-c=3D xlink:href=#MJX-5-TEX-N-3D /></g><g data-mml-node=mo transform=translate(1862.6,0)><use data-c=2212 xlink:href=#MJX-5-TEX-N-2212 /></g><g data-mml-node=mn transform=translate(2640.6,0)><use data-c=31 xlink:href=#MJX-5-TEX-N-31 /></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=1.756ex viewBox="0 -694 2262.6 776"width=5.119ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"id=MJX-6-TEX-I-1D44F /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-6-TEX-N-3D /><path d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"id=MJX-6-TEX-N-30 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=mi><use data-c=1D44F xlink:href=#MJX-6-TEX-I-1D44F /></g><g data-mml-node=mo transform=translate(706.8,0)><use data-c=3D xlink:href=#MJX-6-TEX-N-3D /></g><g data-mml-node=mn transform=translate(1762.6,0)><use data-c=30 xlink:href=#MJX-6-TEX-N-30 /></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=1.756ex viewBox="0 -694 2353.6 776"width=5.325ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z"id=MJX-7-TEX-I-1D451 /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-7-TEX-N-3D /><path d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"id=MJX-7-TEX-N-30 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=mi><use data-c=1D451 xlink:href=#MJX-7-TEX-I-1D451 /></g><g data-mml-node=mo transform=translate(797.8,0)><use data-c=3D xlink:href=#MJX-7-TEX-N-3D /></g><g data-mml-node=mn transform=translate(1853.6,0)><use data-c=30 xlink:href=#MJX-7-TEX-N-30 /></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=1.692ex viewBox="0 -666 2299.6 748"width=5.203ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z"id=MJX-8-TEX-I-1D452 /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-8-TEX-N-3D /><path d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"id=MJX-8-TEX-N-31 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=mi><use data-c=1D452 xlink:href=#MJX-8-TEX-I-1D452 /></g><g data-mml-node=mo transform=translate(743.8,0)><use data-c=3D xlink:href=#MJX-8-TEX-N-3D /></g><g data-mml-node=mn transform=translate(1799.6,0)><use data-c=31 xlink:href=#MJX-8-TEX-N-31 /></g></g></g></svg></mjx-container></ul><p>In order to get a pretty map, we'd like the output data to be centered on the prime meridian again, so:<ul><li><mjx-container class=MathJax jax=SVG><svg height=1.692ex viewBox="0 -666 5211.6 748"width=11.791ex focusable=false role=img style=vertical-align:-.186ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M35 393Q35 417 46 424T89 432Q95 432 112 432T141 431H223Q238 422 241 415T244 393Q244 389 244 383T237 367T216 355Q209 355 209 354L234 319Q259 286 260 286L308 354Q308 355 301 355Q285 356 278 365T270 384L271 393Q271 420 289 428Q295 431 376 431H459Q460 430 465 427T472 422T476 416T480 407T481 393Q481 368 470 362T434 355H425H392L344 290Q295 225 295 223Q294 223 309 203T350 149L405 77L439 76H453Q474 76 484 69T495 38Q495 10 473 1H303Q281 9 281 32V38Q281 49 282 54T290 67T313 76Q324 76 324 77L259 173L197 77Q202 76 209 76Q225 75 233 68T241 55T242 38Q242 28 242 24T236 12T221 1H51Q29 9 29 32V38Q29 48 29 51T31 59T38 67T51 76H117L171 149Q224 222 224 223L124 355H90H78Q54 355 45 361T35 393Z"id=MJX-9-TEX-M-1D6A1 /><path d="M52 216Q52 318 118 379T261 440Q343 440 407 378T472 216Q472 121 410 58T262 -6Q176 -6 114 58T52 216ZM388 225Q388 281 351 322T261 364Q213 364 175 325T136 225Q136 158 174 114T262 70T350 114T388 225Z"id=MJX-9-TEX-M-1D698 /><path d="M43 395Q44 405 44 408T47 416T53 423T66 431H176V461Q176 500 182 518Q201 570 252 593T353 617Q399 614 418 593T437 548Q437 528 424 514T387 499Q365 499 353 511T338 537V541H328Q275 536 261 494Q260 490 260 460V431H327Q334 431 346 431T364 432Q392 432 404 425T416 393T405 362T365 355H327H260V76H319Q375 76 388 71T401 38Q401 27 400 23T395 12T379 1H58Q47 6 42 12T36 23T35 38Q35 65 53 73Q59 76 117 76H176V355H121H93Q64 355 54 362T43 395Z"id=MJX-9-TEX-M-1D68F /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-9-TEX-N-3D /><path d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"id=MJX-9-TEX-N-2212 /><path d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z"id=MJX-9-TEX-N-39 /><path d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"id=MJX-9-TEX-N-30 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=TeXAtom data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A1 xlink:href=#MJX-9-TEX-M-1D6A1 /><use data-c=1D698 xlink:href=#MJX-9-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-9-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-9-TEX-M-1D68F transform=translate(1575,0) /></g></g><g data-mml-node=mo transform=translate(2377.8,0)><use data-c=3D xlink:href=#MJX-9-TEX-N-3D /></g><g data-mml-node=mo transform=translate(3433.6,0)><use data-c=2212 xlink:href=#MJX-9-TEX-N-2212 /></g><g data-mml-node=mn transform=translate(4211.6,0)><use data-c=39 xlink:href=#MJX-9-TEX-N-39 /><use data-c=30 xlink:href=#MJX-9-TEX-N-30 transform=translate(500,0) /></g></g></g></svg></mjx-container><li><mjx-container class=MathJax jax=SVG><svg height=2.023ex viewBox="0 -666 3933.6 894"width=8.899ex focusable=false role=img style=vertical-align:-.516ex xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink><defs><path d="M26 393Q26 417 37 424T80 431H134H217Q232 422 235 416T239 393Q239 379 236 371T226 360T214 356T197 355L179 354V353L188 330Q197 306 209 272T235 201T259 133T271 89V84L274 95Q279 122 298 185T335 300T352 354Q352 355 331 355Q312 355 304 358Q288 368 288 393Q288 408 291 415T310 431H478Q479 430 484 427T491 422T495 416T499 407T500 393Q500 376 493 367T479 357T458 355H452Q426 355 425 353Q420 337 351 124T280 -94Q240 -195 168 -220Q147 -228 125 -228Q89 -228 66 -201T42 -139Q42 -116 56 -102T93 -87Q117 -87 130 -102T144 -135V-138H126Q121 -148 121 -150T130 -152Q182 -147 207 -87Q211 -78 223 -40T236 1Q230 10 102 355H75L49 356Q26 363 26 393Z"id=MJX-10-TEX-M-1D6A2 /><path d="M52 216Q52 318 118 379T261 440Q343 440 407 378T472 216Q472 121 410 58T262 -6Q176 -6 114 58T52 216ZM388 225Q388 281 351 322T261 364Q213 364 175 325T136 225Q136 158 174 114T262 70T350 114T388 225Z"id=MJX-10-TEX-M-1D698 /><path d="M43 395Q44 405 44 408T47 416T53 423T66 431H176V461Q176 500 182 518Q201 570 252 593T353 617Q399 614 418 593T437 548Q437 528 424 514T387 499Q365 499 353 511T338 537V541H328Q275 536 261 494Q260 490 260 460V431H327Q334 431 346 431T364 432Q392 432 404 425T416 393T405 362T365 355H327H260V76H319Q375 76 388 71T401 38Q401 27 400 23T395 12T379 1H58Q47 6 42 12T36 23T35 38Q35 65 53 73Q59 76 117 76H176V355H121H93Q64 355 54 362T43 395Z"id=MJX-10-TEX-M-1D68F /><path d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"id=MJX-10-TEX-N-3D /><path d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"id=MJX-10-TEX-N-30 /></defs><g transform=scale(1,-1) fill=currentColor stroke=currentColor stroke-width=0><g data-mml-node=math><g data-mml-node=TeXAtom data-mjx-texclass=ORD><g data-mml-node=mi><use data-c=1D6A2 xlink:href=#MJX-10-TEX-M-1D6A2 /><use data-c=1D698 xlink:href=#MJX-10-TEX-M-1D698 transform=translate(525,0) /><use data-c=1D68F xlink:href=#MJX-10-TEX-M-1D68F transform=translate(1050,0) /><use data-c=1D68F xlink:href=#MJX-10-TEX-M-1D68F transform=translate(1575,0) /></g></g><g data-mml-node=mo transform=translate(2377.8,0)><use data-c=3D xlink:href=#MJX-10-TEX-N-3D /></g><g data-mml-node=mn transform=translate(3433.6,0)><use data-c=30 xlink:href=#MJX-10-TEX-N-30 /></g></g></g></svg></mjx-container></ul><p>And in SQL like this:<pre><code class=language-pgsql>CREATE TABLE countries_affine AS
  SELECT id, name,
    CASE WHEN SIDE = 'west'
         THEN ST_Affine(geom, -1, 0, 0, 1, -90, 0)
         ELSE ST_Affine(geom, 1, 0, 0, 1, -90, 0)
    END AS geom
  FROM countries_split;

CREATE TABLE places_affine AS
  SELECT ogc_fid AS id, name,
    CASE WHEN ST_X(geom) &#60 0
         THEN ST_Affine(geom, -1, 0, 0, 1, -90, 0)
         ELSE ST_Affine(geom, 1, 0, 0, 1, -90, 0)
    END AS geom
  FROM ne_110m_populated_places
  ORDER BY pop_max DESC;
</code></pre><p>And the final result on the map looks like the <a href=https://xkcd.com/2807/>XKCD map</a>, without the pretty hand-labeling and mountains:<p><img alt=abs(longitude) loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/e076e50d-6dcc-4832-fc9a-7a4c148cb300/public><p>The bad map projections aren't the only cartographic cartoons XKCD explored. If you liked this one, take a look at:<ul><li><a href=https://xkcd.com/977/>Map Projections</a>, all real!<li><a href=https://xkcd.com/850/>World According to Americans</a><li><a href=https://xkcd.com/1500/>Upside Down</a><li><a href=https://xkcd.com/1688/>Map Age Guide</a>, fascinating history!</ul><style>mjx-container[jax=SVG]{direction:ltr}mjx-container[jax=SVG]>svg{overflow:visible;min-height:1px;min-width:1px}mjx-container[jax=SVG]>svg a{fill:blue;stroke:blue}mjx-container[jax=SVG][display=true]{display:block;text-align:center;margin:1em 0}mjx-container[jax=SVG][display=true][width=full]{display:flex}mjx-container[jax=SVG][justify=left]{text-align:left}mjx-container[jax=SVG][justify=right]{text-align:right}g[data-mml-node=merror]>g{fill:red;stroke:red}g[data-mml-node=merror]>rect[data-background]{fill:yellow;stroke:none}g[data-mml-node=mtable]>line[data-line],svg[data-table]>g>line[data-line]{stroke-width:70px;fill:none}g[data-mml-node=mtable]>rect[data-frame],svg[data-table]>g>rect[data-frame]{stroke-width:70px;fill:none}g[data-mml-node=mtable]>.mjx-dashed,svg[data-table]>g>.mjx-dashed{stroke-dasharray:140}g[data-mml-node=mtable]>.mjx-dotted,svg[data-table]>g>.mjx-dotted{stroke-linecap:round;stroke-dasharray:0,140}g[data-mml-node=mtable]>g>svg{overflow:visible}[jax=SVG] mjx-tool{display:inline-block;position:relative;width:0;height:0}[jax=SVG] mjx-tool>mjx-tip{position:absolute;top:0;left:0}mjx-tool>mjx-tip{display:inline-block;padding:.2em;border:1px solid #888;font-size:70%;background-color:#f8f8f8;color:#000;box-shadow:2px 2px 5px #aaa}g[data-mml-node=maction][data-toggle]{cursor:pointer}mjx-status{display:block;position:fixed;left:1em;bottom:1em;min-width:25%;padding:.2em .4em;border:1px solid #888;font-size:90%;background-color:#f8f8f8;color:#000}foreignObject[data-mjx-xml]{font-family:initial;line-height:normal;overflow:visible}mjx-container[jax=SVG] path[data-c],mjx-container[jax=SVG] use[data-c]{stroke-width:3}</style> ]]></content:encoded>
<category><![CDATA[ Fun with Postgres ]]></category>
<author><![CDATA[ Paul.Ramsey@crunchydata.com (Paul Ramsey) ]]></author>
<dc:creator><![CDATA[ Paul Ramsey ]]></dc:creator>
<guid isPermalink="false">a2a7d4740d294717f47250e2a640ab15fe45dd6cf453ce9a001fcaa0affe3288</guid>
<pubDate>Mon, 07 Aug 2023 09:00:00 EDT</pubDate>
<dc:date>2023-08-07T13:00:00.000Z</dc:date>
<atom:updated>2023-08-07T13:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Holy Sheet! Remote Access CSV Files from Postgres ]]></title>
<link>https://www.crunchydata.com/blog/holy-sheet-remote-access-csv-files-from-postgres</link>
<description><![CDATA[ Want to get CSV data into your database? Paul has some great tools to help you use CSV and Google Sheets. Like remote copy, http access, using Python for materialized views, and ogr_fdw. You heard that right, there's a foreign data wrapper for CSV to Postgres! ]]></description>
<content:encoded><![CDATA[ <p>An extremely common problem in fast-moving data architectures is providing a way to feed ad hoc user data into an existing analytical data system.<p>Do you have time to whip up a web app? No! You have a database to feed, and events are spiraling out of control... what to do?<p>How about a Google Sheet? The data layout is obvious, you can even enforce things like data types and required columns using locking and protecting, and unlike an Excel or LibreOffice document, it's always online, so you can hook the data into your system directly.<h2 id=access-sheets-data-remotely><a href=#access-sheets-data-remotely>Access Sheets Data Remotely</a></h2><p>You can pull data in CSV format from a (public) Google Sheets workbook just by plugging the sheet ID into a magic access URL.<p>The URL format looks like this:<pre><code class=language-text>https://docs.google.com/spreadsheets/d/{sheetId}/gviz/tq?tqx=out:csv&#38sheet={sheetName}
</code></pre><p>The <code>sheetId</code> you can pull out of the URL at the top of your browser. The <code>sheetName</code> is at the bottom of the page. You can read one sheet at a time out of a Sheets workbook.<p>This example sheet has 22 rows of population projection data in it.<pre><code class=language-shell>$ curl "https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections"

"Year","18 to 19","Total"
"2024","120107","5485084"
"2025","123484","5563798"
"2026","128627","5641925"
"2027","132540","5719109"
"2028","134067","5796302"
...
</code></pre><h2 id=remote-access-with-copy><a href=#remote-access-with-copy>Remote Access with COPY</a></h2><p>The following examples all use the <a href=https://crunchybridge.com/>Crunchy Bridge</a> database-as-a-service. Not all services will support these methods.<p>To use the <code>COPY</code> command for remote loading you will first need to create a table to load the data into. Our table structure is just three integer columns.<pre><code class=language-pgsql>CREATE TABLE popn_copy (
year integer,
age_18_to_19 integer,
all_ages integer
);
</code></pre><p>We are going to use the <code>PROGRAM</code> option of copy to fire up the <code>curl</code> utility and pull the CSV data from Google, then stream that into the default PostgreSQL CSV reader.<p>In order to use <code>COPY</code> with the <code>PROGRAM</code> option you must be logged in as the <code>postgres</code> superuser.<pre><code class=language-pgsql>COPY popn_copy FROM PROGRAM
'curl "https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections"'
WITH (
 FORMAT csv,
 HEADER true,
 ENCODING utf8
 );
</code></pre><p>Just like that, 22 rows loaded!<pre><code class=language-pgsql>SELECT Count(*) FROM popn_copy;
</code></pre><p>The <code>COPY</code> approach is really the simplest one available, and to refresh your data, you can just run a scheduled <code>TRUNCATE</code> and then re-run the <code>COPY</code>.<p>However, it does have the disadvantage of requiring a superuser login.<h2 id=remote-access-with-http><a href=#remote-access-with-http>Remote Access with HTTP</a></h2><p>The <a href=https://github.com/pramsey/pgsql-http>http extension</a> for PostgreSQL allows users to run web requests and fetch data from any URL. Sounds like exactly what we need!<p>You can check if you have the extension by querying the <code>pg_available_extensions</code> table.<pre><code class=language-pgsql>SELECT *
  FROM pg_available_extensions
  WHERE name = 'http';
</code></pre><pre><code class=language-text>name              | http
default_version   | 1.5
installed_version |
comment           | HTTP client for PostgreSQL, allows web page retrieval inside the database.
</code></pre><p>If you have it, enable the <code>http</code> extension, and create a target table:<pre><code class=language-pgsql>CREATE EXTENSION http;

CREATE TABLE popn_http (
year integer,
age_18_to_19 integer,
all_ages integer
);
</code></pre><p>If we use the <code>http_get()</code> function, we can pull the content from the remote URL in one step.<pre><code class=language-pgsql>SELECT content AS row
FROM http_get('https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections')
</code></pre><p>What we get back is all the data, but in <strong>one big string</strong>. We would <strong>prefer</strong> 22 rows of data. Fortunately, PostgreSQL string processing can help us condition the data before inserting it into our table.<p>First, cut the string up using new-line characters as a delimiter.<pre><code class=language-pgsql>SELECT unnest(string_to_array(content, E'\n')) AS row
FROM http_get('https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections')
</code></pre><p>Now we have 23 rows (22 data rows and one header row), which we can parse the numeric pieces out of using <code>regexp_match()</code>:<pre><code class=language-pgsql>INSERT INTO popn_http
WITH rows AS (
  SELECT unnest(string_to_array(content, E'\n')) AS row
  FROM http_get('https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections')
),
cols AS (
  SELECT regexp_match(row, '"([0-9]+)","([0-9]+)","([0-9]+)"') AS col FROM rows
)
SELECT col[1]::integer AS year,
       col[2]::integer AS age_18_to_19,
       col[3]::integer AS all_ages
  FROM cols
 WHERE col[1] IS NOT NULL
</code></pre><p>Whenever you want to refresh, just <code>TRUNCATE</code> the table and re-run the population query. Unlike the <code>COPY</code> method, this doesn't require super-user access to implement.<h2 id=remote-access-with-plpython><a href=#remote-access-with-plpython>Remote Access with PL/Python</a></h2><p>The <code>http</code> extension is simple, but it leaves a lot of work on the server side. PL/Python is capable of doing remote HTTP access, and it also has a lot of nice string parsing tools that could maybe result in a more pleasant output.<pre><code class=language-pgsql>CREATE EXTENSION IF NOT EXISTS plpython3u;

CREATE OR REPLACE FUNCTION read_csv_from_url(url TEXT)
RETURNS SETOF RECORD
AS $$
  import csv
  import requests
  from io import StringIO

  # Make a GET request to the URL and retrieve the CSV data
  response = requests.get(url)
  response.raise_for_status()
  csv_data = response.text

  # Parse the CSV data
  reader = csv.reader(StringIO(csv_data))

  # Skip the header row
  next(reader, None)

  # Yield each row as a result
  for row in reader:
      yield tuple(row)
$$ LANGUAGE 'plpython3u';
</code></pre><p>Using a set returning function, it is much easier to integrate the function into the database infrastructure, so we can make the data available via a <code>MATERIALIZED VIEW</code>.<pre><code class=language-pgsql>CREATE MATERIALIZED VIEW popn_python AS
SELECT *
  FROM read_csv_from_url('https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections')
    AS f(year integer, age_18_to_19 integer, all_ages integer);
</code></pre><p>Updating the remote data just needs a <code>REFRESH MATERIALIZED VIEW</code>, and super user access is not required.<h2 id=remote-access-with-fdw><a href=#remote-access-with-fdw>Remote Access with FDW</a></h2><p>Our last remote access trick uses a "<a href=https://www.postgresql.org/docs/current/ddl-foreign-data.html>foreign data wrapper</a>", specifically the <a href=https://github.com/pramsey/pgsql-ogr-fdw/>OGR FDW</a> which exposes the multi-format access capabilities of the <a href=https://gdal.org>GDAL</a> library to PostgreSQL.<p>While this example shows CSV file reading, the <a href=https://github.com/pramsey/pgsql-ogr-fdw/>OGR FDW</a> extension can be used to access a <a href=https://gdal.org/drivers/vector/index.html>huge number of different formats</a>, both local and remote.<p>The hardest part of using the OGR FDW driver is figuring out the correct server string to use in setting up the connection. It is best to start by <a href=https://gdal.org/download.html>downloading a copy of GDAL</a> to your workstation and trying out various options using the <code>ogrinfo</code> tool.<p>With some trial and error, I found that a working URL involved:<ul><li>using the <code>vsicurl</code> remote access driver (check out the other "<a href=https://gdal.org/user/virtual_file_systems.html>virtual file system</a>" drivers provided to get a feel for just how flexible GDAL is for remote data access),<li>prepending <code>CSV</code> to hint to GDAL what format driver to use, and<li>appending <code>&#38/popn</code> to the URL to trick GDAL into using "popn" as the layer name instead of something much less attractive.</ul><p>The result can connect to the remote source and understand the CSV file contents.<pre><code class=language-shell>$ ogrinfo CSV:"/vsicurl/https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections&#38/popn"

INFO: Open of `CSV:/vsicurl/https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections&#38/popn'
      using driver `CSV' successful.
1: popn (None)
</code></pre><p>For most sources, like remote databases and so on, the URL will be a lot simpler and obvious. Even a remote CSV file will usually be easier, because it will have a CSV file name at the end of the URL, which GDAL uses to hint the correct driver.<pre><code class=language-pgsql>CREATE SERVER myserver
  FOREIGN DATA WRAPPER ogr_fdw
  OPTIONS (
    datasource 'CSV:/vsicurl/https://docs.google.com/spreadsheets/d/1pBbCabAK6u6EIuyu_2XUul4Yxvf2w_Od6QYC_yEc4q4/gviz/tq?tqx=out:csv&#38sheet=Population_projections&#38/popn',
    format 'CSV');
</code></pre><p>Now that we have a "server", we can import the one layer that exists in that server.<p>If our server was something more sophisticated, like a database, there could potentially be multiple tables that would be imported using this method.<pre><code class=language-pgsql>CREATE SCHEMA fdw;

IMPORT FOREIGN SCHEMA ogr_all
	FROM SERVER myserver
	INTO fdw;

SELECT * FROM fdw.popn;
</code></pre><pre><code class=language-text> fid | year | n18_to_19 |  total
-----+------+-----------+---------
   1 | 2024 | 120107    | 5485084
   2 | 2025 | 123484    | 5563798
   3 | 2026 | 128627    | 5641925
   4 | 2027 | 132540    | 5719109
   5 | 2028 | 134067    | 5796302
</code></pre><p>We have data! Unfortunately, since it is coming from a text CSV file, without any column type mapping, we need to do a little bit of type coercion to get a clean table of integers.<pre><code class=language-pgsql>CREATE MATERIALIZED VIEW popn_fdw AS
  SELECT year::integer      AS year,
         n18_to_19::integer AS age_18_to_19,
         total::integer     AS all_ages
  FROM fdw.popn;
</code></pre><p>Using a materialized view keeps our database from constantly hitting the remote table every time we access the FDW table. Since we have a materialized view, the refresh method is a little prettier than truncating and reloading, though it is effectively the same thing: just refresh the view.<pre><code class=language-pgsql>REFRESH MATERIALIZED VIEW popn_fdw;
</code></pre><p>As with the <code>http</code> approach and unlike using <code>COPY</code>, the FDW approach does not require a superuser to do the refresh step.<h2 id=conclusions><a href=#conclusions>Conclusions</a></h2><ul><li>There are lots of ways to access remote data!<ul><li>Using <code>COPY</code> with <code>PROGRAM</code> is simple but requires superuser powers and only reads CSV.<li>Using the <code>http</code> extension is simple to get data but requires parsing it yourself on the database side.<li>Using a PL/Python function results in a short and reusable piece of code, and does not require superuser powers.<li>Using the <code>ogr_fdw</code> extension involves some fiddly setup but is nice and clean once it is up and running. It can also read vastly more different file formats and data services.</ul></ul> ]]></content:encoded>
<category><![CDATA[ Fun with Postgres ]]></category>
<author><![CDATA[ Paul.Ramsey@crunchydata.com (Paul Ramsey) ]]></author>
<dc:creator><![CDATA[ Paul Ramsey ]]></dc:creator>
<guid isPermalink="false">a841e8558f2e72efb56cbd6fb3c5105e7828aa97cfedbb7c38c1e321595b304b</guid>
<pubDate>Mon, 12 Jun 2023 09:00:00 EDT</pubDate>
<dc:date>2023-06-12T13:00:00.000Z</dc:date>
<atom:updated>2023-06-12T13:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Practical AI with Postgres ]]></title>
<link>https://www.crunchydata.com/blog/practical-ai-with-postgres</link>
<description><![CDATA[ Use some of the newest AI tools for Postgres! Craig shows you how to use ChatGPT for schema creation, data creation, query writing, indexing and more. ]]></description>
<content:encoded><![CDATA[ <p>There's a lot of excitement around AI, and even more discussion than excitement. The question of Postgres and AI isn't a single question, there are a ton of paths you can take under that heading...<ul><li>Can I use Postgres for building AI related apps? <a href=https://www.crunchydata.com/blog/whats-postgres-got-to-do-with-ai>Absolutely</a>.<li>Can Postgres become an autonomous database with AI? Maybe.<li>Should you use AI to help you troubleshoot issues? Perhaps, but better left to experts.<li>Should I avoid AI when working with my database? Not at all!</ul><p>We've already talked about the first item previously here. Today we want to drill into the last item on the list and look at some very practical use cases for using it. In particular ChatGPT can help you be more effective when working with your database.<h3 id=the-basics><a href=#the-basics>The basics</a></h3><p>ChatGPT is absolutely wonderful at the basics of helping you generate schema, queries, and basic Postgres commands. The <a href=https://www.postgresql.org/docs/current/datatype.html>PostgreSQL docs</a> themselves are wonderful <strong>if you know what to look for.</strong> They are wonderful reference docs. But if you don't know what to look for (you mean <a href=https://www.craigkerstiens.com/2013/11/18/best-postgres-feature-youre-not-using/>common table expressions</a> and <a href=https://www.crunchydata.com/developers/playground/ctes-and-window-functions>window functions</a> aren't in your everyday lexicon?) then you find yourself hunting and pecking. But before I get too far ahead of myself, let’s get started with the very basics. I want a schema we can use as an example:<p>I asked Chat GPT: Can you help me generate a schema for a multi-tenant CRM app in Postgres?<p>And here's what it gave me:<pre><code class=language-pgsql>CREATE TABLE tenants (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);

CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    name VARCHAR(255) NOT NULL,
    website VARCHAR(255),
    phone VARCHAR(255),
    address VARCHAR(255),
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);

CREATE TABLE contacts (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    phone VARCHAR(255),
    address VARCHAR(255),
    account_id INTEGER NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
    FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);

CREATE TABLE deals (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    name VARCHAR(255) NOT NULL,
    amount DECIMAL(10,2),
    close_date DATE,
    stage VARCHAR(255),
    account_id INTEGER NOT NULL,
    contact_id INTEGER NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
    FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
    FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
</code></pre><p>In fact it gave me even more tables including <code>activities</code>. I personally wanted to trim this example so our blog post wasn't so long, but you'd lose quite a bit. Now is this schema perfect? No. But did it save me a ton of time? 💯 it very much did. A quick look at <strong>changes I might make</strong>:<ul><li>Would use <code>bigint</code> likely for the primary key<li>May or may not have <code>ON DELETE CASCADE</code> in a lot of cases logical deletes can be nice<li>Lots of use of <code>VARCHAR(255)</code> in places that may not be ideal such as phone, address, email.</ul><p>ChatGPT did do some things I especially like on this schema which includes:<ul><li>Putting the <code>tenant_id</code> on all the tables<li>Setting up foreign keys</ul><h3 id=onward-to-data><a href=#onward-to-data>Onward to data</a></h3><p>Let’s keep progressing, this is maybe my favorite use for ChatGPT which is generating fake data. The tables above aren't entirely complex, but it takes some time to generate data. I simply asked ChatGPT:<p>Can you help me generate some sample records for the above schema?<p>And viola:<pre><code class=language-pgsql>INSERT INTO tenants (name, created_at) VALUES ('Acme Inc.', NOW());
INSERT INTO tenants (name, created_at) VALUES ('Globex Corporation', NOW());

INSERT INTO users (tenant_id, email, password, first_name, last_name, created_at, updated_at)
VALUES (1, 'johndoe@acme.com', 'password123', 'John', 'Doe', NOW(), NOW());
INSERT INTO users (tenant_id, email, password, first_name, last_name, created_at, updated_at)
VALUES (1, 'janedoe@acme.com', 'password456', 'Jane', 'Doe', NOW(), NOW());
INSERT INTO users (tenant_id, email, password, first_name, last_name, created_at, updated_at)
VALUES (2, 'bobsmith@globex.com', 'password789', 'Bob', 'Smith', NOW(), NOW());

INSERT INTO accounts (tenant_id, name, website, phone, address, created_at, updated_at)
VALUES (1, 'Acme Corporation', '&#60https://www.acme.com>', '555-1234', '123 Main St., Anytown, USA', NOW(), NOW());
INSERT INTO accounts (tenant_id, name, website, phone, address, created_at, updated_at)
VALUES (2, 'Globex Inc.', '&#60https://www.globex.com>', '555-5678', '456 Broad St., Anytown, USA', NOW(), NOW());

INSERT INTO contacts (tenant_id, first_name, last_name, email, phone, address, account_id, created_at, updated_at)
VALUES (1, 'Tom', 'Jones', 'tjones@acme.com', '555-4321', '456 Oak St., Anytown, USA', 1, NOW(), NOW());
INSERT INTO contacts (tenant_id, first_name, last_name, email, phone, address, account_id, created_at, updated_at)
VALUES (2, 'Alice', 'Johnson', 'ajohnson@globex.com', '555-8765', '789 Maple St., Anytown, USA', 2, NOW(), NOW());

INSERT INTO deals (tenant_id, name, amount, close_date, stage, account_id, contact_id, created_at, updated_at)
VALUES (1, 'New Contract', 10000.00, '2023-06-01', 'Proposal', 1, 1, NOW(), NOW());
INSERT INTO deals (tenant_id, name, amount, close_date, stage, account_id, contact_id, created_at, updated_at)
VALUES (2, 'Expansion Deal', 5000.00, '2023-07-01', 'Negotiation', 2, 2, NOW(), NOW());

INSERT INTO activities (tenant_id, type, subject, due_date, completed, deal_id, contact_id, user_id, created_at, updated_at)
VALUES (1, 'Call', 'Follow up on proposal', '2023-05-15', false, 1, 1, 1, NOW(), NOW());
INSERT INTO activities (tenant_id, type, subject, due_date, completed, deal_id, contact_id, user_id, created_at, updated_at)
VALUES (2, 'Email', 'Send contract terms', '2023-05-20', false, 2, 2, 3, NOW(), NOW());
</code></pre><p><em>Is it just me or does it feel like asking nicely "Can you" is a smart choice vs. just demanding?</em><p>At first ChatGPT only gave me data for 2 tenants, so I asked it for 10 tenants, and it kindly obliged. I only mention that because tenants number 3 and 4 I especially loved <code>Wayne Enterprises</code> and <code>Stark Industries</code>.<h3 id=indexing><a href=#indexing>Indexing</a></h3><p>In general the best plan for indexing is not to overcomplicate it. Wait to see some of the workload of your application, then add indexes accordingly. One of the best tools for monitoring what consumes time in your database is via the <a href=https://www.crunchydata.com/blog/tentative-smarter-query-optimization-in-postgres-starts-with-pg_stat_statements>pg_stat_statements</a> extension. But let’s still see what ChatGPT can do to help us here:<p><em>What would a good indexing scheme be for common queries if I were to build an app on top of this?</em><pre><code class=language-text>To determine a good indexing scheme for common queries, it's important to
understand the types of queries that will be executed frequently in the
application.

Here are some common queries that may be executed in a CRM app:

Retrieve all the customers for a given tenant.
Retrieve all the orders for a given customer.
Retrieve all the orders for a given tenant.
Retrieve all the products for a given tenant.
Retrieve all the users for a given tenant.
Retrieve all the tasks for a given user.
Retrieve all the tasks for a given customer.
Based on these queries, here are some suggested indexes:

Index on the customers table: (tenant_id).
Index on the orders table: (customer_id).
Index on the orders table: (tenant_id).
Index on the products table: (tenant_id).
Index on the users table: (tenant_id).
Index on the tasks table: (user_id).
Index on the tasks table: (customer_id).

It's important to keep in mind that adding too many indexes can slow down write
performance, so it's important to only add indexes that are necessary for
frequently executed queries. Additionally, you should monitor the performance of
your queries and adjust your indexing scheme as necessary.
</code></pre><p>At first glance we’ve got a decent set of inputs here and could easily create indexes on this, and ChatGPT could help us with the commands for creating those indexes. If you do follow the above approach of creating a bunch of stock indexes just make sure to clean up your <a href=https://www.crunchydata.com/blog/cleaning-up-your-postgres-database>unused indexes</a> over time.<h3 id=reporting><a href=#reporting>Reporting</a></h3><p>How’s your SQL? Do you love writing SQL and use it as the first tool you reach for when you want to know something about your data? No? You’re not alone, the majority of developers aren’t SQL experts, that doesn’t mean SQL isn’t the right tool for the job. Just because you’re not an expert at SQL doesn’t mean you should be scared of it. ChatGPT is great at helping you become more familiar with SQL by expressing in plain english what you want and getting SQL out.<p><em>I want to create a report that shows me open opportunities that are expected to close this quarter, can you help me write a query that would generate that report?</em><pre><code class=language-pgsql>SELECT *
FROM opportunities
WHERE status = 'Open'
AND close_date BETWEEN DATE_TRUNC('quarter', CURRENT_DATE) AND DATE_TRUNC('quarter', CURRENT_DATE) + INTERVAL '3 months' - INTERVAL '1 day';
</code></pre><p>In cases ChatGPT’s SQL may not be perfect, it may pick wrong columns that don’t actually exist or do the wrong calculation. While ChatGPT isn’t perfect it does offer a good method for quickly iterating often faster than having to go reference SQL that you’re looking to create.<h3 id=more-sql-usage><a href=#more-sql-usage>More SQL usage</a></h3><p>As we talked about earlier PostgreSQL has great reference documentation–IF you know what you’re looking for. But if you don’t know the name of the function to truncate a date is <code>date_trunc</code> then you may find yourself spending extra time to guess how Postgres references it or how that is classified in the docs. When it comes to how do I do X in SQL ChatGPT is great for this:<p><em>How do I round a date to the nearest date in Postgres?</em><p>ChatGPT:<p>To round a date to the nearest date in PostgreSQL, you can use the <strong><code>date_trunc</code></strong> function in combination with the <strong><code>round</code></strong> function. Here's an example:<pre><code class=language-pgsql>SELECT round(date_trunc('day', your_date_column)::date) AS rounded_date
FROM your_table;
</code></pre><p>In this example, replace <strong><code>your_date_column</code></strong> with the actual name of the column that contains the date you want to round, and <strong><code>your_table</code></strong> with the name of the table where the column resides.<p>The <strong><code>date_trunc</code></strong> function is used to truncate the time portion of the date, keeping only the year, month, and day. The <strong><code>round</code></strong> function is then used to round the truncated date to the nearest day.<p>By casting the result of <strong><code>date_trunc</code></strong> to <strong><code>date</code></strong> using <strong><code>::date</code></strong>, we ensure that the result is a proper date data type.<p>After executing this query, you will get a result set with a column named <strong><code>rounded_date</code></strong>, which contains the rounded dates for each row in the table.<h3 id=sql-is-your-friend-and-chatgpt-is-its-side-kick><a href=#sql-is-your-friend-and-chatgpt-is-its-side-kick>SQL is your friend and ChatGPT is it’s side kick</a></h3><p>You can have a lot of fun with ChatGPT and go down a lot of rabbit holes. Want to have ChatGPT create a four course menu themed around Postgres? Go for it, we won’t stop you. In fact, you can use our <a href=https://www.crunchydata.com/developers/tutorials>Postgres Playground</a> to load data and play with Postgres queries. We’ve seen a lot of takes ranging from I’m smarter than ChatGPT because I found out something it said wrong to how it’s going change anything.<p>The simple reality is ChatGPT can help you work better with your database. We hope these practical examples make you a little less afraid of SQL and your database. ]]></content:encoded>
<category><![CDATA[ Fun with Postgres ]]></category>
<category><![CDATA[ AI ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">3210e3467efc6d7baa04240fcf9e6d3e80552721b4883b0552a8167a9efa9f29</guid>
<pubDate>Wed, 17 May 2023 09:00:00 EDT</pubDate>
<dc:date>2023-05-17T13:00:00.000Z</dc:date>
<atom:updated>2023-05-17T13:00:00.000Z</atom:updated></item></channel></rss>