I have a PostgreSQL database (version 9.3.6) containing a large number of orders
. As the orders are processed, scan_events
are triggered and stored, with multiple events per order. Scan events have a boolean indicating whether an order can be marked as completed after that event, but multiple complete
scans can occur. Generally speaking however, the only scans I actually care about are the first scan, and the first completed
scan.
I want to know the average and standard deviation of the percentage of orders with a given creation date that have received their first scan within x days of being created.
Schema
CREATE TABLE orders (
id character varying(40) NOT NULL,
date_created timestamp with time zone NOT NULL
);
ALTER TABLE ONLY orders
ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
CREATE TABLE scan_events (
id character varying(100) NOT NULL,
order_id character varying(40) NOT NULL,
"time" timestamp with time zone NOT NULL
);
CREATE INDEX scan_events_order_id_idx ON scan_events USING btree (order_id);
Desired Computation
For days_elapsed
values ranging from 1 to 14 days, I want the means and standard deviations of:
the set of percentages of orders that received any scans within
days_elapsed
days of theirorders.date_created
from each of the past 30 days (aka grouped byDATE(orders.date_created)
)the set of percentage of orders that received a scan with
completed = TRUE
withindays_elapsed
days of theirorders.date_created
from each of the past 30 days (aka grouped byDATE(orders.date_created)
)
Ideally, the output would look something like this, but honestly, anything performant is fine.
output
----------------
days_elapsed
mean_scanned
stddev_scanned
mean_completed
stddev_completed
Current Progress
I have a query that will get me the counts per day (optionally with WHERE scan_events.completed IS TRUE
to get the completed scan results):
SELECT DATE(orders.date_created),
COUNT(DISTINCT orders.id) AS total,
COUNT(DISTINCT CASE WHEN scan_events.id IS NOT NULL AND DATE_PART('day', scan_events.time - orders.date_created) <= 1 THEN orders.id ELSE NULL END) AS scanned,
COUNT(DISTINCT CASE WHEN scan_events.id IS NOT NULL AND scan_events.completed AND DATE_PART('day', scan_events.time - orders.date_created) <= 1 THEN orders.id ELSE NULL END) AS completed
FROM orders
LEFT JOIN scan_events ON orders.id = scan_events.order_id
WHERE orders.date_created BETWEEN '2015-07-01' AND '2015-07-31'
GROUP BY DATE(orders.date_created)
ORDER BY DATE(orders.date_created) ASC;
For days_elapsed = 1
, this query is roughly what I imagine should work:
SELECT AVG(counts.scanned * 1.0 / counts.total) AS mean_scanned,
STDDEV(counts.scanned * 1.0 / counts.total) AS stddev_scanned,
AVG(counts.completed * 1.0 / counts.total) AS mean_completed,
STDDEV(counts.completed * 1.0 / counts.total) AS stddev_completed
FROM (
SELECT DATE(orders.date_created),
COUNT(DISTINCT orders.id) AS total,
COUNT(DISTINCT CASE WHEN scan_events.id IS NOT NULL AND DATE_PART('day', scan_events.time - orders.date_created) <= 1 THEN orders.id ELSE NULL END) AS scanned,
COUNT(DISTINCT CASE WHEN scan_events.id IS NOT NULL AND scan_events.completed AND DATE_PART('day', scan_events.time - orders.date_created) <= 1 THEN orders.id ELSE NULL END) AS completed
FROM orders
LEFT JOIN scan_events ON orders.id = scan_events.order_id
WHERE orders.date_created BETWEEN '2015-07-01' AND '2015-07-31'
GROUP BY DATE(orders.date_created)
) counts
The problem with this is that it's most certainly redoing work that it doesn't have to...
Things that I imagine we can take advantage of:
AVG
andSTDDEV
ignorenull
values so we can do someCASE WHEN ... THEN ... END
trickery- The sets that
AVG
andSTDDEV
are calculated over could maybe be built up incrementally as we increasedays_elapsed
by one
Any help would be much appreciated -- my SQL-fu is not up to snuff :(
WHERE scan_events.id IS NOT NULL
? Why is that column nullable? Isn't it the primary key? By the way, you should add theCREATE TABLE
statements in your question, along with the existing indexes.WHERE scan_events.id IS NOT NULL
was there because I was trying out aLEFT JOIN
so I can get the total count while getting thescanned
/completed
counts, and my latest query (which does that) is now in in the Current Progress section.