1

I'm using Postgres (latest) with node (latest) PG (latest). Some endpoint is receiving json which looks like:

{
   "id": 12345,
   "total": 123.45,
   "items": [
       {
           "name": "blue shirt",
           "url": "someurl"
       },
       {
           "name": "red shirt",
           "url": "someurl"
       }
   ]
}

So I'm storing this in two tables:

CREATE TABLE orders (
    id INT NOT NULL,
    total NUMERIC(10, 2) DEFAULT 0 NOT NULL,
    PRIMARY KEY (id)
);
CREATE INDEX index_orders_id ON orders(id);

CREATE TABLE items (
    id BIGSERIAL NOT NULL,
    order_id INT NOT NULL,
    name VARCHAR(128) NOT NULL,
    url VARCHAR(128) DEFAULT '' NOT NULL,

    PRIMARY KEY (id),
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
);
CREATE INDEX index_items_id ON items(id);

The items table has a FK of order_id to relate the id of the order to its respective items. Now, the issue is I almost always need to fetch the order along with the items.

How do I get an output similar to my input json in one query? I know it can be done in two queries, but this pattern will be all over the place and needs to be efficient. My last resort would be to store the items as JSONB column directly in the orders table, but then if I need to query on the items or do joins with them it won't be as easy.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
LAZ
  • 402
  • 4
  • 15

1 Answers1

1

One of many ways:

SELECT jsonb_pretty(
          to_jsonb(o.*)  -- taking whole row
      || (SELECT jsonb_build_object('items', jsonb_agg(i))
          FROM  (
             SELECT name, url  -- picking columns
             FROM   items i
             WHERE  i.order_id = o.id
             ) i
         )
       )
FROM   orders o
WHERE  o.id = 12345;

This returns formatted text similar to the displayed input. (But keys are sorted, so 'total' comes after 'items'.)

If an order has no items, you get "items": null.

For a jsonb value, strip the jsonb_pretty() wrapper.

I chose jsonb for its additional functionality - like the jsonb || jsonbjsonb operator and the jsonb_pretty() function.

Related:

If you want a json value instead, you can cast the jsonb directly (without format) or the formatted text (with format). Or build a json value with rudimentary formatting directly (faster):

SELECT row_to_json(sub, true)
FROM  (
   SELECT o.*
       , (SELECT json_agg(i)
          FROM  (
             SELECT name, url  -- pick columns to report
             FROM   items i
             WHERE  i.order_id = o.id
             ) i
         ) AS items
   FROM   orders o
   WHERE  o.id = 12345
   ) sub;

db<>fiddle here

It all depends on what you need exactly.

Aside:
Consider type text (or varchar) instead of the seemingly arbitrary varchar(128). See:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • This does exactly as described, thank you! Here's a bit more context for where you mentioned: - Keys being sorted (total after items key) is completely fine - Orders will always have at least *one* item, so potentially no need to check for nulls - The output format doesn't need to be exactly the input, as long as the order row is returned by the select along with: an array of items relating to it or, a json object since the item url is always unique per order. – LAZ Jul 16 '22 at 10:15
  • - The varchar type was used knowing that text is generally simpler to use and offers the same performance. I did that because I wasn't aware of the CHECK (len) clause and even now, I'm not sure I want to check the length repeatedly where user input ends up written to one of these cols. Lastly, here's what I did, maybe we could see how it compares in ease of use and performance compared to the sep tables. I added a JSONB col to orders and changed the input array of items to an object with the url of each item as the top level keys. – LAZ Jul 16 '22 at 10:20
  • Queries looking to return order rows based on the items within look like this: `SELECT * FROM orders WHERE jsonb_path_exists(items, '$ ? (@.*.name like_regex "blue" flag "i")') ORDER BY id DESC LIMIT 100;` Now this might be slower or more complicated (I'm guessing) when a query has less to do with the whole order and more with the specific items across multiple orders. – LAZ Jul 16 '22 at 10:25