<?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 (de)</title><link>https://www.fromdual.com/de/aggregator/categories/12/</link><description>FromDual: PostgreSQL Tech-Feed in German</description><generator>Hugo</generator><language>de-CH</language><atom:link href="https://www.fromdual.com/de/aggregator/categories/12/index.xml" rel="self" type="application/rss+xml"/><item><title>Datenbank Index Optimierer</title><link>https://www.fromdual.com/de/blog/datenbank-index-optimierer/</link><pubDate>Wed, 15 Jul 2026 11:54:00 +0200</pubDate><guid>https://www.fromdual.com/de/blog/datenbank-index-optimierer/</guid><description>&lt;p&gt;Kürzlich hat mich ein Kunde gefragt, ob das &amp;ldquo;aufwändige&amp;rdquo; Index-Prüfen nicht einem Index-Optimierer überlassen werden könnte. Klar geht das&amp;hellip;&lt;/p&gt;
&lt;p&gt;Was wollen wir den genau überprüfen?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tabellen ohne Primary Key&lt;/li&gt;
&lt;li&gt;Doppelte Indices&lt;/li&gt;
&lt;li&gt;Teilweise redundante Indices&lt;/li&gt;
&lt;li&gt;Ungenutzte Indices&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mariadb-mysql-und-percona-server"&gt;MariaDB, MySQL und Percona Server&lt;/h2&gt;
&lt;h3 id="tabellen-ohne-primary-key"&gt;Tabellen ohne 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;Quelle: &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="doppelte-indices"&gt;Doppelte Indices&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;Quelle: &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="teilweise-redundante-indices"&gt;Teilweise redundante Indices&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;Quelle: &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="ungenutzte-indices"&gt;Ungenutzte Indices&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;Achtung&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bei MariaDB muss das &lt;code&gt;PERFORMANCE_SCHEMA&lt;/code&gt; zuerst eingeschaltet werden.&lt;/li&gt;
&lt;li&gt;Die Informationen sind korrekt seit dem letzten Datenbank-Neustart. Wurde ein Index das letzte mal VOR dem letzten Neustart genutzt, wir er hier als ungenutzt angezeigt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quelle: &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="und-jetzt-mit-postgresql"&gt;Und jetzt mit PostgreSQL&lt;/h2&gt;
&lt;h3 id="tabellen-ohne-primary-key-1"&gt;Tabellen ohne 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;Quelle: &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="doppelte-indices-1"&gt;Doppelte Indices&lt;/h3&gt;
&lt;p&gt;Basierend auf dem 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;Quelle: &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="teilweise-redundante-indices-1"&gt;Teilweise redundante Indices&lt;/h3&gt;
&lt;p&gt;Basierend auf dem 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;Quelle: &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="ungenutzte-indices-1"&gt;Ungenutzte Indices&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;Quellen:&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;An den &lt;a href="https://2026.pgday.ch/schedule/" target="_blank"&gt;Swiss PGDay2026&lt;/a&gt;(s) hat Bertrand Hartwig sein Tool &lt;a href="https://github.com/beh74/pgassistant-community" target="_blank"&gt;PG Assistant&lt;/a&gt; vorgestellt. In diesem Zusammenhang wollte ich es gleich mal ausprobieren&amp;hellip;&lt;/p&gt;
&lt;p&gt;Fehlende Primary Keys und doppelte Indices konnte PG Assistant finden. Teilweise redundante Indices oder ungenutzte Indices hat er mir nicht angezeigt, kann aber auch an mir liegen&amp;hellip;&lt;/p&gt;
&lt;figure&gt;
 &lt;a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111018.png" title="volle Grösse"&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="volle Grösse"&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="volle Grösse"&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-von-pg-assistant"&gt;Intallation von 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;Dann mit dem Web-Browser auf die angezeigte URL verbinden.&lt;/p&gt;
