【问题标题】:TSQL query is running slow, how to speed it up?TSQL 查询运行缓慢,如何加快速度?
【发布时间】:2013-03-13 20:43:04
【问题描述】:

我有一个报表的 sql 查询,它包括一些子查询。它运行得很慢。我尝试了几种方法(比如使用连接而不是子查询,添加更多索引)。但他们都没有工作。这是查询:

declare @time_from       datetime
declare @time_to         datetime   
set @time_from ='2012-01-01'
set @time_to = '2014-01-01'
select a.a_id, c.c_id, c.c_chat_line_id, a.a_first_name, a.a_last_name
    ,(select isnull(SUM(ac.ac_amount),0) from t_actress_credit ac join t_order o on o.o_id = ac.order_id where o.o_status = 1 and ac.actress_id = a.a_id and ac.ac_time>=@time_from and ac.ac_time<=@time_to) as  credit
    ,(select isnull(SUM(ac.ac_amount),0) from t_actress_credit ac join t_order o on o.o_id = ac.order_id where o.o_status = 1 and ac.ac_is_paid = 1 and ac.actress_id = a.a_id and ac.ac_time>=@time_from and ac.ac_time<=@time_to) as  paid_credit
    ,(select COUNT(1) from t_message pm join t_call_log l1 on pm.call_log_id = l1.c_id where pm.m_type = 2 and l1.caller_id = c.c_id and pm.m_time>=@time_from and pm.m_time<=@time_to) as pmsg_sent
    ,(select COUNT(1) from t_message pm join t_call_log l2 on pm.m_to_call_log_id = l2.c_id where pm.m_type = 2 and l2.caller_id = c.c_id and pm.m_time>=@time_from and pm.m_time<=@time_to) as pmsg_received
    ,(select COUNT(1) from t_message pm join t_call_log l3 on pm.call_log_id = l3.c_id where pm.m_type = 1 and l3.caller_id = c.c_id and pm.m_time>=@time_from and pm.m_time<=@time_to) as lcmsg_sent
    ,(select COUNT(1) from t_message pm join t_call_log l4 on pm.m_to_call_log_id = l4.c_id where pm.m_type = 1 and l4.caller_id = c.c_id and pm.m_time>=@time_from and pm.m_time<=@time_to) as lcmsg_received
    ,(select COUNT(1) from t_actress_live_minute where actress_id = a.a_id and alm_time>=@time_from and alm_time<=@time_to ) as live_calls
    ,(select isnull(SUM(alm_minutes),0) from t_actress_live_minute where actress_id = a.a_id  and alm_time>=@time_from and alm_time<=@time_to) as live_call_minutes
    ,(select isnull(count(1),0) from t_call_log l where l.caller_id = c.c_id and l.c_time_out is not null and c_time_in >=@time_from and c_time_in <= @time_to) as total_calls
    ,(select isnull(SUM(DATEDIFF(minute, l.c_time_in, l.c_time_out)),0) from t_call_log l where c_time_in >=@time_from and c_time_in <= @time_to and l.caller_id = c.c_id and l.c_time_out is not null ) as total_call_minutes
from t_actress a
join t_caller c on c.c_id = a.caller_id
group by a.a_id,c.c_id, c.c_chat_line_id, a.a_first_name, a.a_last_name

谁能给点建议?

非常感谢!

艾伦

【问题讨论】:

  • 我要做的第一件事是注释掉所有子查询,看看外部查询需要多长时间运行,假设它一次快速添加一个子查询,看看哪些子查询导致最多放慢速度。
  • 请告诉我们执行计划。
  • 从你的表名来看,我猜消息的计数将是最慢的操作。

标签: sql sql-server join subquery


【解决方案1】:

您可以尝试将从同一个表或一组表中提取的子查询组合成一个子查询。要考虑条件的变化,您可以使用条件聚合(使用 CASE 表达式)。

我可以在您的查询中看到四个,可能五个这样的组。这里重写为使用四个子查询:

SELECT
  a.a_id,
  c.c_id,
  c.c_chat_line_id,
  a.a_first_name,
  a.a_last_name,
  ISNULL(cr.credit            , 0) AS credit
  ISNULL(cr.paid_credit       , 0) AS paid_credit
  ISNULL(m.pmsg_sent          , 0) AS pmsg_sent,
  ISNULL(m.pmsg_received      , 0) AS pmsg_received,
  ISNULL(m.lcmsg_sent         , 0) AS lcmsg_sent,
  ISNULL(m.lcmsg_received     , 0) AS lcmsg_received,
  ISNULL(alm.live_calls       , 0) AS live_calls,
  ISNULL(alm.live_call_minutes, 0) AS live_call_minutes,
  ISNULL(l.total_calls        , 0) AS total_calls,
  ISNULL(l.total_call_minutes , 0) AS total_call_minutes,

