Search code examples
mysqlselectdeadlock

Select statement can trigger dead lock on table in mysql?


  • The SQL below is inside a MySQL stored procedure.
  • The procedure run by a cron job every day once at midnight to populate report table with result.
  • this procedure take around 2 min to run.
  • please note that table1 has millions of records.
  • i put this to run at midnight because there are INSERT/UPDATE transactions during the day but unfortunately there are some few transaction at night also.
  • when this procedure runs and if there are other transactions running then a deadlock error on table1 occurs.

my question is

  1. why SELECT statement cause deadlock on table1?
  2. how can I avoid deadlock in this kind of situation?
DROP report;

CREATE TABLE IF NOT EXISTS report AS (
    
    SELECT
        DISTINCT
        
        companies.id company_id,

        (
            SELECT 
                SUM(`message_count`) single_phone
            FROM
                `table1`
            WHERE
                `table1`.`company_id` = companies.id
                AND
                `status` != 'error'
        ) AS single_phone,

        (
            SELECT 
                SUM(`message_count`)
            FROM
                `table1`
            WHERE
                `table1`.`company_id` = companies.id
                AND
                `status` != 'not error'
        ) AS log,

        (
            SELECT 
                SUM(`message_count`)
            FROM
                `table1`
            WHERE
                `table1`.`company_id` = companies.id
                AND
                `status` != 'error'
        ) AS log_monthly,

        (
            SELECT 
                SUM(`number_of_sms`) AS aggregate
            FROM
                `messages`
            WHERE
                `messages`.`company_id` = companies.id
        ) AS p_monthly
    FROM
        companies
        INNER JOIN company_users ON companies.id = company_users.company_id
    WHERE
        company_users.confirmed = 1
        AND
        company_users.deleted_at IS NULL
);

Solution

  • Your field-level queries should be done ONCE in the from clause to get pre-aggregates done ONCE per company ID and left-joined in case a given company may NOT have qualified records in a given category. Additionally, your query to get Single_Phone is the same as your 'log_monthly', but have no criteria showing a break or filter on the dates of activity to filter out a single month vs overall total of everything. So, I added a where clause for filtering, but only GUESSING if such some date exists.

    This query might substantially improve your performance. By moving the COLUMN-based queries for every company ID into its own subquery via left-join, those will be summed() and grouped by company ONCE, then the JOIN for the final result. COALESCE() is used so if no such counts exists, the value returned will be 0 instead of null

    DROP report;
    
    CREATE TABLE IF NOT EXISTS report AS (
          SELECT
                c.id company_id,
                coalesce( PhoneSum.Msgs, 0 ) as Single_Phone,
                coalesce( PhoneLog.Msgs, 0 ) as Log,
                coalesce( MonthLog.Msgs, 0 ) as Log_Monthly,
                coalesce( SMSSummary.Aggregate, 0 ) as p_monthly
            from
                -- this will declare an in-line variable if you do need to filter by a month as a couple of your
                -- column result names infer, but have no other indicator of filtering by a given month.
                ( select @yesterday := date_sub( date(curdate()), interval -1 day ),
                        @beginOfThatMonth := date_sub( @yesterday, interval dayOfMonth( @yesterday ) -1 day ) sqlvars,
                companies c
                    INNER JOIN company_users cu
                        ON m.company.id = cu.company_id
                        AND cu.confirmed = 1
                        AND cu.deleted_at IS NULL
        
                    LEFT JOIN
                    ( SELECT 
                            t.company_id,
                            SUM( t.message_count ) Msgs
                        FROM
                            table1 t
                                INNER JOIN company_users cu
                                    ON t.company.id = cu.company_id
                                    AND cu.confirmed = 1
                                    AND cu.deleted_at IS NULL
                        where
                            t.status != 'error'
                        GROUP BY
                            t.company_id ) AS PhoneSum,
                        on c.id = PhoneSum.company_id
        
                    LEFT JOIN
                    ( SELECT 
                            t.company_id,
                            SUM( t.message_count ) Msgs
                        FROM
                            table1 t
                                INNER JOIN company_users cu
                                    ON t.company.id = cu.company_id
                                    AND cu.confirmed = 1
                                    AND cu.deleted_at IS NULL
                        where
                            t.status != 'not error'
                        GROUP BY
                            t.company_id ) AS PhoneLog,
                        on c.id = PhoneLog.company_id
        
                    LEFT JOIN
                    ( SELECT 
                            t.company_id,
                            SUM( t.message_count ) Msgs
                        FROM
                            table1 t
                                INNER JOIN company_users cu
                                    ON t.company.id = cu.company_id
                                    AND cu.confirmed = 1
                                    AND cu.deleted_at IS NULL
                        where
                                t.status != 'error'
                            -- this would only get counts of activity for current month currently active
                            -- but since you are running at night, you need the day before current
                            AND t.SomeDateFieldOnTable1 >= @beginOfThatMonth
                        GROUP BY
                            t.company_id ) AS MonthLogMsgs,
                        on c.id = MonthLogMsgs.company_id
        
                    LEFT JOIN
                    ( SELECT 
                            m.company_id,
                            SUM( m.number_of_sms ) aggregate
                        FROM
                            messages m
                                INNER JOIN company_users cu
                                    ON m.company.id = cu.company_id
                                    AND cu.confirmed = 1
                                    AND cu.deleted_at IS NULL
                        where
                            m.SomeDateFieldOnMessagesTable >= @beginOfThatMonth
                        GROUP BY
                            company_id ) AS SMSSummary,
                        on c.id = SMSSummary.company_id