<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eugene</title>
    <description>The latest articles on DEV Community by Eugene (@ineron).</description>
    <link>https://dev.to/ineron</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3871400%2F55375df1-59e1-4269-ab25-6d6c01c50a4e.jpg</url>
      <title>DEV Community: Eugene</title>
      <link>https://dev.to/ineron</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ineron"/>
    <language>en</language>
    <item>
      <title>pg_ilib: Compact Typed Binary Serialization for PostgreSQL</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Sun, 26 Apr 2026 16:14:29 +0000</pubDate>
      <link>https://dev.to/ineron/pgilib-compact-typed-binary-serialization-for-postgresql-fh2</link>
      <guid>https://dev.to/ineron/pgilib-compact-typed-binary-serialization-for-postgresql-fh2</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's &lt;code&gt;bytea&lt;/code&gt; type is powerful for storing raw binary data, but it carries no type information. Once you store a value as bytes, you need out-of-band metadata to know whether those bytes represent a number, a UUID, a timestamp, or a JSON object.&lt;/p&gt;

&lt;p&gt;This gets painful when you're building dynamic schemas — EAV tables, schemaless document stores, or audit logs — where a single column holds values of different types. You end up carrying a separate &lt;code&gt;type&lt;/code&gt; column everywhere, writing &lt;code&gt;CASE&lt;/code&gt; expressions to decode it, and hoping they stay in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pg_ilib&lt;/strong&gt; solves this with a simple idea: prefix every serialized value with a 2-byte typed header.&lt;/p&gt;




