{"id":25606996,"url":"https://github.com/chsdwn/sql-notes","last_synced_at":"2025-08-30T18:24:38.675Z","repository":{"id":203704061,"uuid":"710207231","full_name":"chsdwn/sql-notes","owner":"chsdwn","description":null,"archived":false,"fork":false,"pushed_at":"2023-11-17T13:47:20.000Z","size":69,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-22T15:05:59.892Z","etag":null,"topics":["notes","postgresql","sql","t-sql"],"latest_commit_sha":null,"homepage":"https://www.youtube.com/playlist?list=PL1XF9qjV8kH12PTd1WfsKeUQU6e83ldfc","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chsdwn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-26T08:31:59.000Z","updated_at":"2024-04-22T15:05:59.892Z","dependencies_parsed_at":"2023-10-30T13:43:44.085Z","dependency_job_id":"b55af38f-b0a5-4cc5-976e-b734c8dcaead","html_url":"https://github.com/chsdwn/sql-notes","commit_stats":null,"previous_names":["chsdwn/sql-notes"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chsdwn%2Fsql-notes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chsdwn%2Fsql-notes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chsdwn%2Fsql-notes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chsdwn%2Fsql-notes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chsdwn","download_url":"https://codeload.github.com/chsdwn/sql-notes/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240072123,"owners_count":19743526,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["notes","postgresql","sql","t-sql"],"created_at":"2025-02-21T19:17:56.331Z","updated_at":"2025-02-21T19:17:57.309Z","avatar_url":"https://github.com/chsdwn.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# postgres-playground\n\n## Instructions to Run Locally\n\n- Install and start Docker\n- `docker compose up -d`: start postgres container on background\n- `docker ps`: list all running containers\n- `docker exec -it \u003ccontainer_id\u003e psql -U postgres -W postgres`: run psql\n\n## Chapter 2\n\n### 03. Row variables, row values, row types\n\n```sql\nDROP TABLE IF EXISTS \"T\";\nCREATE TABLE \"T\" (a int PRIMARY KEY,\n                  b text,\n                  c boolean,\n                  d int);\n\nINSERT INTO \"T\" VALUES\n  (1, 'x', true, 10),\n  (2, 'y', true, 40),\n  (3, 'x', false, 30),\n  (4, 'y', false, 20),\n  (5, 'x', true, NULL);\n\nTABLE \"T\"; -- SELECT t.* FROM \"T\" as t\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n```\n\n```sql\nDROP TABLE IF EXISTS \"T1\";\n\nDROP TYPE IF EXISTS \"t\";\nCREATE TYPE \"t\" AS (a int, b text, c boolean, d int);\n\nCREATE TABLE \"T1\" OF \"t\";\nALTER TABLE \"T1\" ADD PRIMARY KEY (a);\n\nINSERT INTO \"T1\" TABLE \"T\";\n\nTABLE \"T1\";\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n```\n\n### 04. SELECT/FROM/WHERE\n\n```sql\nSELECT 1 + 2 AS \"sUm\", 'chs' || 'dwn' AS UserName;\n/* # Output #\n sUm | username\n-----+----------\n   3 | chsdwn\n(1 row) */\n\nVALUES (1),\n       (2);\n/* # Output #\n column1\n---------\n       1\n       2\n(2 rows) */\n\nVALUES (1, 2);\n/* # Output #\n column1 | column2\n---------+---------\n       1 |       2\n(1 row) */\n\nVALUES (false, 0),\n       (true, 1),\n       (NULL, NULL);\n/* # Output #\n column1 | column2\n---------+---------\n f       |       0\n t       |       1\n         |\n(3 rows) */\n\nSELECT t.* FROM (VALUES (false, 0),\n                        (true, 1)) AS t(truth, \"binary\");\n/* # Output #\n truth | binary\n-------+--------\n f     |      0\n t     |      1\n(2 rows) */\n\nSELECT t1.*, t2.*\nFROM \"T\" AS t1,\n     \"T\" AS t2(a2, b2, c2, d2);\n/* # Output #\n a | b | c | d  | a2 | b2 | c2 | d2\n---+---+---+----+----+----+----+----\n 1 | x | t | 10 |  1 | x  | t  | 10\n 1 | x | t | 10 |  2 | y  | t  | 40\n 1 | x | t | 10 |  3 | x  | f  | 30\n 1 | x | t | 10 |  4 | y  | f  | 20\n 1 | x | t | 10 |  5 | x  | t  |\n 2 | y | t | 40 |  1 | x  | t  | 10\n 2 | y | t | 40 |  2 | y  | t  | 40\n 2 | y | t | 40 |  3 | x  | f  | 30\n 2 | y | t | 40 |  4 | y  | f  | 20\n 2 | y | t | 40 |  5 | x  | t  |\n 3 | x | f | 30 |  1 | x  | t  | 10\n 3 | x | f | 30 |  2 | y  | t  | 40\n 3 | x | f | 30 |  3 | x  | f  | 30\n 3 | x | f | 30 |  4 | y  | f  | 20\n 3 | x | f | 30 |  5 | x  | t  |\n 4 | y | f | 20 |  1 | x  | t  | 10\n 4 | y | f | 20 |  2 | y  | t  | 40\n 4 | y | f | 20 |  3 | x  | f  | 30\n 4 | y | f | 20 |  4 | y  | f  | 20\n 4 | y | f | 20 |  5 | x  | t  |\n 5 | x | t |    |  1 | x  | t  | 10\n 5 | x | t |    |  2 | y  | t  | 40\n 5 | x | t |    |  3 | x  | f  | 30\n 5 | x | t |    |  4 | y  | f  | 20\n 5 | x | t |    |  5 | x  | t  |\n(25 rows) */\n\nSELECT onetwo.num, t.*\nFROM (VALUES ('1'), ('2')) AS onetwo(num), \"T\" as t;\n/* # Output #\n num | a | b | c | d\n-----+---+---+---+----\n 1   | 1 | x | t | 10\n 2   | 1 | x | t | 10\n 1   | 2 | y | t | 40\n 2   | 2 | y | t | 40\n 1   | 3 | x | f | 30\n 2   | 3 | x | f | 30\n 1   | 4 | y | f | 20\n 2   | 4 | y | f | 20\n 1   | 5 | x | t |\n 2   | 5 | x | t |\n(10 rows) */\n\nSELECT onetwo.num, t.*\nFROM (VALUES ('1'), ('2')) AS onetwo(num), \"T\" as t\nWHERE onetwo.num = '2';\n/* # Output #\n num | a | b | c | d\n-----+---+---+---+----\n 2   | 1 | x | t | 10\n 2   | 2 | y | t | 40\n 2   | 3 | x | f | 30\n 2   | 4 | y | f | 20\n 2   | 5 | x | t |\n(5 rows) */\n\nSELECT t.*\nFROM \"T\" as t\nWHERE t.a * 10 = t.d;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 3 | x | f | 30\n(2 rows) */\n\nSELECT t.*\nFROM \"T\" AS t\nWHERE t.c; -- WHERE t.c = true;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 5 | x | t |\n(3 rows) */\n\nSELECT t.*\nFROM \"T\" as t\nWHERE t.d IS NULL;\n/* # Output #\n a | b | c | d\n---+---+---+---\n 5 | x | t |\n(1 row) */\n\nSELECT t.d, t.d IS NULL, t.d = NULL -- t.d = NULL yields NULL != true\nFROM \"T\" AS t;\n/* # Output #\n d  | ?column? | ?column?\n----+----------+----------\n 10 | f        |\n 40 | f        |\n 30 | f        |\n 20 | f        |\n    | t        |\n(5 rows) */\n\nSELECT t1.a, t1.b || ',' || t2.b AS b1b2, t2.a\nFROM \"T\" AS t1, \"T\" AS t2\nWHERE t1.a BETWEEN t2.a - 1 AND t2.a + 1;\n/* # Output #\n a | b1b2 | a\n---+------+---\n 1 | x,x  | 1\n 2 | y,x  | 1\n 1 | x,y  | 2\n 2 | y,y  | 2\n 3 | x,y  | 2\n 2 | y,x  | 3\n 3 | x,x  | 3\n 4 | y,x  | 3\n 3 | x,y  | 4\n 4 | y,y  | 4\n 5 | x,y  | 4\n 4 | y,x  | 5\n 5 | x,x  | 5\n(13 rows) */\n```\n\n### 05. Subqueries and Correlation\n\n```sql\nSELECT 2 + (SELECT t.d AS _\n            FROM \"T\" as t\n            WHERE t.a = 2) AS \"The Answer\";\n/* # Output #\n The Answer\n------------\n         42\n(1 row) */\n\nSELECT t1.*\nFROM \"T\" as t1\nWHERE t1.b \u003c\u003e (SELECT t2.b\n               FROM \"T\" AS t2\n               WHERE t1.a = t2.a);\n/* # Output #\n a | b | c | d\n---+---+---+---\n(0 rows) */\n```\n\n### 06. ORDER BY/OFFSET/LIMIT/DISTINCT [ON]\n\n- ASC is default.\n- NULL is larger than any non-NULL value.\n\n```sql\nSELECT t.*\nFROM \"T\" AS t\nORDER BY t.d ASC NULLS FIRST;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 5 | x | t |\n 1 | x | t | 10\n 4 | y | f | 20\n 3 | x | f | 30\n 2 | y | t | 40\n(5 rows) */\n\nSELECT t.*\nFROM \"T\" AS t\nORDER BY t.b DESC, t.c;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 4 | y | f | 20\n 2 | y | t | 40\n 3 | x | f | 30\n 1 | x | t | 10\n 5 | x | t |\n(5 rows) */\n\nSELECT t.*, t.d / t.a AS ratio\nFROM \"T\" AS t\nORDER BY ratio;\n/* # Output #\n a | b | c | d  | ratio\n---+---+---+----+-------\n 4 | y | f | 20 |     5\n 1 | x | t | 10 |    10\n 3 | x | f | 30 |    10\n 2 | y | t | 40 |    20\n 5 | x | t |    |\n(5 rows) */\n\nVALUES (1, 'one'),\n       (2, 'two'),\n       (3, 'three')\nORDER BY column1 DESC;\n/* # Output #\n column1 | column2\n---------+---------\n       3 | three\n       2 | two\n       1 | one\n(3 rows) */\n\nSELECT t.*\nFROM \"T\" AS t\nORDER BY t.a DESC\nOFFSET 1\nLIMIT 3;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 4 | y | f | 20\n 3 | x | f | 30\n 2 | y | t | 40\n(3 rows) */\n```\n\n- Groups rows by `t.c` column and picks the row that has the smallest `t.d` value from each group.\n\n```sql\nSELECT DISTINCT ON (t.c) t.*\nFROM \"T\" as t\nORDER BY t.c, t.d;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 4 | y | f | 20\n 1 | x | t | 10\n(2 rows) */\n\nTABLE \"T\" ORDER BY \"T\".c, \"T\".d;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 4 | y | f | 20 -- 1st row of the 1st group\n 3 | x | f | 30\n 1 | x | t | 10 -- 1st row of the 2nd group\n 2 | y | t | 40\n 5 | x | t |\n(5 rows) */\n\nSELECT DISTINCT t.*\nFROM (VALUES ('a', 1),\n             ('a', 1),\n             ('b', 2)) AS t;\n/* # Output #\n column1 | column2\n---------+---------\n b       |       2\n a       |       1\n(2 rows) */\n```\n\n### 07. Aggregates (ordered, filtered, unique)\n\n```sql\nSELECT COUNT(*) AS \"#rows\",\n       COUNT(t.d) AS \"#d\",\n       SUM(t.d) AS \"sum(d)\",\n       MAX(t.b) AS \"max(b)\",\n       bool_and(t.c) AS \"bool_and(c)\",\n       bool_or(t.d = 30) AS \"bool_or(c=30)\"\nFROM \"T\" as t\nWHERE true;\n/* # Output #\n #rows | #d | sum(d) | max(b) | bool_and(c) | bool_or(c=30)\n-------+----+--------+--------+-------------+---------------\n     5 |  4 |    100 | y      | f           | t\n(1 row) */\n\nSELECT COUNT(*) AS \"#rows\",\n       COUNT(t.d) AS \"#d\",\n       SUM(t.d) AS \"sum(d)\",\n       MAX(t.b) AS \"max(b)\",\n       bool_and(t.c) AS \"bool_and(c)\",\n       bool_or(t.d = 30) AS \"bool_or(c=30)\"\nFROM \"T\" as t\nWHERE false; -- empty rows\n/* # Output #\n #rows | #d | sum(d) | max(b) | bool_and(c) | bool_or(c=30)\n-------+----+--------+--------+-------------+---------------\n     0 |  0 |        |        |             |\n(1 row) */\n\nSELECT string_agg(t.a::text, ',' ORDER BY t.d) AS \"all a\"\nFROM \"T\" AS t;\n/* # Output #\n   all a\n-----------\n 1,4,3,2,5\n(1 row) */\n\nSELECT SUM(t.d) FILTER (WHERE t.c) AS \"sum(t.d WHERE t.c)\",\n       SUM(t.d) AS \"sum(t.d)\"\nFROM \"T\" as t;\n/* # Output #\n sum(t.d WHERE t.c) | sum(t.d)\n--------------------+----------\n                 50 |      100\n(1 row) */\n\nSELECT SUM(CASE WHEN t.c THEN t.d ELSE 0 END) AS \"sum(t.d WHERE t.c)\", -- 0 \u003c=\u003e NULL\n       SUM(t.d) AS \"sum(t.d)\"\nFROM \"T\" AS t;\n/* # Output #\n sum(t.d WHERE t.c) | sum(t.d)\n--------------------+----------\n                 50 |      100\n(1 row) */\n\nSELECT SUM(t.d) FILTER (WHERE t.b = 'x') AS \"sum('x')\",\n       SUM(t.d) FILTER (WHERE t.b = 'y') AS \"sum('y')\",\n       SUM(t.d) FILTER (WHERE t.b NOT IN ('x', 'y')) AS \"sum(not 'x' or 'y')\"\nFROM \"T\" AS t;\n/* # Output #\n sum('x') | sum('y') | sum(not 'x' or 'y')\n----------+----------+---------------------\n       40 |       60 |\n(1 row) */\n\nSELECT COUNT(DISTINCT t.c) AS \"count of unique non-null t.c\",\n       COUNT(t.c) AS \"count of non-null t.c\"\nFROM \"T\" AS t;\n/* # Output #\n count of unique non-null t.c | count of non-null t.c\n------------------------------+-----------------------\n                            2 |                     5\n(1 row) */\n```\n\n### 08. GROUP BY, grouping + aggregation, pseudo aggregate the()\n\n```sql\nSELECT t.b AS \"b\",\n       COUNT(*) AS \"count\",\n       SUM(t.d) AS \"sum(d)\",\n       bool_and(t.a % 2 = 0) AS \"a is even\",\n       string_agg(t.a::text, ';') AS \"all a\"\nFROM \"T\" AS t\nGROUP BY t.b; -- HAVING: acts like WHERE but after grouping\n/* # Output #\n b | count | sum(d) | a is even | all a\n---+-------+--------+-----------+-------\n y |     2 |     60 | t         | 2;4\n x |     3 |     40 | f         | 1;3;5\n(2 rows) */\n\nSELECT t.b AS \"b\",\n       COUNT(*) AS \"count\",\n       SUM(t.d) AS \"sum(d)\",\n       bool_and(t.a % 2 = 0) AS \"a is even\",\n       string_agg(t.a::text, ';') AS \"all a\"\nFROM \"T\" AS t\nGROUP BY t.b\nHAVING COUNT(*) \u003e 2;\n/* # Output #\n b | count | sum(d) | a is even | all a\n---+-------+--------+-----------+-------\n x |     3 |     40 | f         | 1;3;5\n(1 row) */\n\nSELECT t.a % 2 AS \"a odd?\",\n       COUNT(*) as \"count\"\nFROM \"T\" as t\nGROUP BY t.a % 2;\n/* # Output #\n a odd? | count\n--------+-------\n      0 |     2\n      1 |     3\n(2 rows) */\n\nSELECT t.b AS \"b\",\n       t.a % 2 AS \"a odd?\"\nFROM \"T\" AS t\nGROUP BY t.b, t.a % 2;\n/* # Output #\n b | a odd?\n---+--------\n y |      0\n x |      1\n(2 rows) */\n```\n\n### 09. Bag/set operations: UNION/INTERSECT/EXCEPT\n\n```sql\nSELECT t.*\nFROM \"T\" AS t\nWHERE t.c\n  UNION ALL\nSELECT t.*\nFROM \"T\" AS t\nWHERE NOT t.c;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 5 | x | t |\n 3 | x | f | 30\n 4 | y | f | 20\n(5 rows) */\n\nSELECT t.b\nFROM \"T\" AS t\nWHERE t.c\n  UNION\nSELECT t.b\nFROM \"T\" AS t\nWHERE NOT t.c;\n/* # Output #\n b\n---\n x\n y\n x\n x\n y\n(5 rows) */\n\nSELECT t.b\nFROM \"T\" AS t\nWHERE t.c\n  UNION\nSELECT t.b\nFROM \"T\" AS t\nWHERE NOT t.c;\n/* # Output #\n b\n---\n x\n y\n(2 rows) */\n\nSELECT 1 AS q, t.b\nFROM \"T\" AS t\nWHERE t.c\n  UNION ALL\nSELECT 2 AS q, t.b\nFROM \"T\" AS t\nWHERE NOT t.c;\n/* # Output #\n q | b\n---+---\n 1 | x\n 1 | y\n 1 | x\n 2 | x\n 2 | y\n(5 rows) */\n\nSELECT t.b\nFROM \"T\" AS t\nWHERE t.c       -- 'x' 'y' 'x'\n  EXCEPT ALL\nSELECT t.b\nFROM \"T\" AS t\nWHERE NOT t.c;  -- 'x' 'y'\n/* # Output #\n b\n---\n x\n(1 row) */\n\nSELECT t.b\nFROM \"T\" AS t\nWHERE NOT t.c -- 'x' 'y'\n  EXCEPT ALL\nSELECT t.b\nFROM \"T\" AS t\nWHERE t.c;    -- 'x' 'y' 'x'\n/* # Output #\n b\n---\n(0 rows) */\n```\n\n### 10. Syntactic sugar: GROUPING SETS/ROLLUP/CUBE\n\n```sql\nDROP TABLE IF EXISTS prehistoric;\nCREATE TABLE prehistoric (class text,\n                          \"herbivore?\" boolean,\n                          legs int,\n                          species text);\n\nINSERT INTO prehistoric VALUES\n  ('mammalia', true, 2, 'Megatherium'),\n  ('mammalia', true, 4, 'Paraceratherium'),\n  ('mammalia', false, 2, NULL),\n  ('mammalia', false, 4, 'Sabretooth'),\n  ('reptilia', true, 2, 'Iguanodon'),\n  ('reptilia', true, 4, 'Brachiosaurus'),\n  ('reptilia', false, 2, 'Velociraptor'),\n  ('reptilia', false, 4, NULL);\n```\n\n#### `GROUPING SETS`\n\n```sql\nSELECT p.class,\n       p.\"herbivore?\",\n       p.legs,\n       string_agg(p.species, ', ') AS species -- string_agg ignores NULL\nFROM prehistoric AS p\nGROUP BY GROUPING SETS ((class), (\"herbivore?\"), (legs));\n/* # Output #\n  class   | herbivore? | legs |                        species\n----------+------------+------+--------------------------------------------------------\n reptilia |            |      | Iguanodon, Brachiosaurus, Velociraptor\n mammalia |            |      | Megatherium, Paraceratherium, Sabretooth\n          | f          |      | Sabretooth, Velociraptor\n          | t          |      | Megatherium, Paraceratherium, Iguanodon, Brachiosaurus\n          |            |    4 | Paraceratherium, Sabretooth, Brachiosaurus\n          |            |    2 | Megatherium, Iguanodon, Velociraptor\n(6 rows) */\n```\n\n- Without using GROUPING SETS the same result can be acquired by using three GROUP BY and two UNION ALL.\n\n```sql\nSELECT p.class,\n       NULL::boolean AS \"herbivore?\",\n       NULL::int AS legs,\n       string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY p.class\n  /* # Output #\n    class   | herbivore? | legs |                 species\n  ----------+------------+------+------------------------------------------\n  reptilia |            |      | Iguanodon, Brachiosaurus, Velociraptor\n  mammalia |            |      | Megatherium, Paraceratherium, Sabretooth\n  (2 rows) */\n\n  UNION ALL\n\nSELECT NULL::text AS class,\n       p.\"herbivore?\",\n       NULL::int AS legs,\n       string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY p.\"herbivore?\"\n  /* # Output #\n  class | herbivore? | legs |                        species\n  -------+------------+------+--------------------------------------------------------\n        | f          |      | Sabretooth, Velociraptor\n        | t          |      | Megatherium, Paraceratherium, Iguanodon, Brachiosaurus\n  (2 rows) */\n\n  UNION ALL\n\nSELECT NULL::text AS class,\n       NULL::boolean AS \"herbivore?\",\n       p.legs AS legs,\n       string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY p.legs;\n  /* # Output #\n  class | herbivore? | legs |                  species\n  -------+------------+------+--------------------------------------------\n        |            |    4 | Paraceratherium, Sabretooth, Brachiosaurus\n        |            |    2 | Megatherium, Iguanodon, Velociraptor\n  (2 rows) */\n\n/* # Final Output #\n  class   | herbivore? | legs |                        species\n----------+------------+------+--------------------------------------------------------\n reptilia |            |      | Iguanodon, Brachiosaurus, Velociraptor\n mammalia |            |      | Megatherium, Paraceratherium, Sabretooth\n          | f          |      | Sabretooth, Velociraptor\n          | t          |      | Megatherium, Paraceratherium, Iguanodon, Brachiosaurus\n          |            |    4 | Paraceratherium, Sabretooth, Brachiosaurus\n          |            |    2 | Megatherium, Iguanodon, Velociraptor\n(6 rows) */\n```\n\n#### `ROLLUP`\n\n```sql\nSELECT p.class,\n       p.\"herbivore?\",\n       p.legs,\n       string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY ROLLUP (class, \"herbivore?\", legs);\n/* # Output #\n  class   | herbivore? | legs |                                     species\n\n----------+------------+------+----------------------------------------------------------------------------------\n mammalia | f          |    2 |\n mammalia | f          |    4 | Sabretooth\n mammalia | f          |      | Sabretooth\n mammalia | t          |    2 | Megatherium\n mammalia | t          |    4 | Paraceratherium\n mammalia | t          |      | Megatherium, Paraceratherium\n mammalia |            |      | Sabretooth, Megatherium, Paraceratherium\n reptilia | f          |    2 | Velociraptor\n reptilia | f          |    4 |\n reptilia | f          |      | Velociraptor\n reptilia | t          |    2 | Iguanodon\n reptilia | t          |    4 | Brachiosaurus\n reptilia | t          |      | Iguanodon, Brachiosaurus\n reptilia |            |      | Velociraptor, Iguanodon, Brachiosaurus\n          |            |      | Sabretooth, Megatherium, Paraceratherium, Velociraptor, Iguanodon, Brachiosaurus\n(15 rows) */\n\nSELECT string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY (); -- same as w/o GROUP BY\n/* # Output #\n                                     species\n----------------------------------------------------------------------------------\n Megatherium, Paraceratherium, Sabretooth, Iguanodon, Brachiosaurus, Velociraptor\n(1 row) */\n```\n\n#### `CUBE`\n\n```sql\nSELECT p.class,\n       p.\"herbivore?\",\n       p.legs,\n       string_agg(p.species, ', ') AS species\nFROM prehistoric AS p\nGROUP BY CUBE (class, \"herbivore?\", legs);\n/* # Output #\n  class   | herbivore? | legs |                                     species\n----------+------------+------+----------------------------------------------------------------------------------\n mammalia | f          |    2 |\n mammalia | f          |    4 | Sabretooth\n mammalia | f          |      | Sabretooth\n mammalia | t          |    2 | Megatherium\n mammalia | t          |    4 | Paraceratherium\n mammalia | t          |      | Megatherium, Paraceratherium\n mammalia |            |      | Sabretooth, Megatherium, Paraceratherium\n reptilia | f          |    2 | Velociraptor\n reptilia | f          |    4 |\n reptilia | f          |      | Velociraptor\n reptilia | t          |    2 | Iguanodon\n reptilia | t          |    4 | Brachiosaurus\n reptilia | t          |      | Iguanodon, Brachiosaurus\n reptilia |            |      | Velociraptor, Iguanodon, Brachiosaurus\n          |            |      | Sabretooth, Megatherium, Paraceratherium, Velociraptor, Iguanodon, Brachiosaurus\n          | f          |    2 | Velociraptor\n          | f          |    4 | Sabretooth\n          | f          |      | Velociraptor, Sabretooth\n          | t          |    2 | Megatherium, Iguanodon\n          | t          |    4 | Paraceratherium, Brachiosaurus\n          | t          |      | Megatherium, Iguanodon, Paraceratherium, Brachiosaurus\n          |            |    4 | Sabretooth, Paraceratherium, Brachiosaurus\n          |            |    2 | Megatherium, Velociraptor, Iguanodon\n reptilia |            |    4 | Brachiosaurus\n mammalia |            |    2 | Megatherium\n mammalia |            |    4 | Sabretooth, Paraceratherium\n reptilia |            |    2 | Velociraptor, Iguanodon\n(27 rows) */\n```\n\n### 11. SQL reading vs. evaluation order, CTEs (WITH)\n\n```sql\nWITH\n  prehistoric(class, \"herbivore?\", legs, species) AS (\n    VALUES ('mammalia', true, 2, 'Megatherium'),\n           ('reptilia', false, 4, NULL)\n  )\nSELECT MAX(p.legs)\nFROM prehistoric AS p;\n/* # Output #\n max\n-----\n   4\n(1 row) */\n```\n\n### 12. Use case for WITH: dinosaur locomotion\n\n```sql\nDROP TABLE IF EXISTS dinosaurs;\nCREATE TABLE dinosaurs (species text, height float, length float, legs int);\n\nINSERT INTO dinosaurs(species, height, length, legs) VALUES\n  ('Ceratosaurus',    4.0,  6.1,  2),\n  ('Deinonychus',     1.5,  2.7,  2),\n  ('Microvenator',    0.8,  1.2,  2),\n  ('Plateosaurus',    2.1,  7.9,  2),\n  ('Spinosaurus',     2.4,  12.2, 2),\n  ('Tyrannosaurus',   7.0,  15.2, 2),\n  ('Velociraptor',    0.6,  1.8,  2),\n  ('Apatosaurus',     2.2,  22.9, 4),\n  ('Brachiosaurus',   7.6,  30.5, 4),\n  ('Diplodocus',      3.6,  27.1, 4),\n  ('Supersaurus',     10.0, 30.5, 4),\n  ('Albertosaurus',   4.6,  9.1,  NULL),\n  ('Argentinosaurus', 10.7, 36.6, NULL),\n  ('Compsognathus',   0.6,  0.9,  NULL),\n  ('Gallimimus',      2.4,  5.5,  NULL),\n  ('Mamenchisaurus',  5.3,  21.0, NULL),\n  ('Oviraptor',       0.9,  1.5,  NULL),\n  ('Ultrasaurus',     8.1,  30.5, NULL);\n```\n\n```sql\nWITH bodies(legs, shape) AS (\n  SELECT d.legs, avg(d.height / d.length) AS shape\n  FROM dinosaurs AS d\n  WHERE d.legs IS NOT NULL\n  GROUP BY d.legs\n)\n  -- TABLE bodies;\n  /* # Output #\n  legs |        shape\n  ------+---------------------\n      4 | 0.20149009443419652\n      2 |  0.4477662389355141\n  (2 rows) */\n\nSELECT d.species, d.height, d.length,\n  (SELECT b.legs\n   FROM bodies AS b\n   ORDER BY abs(b.shape - d.height / d.length)\n   LIMIT 1) AS legs\nFROM dinosaurs AS d\nWHERE d.legs IS NULL\n\n  UNION ALL\n\nSELECT d.*\nFROM dinosaurs AS d\nWHERE d.legs IS NOT NULL;\n/* # Output #\n     species     | height | length | legs\n-----------------+--------+--------+------\n Albertosaurus   |    4.6 |    9.1 |    2\n Argentinosaurus |   10.7 |   36.6 |    4\n Compsognathus   |    0.6 |    0.9 |    2\n Gallimimus      |    2.4 |    5.5 |    2\n Mamenchisaurus  |    5.3 |     21 |    4\n Oviraptor       |    0.9 |    1.5 |    2\n Ultrasaurus     |    8.1 |   30.5 |    4\n Ceratosaurus    |      4 |    6.1 |    2\n Deinonychus     |    1.5 |    2.7 |    2\n Microvenator    |    0.8 |    1.2 |    2\n Plateosaurus    |    2.1 |    7.9 |    2\n Spinosaurus     |    2.4 |   12.2 |    2\n Tyrannosaurus   |      7 |   15.2 |    2\n Velociraptor    |    0.6 |    1.8 |    2\n Apatosaurus     |    2.2 |   22.9 |    4\n Brachiosaurus   |    7.6 |   30.5 |    4\n Diplodocus      |    3.6 |   27.1 |    4\n Supersaurus     |     10 |   30.5 |    4\n(18 rows) */\n```\n\n## Chapter 3\n\n### 13. Built-in data types, CAST, casting text literals\n\n```sql\nSELECT string_agg(t.typname, ', ') AS \"data types\"\nFROM pg_catalog.pg_type AS t\nWHERE t.typelem = 0   -- disregard array element types\n  AND t.typrelid = 0; -- list non-composite types only\n```\n\n- data types:\n\n`bool, bytea, char, int8, int2, int4, regproc, text, oid, tid, xid, cid, json, xml, pg_node_tree, pg_ndistinct, pg_dependencies, pg_mcv_list, pg_ddl_command, path, polygon, float4, float8, unknown, circle, money, macaddr, inet, cidr, macaddr8, aclitem, bpchar, varchar, date, time, timestamp, timestamptz, interval, timetz, bit, varbit, numeric, refcursor, regprocedure, regoper, regoperator, regclass, regtype, regrole, regnamespace, uuid, pg_lsn, tsvector, gtsvector, tsquery, regconfig, regdictionary, jsonb, jsonpath, txid_snapshot, int4range, numrange, tsrange, tstzrange, daterange, int8range, record, cstring, any, anyarray, void, trigger, event_trigger, language_handler, internal, opaque, anyelement, anynonarray, anyenum, fdw_handler, index_am_handler, tsm_handler, table_am_handler, anyrange, cardinal_number, character_data, sql_identifier, time_stamp, yes_or_no`\n\n#### Type casts\n\n- Runtime type conversion\n\n```sql\nSELECT 6.2::int; -- 6\nSELECT 6.8::int; -- 7\nSELECT date('13 Feb, 2000'); -- 2000-02-13\n```\n\n- Implicit conversion\n\n```sql\nINSERT INTO \"T\"(a,b,c,d) VALUES (6.2, NULL, 'true', '0');\n/* # Inserted row #\n a | b | c | d\n---+---+---+---\n 6 |   | t | 0\n(1 row) */\n\nSELECT booleans.yup::boolean, booleans.nope::boolean\nFROM (VALUES ('true', 'false'),\n             ('True', 'False'),\n             ('t',    'f'),\n             ('1',    '0'),\n             ('yes',  'no'),\n             ('on',   'off')) AS booleans(yup, nope);\n/* # Output #\n yup | nope\n-----+------\n t   | f\n t   | f\n t   | f\n t   | f\n t   | f\n t   | f\n(6 rows) */\n```\n\n- XML\n\n```sql\nSELECT $$\u003ct a='42'\u003e\u003cl/\u003e\u003cr/\u003e\u003c/t\u003e$$::xml;\n/* # Output #\n          xml\n------------------------\n \u003ct a='42'\u003e\u003cl/\u003e\u003cr/\u003e\u003c/t\u003e\n(1 row) */\n\nSELECT $int$42$int$::int;\n/* # Output #\n int4\n------\n   42\n(1 row) */\n```\n\n- CSV\n\n```sql\nDELETE FROM \"T\";\n\nCOPY \"T\"(a,b,c,d) FROM STDIN WITH (FORMAT CSV, NULL '*');\n1,x,true,10\n2,y,true,40\n3,x,false,30\n4,y,false,20\n5,x,true,*\n\\.\n\nTABLE \"T\";\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n```\n\n### 14. String data types (char/varchar/text), type numeric(s,p)\n\n- `char`: `char(1)`\n- `char(n)`: fixed length, blank (` `) padded if needed\n- `varchar(n)`: varying length \u003c= n characters\n- `text`: varying length, unlimited\n\n```sql\nSELECT '01234'::char(3);\n/* # Output #\n bpchar -- bpchar: blank-padded character\n--------\n 012\n(1 row) */\n\nSELECT t.c::char(10)\nFROM (VALUES ('01234'),\n             ('0123456789')\n     ) AS t(c);\n/* # Output #\n     c\n------------\n 01234\n 0123456789\n(2 rows) */\n\nSELECT t.c,\n       length(t.c) AS chars,\n       octet_length(t.c) AS bytes\nFROM (VALUES ('x'),\n             ('池'),\n             ('😀😃')\n     ) AS t(c);\n/* # Output #\n  c   | chars | bytes\n------+-------+-------\n x    |     1 |     1\n 池   |     1 |     3\n 😀😃 |     2 |     8\n(3 rows) */\n\nSELECT octet_length('0123456789'::varchar(5)) AS c1,  -- 5 (truncation)\n       octet_length('012'       ::varchar(5)) AS c2,  -- 3 (within limits)\n       octet_length('012'       ::char(5)) AS c3,     -- 5 (blank padding in storage)\n       length('012'             ::char(5)) AS c4,     -- 3 (padding in storage only)\n       length('012  '           ::char(5)) AS c5;     -- 3 (trailing blanks removed)\n/* # Output #\n c1 | c2 | c3 | c4 | c5\n----+----+----+----+----\n  5 |  3 |  5 |  3 |  3\n(1 row) */\n```\n\n#### Numeric\n\n```sql\n\\pset t on\nSELECT (2::numeric)^100000; -- SQL syntax allows numeric(1000) only\n\\pset t off\n/* # Output #\n99900209301438450794403276433003359098042913905418169177152927386314583246425...\n(1 row) */\n\nEXPLAIN ANALYZE\nWITH one_million_rows(x) AS (\n  SELECT t.x::numeric(8,0)\n  FROM generate_series(0,1000000) AS t(x)\n)\nSELECT t.x + t.x AS add\nFROM one_million_rows AS t;\n/* # Output #\n Planning Time: 0.030 ms\n Execution Time: 375.387 ms\n(2 rows) */\n\nEXPLAIN ANALYZE\nWITH one_million_rows(x) AS (\n  SELECT t.x::int\n  FROM generate_series(0,1000000) AS t(x)\n)\nSELECT t.x + t.x AS add\nFROM one_million_rows AS t;\n/* # Output #\n Planning Time: 0.027 ms\n Execution Time: 195.863 ms\n(2 rows) */\n```\n\n### 15. Types date/time/timestamp/interval, date/time arithmetics\n\n```sql\nSELECT 'now'::date AS \"now (date)\",\n       'now'::time AS \"now (time)\",\n       'now'::timestamp AS \"now (timestamp)\";\n/* # Output #\n now (date) |   now (time)    |      now (timestamp)\n------------+-----------------+----------------------------\n 2023-10-30 | 11:48:10.652387 | 2023-10-30 11:48:10.652387\n(1 row) */\n\nSELECT 'now'::timestamp AS \"now\",\n       'now'::timestamp with time zone AS \"now with time zone\",\n       'now'::timestamptz AS \"now tz\";\n/* # Output #\n            now             |      now with time zone       |            now tz\n----------------------------+-------------------------------+-------------------------------\n 2023-10-30 11:50:21.777615 | 2023-10-30 11:50:21.777615+00 | 2023-10-30 11:50:21.777615+00\n(1 row) */\n\nSET datestyle='German,MDY';\nSELECT '1-2-2000'::date;\n/* # Output #\n    date\n------------\n 02.01.2000\n(1 row) */\n\nSET datestyle='German,DMY';\nSELECT '1-2-2000'::date;\n/* # Output #\n    date\n------------\n 01.02.2000\n(1 row) */\n\nSET datestyle='ISO,MDY'; -- default datestyle\n\nSELECT COUNT(DISTINCT birthdays.d::date) AS interpretations\nFROM (VALUES ('August 26, 1968'),\n             ('Aug 26, 1968'),\n             ('08.26.1968'),\n             ('08-26-1968'),\n             ('8/26/1968')) AS birthdays(d);\n/* # Output #\n interpretations\n-----------------\n               1\n(1 row) */\n\nSELECT 'epoch'::timestamp AS epoch,\n       'infinity'::timestamp AS infinity,\n       'today'::date AS today,\n       'yesterday'::date AS yesterday,\n       'tomorrow'::date AS tomorrow;\n/* # Output #\n        epoch        | infinity |   today    | yesterday  |  tomorrow\n---------------------+----------+------------+------------+------------\n 1970-01-01 00:00:00 | infinity | 2023-10-30 | 2023-10-29 | 2023-10-31\n(1 row) */\n\nSELECT '1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval;\n/* # Output #\n           interval\n-------------------------------\n 1 year 2 mons 3 days 04:05:06\n(1 row) */\n\nSELECT 'P1Y2M3DT4H5M6S'::interval; -- ISO 8601\n/* # Output #\n           interval\n-------------------------------\n 1 year 2 mons 3 days 04:05:06\n(1 row) */\n\nSELECT ('now'::timestamp - 'yesterday'::date)::interval;\n/* # Output #\n       interval\n-----------------------\n 1 day 12:30:20.951839\n(1 row) */\n\n\\x on -- expanded display\nSELECT 'Aug 31, 2035'::date - 'now'::timestamp AS retirement,\n       'now'::date + '30 days'::interval AS in_one_month,\n       'now'::date + 2 * '1 month'::interval AS in_two_months,\n       'tomorrow'::date - 'now'::timestamp AS til_midnight,\n       extract(hours from('tomorrow'::date - 'now'::timestamp)) AS hours_til_midnight,\n       'tomorrow'::date - 'yesterday'::date AS two,\n       make_interval(days =\u003e 'tomorrow'::date - 'yesterday'::date) AS two_days;\n\\x off\n/* # Output #\n-[ RECORD 1 ]------+--------------------------\nretirement         | 4322 days 11:19:23.836858\nin_one_month       | 2023-11-29 00:00:00\nin_two_months      | 2023-12-30 00:00:00\ntil_midnight       | 11:19:23.836858\nhours_til_midnight | 11\ntwo                | 2\ntwo_days           | 2 days */\n\nSELECT (make_date(2023, months.m, 1) - '1 day'::interval)::date AS last_day_of_month\nFROM generate_series(1,12) AS months(m);\n/* # Output #\n------------------+-----------\nlast_day_of_month | 2022-12-31\nlast_day_of_month | 2023-01-31\nlast_day_of_month | 2023-02-28\nlast_day_of_month | 2023-03-31\nlast_day_of_month | 2023-04-30\nlast_day_of_month | 2023-05-31\nlast_day_of_month | 2023-06-30\nlast_day_of_month | 2023-07-31\nlast_day_of_month | 2023-08-31\nlast_day_of_month | 2023-09-30\nlast_day_of_month | 2023-10-31\nlast_day_of_month | 2023-11-30 */\n\nSELECT timezones.tz AS timezone,\n       'now'::timestamp with time zone\n        -\n       ('now'::timestamp::text || ' ' || timezones.tz)::timestamp with time zone AS difference\nFROM   (VALUES ('America/New_York'),\n               ('Europe/Istanbul'),\n               ('Asia/Tokyo'),\n               ('PST'),\n               ('UTC'),\n               ('UTC-6'),\n               ('+5')\n       ) AS timezones(tz)\nORDER BY difference;\n/* # Output #\n-[ RECORD 1 ]----------------\ntimezone   | PST\ndifference | -08:00:00\n-[ RECORD 2 ]----------------\ntimezone   | America/New_York\ndifference | -04:00:00\n-[ RECORD 3 ]----------------\ntimezone   | UTC\ndifference | 00:00:00\n-[ RECORD 4 ]----------------\ntimezone   | Europe/Istanbul\ndifference | 03:00:00\n-[ RECORD 5 ]----------------\ntimezone   | +5\ndifference | 05:00:00\n-[ RECORD 6 ]----------------\ntimezone   | UTC-6\ndifference | 06:00:00\n-[ RECORD 7 ]----------------\ntimezone   | Asia/Tokyo\ndifference | 09:00:00 */\n\nSELECT holidays.holiday\nFROM (VALUES ('Easter',    'Apr  6, 2023', 'Apr 18, 2023'),\n             ('Pentecost', 'Jun  2, 2023', 'Jun 13, 2023'),\n             ('Summer',    'Jul 30, 2023', 'Sep  9, 2023'),\n             ('Autumn',    'Oct 26, 2023', 'Oct 31, 2023'),\n             ('Winter',    'Dec 23, 2023', 'Jan  9, 2024')) AS holidays(holiday, \"start\", \"end\")\nWHERE (holidays.start::date, holidays.end::date) overlaps('today', 'today');\n/* # Output #\n-[ RECORD 1 ]---\nholiday | Autumn */\n```\n\n### 16. User-defined types: enumerations (CREATE TYPE ... AS ENUM)\n\n```sql\nDROP TYPE IF EXISTS episode CASCADE; -- CASCADE: deletes episode columns\nCREATE TYPE episode AS ENUM\n  ('ANH', 'ESB', 'TPM', 'AOTC', 'ROTS', 'ROTJ', 'TFA', 'TLJ', 'TROS');\n\nSELECT 'ESB'::episode;\n/* # Output #\n episode\n---------\n ESB\n(1 row) */\n\nDROP TABLE IF EXISTS starwars;\nCREATE TABLE starwars(film episode PRIMARY KEY,\n                      title text,\n                      release date);\n\nINSERT INTO starwars(film,title,release) VALUES\n  ('TPM',  'The Phantom Menace',      'May 19, 1999'),\n  ('AOTC', 'The Empire Strikes Back', 'May 16, 2002'),\n  ('ROTS', 'Revenge of the Sith',     'May 19, 2005'),\n  ('ANH',  'A New Hope',              'May 25, 1977'),\n  ('ESB',  'The Empire Strikes Back', 'May 21, 1980'),\n  ('ROTJ', 'Return of the Jedi',      'May 25, 1983'),\n  ('TFA',  'The Force Awakens',       'Dec 18, 2015'),\n  ('TLJ',  'The Last Jedi',           'Dec 15, 2017'),\n  ('TROS', 'The Rise of Skywalker',   'Dec 19, 2019');\n\nINSERT INTO starwars(film,title,release) VALUES\n  ('R1', 'Rogue One', 'Dec 15, 2016');\n/* # Output #\nERROR:  invalid input value for enum episode: \"R1\" */\n\nSELECT s.*\nFROM starwars AS s\nORDER BY s.release;\n/* # Output #\n film |          title          |  release\n------+-------------------------+------------\n ANH  | A New Hope              | 1977-05-25\n ESB  | The Empire Strikes Back | 1980-05-21\n ROTJ | Return of the Jedi      | 1983-05-25\n TPM  | The Phantom Menace      | 1999-05-19\n AOTC | The Empire Strikes Back | 2002-05-16\n ROTS | Revenge of the Sith     | 2005-05-19\n TFA  | The Force Awakens       | 2015-12-18\n TLJ  | The Last Jedi           | 2017-12-15\n TROS | The Rise of Skywalker   | 2019-12-19\n(9 rows) */\n\nSELECT s.*\nFROM starwars AS s\nORDER BY s.film; -- the Star Wars Machete order\n/* # Output #\n film |          title          |  release\n------+-------------------------+------------\n ANH  | A New Hope              | 1977-05-25\n ESB  | The Empire Strikes Back | 1980-05-21\n TPM  | The Phantom Menace      | 1999-05-19\n AOTC | The Empire Strikes Back | 2002-05-16\n ROTS | Revenge of the Sith     | 2005-05-19\n ROTJ | Return of the Jedi      | 1983-05-25\n TFA  | The Force Awakens       | 2015-12-18\n TLJ  | The Last Jedi           | 2017-12-15\n TROS | The Rise of Skywalker   | 2019-12-19\n(9 rows) */\n```\n\n### 17. Bit strings (bit(n)), BLOBS, and byte types (bytea)\n\n#### Bitwise Operations\n\n- `\u0026`: and\n- `|`: or\n- `#`: xor\n- `~`: not\n- `\u003c\u003c`/`\u003e\u003e`: shift left/right,\n- `get_bit()`\n- `set_bit()`\n\n#### String-like Operations\n\n- `||`: concatenation\n- `length()`\n- `bit_length()`\n- `position( in )`\n\n```sql\nSELECT B'00101010', X'2A', '00101010'::bit(8), 42::bit(8); -- X'2A': 2 * 4 bits\n/* # Output #\n ?column? | ?column? |   bit    |   bit\n----------+----------+----------+----------\n 00101010 | 00101010 | 00101010 | 00101010\n(1 row) */\n```\n\n```sql\nSELECT encode('chsdwn', 'base64');\n/* # Output #\n  encode\n----------\n Y2hzZHdu\n(1 row) */\n```\n\n#### Install Python 3 and create language:\n\n- List running container: `docker ps`\n- Run the postgresql container's bash: `docker exec -it \u003ccontainer_id\u003e bash`\n- Update debian packages: `apt-get update`\n- Install plpython3 package for PostgreSQL 12: `apt install postgresql-plpython3-12`\n- Check python version and make sure its installed: `python3 --version`\n- Create language:\n\n```sql\nCREATE LANGUAGE plpython3u;\n```\n\n```sql\nDROP FUNCTION IF EXISTS read_blob(text) CASCADE;\nCREATE FUNCTION read_blob(blob text) RETURNS bytea AS\n$$\n  try:\n    file = open(blob, \"rb\")\n    return file.read()\n  except:\n    pass\n  # could not read file, return NULL\n  return None\n$$ LANGUAGE plpython3u;\n\nDROP TYPE IF EXISTS edition CASCADE;\nCREATE TYPE edition AS ENUM ('Portal 1', 'Portal 2');\n\nDROP TABLE IF EXISTS glados;\nCREATE TABLE glados (id int PRIMARY KEY,\n                     voice bytea,\n                     line text,\n                     portal edition);\n\n\\set blob_path './glados-voice'\nINSERT INTO glados(id, line, portal, voice)\n  SELECT quotes.id, quotes.line, quotes.portal::edition,\n         read_blob(:'blob_path' || quotes.mp3) AS voice\n  FROM\n    (VALUES (1, 'one', 'Portal 1', '1.mp3'),\n            (2, 'two', 'Portal 2', '2.mp3')) AS quotes(id, line, portal, mp3);\n\nSELECT g.id, g.line, g.portal,\n       left(encode(g.voice, 'base64'), 20) AS voice\nFROM glados AS g;\n/* # Output #\n id | line |  portal  |        voice\n----+------+----------+---------------------\n  1 | one  | Portal 1 | UklGRvpIBABXQVZFZm10\n  2 | two  | Portal 2 | UklGRjc6DABXQVZFZm10\n(2 rows) */\n\nCOPY (\n  SELECT translate(encode(g.voice, 'base64'), E'\\n', '')\n  FROM glados AS g\n  WHERE g.id = 1\n) TO PROGRAM 'base64 -D \u003e /tmp/1.mp3';\n```\n\n### 18. Range/interval types and operations\n\n```sql\nSELECT '[5,10)'::int4range;\n/* # Output #\n int4range\n-----------\n [5,10)\n(1 row) */\n\nSELECT int4range(1,5,'[]');\n/* # Output #\n int4range\n-----------\n [1,6)\n(1 row) */\n\nSELECT int4range(1,5,'[]') * '[5,10)'::int4range;\n/* # Output #\n ?column?\n----------\n [5,6)\n(1 row) */\n\nSELECT int4range(1,5,'[)') * '[5,10)'::int4range;\n/* # Output #\n ?column?\n----------\n empty\n(1 row) */\n```\n\n### 19. Geometric objects and operations, use case: shape scanner\n\n- `point(x,y)`\n- `line(x,y)`\n- `box(p1,p2)`\n- `[p1,...,pn]`: open path\n- `(p1,...,pn)`: polygon\n- `circle(p,r)`\n\n#### Operations\n\n|          | Operation            |                | Operation           |\n| -------- | -------------------- | -------------- | ------------------- |\n| `+`, `-` | translate            | `area()`       |                     |\n| `*`      | scale                | `height()`     | height of box       |\n| `@-@`    | length/circumference | `width()`      | width of box        |\n| `@@`     | center               | `bound_box(,)` | bounding box        |\n| `\u003c-\u003e`    | distance between     | `diameter()`   | diameter of circle  |\n| `\u0026\u0026`     | overlaps?            | `center()`     | center              |\n| `\u003c\u003c`     | strictly left of?    | `isclosed()`   | path closed         |\n| `?-\\|`   | is perpendicular?    | `npoints()`    | # of points in path |\n| `@\u003e`     | contains?            | `pclose()`     | close an open path  |\n\n```sql\n\\set N 100000\nSELECT (COUNT(*)::float / :N) * 4 AS pi\nFROM generate_series(1, :N) AS _\nWHERE circle(point(0.5,0.5), 0.5) @\u003e point(random(),random());\n/* # Output #\n   pi\n--------\n 3.1412\n(1 row) */\n```\n\n### 20. JSON support (type jsonb) [and XML support]\n\n```sql\nVALUES (1, '{ \"b\": 1, \"a\": 2 }'::jsonb),  -- pair order flip\n       (2, '{ \"a\": 1, \"b\": 2, \"a\": 3 }'), -- duplicate field\n       (3, '[0,   false, null]');         -- whitespace normalization\n/* # Output #\n column1 |     column2\n---------+------------------\n       1 | {\"a\": 2, \"b\": 1}\n       2 | {\"a\": 3, \"b\": 2}\n       3 | [0, false, null]\n(3 rows) */\n\nVALUES (1, '{ \"b\": 1, \"a\": 2 }'::json),   -- preserve pair order\n       (2, '{ \"a\": 1, \"b\": 2, \"a\": 3 }'), -- preserve duplicates\n       (3, '[0,   false, null]');         -- preserve whitespace\n/* # Output #\n column1 |          column2\n---------+----------------------------\n       1 | { \"b\": 1, \"a\": 2 }\n       2 | { \"a\": 1, \"b\": 2, \"a\": 3 }\n       3 | [0,   false, null]\n(3 rows) */\n\nSELECT ('{ \"a\": 0, \"b\": { \"b1\": 1, \"b2\": 2 } }'::jsonb -\u003e 'b' -\u003e\u003e 'b2')::int + 40;\n/* # Output #\n ?column?\n----------\n       42\n(1 row) */\n\nSELECT row_to_json(t)::jsonb\nFROM \"T\" AS t;\n/* # Output #\n               row_to_json\n------------------------------------------\n {\"a\": 1, \"b\": \"x\", \"c\": true, \"d\": 10}\n {\"a\": 2, \"b\": \"y\", \"c\": true, \"d\": 40}\n {\"a\": 3, \"b\": \"x\", \"c\": false, \"d\": 30}\n {\"a\": 4, \"b\": \"y\", \"c\": false, \"d\": 20}\n {\"a\": 5, \"b\": \"x\", \"c\": true, \"d\": null}\n(5 rows) */\n\nSELECT array_to_json(array_agg(row_to_json(t)))::jsonb\nFROM \"T\" AS t;\n/* # Output #\n[{\"a\": 1, \"b\": \"x\", \"c\": true, \"d\": 10}, {\"a\": 2, \"b\": \"y\", \"c\": true, \"d\": 40}, {\"a\": 3, \"b\": \"x\", \"c\": false, \"d\": 30}, {\"a\": 4, \"b\": \"y\", \"c\": false, \"d\": 20}, {\"a\": 5, \"b\": \"x\", \"c\": true, \"d\": null}] */\n\nSELECT jsonb_pretty(array_to_json(array_agg(row_to_json(t)))::jsonb)\nFROM \"T\" AS t;\n/* # Output #\n    jsonb_pretty\n---------------------\n [                  +\n     {              +\n         \"a\": 1,    +\n         \"b\": \"x\",  +\n         \"c\": true, +\n         \"d\": 10    +\n     },             +\n     {              +\n         \"a\": 2,    +\n         \"b\": \"y\",  +\n         \"c\": true, +\n         \"d\": 40    +\n     },             +\n     {              +\n         \"a\": 3,    +\n         \"b\": \"x\",  +\n         \"c\": false,+\n         \"d\": 30    +\n     },             +\n     {              +\n         \"a\": 4,    +\n         \"b\": \"y\",  +\n         \"c\": false,+\n         \"d\": 20    +\n     },             +\n     {              +\n         \"a\": 5,    +\n         \"b\": \"x\",  +\n         \"c\": true, +\n         \"d\": null  +\n     }              +\n ]\n(1 row) */\n```\n\n```sql\nDROP TABLE IF EXISTS like_T_but_as_JSON;\nCREATE TEMPORARY TABLE like_T_but_as_JSON(a) AS\n  SELECT array_to_json(array_agg(row_to_json(t)))::jsonb\n  FROM \"T\" AS t;\n\nTABLE like_T_but_as_JSON;\n/* # Output #\n[{\"a\": 1, \"b\": \"x\", \"c\": true, \"d\": 10}, {\"a\": 2, \"b\": \"y\", \"c\": true, \"d\": 40}, {\"a\": 3, \"b\": \"x\", \"c\": false, \"d\": 30}, {\"a\": 4, \"b\": \"y\", \"c\": false, \"d\": 20}, {\"a\": 5, \"b\": \"x\", \"c\": true, \"d\": null}] */\n\nSELECT o\nFROM jsonb_array_elements((TABLE like_T_but_as_JSON)) AS objs(o);\n/* # Output #\n                    o\n------------------------------------------\n {\"a\": 1, \"b\": \"x\", \"c\": true, \"d\": 10}\n {\"a\": 2, \"b\": \"y\", \"c\": true, \"d\": 40}\n {\"a\": 3, \"b\": \"x\", \"c\": false, \"d\": 30}\n {\"a\": 4, \"b\": \"y\", \"c\": false, \"d\": 20}\n {\"a\": 5, \"b\": \"x\", \"c\": true, \"d\": null}\n(5 rows) */\n\nSELECT t.*\nFROM jsonb_array_elements((TABLE like_T_but_as_JSON)) AS objs(o),\n     jsonb_each(o) AS t;\n/* # Output #\n key | value\n-----+-------\n a   | 1\n b   | \"x\"\n c   | true\n d   | 10\n a   | 2\n b   | \"y\"\n c   | true\n d   | 40\n a   | 3\n b   | \"x\"\n c   | false\n d   | 30\n a   | 4\n b   | \"y\"\n c   | false\n d   | 20\n a   | 5\n b   | \"x\"\n c   | true\n d   | null\n(20 rows) */\n\nSELECT t.*\nFROM jsonb_array_elements((TABLE like_T_but_as_JSON)) AS objs(o),\n     jsonb_to_record(o) AS t(a int, b text, c boolean, d int);\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n\nSELECT t.*\nFROM jsonb_array_elements((TABLE like_T_but_as_JSON)) AS objs(o),\n     jsonb_populate_record(NULL::\"T\", o) AS t;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n\nSELECT t.*\nFROM jsonb_populate_recordset(NULL::\"T\", (TABLE like_T_but_as_JSON)) AS t;\n/* # Output #\n a | b | c | d\n---+---+---+----\n 1 | x | t | 10\n 2 | y | t | 40\n 3 | x | f | 30\n 4 | y | f | 20\n 5 | x | t |\n(5 rows) */\n```\n\n### 21. Sequences, key generation via GENERATED ALWAYS AS IDENTITY\n\n```sql\nCREATE SEQUENCE \u003cseq\u003e -- sequence name\n  [ INCREMENT \u003cinc\u003e ] -- advance by \u003cinc\u003e (default: 1)\n  [ MINVALUE \u003cmin\u003e ]  -- range of valid counter values\n  [ MAXVALUE \u003cmax\u003e ]  --  (defaults: [1...2^63-1])\n  [ START \u003cstart\u003e ]   -- start (default: \u003cmin\u003e, \u003cmax\u003e)\n  [ [NO] CYCLE ]      -- wrap around or error\n```\n\n```sql\nDROP SEQUENCE IF EXISTS seq;\nCREATE SEQUENCE seq START 41 MAXVALUE 100 CYCLE;\n\nSELECT nextval('seq');      -- 41\nSELECT nextval('seq');      -- 42\nSELECT currval('seq');      -- 42\nSELECT setval('seq', 100);  -- 100 (side effect)\nSELECT nextval('seq');      -- 1   (wrap around)\n\nTABLE seq;\n/* # Output #\n last_value | log_cnt | is_called\n------------+---------+-----------\n          1 |      32 | t\n(1 row) */\n\nSELECT setval('seq', 100, false); -- 100 (is_called: false)\nSELECT nextval('seq');            -- 100\n```\n\n```sql\nDROP TABLE IF EXISTS self_concious_T;\nCREATE TABLE self_concious_T (me int GENERATED ALWAYS AS IDENTITY,\n                              a int,\n                              b text,\n                              c boolean,\n                              d int);\n\nTABLE self_concious_T_me_seq;\n/* # Output #\n last_value | log_cnt | is_called\n------------+---------+-----------\n          1 |       0 | f\n(1 row) */\n\nINSERT INTO self_concious_T(a,b,c,d)\n  VALUES (1, 'x', true, 10),\n         (2, 'y', true, 40);\n\nINSERT INTO self_concious_T(a,b,c,d)\n  VALUES (5, 'x', true, NULL),\n         (4, 'y', false, 20),\n         (3, 'x', false, 30)\nRETURNING me, c;\n/* # Output #\n me | c\n----+---\n  3 | t\n  4 | f\n  5 | f\n(3 rows) */\n\nTABLE self_concious_T;\n/* # Output #\n me | a | b | c | d\n----+---+---+---+----\n  1 | 1 | x | t | 10\n  2 | 2 | y | t | 40\n  3 | 5 | x | t |\n  4 | 4 | y | f | 20\n  5 | 3 | x | f | 30\n(5 rows) */\n```\n\n## Chapter 4\n\n### 22. Arrays vs. 1NF, array semantics, array literals\n\n- `{x1,...,xn}`: array of elements\n\n```sql\nDROP TABLE IF EXISTS \"Trees\";\nCREATE TABLE \"Trees\" (tree int PRIMARY KEY,\n                      parents int[],\n                      labels text[]);\n\nINSERT INTO \"Trees\"(tree, parents, labels) VALUES\n  (1, array[NULL,1,2,2,1,5], array['a','b','d','e','c','f']),\n  (2, array[4,1,1,6,4,NULL,6], array['d','f','a','b','e','g','c']),\n  (3, array[NULL,1,NULL,1,3], string_to_array('a;b;d;c;e',';'));\n\nTABLE \"Trees\";\n/* # Output #\n tree |      parents       |     labels\n------+--------------------+-----------------\n    1 | {NULL,1,2,2,1,5}   | {a,b,d,e,c,f}\n    2 | {4,1,1,6,4,NULL,6} | {d,f,a,b,e,g,c}\n    3 | {NULL,1,NULL,1,3}  | {a,b,d,c,e}\n(3 rows) */\n```\n\n### 23. Array construction/indexing/slicing, searching in arrays\n\n```sql\nSELECT array_append(array[1,2,3], 4);         -- {1,2,3,4}\nSELECT array[1,2,3] || 4;                     -- {1,2,3,4}\nSELECT array_prepend(4, array[1,2,3]);        -- {4,1,2,3}\nSELECT 4 || array[1,2,3];                     -- {4,1,2,3}\nSELECT array_cat(array[1,2,3], array[4,5,6]); -- {1,2,3,4,5,6}\nSELECT array[1,2,3] || array[4,5,6];          -- {1,2,3,4,5,6}\n\nSELECT (array[4])[1];           -- 4\nSELECT (array[4])[NULL];        -- NULL\nSELECT (array[NULL])[1];        -- NULL\nSELECT (array[1,2,3,4,5])[2:4]; -- {2,3,4}\nSELECT (array[1,2,3,4,5])[2:];  -- {2,3,4,5}\nSELECT (array[1,2,3,4,5])[:4];  -- {1,2,3,4}\n\nSELECT array_length(array[1,2,3], 1); -- 3\nSELECT cardinality(array[1,2,3]);     -- 3\n\nSELECT array_position(array[3,4,5], 4);     -- 2\nSELECT array_position(array[3,4,5], 1);     -- NULL\nSELECT array_positions(array[3,4,5,4], 4);  -- {2,4}\nSELECT array_positions(array[3,4,5,4], 1);  -- {}\nSELECT array_replace(array[1,2,3], 1, 4);   -- {4,2,3}\n```\n\n```sql\nSELECT bool_and(cardinality(t.parents) = cardinality(t.labels))\nFROM \"Trees\" AS t;\n/* # Output #\n bool_and\n----------\n t\n(1 row) */\n\nSELECT t.tree, array_positions(t.labels, 'f') AS \"f nodes\"\nFROM \"Trees\" AS t\nWHERE 'f' = ANY(t.labels);\n/* # Output #\n tree | f nodes\n------+---------\n    1 | {6}\n    2 | {2}\n(2 rows) */\n\nSELECT t.tree, t.labels[array_position(t.parents,NULL)] AS root\nFROM \"Trees\" AS t;\n/* # Output #\n tree | root\n------+------\n    1 | a\n    2 | g\n    3 | a\n(3 rows) */\n\nSELECT t.tree AS forest\nFROM \"Trees\" AS t\nWHERE cardinality(array_positions(t.parents,NULL)) \u003e 1;\n/* # Output #\n forest\n--------\n      3\n(1 row) */\n```\n\n### 24. Array programming via unnest(), array_agg, WITH ORDINALITY\n\n```sql\nSELECT t.elem\nFROM unnest(array[1,2,3]) AS t(elem); -- items order lost\n/* # Output #\n elem\n------\n    1\n    2\n    3\n(3 rows) */\n\nSELECT array_agg(t.elem) AS xs\nFROM (VALUES (1), (2), (3)) AS t(elem);\n/* # Output #\n   xs\n---------\n {1,2,3}\n(1 row) */\n\nSELECT t.*\nFROM unnest(array[6,5,4])\n     WITH ORDINALITY AS t(elem,idx);  -- keep items order on \"idx\" column\n/* # Output #\n elem | idx\n------+-----\n    6 |   1\n    5 |   2\n    4 |   3\n(3 rows) */\n\nSELECT array_agg(t.elem ORDER BY t.idx DESC) AS xs\nFROM (VALUES (6,1), (5,2), (4,3)) AS t(elem,idx);\n/* # Output #\n   xs\n---------\n {4,5,6}\n(1 row) */\n```\n\n```sql\nSELECT node.parent, node.label\nFROM \"Trees\" AS t,\n     unnest(t.parents, t.labels) AS node(parent,label)\nWHERE t.tree = 2;\n/* # Output #\n parent | label\n--------+-------\n      4 | d\n      1 | f\n      1 | a\n      6 | b\n      4 | e\n        | g\n      6 | c\n(7 rows) */\n\nSELECT node.*\nFROM \"Trees\" AS t,\n     unnest(t.parents, t.labels) WITH ORDINALITY AS node(parent,label,idx)\nWHERE t.tree = 2;\n/* # Output #\n parent | label | idx\n--------+-------+-----\n      4 | d     |   1\n      1 | f     |   2\n      1 | a     |   3\n      6 | b     |   4\n      4 | e     |   5\n        | g     |   6\n      6 | c     |   7\n(7 rows) */\n\nSELECT t.tree,\n       array_agg(node.parent ORDER BY node.idx) AS parents,\n       array_agg(upper(node.label) ORDER BY node.idx) AS labels\nFROM \"Trees\" AS t,\n     unnest(t.parents, t.labels) WITH ORDINALITY AS node(parent,label,idx)\nGROUP BY t.tree;\n/* # Output #\n tree |      parents       |     labels\n------+--------------------+-----------------\n    1 | {NULL,1,2,2,1,5}   | {A,B,D,E,C,F}\n    2 | {4,1,1,6,4,NULL,6} | {D,F,A,B,E,G,C}\n    3 | {NULL,1,NULL,1,3}  | {A,B,D,C,E}\n(3 rows) */\n\nSELECT t.tree, t.parents[node.idx] AS \"parent of c\"\nFROM \"Trees\" AS t,\n     unnest(t.labels) WITH ORDINALITY AS node(label,idx)\nWHERE node.label = 'c';\n/* # Output #\n tree | parent of c\n------+-------------\n    1 |           1\n    2 |           6\n    3 |           1\n(3 rows) */\n\nSELECT t.*\nFROM \"Trees\" AS t,\n     unnest(t.parents) AS node(parent)\nWHERE node.parent IS NULL\nGROUP BY t.tree\nHAVING COUNT(*) \u003e 1;\n/* # Output #\n tree |      parents      |   labels\n------+-------------------+-------------\n    3 | {NULL,1,NULL,1,3} | {a,b,d,c,e}\n(1 row) */\n```\n\n### 25. Set-returning/table-generating functions, ROWS FROM (zip)\n\n```sql\nSELECT generate_series(1,10,3); -- (from, to, increment)\n/* # Output #\n generate_series\n-----------------\n               1\n               4\n               7\n              10\n(4 rows) */\n\nSELECT generate_series(0,-2, -1);\n/* # Output #\n generate_series\n-----------------\n               0\n              -1\n              -2\n(3 rows) */\n\nSELECT idx, arr[idx]\nFROM (VALUES (string_to_array('abcde', NULL))) AS _(arr),\n     generate_subscripts(arr, 1) AS idx;\n/* # Output #\n idx | arr\n-----+-----\n   1 | a\n   2 | b\n   3 | c\n   4 | d\n   5 | e\n(5 rows) */\n```\n\n```sql\nSELECT i, xs[i]\nFROM (VALUES (string_to_array('Star Wars', ' '))) AS _(xs),\n     generate_subscripts(xs, 1) AS i;\n/* # Output #\n i |  xs\n---+------\n 1 | Star\n 2 | Wars\n(2 rows) */\n\nSELECT t.word\nFROM regexp_split_to_table('Luke, I am Your Father', '\\s+') AS t(word);\n/* # Output #\n  word\n--------\n Luke,\n I\n am\n Your\n Father\n(5 rows) */\n\nSELECT upper(t.c) AS character, t.pos\nFROM unnest(string_to_array('abcde', NULL))\n     WITH ORDINALITY AS t(c,pos);\n/* # Output #\n character | pos\n-----------+-----\n A         |   1\n B         |   2\n C         |   3\n D         |   4\n E         |   5\n(5 rows) */\n\nSELECT starwars.*\nFROM unnest(array[4,5,1,2,3,6,7,8,9],\n            array['A New Hope',\n                  'The Empire Strikes Back',\n                  'The Phantom Menace',\n                  'Attack of the Clones',\n                  'Revenge of the Sith',\n                  'Return of the Jedi',\n                  'The Force Awakens',\n                  'The Last Jedi',\n                  'The Rise of Skywalker'])\n     WITH ORDINALITY AS starwars(episode,film,watch)\nORDER BY watch;\n/* # Output #\n episode |          film           | watch\n---------+-------------------------+-------\n       4 | A New Hope              |     1\n       5 | The Empire Strikes Back |     2\n       1 | The Phantom Menace      |     3\n       2 | Attack of the Clones    |     4\n       3 | Revenge of the Sith     |     5\n       6 | Return of the Jedi      |     6\n       7 | The Force Awakens       |     7\n       8 | The Last Jedi           |     8\n       9 | The Rise of Skywalker   |     9\n(9 rows) */\n```\n\n### 26. User-defined SQL functions (UDFs), atomic/table-generating\n\n```sql\nDROP FUNCTION IF EXISTS convert_roman_numerals(text);\nCREATE FUNCTION convert_roman_numerals(rn text) RETURNS int AS\n$$\n  SELECT nums.value\n  FROM (VALUES ('I', 1),\n               ('II', 2),\n               ('III', 3),\n               ('IV', 4),\n               ('V', 5),\n               ('VI', 6),\n               ('VII', 7),\n               ('VIII', 8),\n               ('IX', 9)) AS nums(roman, value)\n  WHERE nums.roman = rn\n$$\nLANGUAGE SQL IMMUTABLE;\n\nSELECT convert_roman_numerals('IV');\n/* # Output #\n convert_roman_numerals\n------------------------\n                      4\n(1 row) */\n```\n\n```sql\nDROP TABLE IF EXISTS issue;\nCREATE TABLE issue (\n  id int GENERATED ALWAYS AS IDENTITY,\n  \"when\" timestamp);\n\nDROP FUNCTION IF EXISTS new_ID(text);\nCREATE FUNCTION new_ID(prefix text) RETURNS text AS\n$$\n  INSERT INTO issue(id,\"when\") VALUES\n    (DEFAULT, 'now'::timestamp)\n  RETURNING prefix || id::text\n$$\nLANGUAGE SQL VOLATILE; -- function is side-effecting\n\nTABLE issue;\n/* # Output #\n id | when\n----+------\n(0 rows) */\n\nSELECT new_ID('customer') AS customer, d.species\nFROM dinosaurs AS d\nWHERE d.legs = 2;\n/* # Output #\n customer  |    species\n-----------+---------------\n customer1 | Ceratosaurus\n customer2 | Deinonychus\n customer3 | Microvenator\n customer4 | Plateosaurus\n customer5 | Spinosaurus\n customer6 | Tyrannosaurus\n customer7 | Velociraptor\n(7 rows) */\n\nTABLE issue;\n/* # Output #\n id |            when\n----+----------------------------\n  1 | 2023-11-17 13:35:27.738654\n  2 | 2023-11-17 13:35:27.738654\n  3 | 2023-11-17 13:35:27.738654\n  4 | 2023-11-17 13:35:27.738654\n  5 | 2023-11-17 13:35:27.738654\n  6 | 2023-11-17 13:35:27.738654\n  7 | 2023-11-17 13:35:27.738654\n(7 rows) */\n```\n\n```sql\nCREATE OR REPLACE FUNCTION unnest2(xss anyarray)\n  RETURNS SETOF anyelement AS\n$$\n  SELECT xss[i][j]\n  FROM generate_subscripts(xss,1) _(i),\n       generate_subscripts(xss,2) __(j)\n  ORDER BY j, i\n$$\nLANGUAGE SQL IMMUTABLE;\n\nSELECT t.*\nFROM unnest2(array[array['a','b','c'],\n                   array['d','e','f'],\n                   array['x','y','z']])\n     WITH ORDINALITY AS t(elem,pos);\n/* # Output #\n elem | pos\n------+-----\n a    |   1\n d    |   2\n x    |   3\n b    |   4\n e    |   5\n y    |   6\n c    |   7\n f    |   8\n z    |   9\n(9 rows) */\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchsdwn%2Fsql-notes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchsdwn%2Fsql-notes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchsdwn%2Fsql-notes/lists"}