<?xml version="1.0" encoding="UTF-8" ?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" version="2.0"><channel><title>Craig Kerstiens | CrunchyData Blog</title>
<atom:link href="https://www.crunchydata.com/blog/author/craig-kerstiens/rss.xml" rel="self" type="application/rss+xml" />
<link>https://www.crunchydata.com/blog/author/craig-kerstiens</link>
<image><url>https://www.crunchydata.com/build/_assets/craig-kerstiens.png-JNJT356V.webp</url>
<title>Craig Kerstiens | CrunchyData Blog</title>
<link>https://www.crunchydata.com/blog/author/craig-kerstiens</link>
<width>1512</width>
<height>1378</height></image>
<description>PostgreSQL experts from Crunchy Data share advice, performance tips, and guides on successfully running PostgreSQL and Kubernetes solutions</description>
<language>en-us</language>
<pubDate>Mon, 11 Aug 2025 13:00:00 EDT</pubDate>
<dc:date>2025-08-11T17:00:00.000Z</dc:date>
<dc:language>en-us</dc:language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<item><title><![CDATA[ Indexing JSONB in Postgres ]]></title>
<link>https://www.crunchydata.com/blog/indexing-jsonb-in-postgres</link>
<description><![CDATA[ Level up with JSONB by adding GIN indexes. Also review when not to use a GIN index and how to leverage expression indexes. ]]></description>
<content:encoded><![CDATA[ <p>Postgres is amazing for many reasons. One area that doesn't get near enough attention is datatypes. Postgres has a rich set of datatypes and one important one for devs to be especially excited about is JSONB.<p>JSONB which is structured and indexable. In JSON, the B stands for binary (or as we like to think of it B is for better), which means data is pre-parsed as it is stored. How do you get the most out of JSONB from a retrieval perspective? Enter Postgres' rich indexing support.<h2 id=postgres-index-types><a href=#postgres-index-types>Postgres index types</a></h2><p>Most databases have a standard single index type: B-Tree. B-Tree is a balanced tree structure, and the common type of index you learn about from a CS degree perspective. When you do a standard <code>CREATE INDEX</code> a B-Tree index is what is created. This works for standard <code>WHERE</code> clauses that target that value.<p>But Postgres has other index types including:<ul><li>GIN - Generalized Inverted Index<li>GiST - Generated Search Tree<li>Sp-GiST - Space-Partitioned Generalized Search Tree<li>BRIN - Block Range Index</ul><p>So the go-to Postgres database, the B-Tree, isn’t suited well for JSON documents, or at least not how you may think, because of the nature of nested structures. So how do you index your JSONB to more efficiently query it? Enter GIN indexes.<h2 id=gin-indexes-for-jsonb><a href=#gin-indexes-for-jsonb>GIN indexes for JSONB</a></h2><p>Instead of indexing the entire JSONB document as a unit, a GIN index breaks it apart and indexes the keys and values inside. Think of it as creating a giant lookup table under the hood. If you have a row like this:<pre><code class=language-sql>{
  "status": "active",
  "plan": "pro"
}
</code></pre><p>A GIN index on this row will store entries like:<pre><code>status => active

plan => pro
</code></pre><p>This makes GIN ideal for answering questions like:<pre><code class=language-sql>SELECT *
FROM my_table
WHERE data @> '{"status": "active"}';
</code></pre><p>The <code>@></code> operator (JSONB containment) is GIN indexable, and GIN can quickly find documents where that key value pair exists.<p>Creating the index is straightforward:<pre><code class=language-sql>CREATE INDEX idx_data_gin
ON my_table
USING gin (data);
</code></pre><p>You can also be more specific with a partial or expression index if you only need to index a subset of keys:<pre><code class=language-sql>CREATE INDEX idx_status_gin
ON my_table
USING gin ((data->'status'));
</code></pre><h3 id=what-queries-use-the-gin-index><a href=#what-queries-use-the-gin-index>What queries use the GIN index?</a></h3><p>The key here is: not all JSONB queries will benefit from a GIN index. Queries that can use GIN include:<ul><li>Containment: <code>data @> '{"plan": "pro"}'</code><li>Key existence: <code>data ? 'status'</code><li>Any key match: <code>data ?| array['plan', 'tier']</code><li>All keys match: <code>data ?&#38 array['plan', 'status']</code></ul><p>These are operator-based queries that map well to the inverted index structure.<h3 id=what-jsonb-queries-dont-use-gin><a href=#what-jsonb-queries-dont-use-gin>What JSONB queries don’t use GIN?</a></h3><p>Here’s where sometimes you can get caught off guard. You added a GIN index, set up the query in your app, and have the worst of both worlds: an index being maintained but slow queries because they can't use the index.<p>GIN indexes won’t help with:<ul><li>Path-based navigation: <code>data->'user'->>'email' = 'craig@example.com'</code><li>Comparisons within the JSONB: <code>(data->>'age')::int > 30</code><li>Regex or pattern matches inside values: <code>data->>'name' ILIKE 'craig%'</code></ul><h3 id=maintenance-with-gin-and-jsonb><a href=#maintenance-with-gin-and-jsonb>Maintenance with GIN and JSONB</a></h3><p>While GIN indexes are powerful, they have a larger write overhead than standard B-tree indexes. This overhead becomes especially apparent if you're frequently updating large JSONB columns. Frequent large updates can lead to index bloat, where the index contains too many references to dead rows and becomes inefficient.<p>Due to the potential for bloat, actively monitoring the health of your GIN indexes is key. You can manage this by periodically running the <code>REINDEX CONCURRENTLY</code> command to rebuild the index and reclaim wasted space. We also recommend using internal tools like the <code>pgstattuple</code> extension to check the index's status and identify bloat before it becomes a significant issue.<h2 id=expression-indexes-for-jsonb><a href=#expression-indexes-for-jsonb>Expression indexes for JSONB</a></h2><p>For some cases, creating a typical B-tree expression index can help with JSON that doesn’t fit the GIN use cases. Creating an expression index involves defining an index not just a part of the array, but on the result of an operation performed on that column. For instance:<pre><code class=language-sql>CREATE INDEX idx_orders_total ON orders (((details->>'order_total')::numeric))
</code></pre><p>This builds an index on the orders table. It works by first accessing the details jsonb column, extracting the value associated with the order_total key as text using the ->> operator, and then casting that text value to a numeric type. Postgres then builds a standard B-tree index on these resulting numeric values. This can be efficient for range scans and sorting inside these JSONB rows.<p>Keep in mind that a requirement for using an expression index is that the WHERE clause of your query must exactly match the expression used to define the index. For the index created above, a query like <code>WHERE (details->>'order_total')::numeric > 100</code> would use the index, but a slightly different query, such as <code>WHERE (details->>'order_total')::float > 100</code>, would not.<p>Strict matching means expression indexes are great for optimizing well-defined, static queries that are embedded in your application's code. But not queries that often change.<h2 id=best-practices-for-jsonb-indexing><a href=#best-practices-for-jsonb-indexing>Best practices for JSONB indexing</a></h2><ul><li>Use GIN for containment-style lookups, especially if you don't know the full schema ahead of time.<li>Don’t GIN the whole JSONB column if you only ever query specific keys—use expression indexes or partial indexes instead.<li>Combining GIN with traditional B-tree indexes (ie expression indexes) on structured columns is the key to keeping performance predictable.</ul><p>JSONB is a powerful tool in the PostgreSQL toolbox, but to unlock its performance potential, you have to understand the indexing story. GIN indexes are your best friend when you need to query inside documents but they're not a silver bullet. Knowing when and how to use them is key.<p>If you're working with JSONB-heavy workloads in production, this is one of those "measure twice, index once" situations. If you’re on Crunchy Bridge or managing a large Postgres fleet, having observability into which indexes are getting used (and which aren’t) is just as important.<p><img alt="postgres index types"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/9caa8f10-c587-4fe3-e714-6c26c954e100/public> ]]></content:encoded>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">1687685494c0ad96e74ea1bc8a5f954b5bbeeb55afab4322bae236f07396f1b4</guid>
<pubDate>Mon, 11 Aug 2025 13:00:00 EDT</pubDate>
<dc:date>2025-08-11T17:00:00.000Z</dc:date>
<atom:updated>2025-08-11T17:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Citus: The Misunderstood Postgres Extension ]]></title>
<link>https://www.crunchydata.com/blog/citus-the-misunderstood-postgres-extension</link>
<description><![CDATA[ What applications and use cases make the most sense for Citus. ]]></description>
<content:encoded><![CDATA[ <p>Citus is in a small class of the most advanced Postgres extensions that exist. While there are many Postgres extensions out there, few have as many hooks into Postgres or change the storage and query behavior in such a dramatic way. Most that come to Citus have very wrong assumptions. Citus turns Postgres into a sharded, distributed, horizontally scalable database (that's a mouthful), but it does so for very specific purposes.<p>Citus, in general, is fit for these type of applications and only these type:<ul><li><strong>Sharding a multitenant application</strong>: a SaaS/B2B style app, where data is never joined between customers<li><strong>Low user facing, high data volume analytics</strong>: specifically where the dashboards are hand-curated with minimal levers-and-knobs for the user to change (i.e. customer cannot generate unknown queries)</ul><p>Mistaken use cases for Citus that are not a great fit:<ul><li>Lack of rigid control over queries sent to database<li>Geographic residency goals or requirements; Citus is distributed for scale, not distributed for edge.</ul><p>Let's look closer at each of the two use cases that Citus is a good fit for.<h2 id=multitenantsaas-applications><a href=#multitenantsaas-applications>Multitenant/SaaS applications</a></h2><p>Multitenant or SaaS applications typically follow a pattern: 1) tenant data is siloed and does not intermingle with any other tenant's data, and 2) a "tenant" is a larger entity like a "team" or "organization".<p>An example of this could be Salesforce. Within Salesforce you have the notion of an organization, and the organization has accounts, customers, and opportunities within them. When you create a Salesforce account, all of your customers and opportunities are solely yours — data is not shared with other Salesforce organizations.<p>For these types of applications, Citus distributes the data for each tenant into a shard. Citus handles the splitting of data by creating placement groups that know they are grouped together, and placing the data within shards on specific nodes. A physical node may contain multiple shards. Let me restate that to understand Citus at a high-level:<ul><li><strong>physical node</strong>: the physical container that holds shards<li><strong>shard</strong>: a logical container for data; resides on a physical node, and can be moved between physical nodes<li><strong>placement group</strong>: uses a hash-based algorithm to assign a tenant id to a shard</ul><p>Regarding shards, while possible to split a large shard, it is easier to start with the proper configuration. Getting scaling right in the beginning makes it easier later because moving full shards is easier than splitting them once they already exist, though that is possible.<p>In a very basic Citus cluster, you might have something that looks like:<p><img alt="simple citus"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/749ce551-e328-4a8b-0803-d87c44be3300/public><p>Within Citus, multitenant/SaaS applications can work well because sharding is at the core of what Citus does. In the case of a tenant application, the tenant id becomes the shard key. When you shard all the tables on the same key, Citus places each table on the same physical node. Then, queries with joins are executed local to the instance and faster.<p>Alternatively, poor shard key planning would require joining data across the network. This shuffling of data is detrimental to performance within databases – especially distributed ones. For multitenant/SaaS, leveraging Citus requires the tenant id a column on every table.<p>While in a more simple design, accounts, customers, and opportunities tables may have only a primary key and a foreign key reference to their parent relationship. In Citus, we need to turn those into composite primary keys that leverage both tenant id and the foreign key. Extending the above diagram, if we were to now create accounts, customers, and opportunities tables as sharded tables with Citus, we'd have something that roughly results to the following:<p><img alt="multitenant citus"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/2d0df3d6-dda3-4602-5827-26958e2dac00/public><p>To speed query performance, include a where condition for the tenant id (below <code>org_id</code>) in all queries as well — this ensures that Citus knows how to push down the join to that single node. A query for open opportunities for a specific tenant might look something like:<pre><code class=language-sql>SELECT customer.email, customer.first_name, customer.last_name, opportunity.amount, opportunity.notes
FROM opportunity,
     customer,
     account
WHERE customer.org_id = account.org_id
  AND opportunity.org_id = account.org_id
  AND opportunity.account_id = customer.account_id
  AND account.org_id = 4;
</code></pre><p>Citus would then quietly re-write this query to target the appropriate sharded tables, and effectively execute the query against only the relevant tables:<p><img alt="multitenant citus 2"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/f9f20655-c035-4645-a528-2d1c0a4ab000/public><p>Now, there is a bit more to <a href=https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy>designing multitenant apps</a> to work with Citus. For example, universal data can be placed in reference tables that can be distributed across all nodes, or local tables that can live solely within the coordinator. For the bulk of a Citus multitenant workload, tables will:<ol><li>Contain your shard key<li>Be indexed using a composite key on shard key + foreign key<li>Be distributed based on the shard key / tenant id<li>Be queried used the on the shard key / tenant id</ol><p>Let's shift to the other common use case for Citus: what Citus defines as real-time dashboards or analytics.<h2 id=real-time-analytics-with-citus><a href=#real-time-analytics-with-citus>Real-time analytics with Citus</a></h2><p>Where multitenant leverages the shard separation of Citus, here you're looking to leverage the parallelism of Citus.<p>Real-time analytics is indeed a bit vague. It is often some kind of event data that is high volume and is presented as a dashboard, report, monitoring, or alerting. Query patterns are often aggregating in some form; while there may be joins, they happen at a lower level then bubble up to a higher level for aggregation.<p>When operating a small volume of data, you don't necessarily need Citus — plain old Postgres can work just fine. With high data volume, Postgres is not as suited for analytics (unless you're talking Crunchy Data Warehouse, which is optimized for OLAP workloads – <a href=https://www.crunchydata.com/products/warehouse>see more here</a>).<p>With the multitenant/SaaS example, we wanted the query to be pushed down to a single node and operate within a single physical node. With real-time analytics, we want the opposite: queries execute across all the nodes using as many cores as available within the cluster.<p>Let's make this a little more concrete. Start with the idea of a Google Analytics type of event analytics — similar to what is talked about in the Citus docs. Here we may have something like:<pre><code class=language-sql>CREATE TABLE http_request (
  site_id INT,
  ingest_time TIMESTAMPTZ DEFAULT now(),

  url TEXT,
  request_country TEXT,
  ip_address TEXT,

  status_code INT,
  response_time_msec INT
);
</code></pre><p>Let's jump ahead and look past how we shard the data and to the query itself. The query shows a better idea of how Citus works in these situations. Let's build a query to return how many 404s and 200s from the country "Australia" along with the average response time for each:<pre><code class=language-sql>SELECT
  status_code,
  COUNT(id) AS request_count,
  AVG(response_time_msec) AS average_response_time_msec
FROM http_request
WHERE request_country = 'Australia'
  AND status_code IN (200, 404);
</code></pre><p>This query will run on every single shard. To process the query as fast as possible, the number of shards should match the number of cores available. If you end up with something like 16 shards in a single node, you'd want ideally 16 cores or even more (to handle additional concurrency). The query will be executed as smaller composable building blocks.<p>Citus processing the count of 404s and 200s is easy. It runs the query as a count on the nodes, then the coordinator calculates the sum of counts. We simply get: the sum of count(id) where country = "Australia" and the appropriate status code.<p>But! To calculate the average response time we need to get the count from each shard as well as the sum of the <code>response_time_msec</code> values. From there, Citus recombines all those back on the coordinator. Citus has each shard sending 4 values back (versus all the raw data), and doing the final math on the coordinator.<p><img alt="analytics citus"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/04c7c141-c53b-4ca0-a7e7-544e2b97eb00/public><p>This results in fast aggregations across large datasets. <strong>But</strong> if you haven't thought ahead yet, this only works for very specific queries. Counts and averages are great. If you're looking to do something like median, that gets a little harder. You need the full data set to compute a perfect median. (For now we're setting aside there are probabilistic approaches to getting approximate results that work quite well. Algorithms like t-digest or KLL can work if you're okay with approximate or inexact answers).<p>The other big piece of this is your queries need to be able to be constructed for Citus to push down any joins as locally as possible. While our example in this case is a very basic one, most applications still have data they need to join. This can work on Citus, but you still need to apply some of the thought in making joins to be as low level as possible — similar to the multitenant app.<p>Within the "real-time analytics" model you need the following to work in order to be successful:<ol><li>Ability to push joins down vs. joins that move data between nodes<li>Heavy aggregation or roll-up workload<li>Control over crafting the queries that are created</ol><h3 id=concurrency-and-connections><a href=#concurrency-and-connections>Concurrency and connections</a></h3><p>The one "gotcha" of the real-time analytics use case is concurrency. In our simple example of querying <code>http_request</code>, it's great if you only have 4 shards. But in a world of 64 shards spread across 4 nodes–you have 16 nodes per shard. This means a single query to Postgres could open 16 connections to each node. One weak area of Postgres is connection management and scaling those, so, we recommend and support pgBouncer out of the box across all our products.<h2 id=designing-up-front-for-citus><a href=#designing-up-front-for-citus>Designing up front for Citus</a></h2><p>A success factor with Citus will be your use case. If it is a fit, the more greenfield the application, the better your chance. Existing applications can absolutely be retrofitted to work with Citus, but it often takes some data maneuvering, schema modifications, and query modifications. As with many technologies, if Citus is the right tool for you then "Awesome!", you should absolutely use it. For questions if you think Citus may or may not be a fit, reach out to us <a href=https://www.crunchydata.com/contact>@crunchydata</a>. We've helped a number of customers successfully adopt Citus in cases. In others, we've helped our customers be successful on different paths. While Citus is very powerful, it is a special purpose tool. ]]></content:encoded>
<category><![CDATA[ Analytics ]]></category>
<category><![CDATA[ Crunchy Data Warehouse ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">ca14df7cb65f84286592c6d2b47bb0b7e057db2b9952dced6d71900688dc7310</guid>
<pubDate>Tue, 18 Mar 2025 09:50:00 EDT</pubDate>
<dc:date>2025-03-18T13:50:00.000Z</dc:date>
<atom:updated>2025-03-18T13:50:00.000Z</atom:updated></item>
<item><title><![CDATA[ A change to ResultRelInfo - A Near Miss with Postgres 17.1 ]]></title>
<link>https://www.crunchydata.com/blog/a-change-to-relresultinfo-a-near-miss-with-postgres-17-1</link>
<description><![CDATA[ A new point version was released on Nov 14th for 17.1, 16.5, 15.9, and others. This included an update to the Postgres ABI potentially breaking extensions. Craig digs into the change and what you need to know. ]]></description>
<content:encoded><![CDATA[ <p><span style="background-color: #DCD2CC">Version 17.2 of PostgreSQL has now released which rolls back the changes to ResultRelInfo. See the <a href=https://www.postgresql.org/about/news/postgresql-172-166-1510-1415-1318-and-1222-released-2965/>release notes</a> for more details.</span><p>Since its inception <a href=https://www.crunchydata.com/about>Crunchy Data</a> has released new builds and packages of Postgres on the day community packages are released. Yesterday's minor version release was the first time we made the decision to press pause on a release. Why did we not release it immediately? There appeared to be a very real risk of breaking existing installations. Let's back up and walk through a near miss of Postgres release day.<p>Yesterday when Postgres 17.1 was released there appeared to be breaking changes in the Application Build Interface (ABI). The ABI is the contract that exists between PostgreSQL and its extensions. Initial reports showed that a number of extensions could be affected, triggering <a href=https://www.postgresql.org/message-id/CABOikdNmVBC1LL6pY26dyxAS2f%2BgLZvTsNt%3D2XbcyG7WxXVBBQ%40mail.gmail.com>warning sirens</a> around the community. In other words, if you were to upgrade from 17.0 to 17.1 and use these extensions, you could be left with a non-functioning Postgres database. Further investigation showed that <em>TimescaleDB</em> and <em>Apache AGE</em> were the primarily affected extensions and if you are using them you should hold off at this time upgrading to the latest minor release or ensure to rebuild the extension against the latest PostgreSQL release in coordination with your upgrade.<p>The initial list of extensions for those curious:<table><thead><tr><th align=center>Affected<th align=left>Unaffected<tbody><tr><td align=center>Apache AGE<td align=left>HypoPG<tr><td align=center>TimescaleDB<td align=left>pg-query<tr><td align=center><td align=left>Citus<tr><td align=center><td align=left>pglast<tr><td align=center><td align=left>pglogical<tr><td align=center><td align=left>pgpool2<tr><td align=center><td align=left>ogr-fdw<tr><td align=center><td align=left>pg-squeeze<tr><td align=center><td align=left>mysql-fdw</table><p>First, a little bit on Postgres releases. Postgres releases major versions each year, and minor versions every three months roughly. The major versions are expected to be forward compatible, but do introduce bigger changes that result in catalog changes. Major version upgrades are intended to be treated with caution. Minor version releases in contrast are intended to be only security and bug fix related. They are meant to be able to drop in and continue working within the same existing major version line.<h2 id=about-the-postgres-abi-and-postgres-extension><a href=#about-the-postgres-abi-and-postgres-extension>About the Postgres ABI and Postgres Extension</a></h2><p>The Postgres ABI, Application Binary Interface, refers to the binary-level interface between Postgres and compiled extensions, modules, or clients that interact with it. The ABI includes various <strong>structs</strong> that define key components of the system's internal workings. These structs represent how PostgreSQL manages and manipulates data, query execution, memory. They typically include things like:<ul><li>System catalogs<li>Function signatures<li>Data structure layouts</ul><h3 id=why-does-the-abi-matter><a href=#why-does-the-abi-matter>Why Does the ABI Matter?</a></h3><p>Developers of extensions ensure their extensions are compatible with the Postgres ABI. Changes to the ABI between major versions necessitates recompiling any extensions to prevent runtime issues.<p>ABI compatibility is typically not maintained across major versions. For instance, an extension compiled for PostgreSQL 14 will likely need to be recompiled for PostgreSQL 15 because ABI changes can occur.<p>PostgreSQL typically aims to maintain compatibility for extensions across minor versions. This means if you build an extension for PostgreSQL 15.1, it should work for 15.2. However, this is not always the case. The nuances of PostgreSQL ABI guarantees have been a sufficiently hot topic that they produced new <a href=https://www.postgresql.org/message-id/E1sZ5TL-0020gU-3t%40gemulon.postgresql.org>documentation</a> on the subject back in July.<p>Yesterday there was a major struct change in 17.1.<h3 id=with-us-so-far-lets-go-deeper><a href=#with-us-so-far-lets-go-deeper>With us so far? Let’s go deeper</a></h3><p>Within a PostgreSQL extension there is C code that includes header files from PostgreSQL itself. When the extension is compiled, functions from those headers are represented as abstract symbols in binary. The symbols are linked to the actual implementations of the functions when the extension is loaded based on the function names. That way, an extension compiled against PostgreSQL 17.0 can usually still be loaded into PostgreSQL 17.1, as long as the function names and signatures from headers do not change (i.e. the application binary interface or "ABI" is stable).<p>The header files also declare structs that are passed to functions (as pointers). Strictly speaking, the struct definitions are also part of the ABI, but there is more subtlety around that. After compilation, structs are mostly defined by their size and offsets of fields, so for instance a name change does not affect ABI (though does affect API). A size change does affect ABI, a little.<pre><code class=language-c>typedef struct ResultRelInfo
{
	NodeTag		type;

        /*... (130 other lines) ...*/

	/* updates do LockTuple() before oldtup read; see README.tuplock */
	bool		ri_needLockTagTuple;

} ResultRelInfo;
</code></pre><p>Most of the time, PostgreSQL allocates structs on the heap using a macro that looks at the compile-time size of the struct ("makeNode") and initializes the bytes to 0. The discrepancy that arose in 17.1 is that a new boolean was added to the ResultRelInfo struct, which <strong>increased its size from 376 bytes to 384</strong>.<p>What happens next depends on who calls makeNode. If it's PostgreSQL 17.1 code, then it uses the new size. If it's an extension compiled against 17.0, then it uses the old size. When it calls a PostgreSQL function with a pointer to a block allocated using the old size, the PostgreSQL function still assumes the new size and may write past the allocated block.<p>That is in general quite problematic. It could lead to bytes being written into an unrelated section of memory, or the program crashing. When running tests, PostgreSQL has internal checks (asserts) to detect that situation and throw warnings.<p>So, in general this particular change in the struct does not actually affect the allocation size. There may be uninitialized bytes, but that is usually resolved by calling InitResultRelInfo. The issue primarily causes warnings in tests / assert-enabled builds for extensions that allocate ResultRelInfo, though only when running those tests using the new PostgreSQL version with an extension binary that was compiled against the old PostgreSQL versions.<h3 id=did-we-lose-you-yet-and-so-whats-the-result><a href=#did-we-lose-you-yet-and-so-whats-the-result>Did we lose you yet, and so what’s the result?</a></h3><p>Unfortunately, that's not the end of the story. Extensions that rely heavily on ResultRelInfo (like TimescaleDB) and can do some things that suffer from the size change. For instance, in one of TimescaleDB's <a href=https://github.com/timescale/timescaledb/blob/2.17.2/src/nodes/hypertable_modify.c#L1245>code paths</a>, it needs to find the index of a ResultRelInfo pointer in an array, and to do so it does pointer math. This array was allocated by PostgreSQL (384 bytes), but the Timescale binary assumes 376 bytes and the result is a nonsense number which then hits an assert failure or segmentation fault.<p>To be clear, the code here is not really at fault. The contract with PostgreSQL was simply not quite as assumed. For developers of Postgres extensions that's an interesting lesson for all of us.<p>It's quite possible that there are other issues like this in other extensions. TimescaleDB is quite popular and thus subject to broader testing that identified the issue. That said, as investigation occurred over the past 24 hours most that built against this header thus far do seem to be safe. Another advanced extension is Citus, but from our investigation the Citus extension does seem safe.<h2 id=what-should-you-do><a href=#what-should-you-do>What should you do?</a></h2><p><strong>If you’re a Crunchy Data customer you do not need to worry</strong>. If you’re using Crunchy Data Postgres on any platform, Crunchy Bridge, Crunchy Postgres for Kubernetes - our build, release and certification procedures worked as anticipated and appropriate mitigations were applied to any of our software releases. We are fortunate to have a fantastic build and release team that is largely behind the scenes but ensures issues like this are handled. If you’re a community Postgres user, or have packaged your own extensions, it is worth reading the psql-hackers thread in order to understand which extensions have been determined to potentially be impacted and to understand the potential mitigations for the below affected versions:<ul><li>17.0 -> 17.1<li>16.4 (and earlier) -> 16.5<li>15.8 (and earlier) -> 15.9<li>14.13 (and earlier) -> 14.14<li>13.16 (and earlier) -> 13.17<li>12.20 (and earlier) -> 12.21</ul><p>In short:<ul><li><p>If you are using TimescaleDB extension, Timescale is <a href=https://x.com/michaelfreedman/status/1857148280167174455>recommending</a> that users do not perform the minor version installs at this time.<li><p>If you are using extensions that are indicated as potentially impacted within the pgsql-hackers list thread, additional caution is warranted before upgrading (though our own <a href=https://x.com/marcoslot/status/1857403646134153438>Marco Slot</a> has confirmed that Citus is not impacted)<li><p>If you are compiling Postgres extensions from source, make sure your extensions have been compiled using the latest point version 17.1<li><p>If you are developing or installing custom Postgres extensions, it is worth taking the time to understand the impact of this particular issue and the Postgres ABI ‘commitments’.<p>Ultimately the default guidance of performing Postgres minor version upgrades stands and the impact of this issue was not as broad as was initially feared. The Postgres community once again provided a timely minor version release to address a collection of CVEs and fixes, and the community promptly responded to a report of potential issues. The ecosystem of Postgres providers release processes worked as intended and it appears any potential impact was largely averted.<p>That said, software is hard, databases in particular are tricky. As Postgres extensions grow in popularity these risks will continue to emerge and it is helpful to understand these details or ensure when selecting who is supporting you on your database they understand these issues.</ul> ]]></content:encoded>
<category><![CDATA[ Postgres 17 ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">455f5668fc452c9083751a8b6a1e7f75eacc374196b14cd6808e41185a36b328</guid>
<pubDate>Fri, 15 Nov 2024 10:30:00 EST</pubDate>
<dc:date>2024-11-15T15:30:00.000Z</dc:date>
<atom:updated>2024-11-15T15:30:00.000Z</atom:updated></item>
<item><title><![CDATA[ pg_parquet: An Extension to Connect Postgres and Parquet ]]></title>
<link>https://www.crunchydata.com/blog/pg_parquet-an-extension-to-connect-postgres-and-parquet</link>
<description><![CDATA[ Crunchy Data is excited to release a new extension so you can write Postgres data to Parquet and or pull data from Parquet to Postgres. Craig has the details and sample code. ]]></description>
<content:encoded><![CDATA[ <p>Today, we’re excited to release <a href=https://github.com/CrunchyData/pg_parquet/>pg_parquet</a> - an open source Postgres extension for working with Parquet files. The extension reads and writes parquet files to local disk or to S3 natively from Postgres. With pg_parquet you're able to:<ul><li>Export tables or queries from Postgres to Parquet files<li>Ingest data from Parquet files to Postgres<li>Inspect the schema and metadata of existing Parquet files</ul><p>Code is available at: <a href=https://github.com/CrunchyData/pg_parquet/>https://github.com/CrunchyData/pg_parquet/</a>.<p>Read on for more background on why we built pg_parquet or jump below to get a walkthrough of working with it.</p><!--more--><h2 id=why-pg_parquet><a href=#why-pg_parquet>Why pg_parquet?</a></h2><p>Parquet is a great columnar file format that provides efficient compression of data. Working with data in parquet makes sense when you're sharing data between systems. You might be archiving older data, or a format suitable for analytics as opposed to transactional workloads. While there are plenty of tools to work with Parquet, Postgres users have been left to figure things out on their own. Now, thanks to pg_parquet, Postgres and Parquet easily and natively work together. Better yet, you can work with Parquet without needing yet another data pipeline to maintain.<p><strong>Wait, what is Parquet?</strong> Apache Parquet is an open-source, standard, column-oriented file format that grew out of the Hadoop era of big-data. Using a file, Parquet houses data in a way that is optimized for SQL queries. In the world of data lakes, Parquet is ubiquitous.<h2 id=using-pg_parquet><a href=#using-pg_parquet>Using pg_parquet</a></h2><p>Extending the Postgres <code>copy</code> command we're able to efficiently copy data to and from Parquet, on your local server or in s3.<pre><code class=language-sql>-- Copy a query result into a Parquet file on the postgres server
COPY (SELECT * FROM table) TO '/tmp/data.parquet' WITH (format 'parquet');

-- Copy a query result into Parquet in S3
COPY (SELECT * FROM table) TO 's3://mybucket/data.parquet' WITH (format 'parquet');

-- Load data from Parquet in S3
COPY table FROM 's3://mybucket/data.parquet' WITH (format 'parquet');
</code></pre><p>Let's take an example products table, but not just a basic version, one that has composite Postgres types and arrays:<pre><code class=language-sql>-- create composite types
CREATE TYPE product_item AS (id INT, name TEXT, price float4);
CREATE TYPE product AS (id INT, name TEXT, items product_item[]);

-- create a table with complex types
CREATE TABLE product_example (
    id int,
    product product,
    products product[],
    created_at TIMESTAMP,
    updated_at TIMESTAMPTZ
);

-- insert some rows into the table
INSERT INTO product_example values (
    1,
    ROW(1, 'product 1', ARRAY[ROW(1, 'item 1', 1.0), ROW(2, 'item 2', 2.0), NULL]::product_item[])::product,
    ARRAY[ROW(1, NULL, NULL)::product, NULL],
    now(),
    '2022-05-01 12:00:00-04'
);

-- copy the table to a parquet file
COPY product_example TO '/tmp/product_example.parquet' (format 'parquet', compression 'gzip');

-- copy the parquet file to the table
COPY product_example FROM '/tmp/product_example.parquet';

-- show table
SELECT * FROM product_example;
</code></pre><h2 id=inspecting-parquet-files><a href=#inspecting-parquet-files>Inspecting Parquet files</a></h2><p>In addition to copying data in and out of parquet, you can explore existing Parquet files to start to understand their structure.<pre><code class=language-sql>-- Describe a parquet schema
SELECT name, type_name, logical_type, field_id
FROM parquet.schema('s3://mybucket/data.parquet');
┌──────────────┬────────────┬──────────────┬──────────┐
│     name     │ type_name  │ logical_type │ field_id │
├──────────────┼────────────┼──────────────┼──────────┤
│ arrow_schema │            │              │          │
│ name         │ BYTE_ARRAY │ STRING       │        0 │
│ s            │ INT32      │              │        1 │
└──────────────┴────────────┴──────────────┴──────────┘
(3 rows)

-- Retrieve parquet detailed metadata including column statistics
SELECT row_group_id, column_id, row_group_num_rows, row_group_bytes
FROM parquet.metadata('s3://mybucket/data.parquet');
┌──────────────┬───────────┬────────────────────┬─────────────────┐
│ row_group_id │ column_id │ row_group_num_rows │ row_group_bytes │
├──────────────┼───────────┼────────────────────┼─────────────────┤
│            0 │         0 │                100 │             622 │
│            0 │         1 │                100 │             622 │
└──────────────┴───────────┴────────────────────┴─────────────────┘
(2 rows)

-- Retrieve parquet file metadata such as the total number of rows
SELECT created_by, num_rows, format_version
FROM parquet.file_metadata('s3://mybucket/data.parquet');
┌────────────┬──────────┬────────────────┐
│ created_by │ num_rows │ format_version │
├────────────┼──────────┼────────────────┤
│ pg_parquet │      100 │ 1              │
└────────────┴──────────┴────────────────┘
(1 row)
</code></pre><h2 id=parquet-and-the-cloud><a href=#parquet-and-the-cloud>Parquet and the cloud</a></h2><p>If you’re working with object storage managing your Parquet files – likely S3 of something S3 compatible. If you configure your <code>~/.aws/credentials</code> and <code>~/.aws/config</code> files, pg_parquet will automatically use those credentials allowing you to copy to and from your cloud object storage.<pre><code>$ cat ~/.aws/credentials
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

$ cat ~/.aws/config
[default]
region = eu-central-1
</code></pre><p>Being able to directly access object storage via the <code>COPY</code> command is very useful for archival, analytics, importing data written by other applications, and moving data between servers.<h2 id=in-conclusion><a href=#in-conclusion>In conclusion</a></h2><p>Postgres has long been trusted for transactional workloads, but we believe in the very near future, it will be equally as capable for <a href=https://www.crunchydata.com/products/crunchy-bridge-for-analytics>analytics</a>. We’re excited to release <a href=https://github.com/CrunchyData/pg_parquet/>pg_parquet</a> as one more step towards making Postgres the <strong>only</strong> database you need. ]]></content:encoded>
<category><![CDATA[ Analytics ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">5499e60580bc1ed679f66b5ef575dca5fd3d510feff53b228b9134c3398c2a0c</guid>
<pubDate>Thu, 17 Oct 2024 10:30:00 EDT</pubDate>
<dc:date>2024-10-17T14:30:00.000Z</dc:date>
<atom:updated>2024-10-17T14:30:00.000Z</atom:updated></item>
<item><title><![CDATA[ Row Level Security for Tenants in Postgres ]]></title>
<link>https://www.crunchydata.com/blog/row-level-security-for-tenants-in-postgres</link>
<description><![CDATA[ Craig shows you how to use Row Level Security to isolate and secure data in a multi-tenant application. ]]></description>
<content:encoded><![CDATA[ <p>Row-level security (RLS) in Postgres is a feature that allows you to control which rows a user is allowed to access in a particular table. It enables you to define security policies at the row level based on certain conditions, such as user roles or specific attributes in the data. Most commonly this is used to limit access based on the database user connecting, but it can also be handy to ensure data safety for multi-tenant applications.<h2 id=creating-tables-with-row-level-security><a href=#creating-tables-with-row-level-security>Creating tables with row level security</a></h2><p>We're going to assume our tenants in this case are part of an organization, and we have an events table with events that always correspond to an organization:<pre><code class=language-sql>CREATE TABLE organization (
    org_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    name VARCHAR(255) UNIQUE,
    created_at TIMESTAMPTZ default now(),
    deleted_at TIMESTAMPTZ default now(),
);

CREATE TABLE events (
  org_id UUID,
  event_type TEXT,
  event_value INT,
  occurred_at TIMESTAMPTZ default now(),
);
</code></pre><p>We're going to turn on RLS on our events table.<pre><code class=language-sql>ALTER TABLE events ENABLE ROW LEVEL SECURITY;
</code></pre><p>And then set a policy that is enforced based on the connected database user.<pre><code class=language-sql>CREATE POLICY event_isolation_policy
  ON events
  USING (org_id::TEXT = current_user);
</code></pre><p>Now if you had a org id of <code>c4062d63-335e-4631-b03c-504d0eb88122</code> and created a database user with that login, when they connected to the database and queried the events table they'd only receive their events.<h2 id=using-session-variables><a href=#using-session-variables>Using Session Variables</a></h2><p>The above works great when you're giving out raw database access. But creating a new database user and connecting with that unique user for each new request that comes in is a lot of overhead and takes away many of the tools that exist for managing and scaling Postgres connections.<p>Instead of a user per customer, what we're going to do is set a session variable when we connect.<pre><code class=language-sql>CREATE POLICY event_session_user
  ON events
  TO application
  USING (org_id = NULLIF(current_setting('rls.org_id', TRUE), '')::uuid);
</code></pre><p>Now when you connect in your session you'll set the value, and then can query the <code>events</code> table:<pre><code class=language-sql>SET rls.org_id = 'c4062d63-335e-4631-b03c-504d0eb88122';
SELECT * FROM events;
</code></pre><p>For example, in a web application, you might set the <code>app.current_org_id</code> variable in the application component based on the current user's authentication. Use a function to get current org_id from the request and then pass in the current org in the connection.<pre><code class=language-python>@app.before_request
def before_request():
    org_id = get_current_org_id(request)
    g.db = psycopg2.connect(database="mydb")
    with g.db.cursor() as cur:
        cur.execute("SET app.current_org_id = %s", (org_id,))
</code></pre><p>With this setup, any queries executed within the request will automatically be filtered based on the current tenant ID, ensuring that each tenant can only access their own data.<p>Keep in mind that when <a href=https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy>designing your app for multi-tenancy</a>, ideally you have that ord_id in every table.<p><img alt="sample multitennant schema"loading=lazy src=https://imagedelivery.net/lPM0ntuwQfh8VQgJRu0mFg/7488c2e3-d2f8-4a06-7e21-c1fe6f928d00/public><h2 id=conclusion><a href=#conclusion>Conclusion</a></h2><p>Postgres Row-Level Security provides a powerful mechanism for securing multi-tenant applications, allowing you to enforce data isolation and privacy at the row level. By defining policies based on tenant IDs, org IDs, or other criteria, you can ensure that each tenant can only access their own data, enhancing the overall security of your application.<p>Check out our browser based tutorial for learning more about <a href=https://www.crunchydata.com/developers/playground/row-level-security>Row Level Security</a> and session variables. ]]></content:encoded>
<category><![CDATA[ Tutorial ]]></category>
<author><![CDATA[ Craig.Kerstiens@crunchydata.com (Craig Kerstiens) ]]></author>
<dc:creator><![CDATA[ Craig Kerstiens ]]></dc:creator>
<guid isPermalink="false">0617264416b334ca8f7607ff82a4baf2dd1ef6416da15f25740bd6343e1f682b</guid>
<pubDate>Wed, 03 Apr 2024 09:00:00 EDT</pubDate>
<dc:date>2024-04-03T13:00:00.000Z</dc:date>
<atom:updated>2024-04-03T13:00:00.000Z</atom:updated></item></channel></rss>