&lt;h2&gt;
  
  
  The format
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Byte 0: [ op_id (4 bits) | params_hi (4 bits) ]
Byte 1: [ params_lo (8 bits) ]
Bytes 2…N: payload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;op_id&lt;/code&gt; identifies the type. The &lt;code&gt;params&lt;/code&gt; field carries type-specific metadata: decimal scale for numerics, timezone offset in minutes for timestamps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;op_id&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;params&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;numeric / bigint&lt;/td&gt;
&lt;td&gt;decimal scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x03&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp / date&lt;/td&gt;
&lt;td&gt;tz offset (signed minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x08&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;uuid&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x0E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;jsonb&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x0F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hex bytes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# requires libgmp-dev (Debian/Ubuntu) or gmp-devel (RHEL/CentOS)&lt;/span&gt;
make &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_ilib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with PostgreSQL 11+ on any platform where &lt;code&gt;pg_config&lt;/code&gt; is on &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic usage
&lt;/h2&gt;

&lt;p&gt;Each type has a symmetric pair of functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- bigint&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_bigint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bigint_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123456789&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- 123456789&lt;/span&gt;

&lt;span class="c1"&gt;-- text&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hello world'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- hello world&lt;/span&gt;

&lt;span class="c1"&gt;-- numeric with scale&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numeric_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;14159&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- 3.14159&lt;/span&gt;

&lt;span class="c1"&gt;-- uuid&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-52ae-b223-4d6a-5c26fc34994c'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- ac861c64-52ae-b223-4d6a-5c26fc34994c&lt;/span&gt;

&lt;span class="c1"&gt;-- jsonb&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{"name":"Alice","age":30}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- {"age": 30, "name": "Alice"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implicit CASTs are registered for most types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bytea&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;200000000000002a'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bytea&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The killer feature: value_to_jsonb
&lt;/h2&gt;

&lt;p&gt;Because the type is embedded in the header, a single function can decode anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bigint_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;        &lt;span class="c1"&gt;-- 42&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hello'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;       &lt;span class="c1"&gt;-- "hello"&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;         &lt;span class="c1"&gt;-- true&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numeric_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;-- 3.14&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-52ae-b223-4d6a-5c26fc34994c'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- "ac861c64-52ae-b223-4d6a-5c26fc34994c"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes EAV-style tables genuinely usable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;entity_attributes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;key&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;     &lt;span class="n"&gt;bytea&lt;/span&gt;   &lt;span class="c1"&gt;-- holds any type, self-describing&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Store mixed types in one column&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;entity_attributes&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;bigint_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'score'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;numeric_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;bool_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'joined'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;timestamp_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1700000000&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tag'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;str_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'premium'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Decode everything to JSON in one query, no type column needed&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;entity_attributes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ac861c64-...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;    &lt;span class="k"&gt;key&lt;/span&gt;    &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;jsonb&lt;/span&gt;
&lt;span class="err"&gt;-----------+----------------&lt;/span&gt;
 &lt;span class="k"&gt;age&lt;/span&gt;       &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;30&lt;/span&gt;
 &lt;span class="k"&gt;score&lt;/span&gt;     &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;9.75&lt;/span&gt;
 &lt;span class="k"&gt;active&lt;/span&gt;    &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
 &lt;span class="k"&gt;joined&lt;/span&gt;    &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"2023-11-14T22:13:20Z"&lt;/span&gt;
 &lt;span class="k"&gt;tag&lt;/span&gt;       &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"premium"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Timestamps and timezones
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;params&lt;/code&gt; field stores the timezone offset in signed minutes, so the original offset survives the round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Store UTC&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;timestamp_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1766323245&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Decode as plain timestamp (offset ignored)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1766323245&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- 2026-01-21 00:00:45&lt;/span&gt;

&lt;span class="c1"&gt;-- Decode as timestamptz with UTC+2 offset baked in&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_timestamptz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp_to_bytea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1766323245&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- 2026-01-21 02:00:45+02&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Built-in corruption detection
&lt;/h2&gt;

&lt;p&gt;Every decoder calls &lt;code&gt;pg_ilib_check_header()&lt;/code&gt; before touching payload bytes. Impossible &lt;code&gt;(op_id, params, payload_size)&lt;/code&gt; combinations raise &lt;code&gt;ERRCODE_DATA_CORRUPTED&lt;/code&gt; instead of crashing the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Scale 4095 is impossible for a 1-byte payload (max = 3)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;2FFF2a'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: pg_ilib bytea_to_numeric: numeric scale 4095 is impossible&lt;/span&gt;
&lt;span class="c1"&gt;--        for 1 payload byte(s) (max scale = 3)&lt;/span&gt;

&lt;span class="c1"&gt;-- Timezone offset out of IANA range [-840, 840] minutes&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bytea_to_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;4FFF12345678'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: pg_ilib bytea_to_timestamp[tz]: timezone offset 4095 min&lt;/span&gt;
&lt;span class="c1"&gt;--        is out of valid range [-840, 840]&lt;/span&gt;

&lt;span class="c1"&gt;-- Unknown op_id&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value_to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;&lt;span class="s1"&gt;9000ff'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: pg_ilib value_to_jsonb: unknown op_id 0x09&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Repository structure
&lt;/h2&gt;

&lt;p&gt;The repo contains three independent extensions that share a single directory and build system:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Extension&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_ilib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Typed bytea serialization (this article)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;make&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Build all three at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make all-ext &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;make install-ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing without installing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make testdb        &lt;span class="c"&gt;# create pg_ilib_test database (once)&lt;/span&gt;
make quicktest     &lt;span class="c"&gt;# compile and run test/quick_test.sql against a /tmp copy&lt;/span&gt;

&lt;span class="c"&gt;# Override host/user if needed&lt;/span&gt;
make quicktest &lt;span class="nv"&gt;PG_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.1 &lt;span class="nv"&gt;PG_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/ineron/pg_ilib.git" rel="noopener noreferrer"&gt;github.com/ineron/pg_ilib.git&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache 2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies&lt;/strong&gt;: PostgreSQL 11+, libgmp&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why we built this
&lt;/h2&gt;

&lt;p&gt;pg_ilib started as an internal component of &lt;a href="https://ledgyx.com" rel="noopener noreferrer"&gt;LedgyX&lt;/a&gt; — a low-code API platform that generates FastAPI applications directly from PostgreSQL schemas.&lt;/p&gt;

&lt;p&gt;The core challenge in LedgyX is that every table, column, and type is defined dynamically at runtime. We needed a way to store values of any SQL type in a single &lt;code&gt;bytea&lt;/code&gt; column and decode them correctly later — without carrying a separate &lt;code&gt;type&lt;/code&gt; column everywhere. pg_ilib is the solution we built and have been running in production.&lt;/p&gt;

&lt;p&gt;We decided to open-source it because the problem is general enough to be useful beyond LedgyX. If you're building EAV tables, audit logs, dynamic schemas, or any system where a column holds mixed types — this is for you.&lt;/p&gt;

&lt;p&gt;Feedback and PRs welcome. If you're using this in production or have ideas for new &lt;code&gt;op_id&lt;/code&gt; types, open an issue!&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>c</category>
      <category>opensource</category>
    </item>
    <item>
      <title>JSON vs JSONB in PostgreSQL: I tested 1M rows to find out</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Sun, 12 Apr 2026 07:52:39 +0000</pubDate>
      <link>https://dev.to/ineron/json-vs-jsonb-in-postgresql-i-tested-1m-rows-to-find-ou-3cdj</link>
      <guid>https://dev.to/ineron/json-vs-jsonb-in-postgresql-i-tested-1m-rows-to-find-ou-3cdj</guid>
      <description>&lt;p&gt;Recently I tried to resolve a recurring question in our team:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is JSON or JSONB actually faster in PostgreSQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I couldn’t find a clear answer that matched real-world usage, so I ran my own benchmark.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;I loaded &lt;strong&gt;1 million records&lt;/strong&gt; with identical data into both JSON and JSONB columns and tested common operations.&lt;/p&gt;

&lt;p&gt;Hardware:&lt;br&gt;
Dell PowerEdge R450&lt;br&gt;
2x Intel Xeon Silver 4310 (24/48 cores @ 2.1GHz)&lt;/p&gt;

&lt;p&gt;I intentionally used mid-range hardware so the differences would be easier to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;INSERT performance&lt;/li&gt;
&lt;li&gt;Key-based search (&lt;code&gt;data-&amp;gt;&amp;gt;'field' = 'value'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Nested updates&lt;/li&gt;
&lt;li&gt;Complex multi-condition queries&lt;/li&gt;
&lt;li&gt;Array access (&lt;code&gt;data-&amp;gt;'items'[0]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Key existence (&lt;code&gt;data ? 'key'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Path queries (&lt;code&gt;data #&amp;gt; '{user,profile,name}'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Aggregations&lt;/li&gt;
&lt;li&gt;Storage size&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Insert speed
&lt;/h3&gt;

&lt;p&gt;JSON was faster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: 8.6s&lt;/li&gt;
&lt;li&gt;JSONB: 11.3s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;~31% difference, which makes sense - JSON is stored as plain text.&lt;/p&gt;




&lt;h3&gt;
  
  
  Query performance
&lt;/h3&gt;

&lt;p&gt;This is where things got interesting.&lt;/p&gt;

&lt;p&gt;JSONB was significantly faster across all read operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple key extraction → &lt;strong&gt;6.2x faster&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Nested field access → &lt;strong&gt;7.6x faster&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Array operations → &lt;strong&gt;7.3x faster&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Complex conditions → &lt;strong&gt;9.1x faster&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On average: &lt;strong&gt;~7x faster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GIN indexes + binary format make a huge difference here.&lt;/p&gt;




&lt;h3&gt;
  
  
  Updates
&lt;/h3&gt;

&lt;p&gt;For partial updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSONB was &lt;strong&gt;~71% faster&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;JSON: ~1200 MB&lt;/li&gt;
&lt;li&gt;JSONB: ~888 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JSONB used &lt;strong&gt;~26% less space&lt;/strong&gt;, mainly due to key deduplication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key takeaway
&lt;/h2&gt;

&lt;p&gt;If your workload is read-heavy (which is most backend systems):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSONB pays for itself very quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with slower inserts, the performance gain in queries dominates after relatively few operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  One important detail
&lt;/h2&gt;

&lt;p&gt;Operators like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;only work with JSONB.&lt;/p&gt;

&lt;p&gt;Without them, efficient querying becomes very limited.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;I published the full benchmark here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ineron/postgresql-json-jsonb-benchmark" rel="noopener noreferrer"&gt;https://github.com/ineron/postgresql-json-jsonb-benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL scripts&lt;/li&gt;
&lt;li&gt;Python automation&lt;/li&gt;
&lt;li&gt;Docker setup&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;I expected JSONB to be faster, but not by this margin.&lt;/p&gt;

&lt;p&gt;Curious if anyone has seen similar results in production?&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>database</category>
      <category>performance</category>
      <category>json</category>
    </item>
  </channel>
</rss>