&lt;p&gt;In der Datenbank muss ein User angelegt:&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;sowie die &lt;code&gt;pg_hba.conf&lt;/code&gt; angepasst werden.&lt;/p&gt;
&lt;h2 id="nachtrag"&gt;Nachtrag&lt;/h2&gt;
&lt;p&gt;Ungenutzte Indices findet man mit dem PG Assitant wie folgt: Database Objects ➜ Indexes ➜ Status: Unused ➜ &amp;ldquo;NO INDEX ACTIVITY&amp;rdquo;&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>Wie lädt man Daten am schnellsten in die Datenbank?</title><link>https://www.fromdual.com/de/blog/daten-schnell-in-die-datenbank-laden/</link><pubDate>Tue, 10 Feb 2026 22:48:00 +0100</pubDate><guid>https://www.fromdual.com/de/blog/daten-schnell-in-die-datenbank-laden/</guid><description>&lt;p&gt;Beim letzten Kunden hatten wir wirklich ein paar spannende Fragen zu lösen! Insbesondere auch, weil die Datenbank nicht ganz klein war.&lt;/p&gt;
&lt;p&gt;Hier kurz einige Eckdaten: CPU: 2 Sockel x 24 Kerne x 2 Threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD im RAID-10 und 7 Tbyte Daten, einige Tausend Mandanten, stark wachsend.&lt;/p&gt;
&lt;p&gt;Der aktuelle Durchsatz: 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 gemittelt über 30 Tage. Tendenz stark steigend. Applikation und Queries nicht durchgängig optimiert. Datenbank Konfiguration: &amp;ldquo;state of the art&amp;rdquo; nicht mit Benchmarks verifiziert. CPU-Auslastung ca. 50% im Schnitt, in der Spitze mehr. I/O System hat noch Luft nach oben.&lt;/p&gt;
&lt;p&gt;Der Kunde sammelt Positions- und sonstige Gerätedaten ein und speichert diese in der Datenbank. Also ein klassisches IoT Problem (mit Zeitreihen, Index Clustered Table, etc.).&lt;/p&gt;
&lt;p&gt;Die Frage, die er gestellt hat, ist: Wie kriege ich am schnellsten Daten von einer Tabelle (pending data, eine Art Queue) in eine andere Tabelle (final data, pro Mandant) kopiert.&lt;/p&gt;
&lt;p&gt;Der Datenfluss sieht in etwa wie folgt aus:&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 verschiedene Varianten, die Daten zu kopieren, standen zur Auswahl.&lt;/p&gt;
&lt;h2 id="variante-1-insert-und-delete-einfachste-form"&gt;Variante 1: &lt;code&gt;INSERT&lt;/code&gt; und &lt;code&gt;DELETE&lt;/code&gt; (einfachste Form)&lt;/h2&gt;
&lt;p&gt;Die einfachste Variante ist simples &lt;code&gt;INSERT&lt;/code&gt; und &lt;code&gt;DELETE&lt;/code&gt;. Diese Variante ist insbesondere darum problematisch, weil MariaDB/MySQL und PostgreSQL per default &lt;code&gt;AUTOCOMMIT&lt;/code&gt; eingeschaltet haben (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-autocommit-commit-rollback.html" target="_blank"&gt;hier&lt;/a&gt;, &lt;a href="https://mariadb.com/docs/server/reference/sql-statements/transactions/start-transaction" target="_blank"&gt;hier&lt;/a&gt;, &lt;a href="https://www.postgresql.org/docs/current/ecpg-sql-set-autocommit.html" target="_blank"&gt;hier&lt;/a&gt; und &lt;a href="https://www.cybertec-postgresql.com/en/disabling-autocommit-in-postgresql-can-damage-your-health/" target="_blank"&gt;hier&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Damit man sich das ein bisschen besser vorstellen kann, hier etwas Pseudocode dazu:&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;Wenn wir also 20 k Zeilen umkopieren wollen, verursacht diese Variante: 40 k &lt;code&gt;COMMIT&lt;/code&gt;s (&lt;code&gt;fsync&lt;/code&gt;) und 42 k Netzwerk-Roundtrips!&lt;/p&gt;
&lt;h2 id="variante-2-start-transaction-und-insert-und-delete"&gt;Variante 2: &lt;code&gt;START TRANSACTION&lt;/code&gt; und &lt;code&gt;INSERT&lt;/code&gt; und &lt;code&gt;DELETE&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Diese Variante wird von versierteren Datenbank-Entwicklern verwendet. Hier der passende Pseudocode dazu:&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;Wenn wir in diesem Beispiel 20 k Zeilen umkopieren wollen verursacht diese Variante nur noch 2 k &lt;code&gt;COMMIT&lt;/code&gt;s (&lt;code&gt;fsync&lt;/code&gt;)! Also 20 mal weniger! Aber dafür 46 k Netzwerk-Roundtrips (10% mehr).&lt;/p&gt;
&lt;h2 id="variante-3-start-transaction-und-optimiertes-insert-und-delete"&gt;Variante 3: &lt;code&gt;START TRANSACTION&lt;/code&gt; und optimiertes &lt;code&gt;INSERT&lt;/code&gt; und &lt;code&gt;DELETE&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Diese Variante ist programmiertechnisch ein klein wenig anspruchsvoller. Sie wird verwendet, wenn man den Grenzen des Möglich etwas näher kommen will. Hier der Pseudocode dazu:&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;Und diese 3. Variante verursacht bei 20 k Zeilen ebenfalls nur noch 2 k &lt;code&gt;COMMIT&lt;/code&gt;&amp;rsquo;s (&lt;code&gt;fsync&lt;/code&gt;), spart sich aber die Schleife über die &lt;code&gt;INSERT&lt;/code&gt; und &lt;code&gt;DELETE&lt;/code&gt; Statements in der Datenbank. Wir sparen also einerseits Netzwerk Round-Trips (nur noch 10 k) und CPU-Cycles auf der Datenbank (welche sich schlecht skalieren lassen) zum Parsen der Queries.&lt;/p&gt;
&lt;h2 id="test-set-up"&gt;Test Set-up&lt;/h2&gt;
&lt;p&gt;Um das Ganze zu testen haben wir ein kleines Script vorbereitet: &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="vorbereitung-und-ausführung-mit-mariadbmysql"&gt;Vorbereitung und Ausführung mit MariaDB/MySQL&lt;/h3&gt;
&lt;p&gt;Mit folgenden Befehlen kann man diese Tests selber ausführen:&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;Die Messwerte kann sich jeder selbst mit dem entsprechenden Test-Skript erarbeiten.&lt;/p&gt;
&lt;h3 id="vorbereitung-und-ausführung-mit-postgresql"&gt;Vorbereitung und Ausführung mit PostgreSQL&lt;/h3&gt;
&lt;p&gt;Mit folgenden Befehlen kann man diese Tests selber ausführen:&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;Die Messwerte kann sich jeder selbst mit dem entsprechenden Test-Skript erarbeiten.&lt;/p&gt;
&lt;h2 id="resultate"&gt;Resultate&lt;/h2&gt;
&lt;p&gt;Um unnötige Diskussionen zu vermeiden, haben wir hier &amp;ldquo;nur&amp;rdquo; die relative Performance (Laufzeit) aufgeführt, wie es MarkC in letzter Zeit auch macht. Unsere Messwerte können gerne bilateral zur Verfügung stellen. Sie lassen sich aber einfach mit dem Test-Skript selber reproduzieren.&lt;/p&gt;
&lt;p&gt;Weniger ist besser:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;Variante 1&lt;/th&gt;
					&lt;th&gt;Variante 2&lt;/th&gt;
					&lt;th&gt;Variante 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;Achtung&lt;/strong&gt;: Die Werte von MariaDB/MySQL und PostgreSQL lassen sich NICHT direkt vergleichen!&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;Und hier noch die graphische Auswertung:&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="bemerkungen"&gt;Bemerkungen&lt;/h2&gt;
&lt;p&gt;Mit schnelleren Disks, wäre der Unterschied zwischen 1 und 2/3 wahrscheinlich nicht mehr ganz so gravierend ausgefallen. Tests beim Kunden haben &amp;ldquo;nur&amp;rdquo; ca. Faktor 5 (statt Faktor 9 bis 16) unterschied gezeigt.&lt;/p&gt;
&lt;p&gt;Es gibt sicher weitere Möglichkeiten, wie man diesen Lade-Vorgang noch optimieren kann. Hier kommen mir in den Sinn:&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;, wenn möglich&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 der Resultate?&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vielleicht sollte ich mich mal um einen Profiler kümmern (&lt;a href="https://xdebug.org/" target="_blank"&gt;Xdebug&lt;/a&gt; oder &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="weitere-beiträge"&gt;Weitere Beiträge&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;</description></item><item><title>Wie viel Platz braucht NULL?</title><link>https://www.fromdual.com/de/blog/wie-viel-platz-braucht-null/</link><pubDate>Sun, 08 Feb 2026 10:36:00 +0100</pubDate><guid>https://www.fromdual.com/de/blog/wie-viel-platz-braucht-null/</guid><description>&lt;p&gt;Beim letzten Beratungseinsatz beim Kunden, kam dieser freudestrahlend auf mich zu mit der Bemerkung: Er habe meinen Rat befolgt und sämtiche Primary Key Spalten von &lt;code&gt;BIGINT&lt;/code&gt; (8 byte) auf &lt;code&gt;INT&lt;/code&gt; (4 byte) geändert und das habe viel gebracht! Seine MySQL 8.4er Datenbank sei jetzt um 750 Gbyte kleiner geworden (von 5.5 Tbyte). Schön!&lt;/p&gt;
&lt;p&gt;Und ja, ich weiss, dass wiederspricht den Empfehlungen einiger meiner PostgreSQL Kollegen (&lt;a href="https://www.crunchydata.com/blog/postgres-serials-should-be-bigint-and-how-to-migrate" target="_blank"&gt;hier&lt;/a&gt; und &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;hier&lt;/a&gt;). In der MySQL-Welt wird eher auf solche Dinge Wert gelegt (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank"&gt;Quelle&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;Zudem funktioniert InnoDB ein klein wening anders (Index Clusterd Table und Primary Key in allen Secondary Keys) als PostgreSQL (Heap Table, Indices mit Row Pointer (&lt;code&gt;ctid&lt;/code&gt;)).&lt;/p&gt;
&lt;p&gt;Aber das ist eigentlich nicht das Thema. Sofort im Anschluss kam er nämlich mit der Frage, ob das AusNULLen von Spalten vom Typ &lt;code&gt;DOUBLE&lt;/code&gt; (8 byte, in PostgreSQL-Sprech &lt;code&gt;DOUBLE PRECISION&lt;/code&gt;) auch Platzeinsparungen brächte oder ob er die Spalten lieber gleich droppen solle. Meine erste reflexartige Antwort bei &lt;code&gt;DOUBLE&lt;/code&gt; war: &lt;code&gt;NULL&lt;/code&gt; bringt was, gefolgt von &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; (in PostgreSQL-Sprech &lt;code&gt;VACUUM FULL&lt;/code&gt;). Der zweite Gedanke war aber, &lt;code&gt;DOUBLE&lt;/code&gt; ist ein Datentyp fixer länger, gilt das mit &lt;code&gt;NULL&lt;/code&gt; da auch oder nur bei Datentypen mit variabler Länge? Vorsicht ist die Mutter der Porzelankiste! Liebe zuerst mal das Manual konsultieren&amp;hellip;&lt;/p&gt;
&lt;p&gt;Und dort steht (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank"&gt;Quelle&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;und (&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html" target="_blank"&gt;Quelle&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="versuch-mit-mariadbmysql"&gt;Versuch mit MariaDB/MySQL&lt;/h2&gt;
&lt;h3 id="versuchsaufbau"&gt;Versuchsaufbau&lt;/h3&gt;
&lt;p&gt;Irgendwie ist mir das etwas zu komplizert beschrieben. Eine kleine Skizze würde vielleicht helfen? Also machen wir einen Versuch:&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;Die Tabelle wird sowohl bei MariaDB als auch bei MySQL bei 16 M Rows ca. 1.8 Gbyte gross. Da dieses Informationen im &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; nur sehr unpräzise angegeben werden schauen wir auf dem Filesystem nach:&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="defragementieren-der-tabelle"&gt;Defragementieren der Tabelle&lt;/h3&gt;
&lt;p&gt;Dann &amp;ldquo;defragmentieren&amp;rdquo; wir die Tabelle mit dem &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl:&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;Achtung&lt;/strong&gt;: Die Tabelle wir einmal umkopiert! Es braucht also kurzzeitig die doppelte Menge an Diskplatz! Dies können wird schön beobachten während der &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehls läuft:&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;Das Resultat ist dann aber erstaunlich! Bei MariaDB ist die Tabelle in etwas gleich gross geblieben:&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;Bei MySQL hingegen ist die Tabelle nach dem &amp;ldquo;defragmentieren&amp;rdquo; sogar angewachsen und zwar um ca. 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;Wenn wir den &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl nochmal ausführen bleibt die Grösse sowohl bei MariaDB als auch MySQL konstant:&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="versuch-1-ausnullen"&gt;Versuch 1: AusNULLen&lt;/h3&gt;
&lt;p&gt;Jetzt &lt;code&gt;NULL&lt;/code&gt;en wir die Werte aus:&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;Nach diesem Schritt sind die Grössen der Dateien sogar noch etwas gewachsen:&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;Anschliessend defragemntieren wir die Tabelle erneut mit dem &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl. Die Tabellen schrumpfen wie erwartet.&lt;/p&gt;
&lt;p&gt;MariaDB (auf 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 (auf 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;Ein erneutes &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; bringt anschliessend KEINE Veränderung mehr in der Dateigrösse&amp;hellip;&lt;/p&gt;
&lt;h3 id="versuch-2-löschen-der-spalten"&gt;Versuch 2: Löschen der Spalten&lt;/h3&gt;
&lt;p&gt;Jetzt probieren wir das Ganze nochmal mit dem &lt;code&gt;DROP COLUMN&lt;/code&gt; Befehl. Die Ausganglage ist wieder die selbe wie oben beschrieben:&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;Nach dem &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl sehen die Werte ähnlich aus wie im ersten Versuch:&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;Ein erneutes &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; bring ebenfalls keine weiteren Veränderungen mehr, wie oben:&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;Und jetzt der eigentliche zweite Versuch mit Löschen der Spalten:&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;Als erstes stellen wir fest, dass der Befehl &lt;code&gt;INSTANTANEOUS&lt;/code&gt; ist, als keinerlei Änderung an den Daten vornimmt sondern nur Änderung an den Metadaten. Das ist auf der einen Seite gut, da der Einfluss auf die Applikation dadurch minimal gehalten wird.. Auf der anderen Seite bedeutet das ja auch: Keine Platzersparnis.&lt;/p&gt;
&lt;p&gt;Also rücken wir dem Ganzen wieder mit dem &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl zu Leibe:&lt;/p&gt;
&lt;p&gt;MariaDB (auf 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 (auf 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="fazit"&gt;Fazit&lt;/h3&gt;
&lt;p&gt;Sowohl das ausNULLen der Spalten als auch das Löschen der Spalten bringen signifikante Platzersparnis. Wobei das Löschen der Spalten nochmal ca. 7% mehr bringt als das ausNULLen. Wenn es applikatorisch möglich ist, sollte man daher nicht mehr benötigte Spalen löschen, wenn nicht möglich, diese zumindest ausNULLen.&lt;/p&gt;
&lt;h2 id="versuch-mit-postgresql"&gt;Versuch mit PostgreSQL&lt;/h2&gt;
&lt;p&gt;Und jetzt schauen wir uns das Ganze noch bei PostgreSQL 19devel an.&lt;/p&gt;
&lt;h3 id="versuchsaufbau-1"&gt;Versuchsaufbau&lt;/h3&gt;
&lt;p&gt;Der Versuchsaufbau erfolgt analog zu 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;Zuerst wollen wir mal wissen, wie gross die Tabelle eigentlich geworden ist. PostgreSQL scheint diese Informationen sehr genau zu kennen:&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;Dann wollen wir wissen, wo im Filesystem diese Dateien zu finden sind:&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;Tabellen- und Indexgrösse im Filesystem:&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; bedeutet &amp;ldquo;free space map&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*_vm&lt;/code&gt; bedeutet &amp;ldquo;visibility map&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.1&lt;/code&gt; bedeutet 2. Segment des Objekts (Tabelle oder Index)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL scheint per default mit Segmenten von 1 Gbyte Grösse zu arbeiten und im Unterschied zu MariaDB/MySQL (&lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;) sehr genau zu wissen, wie gross seine Dateien sind. Und die Diskrepanz von oben (zwischen &lt;code&gt;tot_rel_siz&lt;/code&gt; und &lt;code&gt;tab_and_idx_siz&lt;/code&gt;) lässt sich durch die &lt;code&gt;fsm&lt;/code&gt; und die &lt;code&gt;vm&lt;/code&gt;-Dateien erklären.&lt;/p&gt;
&lt;p&gt;Das PostgreSQL Equivalent zum MariaDB/MySQL &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; ist der Befehl &lt;code&gt;VACUUM FULL&lt;/code&gt;:&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;Das erste, was man feststellt, ist, dass PostgreSQL ganz viele Dateien anlangt und die &amp;ldquo;free space map&amp;rdquo; Datei ist verschwunden. Die Tabellen-Segmente sind dafür, im Unterschied zu MariaDB/MySQL gleich gross geblieben. Zudem sieht man, dass die alte Tabelle &amp;ldquo;verschwunden&amp;rdquo; (40965, 40970) und eine neue entstanden (40972 und 40975) ist. Der &lt;code&gt;VACUUM FULL&lt;/code&gt; Befehl bei PostgreSQL erstellt also ebenfalls, wie bei MariaDB/MySQL eine Kopie der Daten.&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;Um zu verstehen, welche Dateien/Objekte denn sonst noch so alles angelangt wurden, hilft folgendes Query:&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="versuch-1-ausnullen-1"&gt;Versuch 1: AusNULLen&lt;/h3&gt;
&lt;p&gt;Dann &lt;code&gt;NULL&lt;/code&gt;en wir die Spalten bei PostgreSQL ebenfalls aus. Die Sicht aufs Datei-System sparen wir uns ab hier, da PostgreSQL die Dateigrössen ja genau zu kennen scheint, wie wir oben gesehen haben:&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;Hier sehen wir, dass die Tabellen-Segmente massiv anwachsen (+37%), was in der PostgreSQL Terminologie als &amp;ldquo;bloat&amp;rdquo; bezeichnet wird. Die MVCC Implementierung von PostgreSQL speichert sowohl die alte auch nie neue Version der Row &amp;ldquo;in-place&amp;rdquo; also direkt in der Tabelle im Gegensatz zu MariaDB/MySQL welche die alte Version im UNDO Space und die neue Row &amp;ldquo;in-place&amp;rdquo; speichert. Auch die Index-Datei vergrössert sich signifikant (+100%). Dazu müssen wir noch weiter forschen, warum das so ist. Zudem wird wieder eine &amp;ldquo;free space map&amp;rdquo; erstellt (Differenz zwischen &lt;code&gt;tot_rel_siz&lt;/code&gt; und &lt;code&gt;tab_and_idx_siz&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Ein anschliessendes &lt;code&gt;VACUUM FULL&lt;/code&gt; verkleinert die Tabelle (auf 28%) und den Index (auf 50%) wieder im Bezug auf die vorherige Grösse:&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;und auch in Bezug zur ursprünglichen Grösse wird die Tabelle (auf 38%) und der Index (auf 100%) wieder kleiner. Warum der Index gleich gross geblieben ist und nur die Tabelle geschrumpft ist, muss noch recherchiert werden&amp;hellip;&lt;/p&gt;
&lt;h3 id="versuch-2-löschen-der-spalten-1"&gt;Versuch 2: Löschen der Spalten&lt;/h3&gt;
&lt;p&gt;Anschliessend werden die Spalten noch mit &lt;code&gt;DROP COLUMN&lt;/code&gt; gelöscht.&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;Da die Rückmeldung sofort kam, kann davon ausgegangen werden, dass diese Operation ebenfalls instant erfolgt. Leider habe ich auf die Schnelle nichts in der PostgreSQL Dokumentation dazu gefunden.&lt;/p&gt;
&lt;p&gt;An der Grösse hat sich nichts signifikant geändert, was bei einer Instant-Operation ja eigentlich auch zu erwarten ist. Dass sich jedoch nach dem &lt;code&gt;VACUUM FULL&lt;/code&gt; Befehl die Grösse nicht mehr geändert hat, hat doch ein wenig erstaunt:&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="bemerkungen"&gt;Bemerkungen&lt;/h2&gt;
&lt;p&gt;Locking bei PostgreSQL funktioniert wie folgt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VACUUM&lt;/code&gt; Nebenläufige DML Befehle sind möglich ähnlich wie beim MariaDB/MySQL &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl. Das Resultat ist aber nicht ganz das Selbe.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VACUUM FULL&lt;/code&gt; verursacht einen &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; Lock. Ähnlich wie bei MariaDB/MySQL 5.5 und älter beim &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; Befehl. DML und &lt;code&gt;SELECT&lt;/code&gt; Befehle sind NICHT zulässig.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quellen"&gt;Quellen&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="zusatzversuche"&gt;Zusatzversuche&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Statt &lt;code&gt;0.0&lt;/code&gt; wurde &lt;code&gt;NULL&lt;/code&gt; in die Spalten &lt;code&gt;d0&lt;/code&gt; - &lt;code&gt;d9&lt;/code&gt; abgefüllt. Die Tabelle blieb klein (&lt;code&gt;tot_rel_siz_prtty = 1068 MB&lt;/code&gt;). Es lohnt sich also auch bei PostgreSQL &lt;code&gt;NULL&lt;/code&gt; statt Dummy-Werte zu speichern.&lt;/li&gt;
&lt;li&gt;Die Spalten &lt;code&gt;d0&lt;/code&gt; - &lt;code&gt;d9&lt;/code&gt; wurden mit &lt;code&gt;DOUBLE PRECISION NOT NULL&lt;/code&gt; angelegt und die Werte &lt;code&gt;0.0&lt;/code&gt; abgefüllt. Keinen Effekt: Die Tabelle blieb gross (&lt;code&gt;tot_rel_siz_prtty = 2232 MB&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Jemand löscht meine Shared Memory Segmente!</title><link>https://www.fromdual.com/de/blog/geloeschte-postgresql-shared-memory-segmente/</link><pubDate>Sat, 07 Feb 2026 19:58:00 +0100</pubDate><guid>https://www.fromdual.com/de/blog/geloeschte-postgresql-shared-memory-segmente/</guid><description>&lt;p&gt;Wenn wir mit PostgreSQL unter unserem &lt;a href="https://www.fromdual.com/myenv/"&gt;myEnv&lt;/a&gt; arbeiten, kriegen wir regelmässig Shared Memory Segment Fehler. Beispiel:&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;oder wir sehen ähnliche Meldungen im 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;Weil ich ein MariaDB/MySQL Admin bin, kenne ich mich nicht so gut mit Shared Memory Problemen aus (MariaDB/MySQL arbeitet nicht mit Shared Memory). Zum Glück hat uns eine Suche im Internet auf eine Fährte geführt (&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;Quelle&lt;/a&gt;). Dort wird vermerkt:&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;Zuerst musste ich mal herausfinden was ein System User unter Linux überhaupt ist. Eine Antwort habe ich hier gefunden: &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;Der LSB Standard meint dazu: &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;Auf meinem Ubuntu System sieht das wie folgt aus:&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;Für den PostgreSQL User würde das ja auch stimmen:&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;Da aber die betroffene PostgreSQL Instanz unter unserm &lt;a href="https://www.fromdual.com/myenv/"&gt;myEnv&lt;/a&gt; läuft, ist das was anderes:&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;Jetzt haben wir also zwei Möglickeiten:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Wir ändern &lt;code&gt;SYS_UID_MAX&lt;/code&gt; und &lt;code&gt;SYS_GID_MAX&lt;/code&gt; auf 1001 (einfache Variante).&lt;/li&gt;
&lt;li&gt;Oder wir ändern die &lt;code&gt;UID&lt;/code&gt; und die &lt;code&gt;GID&lt;/code&gt; unseres Users &lt;code&gt;dba&lt;/code&gt; auf unter 1000.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="einfache-variante-ändern-von-sys_uid_max-und-sys_gid_max-auf-1001"&gt;Einfache Variante: Ändern von &lt;code&gt;SYS_UID_MAX&lt;/code&gt; und &lt;code&gt;SYS_GID_MAX&lt;/code&gt; auf 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;Zur Sicherheit wurde die Maschine neu gebootet. Das ha aber nicht geholfen: Nach kurzer Zeit treten wieder die selber Fehler auf.&lt;/p&gt;
&lt;h2 id="kompliziertere-variante-ändern-der-uid-von-1001-auf-990"&gt;Kompliziertere Variante: Ändern der &lt;code&gt;UID&lt;/code&gt; von 1001 auf 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;Dazu müssen alle Prozesse dieses Users gestoppt sein!&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;Damit scheint das Problem gelöst zu sein!&lt;/p&gt;
&lt;h2 id="zusatzinformationen"&gt;Zusatzinformationen&lt;/h2&gt;
&lt;p&gt;Eine weiter oben erwähnte Quelle hat noch empfohlen, beim &lt;code&gt;systemd-logind&lt;/code&gt; folgende Parameter zu setzen:&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;Diese Massnahme ist jetzt aber nicht mehr notwendig, das es auch ohne diese Änderung funktioniert.&lt;/p&gt;</description></item><item><title>CSV Dateien in die Datenbank laden</title><link>https://www.fromdual.com/de/blog/csv-dateien-in-die-datenbank-laden/</link><pubDate>Fri, 06 Feb 2026 17:12:00 +0100</pubDate><guid>https://www.fromdual.com/de/blog/csv-dateien-in-die-datenbank-laden/</guid><description>&lt;p&gt;Kürzlich wollte ich für eine persönliche kleine Spielerei die Wohnorte der Vereinsmitlieder meines Vereins auf einer Karte darstellen (&lt;a href="https://www.shinguz.ch/computer/gis/igoc-mitglieder/" target="_blank"&gt;IGOC Mitglieder&lt;/a&gt;). Die Adressen der Vereinsmitglieder waren mir bekannt. Nicht aber die Koordinaten der Wohnorte.&lt;/p&gt;
&lt;p&gt;Also machte ich mich auf die Suche nach den Koordinaten und wurde beim Bundesamt für Landestopografie (&lt;a href="https://www.swisstopo.admin.ch/de" target="_blank"&gt;swisstopo&lt;/a&gt;) fündig.&lt;/p&gt;
&lt;p&gt;Die Daten werden dort als CSV Datei zur Verfügung gestellt. Details hier: &lt;a href="https://www.shinguz.ch/computer/gis/schweizer-ortschafts-koordinaten/" target="_blank"&gt;Schweizer Ortschafts-Koordinaten&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Wie lädt man jetzt diese Daten in eine Datenbank?&lt;/p&gt;
&lt;h2 id="laden-der-daten-mit-mariadbmysql"&gt;Laden der Daten mit MariaDB/MySQL&lt;/h2&gt;
&lt;p&gt;MariaDB und MySQL haben dafür den Befehl &lt;code&gt;LOAD DATA INFILE&lt;/code&gt;:&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;Anschliessend kann man die Daten in der Datenbank abfragen:&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;Bzw. etwas präziser:&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;Das ausmisten der Duplikate überlasse ich der geneigten Lesering oder dem geneigten Leser&amp;hellip; :-)&lt;/p&gt;
&lt;p&gt;Soweit so gut, jetzt zu den Feinheiten:&lt;/p&gt;
&lt;h3 id="unterschiede-zwischen-mariadb-und-mysql"&gt;Unterschiede zwischen MariaDB und MySQL&lt;/h3&gt;
&lt;p&gt;Das oben beschriebene Verfahren funktioniert einwandfrei bei MariaDB 11.4 und 11.8. Bei MySQL 8.4 gibt es kleine Unterschiede:&lt;/p&gt;
&lt;p&gt;Die erste Fehlermeldung, die das Laden verhindert ist diese:&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;Sie kann relativ einfach umgangen werden mit dem Befehl:&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;Beim nächsten Versuch wird man wie folgt scheitern:&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;Dieses Problem kann gelöst werden, indem man den MySQL Client wie folgt startet:&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="quellen"&gt;Quellen&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="laden-der-daten-mit-postgresql"&gt;Laden der Daten mit PostgreSQL&lt;/h2&gt;
&lt;p&gt;PostgreSQL hat hierfür den Befehl &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;Auch hier erhalten wir das Resultat wie erwartet in der für PostgreSQL üblichen 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="quellen-1"&gt;Quellen&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="kleine-unterschiede-zwischen-mariadbmysql-und-postgresql"&gt;Kleine Unterschiede zwischen MariaDB/MySQL und PostgreSQL&lt;/h2&gt;
&lt;p&gt;Grundsätzlich lautet der Ladebefehl in den beiden Datenbankwelten gänzlich unterschiedlich.&lt;/p&gt;
&lt;p&gt;Bei MariaDB und PostgreSQL laufen die Befehle &amp;lsquo;out-of-the-box&amp;rsquo;. MySQL hat hier noch zwei zusätzliche Sicherheitshürden eingebaut.&lt;/p&gt;
&lt;p&gt;PostgreSQL kennt keine &lt;code&gt;UNSIGNED&lt;/code&gt; Integer Datentypen, daher muss der nächstgrössere Datentyp (&lt;code&gt;INT&lt;/code&gt;) verwendet werden, was ein klein wenig weniger platzsparender ist als bei MariaDB/MySQL.&lt;/p&gt;
&lt;h2 id="bemerkungen"&gt;Bemerkungen&lt;/h2&gt;
&lt;p&gt;Als wir den selben Test vor ein paar wenigen Tagen gemacht hatten, gab es noch eine Fehler beim Laden. Es scheint also, dass sich die Datenquelle ebenfalls minimal geändert hat&amp;hellip;&lt;/p&gt;
&lt;p&gt;Ich habe auf die Schnelle nicht herausgefunden, ob es zu diesen Lade-Befehlen einen SQL-Standard gibt und wenn ja, ob MariaDB/MySQL oder PostgreSQL hier Standard konform sind.&lt;/p&gt;
&lt;p&gt;Und natürlich gibt es noch andere Möglichkeiten, wie man seine CSV Daten in die Datenbank rein kriegt&amp;hellip;&lt;/p&gt;
&lt;p&gt;Die Tools &lt;code&gt;mariadb-import&lt;/code&gt;/&lt;code&gt;mysqlimport&lt;/code&gt; verwendet man, wenn man das von der Kommandozeile aus machen möchte. Die CSV Storage Engine kann ebenfalls dazu missbraucht werden (Details siehe &lt;a href="https://www.fromdual.com/blog/csv-storage-engine/"&gt;hier&lt;/a&gt;). Eine offiziell unterstütze Variante ist ist die MariaDB CONNECT Storage Engine mit dem CSV Type (siehe &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;hier&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;So wie es aussieht, wird die CONNECT Storage Engine von MariaDB leider nicht mehr weiter supportet?!? Und auch das Tool &lt;code&gt;mydumper&lt;/code&gt;/&lt;code&gt;myloader&lt;/code&gt; scheint mit CSV Dateien umgehen zu können.&lt;/p&gt;
&lt;p&gt;Und natürlich kann das ganze auch applikatorisch gelöst werden&amp;hellip;&lt;/p&gt;
&lt;p&gt;Bei PostgreSQL gibt es folgende Möglichkeiten:&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;dann von der Shell aus:&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;Und die Variante über den Foreign Data Wrapper (FWD). Habe ich aber nicht ausprobiert:&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="nachtrag"&gt;Nachtrag&lt;/h2&gt;
&lt;p&gt;Der MariaDB/MySQL Datentyp &lt;code&gt;DOUBLE&lt;/code&gt; heist in ProsgreSQL &lt;code&gt;DOUBLE PRECISION&lt;/code&gt;.&lt;/p&gt;</description></item></channel></rss>