stefan
stefan

Reputation: 115

How to simplify this postgres query

How can I simplify this query?

What I am trying to do is derive the column S9_Unlock via a subquery in which I only look for user_ids which are returned from the main query but this looks very awkward to me, especially as this query here is just an excerpt. In reality I am doing multiple of these subqueries to derive different columns...

SELECT userid, CAST(to_char(S9_unlock,'YYYY/MM/DD') AS timestamp) AS "S9_Unlock"
FROM (
  SELECT ca.user_id AS userid
  FROM shop_db.invoices AS inv
  LEFT JOIN shop_db.carts AS ca ON inv.id = ca.invoice_id
  LEFT JOIN shop_db.cart_items AS ci ON ca.id = ci.cart_id
  WHERE (inv.created BETWEEN '2014-11-13' AND '2014-11-14' OR inv.created BETWEEN '2013-11-14' AND '2013-11-15')
    AND inv.status <> 'do_not_book'
    AND inv.id IS NOT NULL
  GROUP BY user_id) AS master
LEFT JOIN (
  SELECT MIN(s3.unl) AS "S9_Unlock", s3.user_id
  FROM (
    SELECT user_id, challenge_codes.created AS unl,
           MAX /* Check if license contains Suite9 */
             (CASE WHEN substring(bundle_article_code,1,6) = 'BuSu90' THEN 1 ELSE 0 END) AS "S9_Unlock"
    FROM licensing_db.serial_numbers
    LEFT JOIN licensing_db.licenses ON licenses.id = serial_numbers.license_id 
    LEFT JOIN user_db.users ON users.id = licenses.user_id
    LEFT JOIN licensing_db.challenge_codes ON challenge_codes.serial_number_id = serial_numbers.id
    WHERE user_id IN (
      SELECT ca.user_id AS userid
      FROM shop_db.invoices AS inv
      LEFT JOIN shop_db.carts AS ca ON inv.id = ca.invoice_id
      LEFT JOIN shop_db.cart_items AS ci ON ca.id = ci.cart_id
      WHERE (inv.created BETWEEN '2014-11-13' AND '2014-11-14' OR inv.created BETWEEN '2013-11-14' AND '2013-11-15')
        AND inv.status <> 'do_not_book'
        AND inv.id IS NOT NULL
      GROUP BY user_id
    )
    GROUP BY user_id, challenge_codes.created) AS s3
  )
  WHERE "S9_Unlock" = 1
    AND s3.unl IS NOT NULL
  GROUP BY s3.user_id) AS "S9_Unlock" ON "S9_Unlock".user_id = master.userid

Upvotes: 0

Views: 78

Answers (2)

Patrick
Patrick

Reputation: 32179

In your query you have two sub-queries that are identical; this screams for a CTE.

In the sub-query on licensing issues you can filter out the valid licenses after the GROUP BY clause using a HAVING clause. Make that a WITH QUERY too and you end up with the rather more readable:

WITH inv AS (
  SELECT ca.user_id AS userid
  FROM shop_db.invoices AS inv
  LEFT JOIN shop_db.carts AS ca ON ca.invoice_id = inv.id
  LEFT JOIN shop_db.cart_items AS ci ON ci.cart_id = ca.id
  WHERE (inv.created BETWEEN '2014-11-13' AND '2014-11-14' OR inv.created BETWEEN '2013-11-14' AND '2013-11-15')
    AND inv.status <> 'do_not_book'
    AND inv.id IS NOT NULL
), s3 AS (
    SELECT u.user_id, min(cc.created) AS first_unlocked, bundle_article_code
    FROM licensing_db.serial_numbers AS sn
    LEFT JOIN licensing_db.licenses AS lic ON lic.id = sn.license_id 
    LEFT JOIN user_db.users AS u ON u.id = lic.user_id
    LEFT JOIN licensing_db.challenge_codes AS cc ON cc.serial_number_id = sn.id
    WHERE u.user_id IN (SELECT userid FROM inv)
    GROUP BY u.user_id, bundle_article_code
    HAVING bundle_article_code LIKE 'BuSu90%'
       AND first_unlocked IS NOT NULL
)
SELECT userid, date_trunc('day', first_unlocked) AS "S9_Unlock"
FROM inv
LEFT JOIN s3 ON s3.user_id = inv.userid;

So the main query is now reduced to 3 lines and both the WITH-QUERY's perform a logically self-contained query of the database. The other sub-queries you refer to can similarly become a WITH-QUERY and then you assemble them in the main query. Remember that you can refer to earlier named queries in the list of with-queries, as is shown above with inv being referred to by s3. While such CTE's are syntactically not providing new functionality (except for the RECURSIVE variant), they do make complex queries much more readable and therefore easier to maintain.

Another approach would be to factor out logical sub-components (such as the inv sub-query) and make a VIEW out of those. Then you can simply reference the view in the main query. Making the whole thing a view is probably also a good idea if you want to make the query more flexible. What if you want to query for Suite9.1 ('BuSu91%') on 27 March 2014? Taken those literals out and then using them as WHERE clauses in a view makes your query more versatile; this can be either with sub-queries or with the complete CTE.

(Please check if the semantics are still right in the s3 with-query because without your table structures and sample data I ccannot test my code above.)

Upvotes: 1

plang
plang

Reputation: 5646

Instead of solving your problem as one big monolithic relational sql query, I would seriously consider going the "procedural" way, by using the built-in "plpgsql" language of postgresql. This could bring a lot of clarity in your application.

Upvotes: 0

Related Questions