I don’t think you need to use subqueries at all, and you don’t have to go through the months.
Instead, I recommend that you create a table to store all months. Even if you sprinkle it with 100-year months, it will only have 1,200 lines, which is trivial.
CREATE TABLE Months ( start_date DATE, end_date DATE, PRIMARY KEY (start_date, end_date) ); INSERT INTO Months (start_date, end_date) VALUES ('2011-03-01', '2011-03-31');
Keep the actual start and end dates, so you can use the DATE data type and index two columns correctly.
edit: I think I understand your requirement a little better, and I cleared this answer. The following query may be right for you:
SELECT DATE_FORMAT(m.start_date, '%Y-%m') AS month, COUNT(DISTINCT cev.customer_email) AS current, GROUP_CONCAT(DISTINCT cev.customer_email) AS current_email, COUNT(DISTINCT prev.customer_email) AS earlier, GROUP_CONCAT(DISTINCT prev.customer_email) AS earlier_email FROM Months AS m LEFT OUTER JOIN _pj_cust_email_view AS cev ON cev.order_date BETWEEN m.start_date AND m.end_date INNER JOIN Months AS mprev ON mprev.start_date <= m.start_date LEFT OUTER JOIN _pj_cust_email_view AS prev ON prev.order_date BETWEEN mprev.start_date AND mprev.end_date GROUP BY month;
If you create the following composite index in your table:
CREATE INDEX order_email on _pj_cust_email_view (order_date, customer_email);
Then the query has the best chance of being a query only for the index and will work much faster.
Below is an EXPLAIN optimization report from this query. Note type: index for each table.
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: m type: index possible_keys: PRIMARY key: PRIMARY key_len: 6 ref: NULL rows: 4 Extra: Using index; Using temporary; Using filesort *************************** 2. row *************************** id: 1 select_type: SIMPLE table: mprev type: index possible_keys: PRIMARY key: PRIMARY key_len: 6 ref: NULL rows: 4 Extra: Using where; Using index; Using join buffer *************************** 3. row *************************** id: 1 select_type: SIMPLE table: cev type: index possible_keys: order_email key: order_email key_len: 17 ref: NULL rows: 10 Extra: Using index *************************** 4. row *************************** id: 1 select_type: SIMPLE table: prev type: index possible_keys: order_email key: order_email key_len: 17 ref: NULL rows: 10 Extra: Using index
Here are some test data:
INSERT INTO Months (start_date, end_date) VALUES ('2011-03-01', '2011-03-31'), ('2011-02-01', '2011-02-28'), ('2011-01-01', '2011-01-31'), ('2010-12-01', '2010-12-31'); INSERT INTO _pj_cust_email_view VALUES ('ron', '2011-03-10'), ('hermione', '2011-03-15'), ('hermione', '2011-02-15'), ('hermione', '2011-01-15'), ('hermione', '2010-12-15'), ('neville', '2011-01-10'), ('harry', '2011-03-19'), ('harry', '2011-02-10'), ('molly', '2011-03-25'), ('molly', '2011-01-10');
Here, the result provides data, including a combined list of letters, to make it easier to see.
+---------+---------+--------------------------+---------+----------------------------------+ | month | current | current_email | earlier | earlier_email | +---------+---------+--------------------------+---------+----------------------------------+ | 2010-12 | 1 | hermione | 1 | hermione | | 2011-01 | 3 | neville,hermione,molly | 3 | hermione,molly,neville | | 2011-02 | 2 | hermione,harry | 4 | harry,hermione,molly,neville | | 2011-03 | 4 | molly,ron,harry,hermione | 5 | molly,ron,hermione,neville,harry | +---------+---------+--------------------------+---------+----------------------------------+