<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>FromDual: PostgreSQL Tech-Feed (en)</title><link>https://www.fromdual.com/aggregator/categories/11/</link><description>FromDual: PostgreSQL Tech-Feed in English</description><generator>Hugo</generator><language>en-US</language><atom:link href="https://www.fromdual.com/aggregator/categories/11/index.xml" rel="self" type="application/rss+xml"/><item><title>Database Index Optimizer</title><link>https://www.fromdual.com/blog/database_index_optimizer/</link><pubDate>Wed, 15 Jul 2026 16:55:00 +0200</pubDate><guid>https://www.fromdual.com/blog/database_index_optimizer/</guid><description>&lt;p&gt;Recently, a client asked me if the “time-consuming” task of checking indexes could be left to an index optimizer. Of course it can&amp;hellip;&lt;/p&gt;
&lt;p&gt;What exactly do we want to check?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tables without a Primary Key&lt;/li&gt;
&lt;li&gt;Duplicate indexes&lt;/li&gt;
&lt;li&gt;Partially redundant indexes&lt;/li&gt;
&lt;li&gt;Unused indexes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mariadb-mysql-and-percona-server"&gt;MariaDB, MySQL, and Percona Server&lt;/h2&gt;
&lt;h3 id="tables-without-a-primary-key"&gt;Tables without a Primary Key&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT DISTINCT t.table_schema, t.table_name
 FROM information_schema.tables AS t
 LEFT JOIN information_schema.columns AS c ON t.table_schema = c.table_schema AND t.table_name = c.table_name
 AND c.column_key = &amp;#34;PRI&amp;#34;
 WHERE t.table_schema NOT IN (&amp;#39;information_schema&amp;#39;, &amp;#39;mysql&amp;#39;, &amp;#39;performance_schema&amp;#39;)
 AND c.table_name IS NULL AND t.table_type NOT IN(&amp;#39;VIEW&amp;#39;, &amp;#39;SEQUENCE&amp;#39;)
 AND t.table_schema = &amp;#39;testtest&amp;#39;
;
+--------------+------------+
| table_schema | table_name |
+--------------+------------+
| testtest | archived |
+--------------+------------+
1 row in set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#tables-without-primary-key" target="_blank"&gt;Tables without a Primary Key&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="duplicate-indexes"&gt;Duplicate indexes&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index
 FROM sys.schema_redundant_indexes
 WHERE redundant_index_columns = dominant_index_columns
 AND table_schema = &amp;#39;testtest&amp;#39;
;
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
| table_name | redundant_index_name | redundant_index_columns | dominant_index_name | dominant_index_columns | sql_drop_index |
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
| archived | dupl2 | category_id | dupl1 | category_id | ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` |
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
1 row in set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank"&gt;Duplicate and redundant indices&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="partially-redundant-indexes"&gt;Partially redundant indexes&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index
 FROM sys.schema_redundant_indexes
 WHERE table_schema = &amp;#39;testtest&amp;#39;
;
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
| table_name | redundant_index_name | redundant_index_columns | dominant_index_name | dominant_index_columns | sql_drop_index |
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
| access | customer | customer | customer_2 | customer,callerid_internal | ALTER TABLE `testtest`.`access` DROP INDEX `customer` |
| access | customer | customer | customer_3 | customer,callerid_external | ALTER TABLE `testtest`.`access` DROP INDEX `customer` |
| active_customers | uniqueid | uniqueid | PRIMARY | uniqueid,scustomer | ALTER TABLE `testtest`.`active_customers` DROP INDEX `uniqueid` |
| analytics_include | analytics | analytics | PRIMARY | analytics,feature,dtype,dnumber | ALTER TABLE `testtest`.`analytics_include` DROP INDEX `analytics` |
| archived | dupl2 | category_id | dupl1 | category_id | ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` |
...
| texts_media | uniqueid | uniqueid | PRIMARY | uniqueid,filename | ALTER TABLE `testtest`.`texts_media` DROP INDEX `uniqueid` |
| unlimited_access | customer | customer | customer_2 | customer,callerid_internal | ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` |
| unlimited_access | customer | customer | customer_3 | customer,callerid_external | ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` |
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
26 rows in set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank"&gt;Duplicate and redundant indices&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="unused-indexes"&gt;Unused indexes&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT object_name, index_name
 FROM sys.schema_unused_indexes
 WHERE object_schema = &amp;#39;testtest&amp;#39;
;
+------------------------+------------------------+
| object_name | index_name |
+------------------------+------------------------+
| access | customer_3 |
| access | customer_2 |
| actions | class |
| actions | action |
| active | channel |
...
| urls | customer |
| voucher_batches | customer |
| vouchers | batch |
+------------------------+------------------------+
413 rows in set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For MariaDB, the &lt;code&gt;PERFORMANCE_SCHEMA&lt;/code&gt; must be enabled first.&lt;/li&gt;
&lt;li&gt;The information is accurate as of the last database restart. If an index was last used BEFORE the most recent restart, it will be shown here as unused.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#unused-indexes" target="_blank"&gt;Unused indexes&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="and-now-with-postgresql"&gt;And now with PostgreSQL&lt;/h2&gt;
&lt;h3 id="tables-without-a-primary-key-1"&gt;Tables without a Primary Key&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT tab.table_schema, tab.table_name
 FROM information_schema.tables tab
 LEFT JOIN information_schema.table_constraints tco
 ON tab.table_schema = tco.table_schema
 AND tab.table_name = tco.table_name 
 AND tco.constraint_type = &amp;#39;PRIMARY KEY&amp;#39;
 WHERE tab.table_type = &amp;#39;BASE TABLE&amp;#39;
 AND tab.table_schema NOT IN (&amp;#39;pg_catalog&amp;#39;, &amp;#39;information_schema&amp;#39;)
 AND tco.constraint_name IS NULL
 ORDER BY table_schema, table_name
;
 table_schema | table_name 
--------------+------------
 public | archived
(1 row)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://dataedo.com/kb/query/postgresql/find-tables-without-primary-keys" target="_blank"&gt;Find tables without primary keys (PKs) in PostgreSQL database&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="duplicate-indexes-1"&gt;Duplicate indexes&lt;/h3&gt;
&lt;p&gt;Based on the MySQL &lt;code&gt;sys&lt;/code&gt; schema:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; WITH schema_flattened_keys AS (
 SELECT sai.relid, sai.indexrelid
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name
 , CASE pi.indisunique WHEN &amp;#39;f&amp;#39; THEN 1 ELSE 0 END AS non_unique
 , index_columns.columns AS index_columns
 FROM pg_stat_all_indexes AS sai
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid
 JOIN (
 SELECT attrelid, string_agg(attname, &amp;#39;,&amp;#39; ORDER BY attnum ASC) AS columns
 FROM pg_attribute GROUP BY attrelid
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid
 WHERE sai.schemaname NOT IN (&amp;#39;pg_toast&amp;#39;, &amp;#39;pg_catalog&amp;#39;)
)
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique
 , CONCAT(&amp;#39;ALTER TABLE &amp;#39;, redundant_keys.table_schema, &amp;#39;.&amp;#39;, redundant_keys.table_name, &amp;#39; DROP INDEX &amp;#39;, redundant_keys.index_name, &amp;#39;&amp;#39;) AS sql_drop_index
 FROM schema_flattened_keys redundant_keys
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name
 WHERE (redundant_keys.index_name &amp;lt;&amp;gt; dominant_keys.index_name
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)
 AND ((redundant_keys.non_unique &amp;gt; dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))
 )
 OR ((POSITION(CONCAT(redundant_keys.index_columns,&amp;#39;,&amp;#39;) IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))
 OR ((POSITION(CONCAT(dominant_keys.index_columns,&amp;#39;,&amp;#39;) IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))
 )
 AND redundant_keys.index_columns = dominant_keys.index_columns
;
 table_schema | table_name | redundant_index_name | redundant_index_columns | redundant_index_non_unique | dominant_index_name | dominant_index_columns | dominant_index_non_unique | sql_drop_index 
--------------+------------+----------------------+-------------------------+----------------------------+---------------------+------------------------+---------------------------+----------------------------------------------
 public | archived | dupl1 | category_id | 1 | dupl2 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl1
 public | archived | dupl2 | category_id | 1 | dupl1 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl2
(2 rows)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank"&gt;Duplicate and redundant indices&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="partially-redundant-indexes-1"&gt;Partially redundant indexes&lt;/h3&gt;
&lt;p&gt;Based on the MySQL &lt;code&gt;sys&lt;/code&gt; schema:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; WITH schema_flattened_keys AS (
 SELECT sai.relid, sai.indexrelid
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name
 , CASE pi.indisunique WHEN &amp;#39;f&amp;#39; THEN 1 ELSE 0 END AS non_unique
 , index_columns.columns AS index_columns
 FROM pg_stat_all_indexes AS sai
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid
 JOIN (
 SELECT attrelid, string_agg(attname, &amp;#39;,&amp;#39; ORDER BY attnum ASC) AS columns
 FROM pg_attribute GROUP BY attrelid
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid
 WHERE sai.schemaname NOT IN (&amp;#39;pg_toast&amp;#39;, &amp;#39;pg_catalog&amp;#39;)
)
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique
 , CONCAT(&amp;#39;ALTER TABLE &amp;#39;, redundant_keys.table_schema, &amp;#39;.&amp;#39;, redundant_keys.table_name, &amp;#39; DROP INDEX &amp;#39;, redundant_keys.index_name, &amp;#39;&amp;#39;) AS sql_drop_index
 FROM schema_flattened_keys redundant_keys
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name
 WHERE (redundant_keys.index_name &amp;lt;&amp;gt; dominant_keys.index_name
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)
 AND ((redundant_keys.non_unique &amp;gt; dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))
 )
 OR ((POSITION(CONCAT(redundant_keys.index_columns,&amp;#39;,&amp;#39;) IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))
 OR ((POSITION(CONCAT(dominant_keys.index_columns,&amp;#39;,&amp;#39;) IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))
 )
;
 table_schema | table_name | redundant_index_name | redundant_index_columns | redundant_index_non_unique | dominant_index_name | dominant_index_columns | dominant_index_non_unique | sql_drop_index 
--------------+-----------------------+------------------------------------------+-------------------------+----------------------------+-------------------------------------------------+-----------------------------------------+---------------------------+---------------------------------------------------------------------------------------------
 public | numbers | numbers_customer_idx | customer | 1 | numbers_customer_text_dtype_text_dnumber_idx | customer,text_dtype,text_dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_customer_idx | customer | 1 | numbers_customer_fax_dtype_fax_dnumber_idx | customer,fax_dtype,fax_dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_customer_idx | customer | 1 | numbers_pkey | customer,stype,snumber | 0 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_dtype_idx | dtype | 1 | numbers_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_dtype_idx
 public | number_callers | number_callers_dtype_idx | dtype | 1 | number_callers_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_callers DROP INDEX number_callers_dtype_idx
 public | prefixes | prefixes_customer_idx | customer | 1 | prefixes_customer_dtype_dnumber_idx | customer,dtype,dnumber | 1 | ALTER TABLE public.prefixes DROP INDEX prefixes_customer_idx
 public | number_times | number_times_dtype_idx | dtype | 1 | number_times_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_times DROP INDEX number_times_dtype_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_location_idx | customer,callerid_location | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones_hardware | phones_hardware_phone_idx | phone | 1 | phones_hardware_phone_hardware_address_idx | phone,hardware_address | 0 | ALTER TABLE public.phones_hardware DROP INDEX phones_hardware_phone_idx
 public | speeddials | speeddials_stype_idx | stype | 1 | speeddials_stype_snumber_idx | stype,snumber | 1 | ALTER TABLE public.speeddials DROP INDEX speeddials_stype_idx
 public | speeddials | speeddials_dtype_idx | dtype | 1 | speeddials_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.speeddials DROP INDEX speeddials_dtype_idx
 public | mailbox_destinations | mailbox_destinations_context_mailbox_idx | context,mailbox | 1 | mailbox_destinations_pkey | context,mailbox,dcustomer,dtype,dnumber | 0 | ALTER TABLE public.mailbox_destinations DROP INDEX mailbox_destinations_context_mailbox_idx
 public | outgroup_times | outgroup_times_outgroup_idx | outgroup | 1 | outgroup_times_outgroup_name_idx | outgroup,name | 0 | ALTER TABLE public.outgroup_times DROP INDEX outgroup_times_outgroup_idx
 public | ingroup_times | ingroup_times_ingroup_idx | ingroup | 1 | ingroup_times_ingroup_name_idx | ingroup,name | 0 | ALTER TABLE public.ingroup_times DROP INDEX ingroup_times_ingroup_idx
 public | active_customers | active_customers_uniqueid_idx | uniqueid | 1 | active_customers_pkey | uniqueid,scustomer | 0 | ALTER TABLE public.active_customers DROP INDEX active_customers_uniqueid_idx
 public | access | access_customer_idx | customer | 1 | access_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.access DROP INDEX access_customer_idx
 public | access | access_customer_idx | customer | 1 | access_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.access DROP INDEX access_customer_idx
 public | unlimited_access | unlimited_access_customer_idx | customer | 1 | unlimited_access_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx
 public | unlimited_access | unlimited_access_customer_idx | customer | 1 | unlimited_access_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx
 public | texts | texts_dcustomer_idx | dcustomer | 1 | texts_dcustomer_dtype_dnumber_idx | dcustomer,dtype,dnumber | 1 | ALTER TABLE public.texts DROP INDEX texts_dcustomer_idx
 public | texts_media | texts_media_uniqueid_idx | uniqueid | 1 | texts_media_pkey | uniqueid,filename | 0 | ALTER TABLE public.texts_media DROP INDEX texts_media_uniqueid_idx
 public | number_calleridgroups | number_calleridgroups_dtype_idx | dtype | 1 | number_calleridgroups_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_calleridgroups DROP INDEX number_calleridgroups_dtype_idx
 public | analytics_include | analytics_i | analytics | 1 | analytics_include_pkey | analytics,feature,dtype,dnumber | 0 | ALTER TABLE public.analytics_include DROP INDEX analytics_i
 public | archived | dupl1 | category_id | 1 | dupl2 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl1
 public | archived | dupl2 | category_id | 1 | dupl1 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl2
(27 rows)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source: &lt;a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank"&gt;Duplicate and redundant indices&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="unused-indexes-1"&gt;Unused indexes&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT relid::regclass AS table, indexrelid::regclass AS index
 , pg_size_pretty(pg_relation_size(indexrelid::regclass)) AS index_size
 , idx_tup_read, idx_tup_fetch, idx_scan
 FROM pg_stat_user_indexes 
 JOIN pg_index USING (indexrelid) 
 WHERE idx_scan = 0 
 AND indisunique IS FALSE
;
 table | index | index_size | idx_tup_read | idx_tup_fetch | idx_scan 
------------------------+-----------------------------------------------------------------+------------+--------------+---------------+----------
 customers | customers_prefix_idx | 16 kB | 0 | 0 | 0
 customers | customers_parent_idx | 16 kB | 0 | 0 | 0
 customers | customers_email_idx | 16 kB | 0 | 0 | 0
 customers | customers_affiliate_customer_idx | 16 kB | 0 | 0 | 0
 customers | customers_bill_ref_idx | 16 kB | 0 | 0 | 0
...
 analytics_include | analytics_i | 8192 bytes | 0 | 0 | 0
 archived | dupl1 | 8192 bytes | 0 | 0 | 0
 archived | dupl2 | 8192 bytes | 0 | 0 | 0
(413 rows)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wiki.postgresql.org/wiki/Index_Maintenance#Unused_Indexes" target="_blank"&gt;Unused Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jmorano.moretrix.com/2014/02/postgresql-monitor-unused-indexes/" target="_blank"&gt;Postgresql: Monitor unused indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="pg-assistant"&gt;PG Assistant&lt;/h3&gt;
&lt;p&gt;At the &lt;a href="https://2026.pgday.ch/schedule/" target="_blank"&gt;Swiss PGDay2026&lt;/a&gt;(s), Bertrand Hartwig presented his tool &lt;a href="https://github.com/beh74/pgassistant-community" target="_blank"&gt;PG Assistant&lt;/a&gt;. In that context, I wanted to try it out right away&amp;hellip;&lt;/p&gt;
&lt;p&gt;PG Assistant was able to find missing Primary Keys and duplicate indexes. It didn’t show me any partially redundant or unused indexes, but that might just be on my end&amp;hellip;&lt;/p&gt;
&lt;figure&gt;
 &lt;a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111018.png" title="full size"&gt;&lt;img src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111018_640x509.png" alt="pgAssistant-1"&gt;&lt;/a&gt;
 &lt;figcaption&gt;PG Assistant: Dashboard / Dev advisor&lt;/figcaption&gt;
&lt;/figure&gt;&lt;br&gt;
&lt;figure&gt;
 &lt;a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111135.png" title="full size"&gt;&lt;img src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111135_640x533.png" alt="pgAssistant-2"&gt;&lt;/a&gt;
 &lt;figcaption&gt;PG Assistant: Global Advisor / Dev advisor&lt;/figcaption&gt;
&lt;/figure&gt;&lt;br&gt;
&lt;figure&gt;
 &lt;a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111230.png" title="full size"&gt;&lt;img src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111230_640x532.png" alt="pgAssistant-3"&gt;&lt;/a&gt;
 &lt;figcaption&gt;PG Assistant: Strictly duplicate unused index&lt;/figcaption&gt;
&lt;/figure&gt;&lt;br&gt;
&lt;h4 id="intallation-of-pg-assistant"&gt;Intallation of PG Assistant&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ apt update
$ apt install python3 python3.13-venv unzip pip
$ wget https://github.com/beh74/pgassistant-community/archive/refs/heads/main.zip
$ unzip main.zip 
$ cd pgassistant-community-main/
$ python3 -m venv env
$ source env/bin/activate
$ pip3 install -r requirements.txt
$ export FLASK_APP=run.py
$ flask run --host=0.0.0.0 --port=80
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then connect to the displayed URL using a web browser.&lt;/p&gt;
&lt;p&gt;A user must be created in the database first:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; CREATE ROLE pgassistant WITH LOGIN SUPERUSER PASSWORD &amp;#39;secret&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and the &lt;code&gt;pg_hba.conf&lt;/code&gt; file must be adapted.&lt;/p&gt;
&lt;h2 id="addendum"&gt;Addendum&lt;/h2&gt;
&lt;p&gt;You can find unused indexes using the PG Assistant as follows: Database Objects ➜ Indexes ➜ Status: Unused ➜ “NO INDEX ACTIVITY”&lt;/p&gt;
&lt;figure&gt;
 &lt;a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260716_093730.png" title="volle Grösse"&gt;&lt;img src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260716_093730_640x459.png" alt="pgAssistant-4"&gt;&lt;/a&gt;
 &lt;figcaption&gt;PG Assistant: Unused Indexes&lt;/figcaption&gt;
&lt;/figure&gt;&lt;br&gt;</description></item><item><title>MariaDB/MySQL Environment MyEnv 3.0.0 has been released</title><link>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/</link><pubDate>Mon, 23 Mar 2026 17:42:00 +0100</pubDate><guid>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/</guid><description>&lt;p&gt;FromDual has the pleasure to announce the release of the new version 3.0.0 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment &lt;a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment"&gt;MyEnv&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The new MyEnv can be downloaded &lt;a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download"&gt;here&lt;/a&gt;. How to install MyEnv is described in the &lt;a href="https://support.fromdual.com/documentation/myenv/myenv.html#installation-guide" target="_blank"&gt;MyEnv Installation Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the inconceivable case that you find a bug in the MyEnv please report it to us by sending an &lt;a href="mailto:contact@fromdual.com?Subject=Bug report for myenv"&gt;email&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Any feedback, statements and testimonials are welcome as well! Please &lt;a href="mailto:feedback@fromdual.com?Subject=Feedback for fpmmm"&gt;send them to us&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="upgrade-from-2x-to-30"&gt;Upgrade from 2.x to 3.0&lt;/h2&gt;
&lt;p&gt;Please check the &lt;a href="https://support.fromdual.com/documentation/myenv/myenv.html#upgrade" target="_blank" title="Upgrading MyEnv"&gt;MyEnv Installation Guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="changes-in-myenv-300"&gt;Changes in MyEnv 3.0.0&lt;/h2&gt;
&lt;h3 id="myenv"&gt;MyEnv&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Template warning improved.&lt;/li&gt;
&lt;li&gt;Distro version in &lt;code&gt;--version&lt;/code&gt; added.&lt;/li&gt;
&lt;li&gt;Check MyEnv configuration permissions.&lt;/li&gt;
&lt;li&gt;#fd increased for MyEnv.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;myenv.conf&lt;/code&gt; should have more secure permissions now.&lt;/li&gt;
&lt;li&gt;Situation caught when &lt;code&gt;my.cnf&lt;/code&gt; is missing in &lt;code&gt;myenv.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Directories &lt;code&gt;home&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt; moved to &lt;code&gt;dba&lt;/code&gt; and &lt;code&gt;myenv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Unit file &lt;code&gt;mariadb.service&lt;/code&gt; and &lt;code&gt;mysql.service&lt;/code&gt; replaced by &lt;code&gt;dba.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;User &lt;code&gt;mysql&lt;/code&gt; replaced by &lt;code&gt;dba&lt;/code&gt; in template.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start_stop&lt;/code&gt; fixed warning in case &lt;code&gt;argv[1]&lt;/code&gt; is missing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys_uid&lt;/code&gt; filter fixed for Rocky Linux.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dba&lt;/code&gt; unit file added to package.&lt;/li&gt;
&lt;li&gt;Nagios plugins detection removed from &lt;code&gt;showMyEnvVersion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Old SysV init files removed and replaced by Systemd unit files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dba&lt;/code&gt; user was introduced and check for system user added.&lt;/li&gt;
&lt;li&gt;User &lt;code&gt;dba&lt;/code&gt; changed an cosmetic fixes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="myenv-installer"&gt;MyEnv Installer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2 concurrent &lt;code&gt;installMyEnv&lt;/code&gt; versions cannot run any more.&lt;/li&gt;
&lt;li&gt;Directroy &lt;code&gt;binlog&lt;/code&gt;, &lt;code&gt;cgroups&lt;/code&gt; and &lt;code&gt;angel&lt;/code&gt; removed from &lt;code&gt;postgresql&lt;/code&gt; &lt;code&gt;type&lt;/code&gt; installation.&lt;/li&gt;
&lt;li&gt;Wrapper script &lt;code&gt;installMyEnv.sh&lt;/code&gt; removed.&lt;/li&gt;
&lt;li&gt;Cosmetics fixed in installer.&lt;/li&gt;
&lt;li&gt;Installation made more mysql friendly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;libaio1t64&lt;/code&gt; considered during installation recommendations on DEB systems.&lt;/li&gt;
&lt;li&gt;Next free port suggestion during &lt;code&gt;installMyEnv&lt;/code&gt; improved. It will suggest the first free port now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apt-get&lt;/code&gt; and &lt;code&gt;yum&lt;/code&gt; replaced by &lt;code&gt;apt&lt;/code&gt; and &lt;code&gt;dnf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="myenv-utilities"&gt;MyEnv Utilities&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert_test.sh&lt;/code&gt; made PostgreSQL ready.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="postgresql"&gt;PostgreSQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL instance is stopped with fast instead of immediate now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;show_create_table.sh&lt;/code&gt; for PostgreSQL made nicer.&lt;/li&gt;
&lt;li&gt;PostgreSQL &lt;code&gt;status.sql&lt;/code&gt; added.&lt;/li&gt;
&lt;li&gt;Minor fixes for PostgreSQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="general"&gt;General&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CHANGELOG&lt;/code&gt; updated.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rc&lt;/code&gt; made unique.&lt;/li&gt;
&lt;li&gt;Minor bugs fixed.&lt;/li&gt;
&lt;li&gt;Copyright year updated from 2024 to 2026.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir&lt;/code&gt; changed from &lt;code&gt;/bin&lt;/code&gt; to &lt;code&gt;/usr/bin&lt;/code&gt; which is the new/right standard on all our 3 supported distributions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="documentation"&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;README updated.&lt;/li&gt;
&lt;li&gt;Installation documentation improved, library related stuff documented.&lt;/li&gt;
&lt;li&gt;PostgreSQL added to documentation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lsb_release&lt;/code&gt; removed from documentation.&lt;/li&gt;
&lt;li&gt;Documentation restructured.&lt;/li&gt;
&lt;li&gt;Documentation made more resilient agaist errors.&lt;/li&gt;
&lt;li&gt;Documentation process improved.&lt;/li&gt;
&lt;li&gt;Documentation moved completely to asciidoc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="packaging"&gt;Packaging&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Package list completed.&lt;/li&gt;
&lt;li&gt;Makefile fixed.&lt;/li&gt;
&lt;li&gt;Old distro stuff and initV stuff removed.&lt;/li&gt;
&lt;li&gt;Build scripts fixed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For subscriptions of commercial use of MyEnv please &lt;a href="mailto:contact@fromdual.com?Subject=Commercial use of MyEnv"&gt;get in contact&lt;/a&gt; with us.&lt;/p&gt;</description></item><item><title>FromDual Performance Monitor 2.2.1 has been released</title><link>https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/</link><pubDate>Thu, 19 Feb 2026 18:18:00 +0100</pubDate><guid>https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/</guid><description>&lt;p&gt;FromDual has the pleasure to announce the release of the new version 2.2.1 of its popular Database Performance Monitor for MariaDB, Galera Cluster, MySQL and PostgreSQL &lt;a href="https://www.fromdual.com/software/fromdual-performance-monitor/"&gt;&lt;code&gt;fpmmm&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The FromDual Performance Monitor enables Database and System Administrators to monitor and understand what is going on inside their databases and on the machines where the databases reside.&lt;/p&gt;
&lt;p&gt;More information you can find here: &lt;a href="https://www.fromdual.com/software/fromdual-performance-monitor/"&gt;FromDual Performance Monitor&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="download"&gt;Download&lt;/h2&gt;
&lt;p&gt;The new FromDual Performance Monitor can be downloaded from our &lt;a href="https://support.fromdual.com/admin/public/download.php" target="_blank"&gt;Sofware Download&lt;/a&gt; page or you can use our &lt;a href="https://www.fromdual.com/repositories/"&gt;repositories&lt;/a&gt;. How to install and use the FromDual Performance Monitor is documented in the &lt;a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html" target="_blank"&gt;Documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the inconceivable case that you find a bug in the FromDual Performance Monitor please report it to us by sending an &lt;a href="mailto:contact@fromdual.com?Subject=Bug report for fpmmm"&gt;email&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Any feedback, statements and testimonials are welcome as well! Please send them &lt;a href="mailto:feedback@fromdual.com?Subject=Feedback for fpmmm"&gt;to us&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="monitoring-as-a-service-maas"&gt;Monitoring as a Service (MaaS)&lt;/h2&gt;
&lt;p&gt;You do not want to set-up your database monitoring yourself? No problem: Choose our &lt;a href="https://www.fromdual.com/services/monitoring-as-a-service-maas/"&gt;Monitoring as a Service&lt;/a&gt; (MaaS) to safe time and costs!&lt;/p&gt;
&lt;h2 id="installation-of-performance-monitor-221"&gt;Installation of Performance Monitor 2.2.1&lt;/h2&gt;
&lt;p&gt;How to install the FromDual Performance Monitor you can find in the &lt;a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html#installation-guide" target="_blank"&gt;Installation Guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="upgrade-of-fpmmm-tar-ball-from-1x-to-221"&gt;Upgrade of fpmmm tar ball from 1.x to 2.2.1&lt;/h2&gt;
&lt;p&gt;There are some changes in the configuration file (&lt;code&gt;fpmmm.conf&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The access rights should be change as follows: &lt;code&gt;chmod 600 /etc/fpmmm.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The key &lt;code&gt;Methode&lt;/code&gt; was spelled wrong in the configuration file. It was renamed to &lt;code&gt;Method&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The key &lt;code&gt;PidFile&lt;/code&gt; is ambiguous which could lead to problems and bugs. Thus it was changed to either &lt;code&gt;MyPidFile&lt;/code&gt; for fpmmm and &lt;code&gt;DbPidFile&lt;/code&gt; for the database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Upgrade with DEB/RPM packages should happen automatically. For tar balls follow this instruction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd /opt
$ tar xf /download/fpmmm-2.2.1.tar.gz
$ rm -f fpmmm
$ ln -s fpmmm-2.2.1 fpmmm
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="changes-in-fromdual-performance-monitor-221"&gt;Changes in FromDual Performance Monitor 2.2.1&lt;/h2&gt;
&lt;p&gt;These release notes include both the changes that came with version 2.2.0 and version 2.2.1.&lt;/p&gt;
&lt;p&gt;This release contains new features and various bug fixes.&lt;/p&gt;
&lt;p&gt;You can verify your current FromDual Performance Monitor version with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ /opt/fpmmm/bin/fpmmm --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="general"&gt;General&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Updated to latest myEnv library.&lt;/li&gt;
&lt;li&gt;PHP 8.5 incompatibilities fixed.&lt;/li&gt;
&lt;li&gt;Typos fixed.&lt;/li&gt;
&lt;li&gt;Error messages improved.&lt;/li&gt;
&lt;li&gt;Function &lt;code&gt;real_connect&lt;/code&gt; warnings send to console are suppressed now.&lt;/li&gt;
&lt;li&gt;Connection problems timeout reduced so in case of troubles we should see more and earlier&amp;hellip;&lt;/li&gt;
&lt;li&gt;Other cosmetic errors and debugging information fixed.&lt;/li&gt;
&lt;li&gt;Data are gathered and set to zero even thought database is not reachable.&lt;/li&gt;
&lt;li&gt;Indention of logged messages fixed.&lt;/li&gt;
&lt;li&gt;Function exit is logged now as well.&lt;/li&gt;
&lt;li&gt;SSL connection handling added.&lt;/li&gt;
&lt;li&gt;Fix of error: array_sum(): Addition is not supported on type string in warning after upgrade to Ubuntu 24.04/PHP 8.3.&lt;/li&gt;
&lt;li&gt;Error log parsing had problems with huge error logs. Now we have added a size barrier.&lt;/li&gt;
&lt;li&gt;Function &lt;code&gt;getDistributions&lt;/code&gt; updated/cleaned-up.&lt;/li&gt;
&lt;li&gt;Command lsb_release removed.&lt;/li&gt;
&lt;li&gt;Documentation added.&lt;/li&gt;
&lt;li&gt;Nagios: Tests fixed for MariaDB 11.8.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="templates"&gt;Templates&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Server: Available I/O system information added to each I/O system on top, pages named.&lt;/li&gt;
&lt;li&gt;InnoDB: Pages named, row write operations graph added.&lt;/li&gt;
&lt;li&gt;MySQL: Some graphs and query dashboard made nicer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="agent"&gt;Agent&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;none&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="server"&gt;Server&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Items &lt;code&gt;FromDual.MySQL.server.disk.avg_io_read_wait&lt;/code&gt; and &lt;code&gt;FromDual.MySQL.server.disk.avg_io_write_wait&lt;/code&gt; removed because they are showing completely wrong values. Use &lt;code&gt;FromDual.MySQL.server.disk.r_await&lt;/code&gt; and &lt;code&gt;FromDual.MySQL.server.disk.w_await&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Workaround for missing cpuinfo old cachefile implemented.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="galera"&gt;Galera&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Old style variable fixed which causes problems with newer version.&lt;/li&gt;
&lt;li&gt;Default values on database stop added.&lt;/li&gt;
&lt;li&gt;Workaround for cut &lt;code&gt;wsrep_provider_options&lt;/code&gt; bug in MySQL Galera Cluster added.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="innodb"&gt;InnoDB&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Variable &lt;code&gt;innodb_log_file_size&lt;/code&gt; made consistent for MariaDB and MySQL.&lt;/li&gt;
&lt;li&gt;Deprecated and removed variable &lt;code&gt;innodb_log_files_in_group&lt;/code&gt; removed.&lt;/li&gt;
&lt;li&gt;Fix for &lt;code&gt;innodb_log_file_size&lt;/code&gt; in MySQL 9.4.&lt;/li&gt;
&lt;li&gt;Log occupancy graph added and graph added to dashboard.&lt;/li&gt;
&lt;li&gt;Variable &lt;code&gt;tx_isolation&lt;/code&gt; replaced by transaction isolation which is deprecated in MariaDB 11.2 and MySQL 5.7.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mysql"&gt;MySQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Variable &lt;code&gt;vendor_versions_behind&lt;/code&gt; special case caught.&lt;/li&gt;
&lt;li&gt;Connection charset changed from &lt;code&gt;utf8&lt;/code&gt; to &lt;code&gt;utf8mb4&lt;/code&gt; due to errors in MariaDB 11.8.&lt;/li&gt;
&lt;li&gt;Template pages named.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="process"&gt;Process&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;none&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="security"&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Module improved for new behaviour in MariaDB 11.8.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="master"&gt;Master&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Wrong version check for master fixed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="slave"&gt;Slave&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Slave lagging problem fixed.&lt;/li&gt;
&lt;li&gt;Wrong version check for slave fixed.&lt;/li&gt;
&lt;li&gt;MySQL 8.4 commands added for replication monitoring.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="backup"&gt;Backup&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;none&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="postgresql"&gt;PostgreSQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Rudimentary PostgreSQL monitoring added.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="packaging"&gt;Packaging&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;RHEL 8 added again.&lt;/li&gt;
&lt;li&gt;RPM spec adapted for RHEL 10.&lt;/li&gt;
&lt;li&gt;SNMP library updated.&lt;/li&gt;
&lt;li&gt;Debian 10 and RHEL 7 removed.&lt;/li&gt;
&lt;li&gt;DEB sign stuff added.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For subscriptions of commercial use of &lt;code&gt;fpmmm&lt;/code&gt; please &lt;a href="mailto:contact@fromdual.com?Subject=Commercial use of fpmmm"&gt;get in contact&lt;/a&gt; with us.&lt;/p&gt;</description></item><item><title>What is the quickest way to load data into the database?</title><link>https://www.fromdual.com/blog/load-data-quick-into-the-database/</link><pubDate>Wed, 11 Feb 2026 10:04:00 +0100</pubDate><guid>https://www.fromdual.com/blog/load-data-quick-into-the-database/</guid><description>&lt;p&gt;We had some really exciting problems to solve for the last customer! Especially because the database wasn&amp;rsquo;t exactly small.&lt;/p&gt;
&lt;p&gt;Here are some key data: CPU: 2 sockets x 24 cores x 2 threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD in RAID-10 and 7 Tbyte data, several thousand clients, rapidly growing.&lt;/p&gt;
&lt;p&gt;The current throughput: 1 M &lt;code&gt;SELECT&lt;/code&gt;/min, 56 k &lt;code&gt;INSERT&lt;/code&gt;/min, 44 k &lt;code&gt;UPDATE&lt;/code&gt;/min, 7 k &lt;code&gt;DELETE&lt;/code&gt;/min averaged over 30 days. With a strong upward trend. Application and queries not consistently optimised. Database configuration: ‘state of the art’ not verified with benchmarks. CPU utilisation approx. 50% on average, more at peak times. I/O system still has available resources.&lt;/p&gt;
&lt;p&gt;The customer collects position and other device data and stores it in the database. In other words, a classic IoT problem (with time series, index clustered table, etc.).&lt;/p&gt;
&lt;p&gt;The question he has asked is: What is the fastest way to copy data from one table (pending data, a kind of queue) to another table (final data, per client)?&lt;/p&gt;
&lt;p&gt;The data flow looks something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;+------------+
| IoT Device |--+
+------------+ \
 \ +-----+
+------------+ \ | AS | +--------------+ Processing +------------+
| IoT Device |------+--&amp;gt;| |--&amp;gt;| Pending data |-------------&amp;gt;| Final data |
+------------+ / | 400 | +--------------+ of data +------------+
 / +-----+
+------------+ /
| IoT Device |--+
+------------+
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;3 different variants to copy the data were available for selection.&lt;/p&gt;
&lt;h2 id="variant-1-insert-and-delete-simplest-form"&gt;Variant 1: &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; (simplest form)&lt;/h2&gt;
&lt;p&gt;The simplest variant is a simple &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt;. This variant is particularly problematic because MariaDB/MySQL and PostgreSQL have &lt;code&gt;AUTOCOMMIT&lt;/code&gt; enabled by default (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-autocommit-commit-rollback.html" target="_blank"&gt;here&lt;/a&gt;, &lt;a href="https://mariadb.com/docs/server/reference/sql-statements/transactions/start-transaction" target="_blank"&gt;here&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/ecpg-sql-set-autocommit.html" target="_blank"&gt;here&lt;/a&gt; and &lt;a href="https://www.cybertec-postgresql.com/en/disabling-autocommit-in-postgresql-can-damage-your-health/" target="_blank"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;To help you visualise this a little better, here is some pseudocode:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 20k rows
for (i = 1; i &amp;lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 foreach ( row ) {
 INSERT INTO final;
 -- implicit COMMIT
 DELETE FROM pending WHERE id = row[id];
 -- implicit COMMIT
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So if we want to copy 20 k rows, this variant causes: 40 k &lt;code&gt;COMMIT&lt;/code&gt;s (&lt;code&gt;fsync&lt;/code&gt;) and 42 k network round trips!&lt;/p&gt;
&lt;h2 id="variant-2-start-transaction-and-insert-ad-delete"&gt;Variant 2: &lt;code&gt;START TRANSACTION&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; ad &lt;code&gt;DELETE&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;This variant is used by more experienced database developers. Here is the corresponding pseudocode:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 20k rows
for (i = 1; i &amp;lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 foreach ( row ) {
 INSERT INTO final;
 DELETE FROM pending WHERE id = row[id];
 }
 COMMIT;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we want to copy 20 k rows in this example, this variant only causes 2 k &lt;code&gt;COMMIT&lt;/code&gt;s (&lt;code&gt;fsync&lt;/code&gt;)! So 20 times less! But 46 k network round trips (10% more).&lt;/p&gt;
&lt;h2 id="variant-3-start-transaction-and-optimised-insert-and-delete"&gt;Variant 3: &lt;code&gt;START TRANSACTION&lt;/code&gt; and optimised &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;This variant is a little more demanding in terms of programming. It is used if you want to get a little closer to the limits of what is possible. Here is the pseudo code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// 20k rows
for (i = 1; i &amp;lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 INSERT INTO final (), (), (), (), (), (), (), (), (), ();
 DELETE FROM pending WHERE id = IN (...);
 COMMIT;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And this 3rd variant also only causes 2 k &lt;code&gt;COMMIT&lt;/code&gt;&amp;rsquo;s (&lt;code&gt;fsync&lt;/code&gt;) with 20 k rows, but saves the loop via the &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; statements in the database. So on the one hand we save network round trips (only 10 k) and CPU cycles on the database (which are difficult to scale) for parsing the queries.&lt;/p&gt;
&lt;h2 id="test-set-up"&gt;Test set-up&lt;/h2&gt;
&lt;p&gt;To test the whole thing, we have prepared a small script: &lt;a href="https://www.fromdual.com/code-examples/load_data.php.txt"&gt;load_data.php&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="preparation-and-execution-with-mariadbmysql"&gt;Preparation and execution with MariaDB/MySQL&lt;/h3&gt;
&lt;p&gt;You can execute these tests yourself with the following commands:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; CREATE DATABASE test;
SQL&amp;gt; CREATE USER &amp;#39;app&amp;#39;@&amp;#39;127.0.0.1&amp;#39; IDENTIFIED BY &amp;#39;secret&amp;#39;;
SQL&amp;gt; GRANT ALL ON *.* TO &amp;#39;app&amp;#39;@&amp;#39;127.0.0.1&amp;#39;;

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --clean-up
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Everyone can work out the measured values themselves with the corresponding test script.&lt;/p&gt;
&lt;h3 id="preparation-and-execution-with-postgresql"&gt;Preparation and execution with PostgreSQL&lt;/h3&gt;
&lt;p&gt;You can execute these tests yourself with the following commands:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres# CREATE DATABASE test;
postgres# CREATE USER app PASSWORD &amp;#39;secret&amp;#39;;
postgres# GRANT ALL ON DATABASE test TO app;
postgres# GRANT ALL ON SCHEMA public TO app;

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --clean-up
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Anyone can work out the measured values themselves with the corresponding test script.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;To avoid unnecessary discussions, we have ‘only’ listed the relative performance (runtime) here, as MarkC has been doing recently. We are happy to provide our measured values bilaterally. However, they can be easily reproduced with the test script itself.&lt;/p&gt;
&lt;p&gt;Less is better:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;Variant 1&lt;/th&gt;
					&lt;th&gt;Variant 2&lt;/th&gt;
					&lt;th&gt;Variant 3&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;MariaDB 11.8, avg(5)&lt;/td&gt;
					&lt;td&gt;100.0%&lt;/td&gt;
					&lt;td&gt;7.5%&lt;/td&gt;
					&lt;td&gt;6.2%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PostgreSQL 19dev, avg(5)&lt;/td&gt;
					&lt;td&gt;100.0%&lt;/td&gt;
					&lt;td&gt;11.2%&lt;/td&gt;
					&lt;td&gt;7.0%&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Attention&lt;/strong&gt;: The values of MariaDB/MySQL and PostgreSQL can NOT be compared directly!&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;And here is the graphical evaluation:&lt;/p&gt;
&lt;img src="https://www.fromdual.com/images/mariadb-data-load.png" alt="mariadb"&gt;
&lt;p&gt; &lt;/p&gt;
&lt;img src="https://www.fromdual.com/images/postgresql-data-load.png" alt="postgresql"&gt;
&lt;h2 id="remarks"&gt;Remarks&lt;/h2&gt;
&lt;p&gt;With faster discs, the difference between 1 and 2/3 would probably not have been quite so significant. Customer tests have shown a difference of ‘only’ about a factor of 5 (instead of a factor of 9 to 16).&lt;/p&gt;
&lt;p&gt;There are certainly other ways in which this loading process can be optimised. Here are a few that come to mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INSERT INTO ... SELECT * FROM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOAD DATA INFILE&lt;/code&gt;/&lt;code&gt;COPY&lt;/code&gt;, if possible&lt;/li&gt;
&lt;li&gt;Prepared statements&lt;/li&gt;
&lt;li&gt;Server side Stored Language (SQL/PSM, PL/pgSQL, &amp;hellip;) :-(&lt;/li&gt;
&lt;li&gt;PDO-Fetch of the results?&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maybe I should look for a profiler (&lt;a href="https://xdebug.org/" target="_blank"&gt;Xdebug&lt;/a&gt; or &lt;a href="https://www.php.net/manual/en/book.xhprof.php" target="_blank"&gt;xhprof&lt;/a&gt;)?&lt;/p&gt;
&lt;h2 id="further-contributions"&gt;Further contributions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/"&gt;Load CSV files into the database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fromdual.com/blog/mariadb-prepared-statements-transactions-and-multi-row-inserts/"&gt;MariaDB Prepared Statements, Transactions and Multi-Row Inserts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fromdual.com/blog/how-good-is-mysql-insert-trigger-performance/"&gt;How good is MySQL INSERT TRIGGER performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This page was translated using &lt;a href="https://www.deepl.com/en/translator" target="_blank"&gt;deepl.com&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>How much space does NULL need?</title><link>https://www.fromdual.com/blog/how-much-space-does-null-need/</link><pubDate>Sun, 08 Feb 2026 16:15:00 +0100</pubDate><guid>https://www.fromdual.com/blog/how-much-space-does-null-need/</guid><description>&lt;p&gt;The last time I consulted a customer, he came up to me beaming with joy and said that he had taken my advice and changed all the primary key columns from &lt;code&gt;BIGINT&lt;/code&gt; (8 bytes) to &lt;code&gt;INT&lt;/code&gt; (4 bytes) and that had made a big difference! His MySQL 8.4 database is now 750 Gbyte smaller (from 5.5 Tbyte). Nice!&lt;/p&gt;
&lt;p&gt;And yes, I know that contradicts the recommendations of some of my PostgreSQL colleagues (&lt;a href="https://www.crunchydata.com/blog/postgres-serials-should-be-bigint-and-how-to-migrate" target="_blank"&gt;here&lt;/a&gt; and &lt;a href="https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/#should-i-use-integerserial-or-bigintbigserial-for-my-auto-generated-primary-key" target="_blank"&gt;here&lt;/a&gt;). In the MySQL world, more emphasis is placed on such things (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank"&gt;source&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also, InnoDB works a wee bit differently (index clustered table and primary key in all secondary keys) than PostgreSQL (heap table, indices with row pointer (&lt;code&gt;ctid&lt;/code&gt;)).&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s not really the issue. Immediately afterwards, he asked me whether the deletion of columns of type &lt;code&gt;DOUBLE&lt;/code&gt; (8 bytes, in PostgreSQL-speak &lt;code&gt;DOUBLE PRECISION&lt;/code&gt;) would also save space or whether he should rather drop the columns straight away. My first reflex response to &lt;code&gt;DOUBLE&lt;/code&gt; was: &lt;code&gt;NULL&lt;/code&gt; is good, followed by &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; (&lt;code&gt;VACUUM FULL&lt;/code&gt; in PostgreSQL parlance). But the second thought was, &lt;code&gt;DOUBLE&lt;/code&gt; is a data type of fixed length, does &lt;code&gt;NULL&lt;/code&gt; also apply there or only for data types with variable length? Caution is the mother of the porcelain box! Love to consult the manual first&amp;hellip;&lt;/p&gt;
&lt;p&gt;And there it says (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank"&gt;source&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html" target="_blank"&gt;source&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The variable-length part of the record header contains a bit vector for indicating NULL columns. &amp;hellip; Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="experiment-with-mariadbmysql"&gt;Experiment with MariaDB/MySQL&lt;/h2&gt;
&lt;h3 id="test-setup"&gt;Test setup&lt;/h3&gt;
&lt;p&gt;Somehow the description is a bit too complicated for me. Perhaps a small sketch would help? So let&amp;rsquo;s give it a try:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; -- DROP TABLE IF EXISTS tracking;

SQL&amp;gt; CREATE TABLE tracking (
 id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE
);

SQL&amp;gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
SQL&amp;gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The table is approx. 1.8 Gbyte in size for both MariaDB and MySQL with 16 M rows. Since this information is only given very imprecisely in &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;, let&amp;rsquo;s take a look at the file system:&lt;/p&gt;
&lt;p&gt;MariaDB 11.8:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; system ls -l tracking.ibd
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL 8.4:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; system ls -l tracking.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="defragment-the-table"&gt;Defragment the table&lt;/h3&gt;
&lt;p&gt;Then we ‘defragment’ the table with the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; OPTIMIZE TABLE tracking;
+---------------+----------+----------+-------------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+---------------+----------+----------+-------------------------------------------------------------------+
| test.tracking | optimize | note | Table does not support optimize, doing recreate + analyze instead |
| test.tracking | optimize | status | OK |
+---------------+----------+----------+-------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Attention&lt;/strong&gt;: The table is copied once! It therefore needs twice the amount of disc space for a short time! This can be observed while the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command is running:&lt;/p&gt;
&lt;p&gt;MariaDB:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ watch -d -n 1 &amp;#39;ls -l trac* \#*&amp;#39;
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 &amp;#39;#sql-alter-d57-8c.frm&amp;#39;
-rw-rw---- 1 mysql mysql 968884224 Feb 7 10:39 &amp;#39;#sql-alter-d57-8c.ibd&amp;#39;
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ watch -d -n 1 &amp;#39;ls -l trac* \#*&amp;#39;
-rw-r----- 1 mysql mysql 369098752 Feb 7 10:40 #sql-ib1594-4164062678.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The result is amazing! With MariaDB, the table has remained somewhat the same size:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:39 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With MySQL, on the other hand, the table has actually grown after the ‘defragmentation’, namely by approx. 14%:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:41 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we execute the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command again, the size remains constant for both MariaDB and MySQL:&lt;/p&gt;
&lt;p&gt;MariaDB:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 10:46 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:48 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:48 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="attempt-1-null-out"&gt;Attempt 1: &lt;code&gt;NULL&lt;/code&gt; out&lt;/h3&gt;
&lt;p&gt;Now we &lt;code&gt;NULL&lt;/code&gt; out the values:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After this step, the sizes of the files have even grown slightly:&lt;/p&gt;
&lt;p&gt;MariaDB (+1.3%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 10:49 tracking.frm
-rw-rw---- 1 mysql mysql 1937768448 Feb 7 11:04 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL (+0.2%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 2202009600 Feb 7 11:04 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We then defragment the table again with the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command. The tables shrink as expected.&lt;/p&gt;
&lt;p&gt;MariaDB (to 23%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 11:09 tracking.frm
-rw-rw---- 1 mysql mysql 448790528 Feb 7 11:10 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL (to 24%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 520093696 Feb 7 11:10 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; again does NOT change the file size any more&amp;hellip;&lt;/p&gt;
&lt;h3 id="attempt-2-deleting-the-columns"&gt;Attempt 2: Deleting the columns&lt;/h3&gt;
&lt;p&gt;Now we try the whole thing again with the &lt;code&gt;DROP COLUMN&lt;/code&gt; command. The starting position is again the same as described above:&lt;/p&gt;
&lt;p&gt;MariaDB:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 11:15 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 11:18 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 1929379840 Feb 7 11:19 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command, the values look similar to the first attempt:&lt;/p&gt;
&lt;p&gt;MariaDB:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 11:20 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:21 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:21 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; again also brings no further changes, as above:&lt;/p&gt;
&lt;p&gt;MariaDB:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 1206 Feb 7 11:22 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:23 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:24 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And now the actual second attempt with dropping the columns:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing we notice is that the command is &lt;code&gt;INSTANTANEOUS&lt;/code&gt;, i.e. it does not make any changes to the data but only changes the metadata. On the one hand, this is good, as it minimises the impact on the application. On the other hand, it also means that no space is saved.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s get to grips with the whole thing again with the &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;MariaDB (to 93%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-rw---- 1 mysql mysql 925 Feb 7 11:28 tracking.frm
-rw-rw---- 1 mysql mysql 415236096 Feb 7 11:29 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;MySQL (to 92%):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-rw-r----- 1 mysql mysql 478150656 Feb 7 11:28 tracking.ibd
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Both, dropping the columns and the &lt;code&gt;NULL&lt;/code&gt; out of columns save a significant amount of space. Dropping the columns saves about 7% more space than &lt;code&gt;NULL&lt;/code&gt; them out. If it is possible from an application point of view, you should therefore drop columns that are no longer required, or if not possible, at least &lt;code&gt;NULL&lt;/code&gt; them out.&lt;/p&gt;
&lt;h2 id="experiment-with-postgresql"&gt;Experiment with PostgreSQL&lt;/h2&gt;
&lt;p&gt;And now let&amp;rsquo;s take a look at the whole thing with PostgreSQL 19devel.&lt;/p&gt;
&lt;h3 id="test-setup-1"&gt;Test setup&lt;/h3&gt;
&lt;p&gt;The test setup is analogous to MariaDB/MySQL:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# -- DROP TABLE IF EXISTS tracking;

postgres=# CREATE TABLE tracking (
 id SERIAL PRIMARY KEY
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION
);

postgres=# \timing

postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Firstly, we want to know how big the table has actually become. PostgreSQL seems to know this information very precisely:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# SELECT pg_relation_size(&amp;#39;tracking&amp;#39;) AS tab_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;)) AS tab_siz_prtty
 , pg_indexes_size(&amp;#39;tracking&amp;#39;) AS idx_siz
 , pg_size_pretty(pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS idx_siz_prtty
 , pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;) AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS tab_and_idx_siz_prtty
 , pg_total_relation_size(&amp;#39;tracking&amp;#39;) AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size(&amp;#39;tracking&amp;#39;)) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376856576 | 359 MB | 2340274176 | 2232 MB | 2340798464 | 2232 MB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we want to know where these files can be found in the file system:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();
 db_oid
--------
 5

postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode
 FROM pg_class WHERE relname = &amp;#39;tracking&amp;#39;;
 table_oid | relname | relnamespace | relfilenode
-----------+----------+--------------+-------------
 40965 | tracking | 2200 | 40965

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 WHERE c.relname = &amp;#39;tracking&amp;#39;;
 index_name | index_oid
---------------+-----------
 tracking_pkey | 40970

postgres=# SELECT pg_relation_filepath(&amp;#39;tracking&amp;#39;);
 pg_relation_filepath
----------------------
 base/5/40965
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Table and index size in the file system:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ ls -ltr 40965* 40970*
-rw------- 1 mysql mysql 40960 Feb 7 18:33 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:33 40965_fsm
-rw------- 1 mysql mysql 889675776 Feb 7 18:34 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:34 40965
-rw------- 1 mysql mysql 376856576 Feb 7 18:35 40970
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*_fsm&lt;/code&gt; means &amp;ldquo;free space map&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_vm&lt;/code&gt; means &amp;ldquo;visibility map&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.1&lt;/code&gt; means 2nd segment of the object (table or index)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL seems to work with segments of 1 Gbyte by default and, unlike MariaDB/MySQL (&lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;), knows exactly how large its files are. And the discrepancy from above (between &lt;code&gt;tot_rel_siz&lt;/code&gt; and &lt;code&gt;tab_and_idx_siz&lt;/code&gt;) can be explained by the &lt;code&gt;fsm&lt;/code&gt; and &lt;code&gt;vm&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;The PostgreSQL equivalent of the MariaDB/MySQL &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; is the &lt;code&gt;VACUUM FULL&lt;/code&gt; command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# VACUUM FULL tracking;

$ ls -ltr
-rw------- 1 mysql mysql 40960 Feb 7 18:39 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:39 40965_fsm
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40965
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 0 Feb 7 18:39 40975

...

-rw------- 1 mysql mysql 49152 Feb 7 18:39 2704
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2703
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2696
-rw------- 1 mysql mysql 65536 Feb 7 18:39 2674
-rw------- 1 mysql mysql 81920 Feb 7 18:39 2673
-rw------- 1 mysql mysql 98304 Feb 7 18:39 2659
-rw------- 1 mysql mysql 139264 Feb 7 18:39 2658
-rw------- 1 mysql mysql 24576 Feb 7 18:39 2619_fsm
-rw------- 1 mysql mysql 163840 Feb 7 18:39 2619
-rw------- 1 mysql mysql 106496 Feb 7 18:39 2608
-rw------- 1 mysql mysql 491520 Feb 7 18:39 1249
-rw------- 1 mysql mysql 122880 Feb 7 18:39 1247
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2662
-rw------- 1 mysql mysql 114688 Feb 7 18:39 1259
-rw------- 1 mysql mysql 16384 Feb 7 18:39 3455
-rw------- 1 mysql mysql 49152 Feb 7 18:39 2663
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 376864768 Feb 7 18:39 40975
-rw------- 1 mysql mysql 0 Feb 7 18:39 40965
-rw------- 1 mysql mysql 0 Feb 7 18:39 40970
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing you notice is that PostgreSQL touches quite a few files and the ‘free space map’ file has disappeared. In contrast to MariaDB/MySQL, the table segments have remained the same size. You can also see that the old table has ‘disappeared’ (40965, 40970) and a new one has been created (40972 and 40975). The &lt;code&gt;VACUUM FULL&lt;/code&gt; command in PostgreSQL also creates a copy of the data, as in MariaDB/MySQL.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# SELECT pg_relation_size(&amp;#39;tracking&amp;#39;) AS tab_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;)) AS tab_siz_prtty
 , pg_indexes_size(&amp;#39;tracking&amp;#39;) AS idx_siz
 , pg_size_pretty(pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS idx_siz_prtty
 , pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;) AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS tab_and_idx_siz_prtty
 , pg_total_relation_size(&amp;#39;tracking&amp;#39;) AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size(&amp;#39;tracking&amp;#39;)) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376864768 | 359 MB | 2340282368 | 2232 MB | 2340282368 | 2232 MB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The following query helps to understand which other files/objects have been created:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# SELECT c.oid, c.relname, ns.nspname
FROM pg_class AS c
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 oid | relname | nspname
-------+-----------------------------------+------------
 40965 | tracking | public
 40970 | tracking_pkey | public
 2619 | pg_statistic | pg_catalog
 1247 | pg_type | pg_catalog
 2703 | pg_type_oid_index | pg_catalog
 2704 | pg_type_typname_nsp_index | pg_catalog
 2658 | pg_attribute_relid_attnam_index | pg_catalog
 2659 | pg_attribute_relid_attnum_index | pg_catalog
 2662 | pg_class_oid_index | pg_catalog
 2663 | pg_class_relname_nsp_index | pg_catalog
 3455 | pg_class_tblspc_relfilenode_index | pg_catalog
 2696 | pg_statistic_relid_att_inh_index | pg_catalog
 2673 | pg_depend_depender_index | pg_catalog
 2674 | pg_depend_reference_index | pg_catalog
 1249 | pg_attribute | pg_catalog
 1259 | pg_class | pg_catalog
 2608 | pg_depend | pg_catalog

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 index_name | index_oid | nspname
-----------------------------------+-----------+------------
 pg_type_typname_nsp_index | 2704 | pg_catalog
 pg_attribute_relid_attnam_index | 2658 | pg_catalog
 tracking_pkey | 40970 | public
 pg_class_relname_nsp_index | 2663 | pg_catalog
 pg_class_tblspc_relfilenode_index | 3455 | pg_catalog
 pg_type_oid_index | 2703 | pg_catalog
 pg_attribute_relid_attnum_index | 2659 | pg_catalog
 pg_statistic_relid_att_inh_index | 2696 | pg_catalog
 pg_depend_depender_index | 2673 | pg_catalog
 pg_depend_reference_index | 2674 | pg_catalog
 pg_class_oid_index | 2662 | pg_catalog
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="attempt-1-null-out-1"&gt;Attempt 1: &lt;code&gt;NULL&lt;/code&gt; out&lt;/h3&gt;
&lt;p&gt;Then we also &lt;code&gt;NULL&lt;/code&gt; the columns in PostgreSQL. From here on, we save the view of the file system, as PostgreSQL seems to know the file sizes exactly, as we have seen above:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;

postgres=# SELECT pg_relation_size(&amp;#39;tracking&amp;#39;) AS tab_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;)) AS tab_siz_prtty
 , pg_indexes_size(&amp;#39;tracking&amp;#39;) AS idx_siz
 , pg_size_pretty(pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS idx_siz_prtty
 , pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;) AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS tab_and_idx_siz_prtty
 , pg_total_relation_size(&amp;#39;tracking&amp;#39;) AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size(&amp;#39;tracking&amp;#39;)) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 2695716864 | 2571 MB | 753696768 | 719 MB | 3449413632 | 3290 MB | 3450101760 | 3290 MB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here we see that the table segments grow massively (+37%), which is called ‘bloat’ in PostgreSQL terminology. The MVCC implementation of PostgreSQL stores both the old and never new version of the row ‘in-place’ directly in the table, in contrast to MariaDB/MySQL which stores the old version in UNDO space and the new row ‘in-place’. The index file also increases significantly (+100%). We need to do more research to find out why this is the case. In addition, a ‘free space map’ is created again (difference between &lt;code&gt;tot_rel_siz&lt;/code&gt; and &lt;code&gt;tab_and_idx_siz&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;A subsequent &lt;code&gt;VACUUM FULL&lt;/code&gt; reduces the table (to 28%) and the index (to 50%) again in relation to the previous size:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# VACUUM FULL tracking;

postgres=# SELECT pg_relation_size(&amp;#39;tracking&amp;#39;) AS tab_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;)) AS tab_siz_prtty
 , pg_indexes_size(&amp;#39;tracking&amp;#39;) AS idx_siz
 , pg_size_pretty(pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS idx_siz_prtty
 , pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;) AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size(&amp;#39;tracking&amp;#39;) + pg_indexes_size(&amp;#39;tracking&amp;#39;)) AS tab_and_idx_siz_prtty
 , pg_total_relation_size(&amp;#39;tracking&amp;#39;) AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size(&amp;#39;tracking&amp;#39;)) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and also in relation to the original size, the table (to 38%) and the index (to 100%) become smaller again. Why the index has remained the same size and only the table has shrunk remains to be investigated&amp;hellip;&lt;/p&gt;
&lt;h3 id="experiment-2-deleting-the-columns"&gt;Experiment 2: Deleting the columns&lt;/h3&gt;
&lt;p&gt;The columns are then dropped with &lt;code&gt;DROP COLUMN&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As the response was immediate, it can be assumed that this operation is also instantaneous. Unfortunately, I couldn&amp;rsquo;t find anything about this in the PostgreSQL documentation.&lt;/p&gt;
&lt;p&gt;Nothing has changed significantly in terms of size, which is actually to be expected with an instant operation. However, the fact that the size did not change after the &lt;code&gt;VACUUM FULL&lt;/code&gt; command was a little surprising:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1120010240 | 1068 MB

postgres=# VACUUM FULL tracking;

 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="remarks"&gt;Remarks&lt;/h2&gt;
&lt;p&gt;Locking in PostgreSQL works as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VACUUM&lt;/code&gt; Concurrent DML commands are possible similar to the MariaDB/MySQL &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command. However, the result is not quite the same.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VACUUM FULL&lt;/code&gt; causes an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock. Similar to the MariaDB/MySQL 5.5 and older &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; command. DML and &lt;code&gt;SELECT&lt;/code&gt; commands are NOT permitted.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neon.com/postgresql/postgresql-administration/postgresql-database-indexes-table-size" target="_blank"&gt;How to Get Sizes of Database Objects in PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/storage-file-layout.html" target="_blank"&gt;Database File Layout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/functions-admin.html" target="_blank"&gt;System Administration Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-cluster.html" target="_blank"&gt;CLUSTER&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-vacuum.html" target="_blank"&gt;VACUUM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" target="_blank"&gt;Explicit Locking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="additional-attempts"&gt;Additional attempts&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Instead of &lt;code&gt;0.0&lt;/code&gt;, &lt;code&gt;NULL&lt;/code&gt; was filled into the columns &lt;code&gt;d0&lt;/code&gt; - &lt;code&gt;d9&lt;/code&gt;. The table remained small (&lt;code&gt;tot_rel_siz_prtty = 1068 MB&lt;/code&gt;). It is therefore also worth saving &lt;code&gt;NULL&lt;/code&gt; instead of dummy values with PostgreSQL.&lt;/li&gt;
&lt;li&gt;The columns &lt;code&gt;d0&lt;/code&gt; - &lt;code&gt;d9&lt;/code&gt; were created with &lt;code&gt;DOUBLE PRECISION NOT NULL&lt;/code&gt; and the values &lt;code&gt;0.0&lt;/code&gt; were filled. No effect: The table remained large (&lt;code&gt;tot_rel_siz_prtty = 2232 MB&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This page was translated using &lt;a href="https://www.deepl.com/en/translator" target="_blank"&gt;deepl.com&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Someone is deleting my shared memory segments!</title><link>https://www.fromdual.com/blog/deleted-postgresql-shared-memory-segments/</link><pubDate>Sun, 08 Feb 2026 06:27:00 +0100</pubDate><guid>https://www.fromdual.com/blog/deleted-postgresql-shared-memory-segments/</guid><description>&lt;p&gt;When we work with PostgreSQL under our &lt;a href="https://www.fromdual.com/myenv/"&gt;myEnv&lt;/a&gt;, we regularly get shared memory segment errors. Example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;psql: error: connection to server on socket &amp;#34;/tmp/.s.PGSQL.5433&amp;#34; failed:
FATAL: could not open shared memory segment &amp;#34;/PostgreSQL.4220847662&amp;#34;:
No such file or directory
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or we see similar messages in the PostgreSQL error log:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ERROR: could not open shared memory segment &amp;#34;/PostgreSQL.4220847662&amp;#34;:
No such file or directory
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Because I am a MariaDB/MySQL admin, I am not very familiar with shared memory problems (MariaDB/MySQL does not work with shared memory). Fortunately, a search on the Internet led us on the right track (&lt;a href="https://www.postgresql.org/message-id/56A52018.1030001%40gmx.net" target="_blank" title="Re: systemd deletes shared memory segment in /dev/shm/Postgresql.NNNNNN"&gt;source&lt;/a&gt;). It is noted there:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The documentation of systemd states that this only happens for
non-system users. Can you check whether your &amp;ldquo;postgres&amp;rdquo; user (or
whatever you are using) is a system user?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="linux-system-user"&gt;Linux System User&lt;/h2&gt;
&lt;p&gt;First I had to find out what a system user under Linux actually is. I found an answer here: &lt;a href="https://unix.stackexchange.com/questions/80277/whats-the-difference-between-a-normal-user-and-a-system-user" target="_blank"&gt;What&amp;rsquo;s the difference between a normal user and a system user?&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That is not a technical difference but an organizational decision. E.g. it makes sense to show normal users in a login dialog (so that you can click them instead of having to type the user name) but it wouldn&amp;rsquo;t to show system accounts (the UIDs under which daemons and other automatic processes run) there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The LSB standard says: &lt;a href="https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/uidrange.html" target="_blank"&gt;User ID Ranges&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The system User IDs from 0 to 99 should be statically allocated by the system, and shall not be created by applications.&lt;br&gt;
The system User IDs from 100 to 499 should be reserved for dynamic allocation by system administrators and post install scripts using useradd.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On my Ubuntu system it looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ grep SYS_ /etc/login.defs
#SYS_UID_MIN 100
#SYS_UID_MAX 999
#SYS_GID_MIN 100
#SYS_GID_MAX 999
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This would be correct for the PostgreSQL user:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ id postgres
uid=130(postgres) gid=142(postgres) groups=142(postgres),116(ssl-cert)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But since the PostgreSQL instance in question runs under our &lt;a href="https://www.fromdual.com/myenv/"&gt;myEnv&lt;/a&gt;, that&amp;rsquo;s different:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So now we have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We change &lt;code&gt;SYS_UID_MAX&lt;/code&gt; and &lt;code&gt;SYS_GID_MAX&lt;/code&gt; to 1001 (simple variant).&lt;/li&gt;
&lt;li&gt;Or we change the &lt;code&gt;UID&lt;/code&gt; and the &lt;code&gt;GID&lt;/code&gt; of our user &lt;code&gt;dba&lt;/code&gt; to less than 1000.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="simple-variant-change-sys_uid_max-and-sys_gid_max-to-1001"&gt;Simple variant: Change &lt;code&gt;SYS_UID_MAX&lt;/code&gt; and &lt;code&gt;SYS_GID_MAX&lt;/code&gt; to 1001&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# /etc/login.defs
SYS_UID_MAX 1001
SYS_GID_MAX 1001
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To be on the safe side, the machine was rebooted. But that did not help: After a short time, the same errors occur again.&lt;/p&gt;
&lt;h2 id="more-complicated-variant-changing-the-uid-from-1001-to-990"&gt;More complicated variant: Changing the &lt;code&gt;UID&lt;/code&gt; from 1001 to 990&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ cat /etc/passwd
...
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
tomcat:x:999:999:Apache Tomcat:/:/sbin/nologin
oli:x:1000:1000:Oli Sennhauser,,,:/home/oli:/bin/bash
dba:x:1001:1001:DBA user:/home/dba:/bin/bash
...

$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To do this, all processes of this user must be stopped!&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ usermod --uid 990 dba
$ groupmod --gid 990 dba

$ find / -user 1001 -exec chown --no-dereference dba {} \;
$ find / -group 1001 -exec chgrp --no-dereference dba {} \;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This seems to have solved the problem!&lt;/p&gt;
&lt;h2 id="additional-information"&gt;Additional information&lt;/h2&gt;
&lt;p&gt;A source mentioned above also recommended setting the following parameters in &lt;code&gt;systemd-logind&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# /etc/systemd/system/systemd-logind.service.d/override.conf
RemoveIPC=no
RuntimeDirectorySize=1%
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;However, this measure is no longer necessary as it works without this change.&lt;/p&gt;
&lt;p&gt;This page was translated using &lt;a href="https://www.deepl.com/en/translator" target="_blank"&gt;deepl.com&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Load CSV files into the database</title><link>https://www.fromdual.com/blog/load-csv-files-into-the-database/</link><pubDate>Fri, 06 Feb 2026 18:04:00 +0100</pubDate><guid>https://www.fromdual.com/blog/load-csv-files-into-the-database/</guid><description>&lt;p&gt;Recently, I wanted to display the places of residence of the members of my club on a map for a personal gimmick (&lt;a href="https://www.shinguz.ch/computer/gis/igoc-mitglieder/" target="_blank"&gt;IGOC members&lt;/a&gt;). I knew the addresses of the club members. But not the coordinates of their places of residence.&lt;/p&gt;
&lt;p&gt;So I went in search of the coordinates and found what I was looking for at the Federal Office of Topography (&lt;a href="https://www.swisstopo.admin.ch/en" target="_blank"&gt;swisstopo&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The data is available there as a CSV file. Details here: &lt;a href="https://www.shinguz.ch/computer/gis/schweizer-ortschafts-koordinaten/" target="_blank"&gt;Swiss town coordinates&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How do I load this data into a database?&lt;/p&gt;
&lt;h2 id="loading-the-data-with-mariadbmysql"&gt;Loading the data with MariaDB/MySQL&lt;/h2&gt;
&lt;p&gt;MariaDB and MySQL have the &lt;code&gt;LOAD DATA INFILE&lt;/code&gt; command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; DROP TABLE IF EXISTS wgs84;

SQL&amp;gt; -- SET GLOBAL local_infile = ON; -- Only needed with MySQL

SQL&amp;gt; CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id SMALLINT UNSIGNED
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE
, n DOUBLE
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

SQL&amp;gt; -- TRUNCATE TABLE wgs84;

SQL&amp;gt; LOAD DATA LOCAL INFILE &amp;#39;/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39;
INTO TABLE wgs84
FIELDS TERMINATED BY &amp;#39;;&amp;#39;
LINES TERMINATED BY &amp;#39;\r\n&amp;#39;
IGNORE 1 LINES
;
Query OK, 5713 rows affected
Records: 5713 Deleted: 0 Skipped: 0 Warnings: 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can then query the data in the database:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT * FROM wgs84 ORDER BY ortschaftsname LIMIT 5;
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| ortschaftsname | plz4 | zusatzziffer | zip_id | gemeindename | bfs_nr | kantonskuerzel | adressenanteil | e | n | sprache | validity |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| Aadorf | 8355 | 0 | 4672 | Aadorf | 4551 | TG | 96.802 % | 8.903193007810433 | 47.491079014637265 | de | 2008-07-01 |
| Aadorf | 8355 | 0 | 4672 | Elgg | 294 | ZH | 3.198 % | 8.89206766645808 | 47.4933781685032 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Aarau | 4001 | AG | 99.713 % | 8.048148371736266 | 47.38973523857376 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Suhr | 4012 | AG | 0.287 % | 8.059410934099922 | 47.383298214804334 | de | 2008-07-01 |
| Aarau | 5004 | 0 | 2932 | Aarau | 4001 | AG | 100 % | 8.060698546432551 | 47.400587704180744 | de | 2008-07-01 |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
5 rows in set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or something more precise:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
+-------------+-----------+-------------------+--------------------+
| city | city_code | lon | lat |
+-------------+-----------+-------------------+--------------------+
| Aarburg | 4663 | 7.904271716719409 | 47.321443418782955 |
| Aarburg | 4663 | 7.889249714098425 | 47.313536073562474 |
| Aarburg | 4663 | 7.880309179095798 | 47.31255194439023 |
| Adligenswil | 6043 | 8.364849060491428 | 47.07037816052481 |
| Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056 |
| Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134 |
| Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145 |
+-------------+-----------+-------------------+--------------------+
7 rows in set (0.003 sec)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I will leave it to the reader to clean out the duplicates&amp;hellip; :-)&lt;/p&gt;
&lt;p&gt;So far so good, now to the finer points::&lt;/p&gt;
&lt;h3 id="differences-between-mariadb-and-mysql"&gt;Differences between MariaDB and MySQL&lt;/h3&gt;
&lt;p&gt;The procedure described above works perfectly with MariaDB 11.4 and 11.8. There are small differences with MySQL 8.4:&lt;/p&gt;
&lt;p&gt;The first error message that prevents loading is this one:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It can be bypassed relatively easily with the command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; SET GLOBAL local_infile = ON;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The next attempt will fail as follows:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ERROR 2068 (HY000): LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This problem can be solved by starting the MySQL client as follows:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ mysql --local-infile=1 --user=root test
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="sources"&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;MariaDB: &lt;a href="https://mariadb.com/docs/server/reference/sql-statements/data-manipulation/inserting-loading-data/load-data-into-tables-or-index/load-data-infile" target="_blank"&gt;LOAD DATA INFILE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MySQL: &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/load-data.html" target="_blank"&gt;LOAD DATA Statement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="loading-the-data-with-postgresql"&gt;Loading the data with PostgreSQL&lt;/h2&gt;
&lt;p&gt;PostgreSQL has the command &lt;code&gt;COPY ... FROM&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# DROP TABLE IF EXISTS wgs84;

postgres=# CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id INT
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE PRECISION
, n DOUBLE PRECISION
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

postgres=# -- TRUNCATE TABLE wgs84;

postgres=# COPY wgs84
FROM &amp;#39;/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39;
DELIMITER &amp;#39;;&amp;#39;
CSV HEADER
;
COPY 5713
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here, too, we receive the result as expected in the usual PostgreSQL form:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
 city | city_code | lon | lat
-------------+-----------+-------------------+--------------------
 Aarburg | 4663 | 7.904271716719409 | 47.321443418782955
 Aarburg | 4663 | 7.889249714098425 | 47.313536073562474
 Aarburg | 4663 | 7.880309179095798 | 47.31255194439023
 Adligenswil | 6043 | 8.36487538940682 | 47.07037794822416
 Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056
 Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134
 Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145
(7 rows)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="sources-1"&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL: &lt;a href="https://www.postgresql.org/docs/current/sql-copy.html" target="_blank"&gt;COPY&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="small-differences-between-mariadbmysql-and-postgresql"&gt;Small differences between MariaDB/MySQL and PostgreSQL&lt;/h2&gt;
&lt;p&gt;Basically, the load command is completely different in the two database worlds.&lt;/p&gt;
&lt;p&gt;With MariaDB and PostgreSQL, the commands run “out-of-the-box”. MySQL has two additional security hurdles built in here.&lt;/p&gt;
&lt;p&gt;PostgreSQL does not recognise &lt;code&gt;UNSIGNED&lt;/code&gt; integer data types, so the next largest data type (&lt;code&gt;INT&lt;/code&gt;) must be used, which is a little less space-saving than with MariaDB/MySQL.&lt;/p&gt;
&lt;h2 id="remarks"&gt;Remarks&lt;/h2&gt;
&lt;p&gt;When we did the same test a few days ago, there was still a loading error. So it seems that the data source has also changed slightly&amp;hellip;&lt;/p&gt;
&lt;p&gt;I have not found out quickly whether there is an SQL standard for these load commands and if so, whether MariaDB/MySQL or PostgreSQL are standard-compliant here.&lt;/p&gt;
&lt;p&gt;And of course there are other ways to get your CSV data into the database&amp;hellip;&lt;/p&gt;
&lt;p&gt;The tools &lt;code&gt;mariadb-import&lt;/code&gt;/&lt;code&gt;mysqlimport&lt;/code&gt; are used if you want to do this from the command line. The CSV Storage Engine can also be misused for this purpose (see &lt;a href="https://www.fromdual.com/blog/csv-storage-engine/"&gt;here&lt;/a&gt; for details). An officially supported variant is the MariaDB CONNECT Storage Engine with the CSV type (see &lt;a href="https://mariadb.com/docs/server/server-usage/storage-engines/connect/connect-table-types/connect-csv-and-fmt-table-types" target="_blank"&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;SQL&amp;gt; INSTALL SONAME &amp;#39;ha_connect&amp;#39;;

SQL&amp;gt; CREATE TABLE wgs84_fdw
ENGINE = CONNECT
table_type = CSV
file_name=&amp;#39;/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39;
header = 1
sep_char = &amp;#39;;&amp;#39;
quoted = 0;

SQL&amp;gt; INSERT INTO wgs84 SELECT * FROM wgs84_fdw;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unfortunately, it looks like the CONNECT Storage Engine will no longer be supported by MariaDB! And the &lt;code&gt;mydumper&lt;/code&gt;/&lt;code&gt;myloader&lt;/code&gt; tool also seems to be able to handle CSV files.&lt;/p&gt;
&lt;p&gt;And of course the whole thing can also be solved using applications&amp;hellip;&lt;/p&gt;
&lt;p&gt;With PostgreSQL there are the following options:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# \copy wgs84 FROM &amp;#39;/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39; DELIMITER &amp;#39;;&amp;#39; CSV HEADER
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;then from the shell:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$ psql --user=dba -c &amp;#34;\copy wgs84 FROM &amp;#39;/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39; DELIMITER &amp;#39;;&amp;#39; CSV HEADER&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the variant via the Foreign Data Wrapper (FWD). But I have not tried this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;postgres=# CREATE EXTENSION postgres_fdw;

postgres=# CREATE SERVER foreign_server
 FOREIGN DATA WRAPPER postgres_fdw
 OPTIONS (
 datasource &amp;#39;CSV:/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv&amp;#39;,
 format &amp;#39;CSV&amp;#39;
 )
;

postgres=# CREATE USER MAPPING FOR local_user
 SERVER foreign_server
 OPTIONS (user &amp;#39;foreign_user&amp;#39;, password &amp;#39;password&amp;#39;)
;

postgres=# CREATE FOREIGN TABLE foreign_table (
 id integer NOT NULL,
 data text
)
 SERVER foreign_server
 OPTIONS (schema_name &amp;#39;some_schema&amp;#39;, table_name &amp;#39;some_table&amp;#39;)
;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="addendum"&gt;Addendum&lt;/h2&gt;
&lt;p&gt;The MariaDB/MySQL data type &lt;code&gt;DOUBLE&lt;/code&gt; is called &lt;code&gt;DOUBLE PRECISION&lt;/code&gt; in ProsgreSQL.&lt;/p&gt;
&lt;p&gt;This page was translated using &lt;a href="https://www.deepl.com/en/translator" target="_blank"&gt;deepl.com&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>MariaDB/MySQL Environment MyEnv 2.1.1 has been released</title><link>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/</link><pubDate>Fri, 12 Dec 2025 17:43:00 +0100</pubDate><guid>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/</guid><description>&lt;p&gt;FromDual has the pleasure to announce the release of the new version 2.1.1 of its popular MariaDB, Galera Cluster and MySQL multi-instance environment &lt;a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment"&gt;MyEnv&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The new MyEnv can be downloaded &lt;a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download"&gt;here&lt;/a&gt;. How to install MyEnv is described in the &lt;a href="https://support.fromdual.com/documentation/myenv/myenv.html" target="_blank"&gt;MyEnv Installation Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the inconceivable case that you find a bug in MyEnv please report it to us by sending an &lt;a href="mailto:contact@fromdual.com?Subject=Bug report for myenv"&gt;email&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Any feedback, statements and testimonials are welcome as well! Please &lt;a href="mailto:feedback@fromdual.com?Subject=Feedback for fpmmm"&gt;send them to us&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="upgrade-from-11x-to-20"&gt;Upgrade from 1.1.x to 2.0&lt;/h2&gt;
&lt;p&gt;Please look at the &lt;a href="https://www.fromdual.com/mysql-mariadb-environment-myenv-2.0.0-has-been-released" title="MySQL Environment MyEnv 2.0.0 has been released"&gt;MyEnv 2.0.0 Release Notes&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="upgrade-from-20x-to-211"&gt;Upgrade from 2.0.x to 2.1.1&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ sudo -i -u mysql
$ cd ${HOME}/product
$ tar xf /download/myenv-2.1.1.tar.gz
$ rm -f myenv
$ ln -s myenv-2.1.1 myenv
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="plug-ins"&gt;Plug-ins&lt;/h3&gt;
&lt;p&gt;If you are using plug-ins for &lt;code&gt;showMyEnvStatus&lt;/code&gt; create all the links in the new directory structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd ${HOME}/product/myenv
$ ln -s ../../utl/oem_agent.php plg/showMyEnvStatus/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="upgrade-of-the-instance-directory-structure"&gt;Upgrade of the instance directory structure&lt;/h3&gt;
&lt;p&gt;From MyEnv 1.0 to 2.0 the directory structure of instances has fundamentally changed. Nevertheless MyEnv 2.0 works fine with MyEnv 1.0 directory structures.&lt;/p&gt;
&lt;h2 id="changes-in-myenv-211"&gt;Changes in MyEnv 2.1.1&lt;/h2&gt;
&lt;h3 id="myenv"&gt;MyEnv&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;addInstance&lt;/code&gt; for PostgreSQL added.&lt;/li&gt;
&lt;li&gt;Basic PostgreSQL functionality implemented.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up&lt;/code&gt; working now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stop&lt;/code&gt;, &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;restart&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt; implemented for PostgreSQL.&lt;/li&gt;
&lt;li&gt;Version extraction for PostgreSQL added.&lt;/li&gt;
&lt;li&gt;Function &lt;code&gt;extractVersion&lt;/code&gt; rewritten so section config is passed and not &lt;code&gt;basedir&lt;/code&gt; any more.&lt;/li&gt;
&lt;li&gt;Configuration type &lt;code&gt;mysqld&lt;/code&gt; changed to &lt;code&gt;mysql&lt;/code&gt; and now also usable for &lt;code&gt;mariadb&lt;/code&gt; and &lt;code&gt;postgresql&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;New variant of version comment added for MySQL 8.0.&lt;/li&gt;
&lt;li&gt;Comment added to &lt;code&gt;my_exec()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="myenv-installer"&gt;MyEnv Installer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;mysql/mariadb fork installation implemented and tested.&lt;/li&gt;
&lt;li&gt;Example in &lt;code&gt;installMyEnv&lt;/code&gt; corrected/improved.&lt;/li&gt;
&lt;li&gt;Made nasty warning during installation of new instance go away.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="myenv-utilities"&gt;MyEnv Utilities&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;New script added.&lt;/li&gt;
&lt;li&gt;SSL added to monitor.&lt;/li&gt;
&lt;li&gt;Slave monitor made MySQL 8+ ready.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checksum_table&lt;/code&gt; works now also with tables consisting of protected keywords.&lt;/li&gt;
&lt;li&gt;More debugging info added.&lt;/li&gt;
&lt;li&gt;All scripts checked by &lt;code&gt;shellcheck&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Missing runtime directory is caught and fixed now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shellcheck&lt;/code&gt; suggestions applied.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checksum_table.sh&lt;/code&gt; added.&lt;/li&gt;
&lt;li&gt;Utility scripts brought to new state.&lt;/li&gt;
&lt;li&gt;Build slave script added.&lt;/li&gt;
&lt;li&gt;table diff refactored.&lt;/li&gt;
&lt;li&gt;Chunking added.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table_diff&lt;/code&gt; is now ready for chunking.&lt;/li&gt;
&lt;li&gt;Faster checksum implemented and output shortened.&lt;/li&gt;
&lt;li&gt;Row by row crc32 checksum.&lt;/li&gt;
&lt;li&gt;Moved the checksum table in its own function.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table_diff.php&lt;/code&gt; added.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="postgresql"&gt;PostgreSQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;show_create_table.sh&lt;/code&gt; for PostgreSQL added.&lt;/li&gt;
&lt;li&gt;PostgreSQL files added here until we have a better location.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="general"&gt;General&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Code clean-up and configuration check added.&lt;/li&gt;
&lt;li&gt;Recursive directory removal improved.&lt;/li&gt;
&lt;li&gt;rc made unique.&lt;/li&gt;
&lt;li&gt;Minor typos fixed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;my.cnf.template&lt;/code&gt; cleaned-up and synced with website.&lt;/li&gt;
&lt;li&gt;2 bugs with PHP 8.5 fixed.&lt;/li&gt;
&lt;li&gt;Libraries updated.&lt;/li&gt;
&lt;li&gt;Some return codes can be ignored because Redhat tools return error codes &amp;gt; 100 as OK.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="documentation"&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Documentation added and ready to start.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="packaging"&gt;Packaging&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Distro versions centralized in one place.&lt;/li&gt;
&lt;li&gt;Zabbix repo added for Ubuntu 24.04.&lt;/li&gt;
&lt;li&gt;Rocky 10 and Debian 11 added.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lxc&lt;/code&gt; replaced by &lt;code&gt;incus&lt;/code&gt;, distros cleaned-up.&lt;/li&gt;
&lt;li&gt;Ubuntu 24.04 and MariaDB 10.11 and 11.4 enabled.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brman&lt;/code&gt; as build project added.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glb&lt;/code&gt; included in build scripts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restartContainer&lt;/code&gt; implemented.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gid&lt;/code&gt; and &lt;code&gt;uid&lt;/code&gt; added to &lt;code&gt;pushFileToContainer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build cleaned-up.&lt;/li&gt;
&lt;li&gt;Build scrips improved.&lt;/li&gt;
&lt;li&gt;Container file push and pull added.&lt;/li&gt;
&lt;li&gt;Build infrastructure reorganized.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stopContainer&lt;/code&gt; and &lt;code&gt;startContainer&lt;/code&gt; added.&lt;/li&gt;
&lt;li&gt;Container library started.&lt;/li&gt;
&lt;li&gt;Debian 10 removed.&lt;/li&gt;
&lt;li&gt;Update container script is waiting to avoid infrastructure build failure.&lt;/li&gt;
&lt;li&gt;Old package for RPM changed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;install_base&lt;/code&gt; separted from &lt;code&gt;mysql_home&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Installation moved from &lt;code&gt;/home/mysql&lt;/code&gt; to &lt;code&gt;/opt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fix DEB package.&lt;/li&gt;
&lt;li&gt;Ubuntu 22.04 added for package build.&lt;/li&gt;
&lt;li&gt;Bug in Debian build script fixed.&lt;/li&gt;
&lt;li&gt;Ubuntu 24.04 added to build infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recreate_build_infrastructure.sh&lt;/code&gt; rewritten in PHP.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update_container_templates.sh&lt;/code&gt; rewritten in PHP.&lt;/li&gt;
&lt;li&gt;Made package build infrastructure more generic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For subscriptions of commercial use of MyEnv please &lt;a href="mailto:contact@fromdual.com?Subject=Commercial use of MyEnv"&gt;get in contact&lt;/a&gt; with us.&lt;/p&gt;</description></item></channel></rss>