FROM t_actress AS a

INNER JOIN t_caller AS c
  ON c.c_id = a.caller_id

LEFT JOIN (
  SELECT
    ac.actress_id,
    SUM(                               ac.ac_amount    ) AS credit,
    SUM(CASE ac.ac_is_paid WHEN 1 THEN ac.ac_amount END) AS paid_credit
  FROM t_actress_credit AS ac
  JOIN t_order o ON o.o_id = ac.order_id
  WHERE o.o_status = 1
    AND ac.ac_time BETWEEN @time_from AND @time_to
  GROUP BY ac.actress_id
) AS ac
  ON ac.actress_id = a.a_id

LEFT JOIN (
  SELECT
    l.caller_id,
    COUNT(CASE WHEN m.     call_log_id = l1.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_sent,
    COUNT(CASE WHEN m.m_to_call_log_id = l2.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_received,
    COUNT(CASE WHEN m.     call_log_id = l3.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_sent,
    COUNT(CASE WHEN m.m_to_call_log_id = l4.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_received
  FROM t_message AS m
  JOIN t_call_log AS l ON l.c_id IN (m.call_log_id, m.m_to_call_log_id)
  WHERE m.m_type IN (1, 2)
    AND m.m_time BETWEEN @time_from AND @time_to
  GROUP BY l.caller_id
) AS m
  ON m.caller_id = c.c_id

LEFT JOIN (
  SELECT
    actress_id,
    COUNT(*)         AS live_calls,
    SUM(alm_minutes) AS live_call_minutes
  FROM t_actress_live_minute
  WHERE alm_time BETWEEN @time_from AND @time_to
  GROUP BY actress_id
) AS alm
  ON alm.actress_id = a.a_id

LEFT JOIN (
  SELECT
    caller_id,
    COUNT(*)                                     AS total_calls,
    SUM(DATEDIFF(MINUTE, c_time_in, c_time_out)) AS total_call_minutes
  FROM t_call_log
  WHERE c_time_out IS NOT NULL
    AND c_time_in BETWEEN @time_from AND @time_to
  GROUP BY caller_id
) AS l
  ON l.actress_id = a.a_id
;

如果您通过分别加入call_log_idm_to_call_log_idm 子查询分成两个,则它可能是五个子查询(从而可能为查询规划器提供更多优化空间),即,而不是

LEFT JOIN (
  SELECT
    l.caller_id,
    COUNT(CASE WHEN m.     call_log_id = l1.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_sent,
    COUNT(CASE WHEN m.m_to_call_log_id = l2.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_received,
    COUNT(CASE WHEN m.     call_log_id = l3.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_sent,
    COUNT(CASE WHEN m.m_to_call_log_id = l4.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_received
  FROM t_message AS m
  JOIN t_call_log AS l ON l.c_id IN (m.call_log_id, m.m_to_call_log_id)
  WHERE m.m_type IN (1, 2)
    AND m.m_time BETWEEN @time_from AND @time_to
  GROUP BY l.caller_id
) AS m
  ON m.caller_id = c.c_id

应该是

LEFT JOIN (
  SELECT
    l.caller_id,
    COUNT(CASE WHEN m.call_log_id = l1.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_sent,
    COUNT(CASE WHEN m.call_log_id = l3.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_sent,
  FROM t_message AS m
  JOIN t_call_log AS l ON l.c_id = m.call_log_id
  WHERE m.m_type IN (1, 2)
    AND m.m_time BETWEEN @time_from AND @time_to
  GROUP BY l.caller_id
) AS mf
  ON mf.caller_id = c.c_id

LEFT JOIN (
  SELECT
    l.caller_id,
    COUNT(CASE WHEN m.m_to_call_log_id = l2.c_id AND m.m_type = 2 THEN 1 END) AS pmsg_received,
    COUNT(CASE WHEN m.m_to_call_log_id = l4.c_id AND m.m_type = 1 THEN 1 END) AS lcmsg_received
  FROM t_message AS m
  JOIN t_call_log AS l ON l.c_id = m.m_to_call_log_id
  WHERE m.m_type IN (1, 2)
    AND m.m_time BETWEEN @time_from AND @time_to
  GROUP BY l.caller_id
) AS mt
  ON mt.caller_id = c.c_id

同时更改主 SELECT 子句中的相应引用。

我不确定哪个变体更好,您需要测试两者才能找出答案。

请注意,我省略了主查询的 GROUP BY 子句。在您的查询和我的查询中似乎都没有必要,因为据我所知,它包括来自 t_actresst_caller 的主键,而且这些组合无论如何都是唯一的。我假设 GROUP BY 是您之前尝试使用联接重写查询的遗留物。

【讨论】:

    【解决方案2】:

    感谢您的回复。我试过你的方法,还是很慢。这就是我所做的,终于奏效了。我基本上将所有子查询放入表中,然后在最后加入表。不知道为什么,但它更快:这是代码;

    -- total calls
    declare @t_call table(
        a_id bigint,
        total_calls bigint,
        total_call_minutes bigint
    );
    insert into @t_call
        SELECT  a_id, COUNT(1) AS total_calls, isnull(SUM(DATEDIFF(MINUTE, c_time_in, c_time_out)),0) AS total_call_minutes
          FROM t_actress aa
                join t_call_log l on aa.caller_id = l.caller_id and c_time_in BETWEEN @time_from AND @time_to and c_time_out IS NOT NULL
          GROUP BY a_id;
    
    -- total live minutes
    declare @t_live table(
        a_id bigint,
        live_calls bigint,
        live_call_minutes bigint
    );
    insert into @t_live
        SELECT a_id, COUNT(*) AS live_calls, isnull(SUM(alm_minutes),0) AS live_call_minutes
          FROM t_actress a
              join t_actress_live_minute alm on alm.actress_id = a.a_id and alm_time BETWEEN @time_from AND @time_to
          GROUP BY a_id 
    
    -- total message by caller
    declare @t_cm table(
        caller_id bigint,
        pmsg_sent bigint,
        pmsg_received bigint,
        lcmsg_sent bigint,
        lcmsg_received bigint
    )
    insert into @t_cm
        SELECT l.caller_id,
                COUNT(CASE WHEN m.call_log_id       = l.c_id AND m.m_type = 2 THEN 1 END) AS _pmsg_sent,
                COUNT(CASE WHEN m.m_to_call_log_id  = l.c_id AND m.m_type = 2 THEN 1 END) AS _pmsg_received,
                COUNT(CASE WHEN m.call_log_id       = l.c_id AND m.m_type = 1 THEN 1 END) AS _lcmsg_sent,
                COUNT(CASE WHEN m.m_to_call_log_id  = l.c_id AND m.m_type = 1 THEN 1 END) AS _lcmsg_received
              FROM t_message m
                join t_call_log l on l.c_id in (m.call_log_id, m.m_to_call_log_id)  
              where m.m_time BETWEEN @time_from AND @time_to
          GROUP BY l.caller_id
    
    -- total message by actress
    declare @t_msg table(
        a_id bigint,
        pmsg_sent bigint,
        pmsg_received bigint,
        lcmsg_sent bigint,
        lcmsg_received bigint
    )
    insert into @t_msg
        select a_id, isnull(SUM(cm.pmsg_sent),0), isnull(SUM(cm.pmsg_received),0), isnull(SUM(cm.lcmsg_sent),0), isnull(SUM(cm.lcmsg_received),0)
            from t_actress a
                join @t_cm cm on a.caller_id = cm.caller_id
        group by a_id
    
    -- total credit
    declare @t_credit table(
        a_id bigint,
        credit money,
        paid_credit money
    )   
    insert into @t_credit
        SELECT a_id,    isnull(SUM(ac.ac_amount),0) AS credit, isnull(SUM(CASE ac.ac_is_paid WHEN 1 THEN ac.ac_amount else 0 END),0) AS paid_credit
            FROM t_actress a
                join t_actress_credit ac on ac.actress_id = a.a_id AND ac.ac_time BETWEEN @time_from AND @time_to
                JOIN t_order o ON o.o_id = ac.order_id and o_status = 1
        GROUP BY a_id   
    
    -- the report       
    select a.a_id, cl.c_id, cl.c_chat_line_id, a.a_first_name, a.a_last_name, 
            isnull(ac.credit,0) credit, isnull(ac.paid_credit,0) paid_credit, 
            isnull(m.pmsg_sent,0) pmsg_sent, isnull(m.pmsg_received,0) pmsg_received, isnull(m.lcmsg_sent,0) lcmsg_sent, isnull(m.lcmsg_received,0) lcmsg_received,
            isnull(l.live_calls,0) live_calls, isnull(l.live_call_minutes,0) live_call_minutes,
            isnull(c.total_calls,0) total_calls, isnull(c.total_call_minutes,0) total_call_minutes
    from t_actress a
        join t_caller cl on cl.c_id = a.caller_id
        left outer join @t_call c on c.a_id = a.a_id
        left outer join @t_live l on l.a_id = a.a_id                
        left outer join @t_msg m on m.a_id = a.a_id
        left outer join @t_credit ac on ac.a_id = a.a_id
    order by a_id
    

    【讨论】:

    • 它更快,因为相关子查询逐行运行(因此在我看来,永远不应该在生产代码中使用)并且连接在集合上运行。如果这些表中的任何一个有很多记录,您可以使用已编入索引的临时表进一步加快速度。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-20
    • 2014-10-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多