jin*_*ch 5 performance sql-server optimization t-sql execution-plan query-performance
我正在尝试优化这个表值函数。如果可以,我会将其更改为程序,但我不能。问题在于两个更新语句。我只在函数中保留了这两个,因为它们导致了主要的性能问题。我将第一个从外部应用重写为内部连接,我查看了统计数据,发现它们是错误的,所以我添加了一个选项(重新编译),它有很大帮助。问题出在第二次更新中。统计有误,不知道如何制定合适的执行计划,并通过提示进行优化。你知道如何缩短时间吗?我试图索引表变量,但没有结果。
这是一个执行计划https://www.brentozar.com/pastetheplan/?id=B1EdBo5e4
谢谢。
CREATE FUNCTION [dbo].[cfn_PlanServis_Seznam](
@IDVazRole INT,
@IDUzivatel INT,
@IDRole INT,
@IDLokalita INT,
@lCid INT
)
RETURNS @PlanServis TABLE(
lIDAuto INT,
szSPZ VARCHAR(100),
lDepozit INT,
szTypVozidla varchar(100),
szTypServisu NVARCHAR(300),
szServisniPlan NVARCHAR(300),
lZbyvaDni INT,
lZbyvaKm INT,
lNajetoKm INT,
dtServis DATETIME,
dcZbyvaMotohodin DECIMAL(15,1),
dcNajetoMotohodin DECIMAL(15,1),
IDVazPlanServisAuto INT,
IDPlanServisDefinice INT,
lBarva INT
)
AS
BEGIN
DECLARE @Auto TABLE(
lIDAuto INT,
szSPZ VARCHAR(100),
szTyp VARCHAR(100),
IDCisTypServis INT,
szTypServisu NVARCHAR(500),
szServisniPlan NVARCHAR(500),
lKmStart INT,
dtStart DATETIME,
lKmPriZavedeni INT,
lUjetoPredZavedenim INT,
dcMotohodinyStart DECIMAL(15,1),
lIntervalKm INT,
dcIntervalMotohodiny DECIMAL(15,1),
lUjeto INT,
dcMotohodiny DECIMAL(15,1),
IDServis INT,
lKmServis INT,
dcMotohodinyServis DECIMAL(15,1),
dtServis DATETIME,
lIntervalDatum INT,
lDniUbehlo INT,
lBarva INT,
lZbyvaKm INT,
dcZbyvaMotohodin DECIMAL(15,2),
lZbyvaDni INT,
lDepozit INT,
IDVazPlanServisAuto INT,
IDPlanServisDefinice INT,
lMaxTachograf INT,
lKmPretaceni INT,
dtOd DATE,
lKmPosledniServis INT
)
DECLARE @IDCisAutoParametrKmPriZavadeni INT = 10012
DECLARE @lKmPred INT = 30000
DECLARE @lKmPredMensi INT = 15000
DECLARE @lDniPred INT = 60
DECLARE @lDniPredMensi INT = 30
DECLARE @lMotohodinyPred INT = 100
DECLARE @lMotohodinyPredMensi INT = 50
DECLARE @IDBarvaBlizi INT = 1010 --Odkaz do CisTermBarva
DECLARE @IDBarvaBliziMensi INT = 1016 --Odkaz do CisTermBarva
DECLARE @IDBarvaPres INT = 1017 --Odkaz do CisTermBarva
--============ Koenc deklarace promennych ===========
INSERT INTO @Auto (lIDAuto, szSPZ, szTyp, IDCisTypServis, szTypServisu, lKmStart, dtStart, lKmPriZavedeni, dcMotohodinyStart,[@Auto].lIntervalKm,[@Auto].dcIntervalMotohodiny,[@Auto].lIntervalDatum,szServisniPlan,[@Auto].IDVazPlanServisAuto,[@Auto].IDPlanServisDefinice)
SELECT Auto.lIDAuto,
Auto.szSpz,
CASE WHEN Auto.lTyp = 0 THEN 'Taha?' WHEN Auto.lTyp = 1 THEN 'N?v?s' ELSE '' END,
PlanServisDefinice.IDCisTypServis,
dbo.GetLocalText('CisTypServis','szNazev',CisTypServis.lIDCisTypServis,@lCid,CisTypServis.szNazev,''),
PlanServisDefinice.lStartKM,
PlanServisDefinice.dtStartDatum,
CONVERT(INT,VazAutoParametr.varHodnota),
PlanServisDefinice.dcMotohodinyStart,
PlanServisDefinice.lIntervalKM,
PlanServisDefinice.dcIntervalMotohodin,
PlanServisDefinice.lIntervalDatum,
PlanServis.szNazev,
PlanServisDefinice.IDVazPlanServisAuto,
PlanServisDefinice.lIDPlanServisDefinice
FROM Auto INNER JOIN VazPlanServisAuto ON Auto.lIDAuto = VazPlanServisAuto.IDAuto
INNER JOIN PlanServisDefinice ON VazPlanServisAuto.lIDVazPlanServisAuto = PlanServisDefinice.IDVazPlanServisAuto
INNER JOIN CisTypServis ON PlanServisDefinice.IDCisTypServis = CisTypServis.lIDCisTypServis
LEFT OUTER JOIN VazAutoParametr ON VazAutoParametr.IDAuto = Auto.lIDAuto AND VazAutoParametr.IDCisAutoParametr = @IDCisAutoParametrKmPriZavadeni
INNER JOIN PlanServis ON VazPlanServisAuto.IDPlanServisu = PlanServis.lIDPlanServis
UPDATE @Auto SET lUjeto = Km.lKm
FROM @Auto INNER JOIN
(SELECT
SUM(JizdaTachograf.lkmDo - JizdaTachograf.lkmOd) AS lKm, JizdaTachograf.IDAuto,JizdaTachograf.IDNaves
FROM Jizda
INNER JOIN JizdaTachograf ON JizdaTachograf.IDJizda = Jizda.lIDJizda
WHERE JizdaTachograf.lkmOd IS NOT NULL
AND JizdaTachograf.lkmDo IS NOT NULL
AND Jizda.lProvozne = 1
GROUP BY
JizdaTachograf.IDAuto,JizdaTachograf.IDNaves
) as Km
ON Km.IDAuto = [@Auto].lIDAuto OR Km.IDNaves = [@Auto].lIDAuto
OPTION (RECOMPILE)
UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
FROM @Auto
OUTER APPLY
(SELECT TOP 1 JizdaTachograf.lkmDo lKm
FROM Jizda INNER JOIN
JizdaTachograf ON JizdaTachograf.IDJizda = Jizda.lIDJizda
WHERE JizdaTachograf.lkmOd IS NOT NULL AND
JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1
AND (JizdaTachograf.IDAuto = [@Auto].lIDAuto
OR JizdaTachograf.IDNaves = [@Auto].lIDAuto)
AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis,
[@Auto].dtStart),DATEADD(YEAR,-100,GETDATE()))
ORDER BY
Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc) Km
INSERT INTO @PlanServis (lIDAuto,
szSPZ,
lDepozit,
szTypVozidla,
szTypServisu,
szServisniPlan,
lZbyvaDni,
lZbyvaKm,
lNajetoKm,
dtServis,
dcZbyvaMotohodin,
dcNajetoMotohodin,
IDVazPlanServisAuto,
IDPlanServisDefinice,
lBarva)
SELECT lIDAuto,
szSPZ,
lDepozit,
szTyp,
szTypServisu,
szServisniPlan,
lZbyvaDni,
lZbyvaKm,
lUjeto,--lNajetoKm,
dtServis,
dcZbyvaMotohodin,
dcMotohodiny,--dcNajetoMotohodin,
IDVazPlanServisAuto,
IDPlanServisDefinice,
lBarva
FROM @Auto
RETURN
END
GO
Run Code Online (Sandbox Code Playgroud)
对于这样的问题,提供MCVE非常有帮助。事实上,我必须对表结构和数据分布做出很多猜测。你说这部分查询计划太慢了,没有任何进一步的阐述:
我可以看到该部分可能缓慢的三个原因。第一个问题是两个表中总共只有 176k 行,但索引查找从两个表中提取了超过 800k 行。第二个问题是 JizdaTachograf 上的索引查找仅具有以下查找谓词:[Lori_MDL].[dbo].[JizdaTachograf].lkmOd IS NOT NULL
。我认为这可能是有选择性的,但如果不是,那么您将有效地扫描大多数索引 845 表。第三个问题是总共对 800k 行进行排序,尽管排序分为 846 次迭代。
可能有一种方法可以得到一个只对两个表进行一次扫描的计划,但如果不了解数据分布,我不知道这是否值得。查询的要求(不等式、排序、OR 逻辑)使得合并联接或散列联接难以发挥作用。
您可以解决的一个问题是第二个问题。如果您定义正确的索引并分成两个子查询,那么您可以在直接查找相关行的过程(JizdaTachograf.IDAuto = [@Auto].lIDAuto OR JizdaTachograf.IDNaves = [@Auto].lIDAuto)
中获得更有效的索引查找。JizdaTachograf
如果表中的大多数行都具有非 NULL 值,则可以节省大量时间lkmOd
。有许多不同的索引定义可以工作。下面有两个:
CREATE INDEX IX2 ON JizdaTachograf (IDAuto, IDJizda, lkmDo) INCLUDE (lkmOd)
WHERE lkmOd IS NOT NULL AND lkmDo IS NOT NULL;
CREATE INDEX IX3 ON JizdaTachograf (IDNaves, IDJizda, lkmDo) INCLUDE (lkmOd)
WHERE lkmOd IS NOT NULL AND lkmDo IS NOT NULL;
Run Code Online (Sandbox Code Playgroud)
然后我拆分查询,以便 SQL Server 可以利用索引。
UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
FROM @Auto
OUTER APPLY
(
SELECT TOP (1) lKm
FROM
(
SELECT TOP (1) Jizda.dtZacatek, JizdaTachograf.lkmDo lKm
FROM JizdaTachograf
INNER JOIN Jizda WITH (INDEX(1)) ON JizdaTachograf.IDJizda = Jizda.lIDJizda
WHERE JizdaTachograf.lkmOd IS NOT NULL AND
JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1
AND JizdaTachograf.IDAuto = [@Auto].lIDAuto -- first half
AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE()))
ORDER BY Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc
UNION ALL
SELECT TOP (1) Jizda.dtZacatek, JizdaTachograf.lkmDo lKm
FROM JizdaTachograf
INNER JOIN Jizda WITH (INDEX(1)) ON JizdaTachograf.IDJizda = Jizda.lIDJizda
WHERE JizdaTachograf.lkmOd IS NOT NULL AND
JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1
AND JizdaTachograf.IDNaves = [@Auto].lIDAuto -- second half
AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE()))
ORDER BY Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc
) IDNaves_IDAuto
ORDER BY dtZacatek DESC, lKm DESC
) Km;
Run Code Online (Sandbox Code Playgroud)
我正在使用空表,但我可以证明至少有可能获得所需的计划形状:
该计划的优点是它会执行更少的 IO JizdaTachograf
,并且分类会进一步分散。但是,您仍然从两个索引中提取相同数量的行并对相同的总行数进行排序。
可以编写此查询而不进行排序。IO 模式不同,这可能会导致总体读取次数减少。您将需要另一个索引。下面是一个有效的方法:
CREATE INDEX IX1 ON Jizda (dtZacatek) INCLUDE (lIDJizda, lProvozne)
WHERE lProvozne = 1;
Run Code Online (Sandbox Code Playgroud)
优化器并不总是能对排序数据做出相同的推断,因此我更改了查询以使其了解不需要排序:
UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
FROM @Auto
OUTER APPLY
(
SELECT TOP (1) lkmDo lKm
FROM
(
SELECT TOP (1) Jizda.dtZacatek, ca.lkmDo
FROM Jizda
CROSS APPLY (
SELECT TOP (1) JizdaTachograf.lkmDo
FROM JizdaTachograf
WHERE JizdaTachograf.IDJizda = Jizda.lIDJizda
AND JizdaTachograf.lkmOd IS NOT NULL AND
JizdaTachograf.lkmDo IS NOT NULL
AND JizdaTachograf.IDNaves = [@Auto].lIDAuto -- this line is different
ORDER BY JizdaTachograf.lkmDo DESC
) ca
WHERE Jizda.lProvozne = 1
AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis,[@Auto].dtStart),DATEADD(YEAR,-100,GETDATE()))
ORDER BY Jizda.dtZacatek DESC
UNION ALL
SELECT TOP (1) Jizda.dtZacatek, ca.lkmDo
FROM Jizda
CROSS APPLY (
SELECT TOP (1) JizdaTachograf.lkmDo
FROM JizdaTachograf
WHERE JizdaTachograf.IDJizda = Jizda.lIDJizda
AND JizdaTachograf.lkmOd IS NOT NULL AND
JizdaTachograf.lkmDo IS NOT NULL
AND JizdaTachograf.IDAuto = [@Auto].lIDAuto -- this line is different
ORDER BY JizdaTachograf.lkmDo DESC
) ca
WHERE Jizda.lProvozne = 1
AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis,[@Auto].dtStart),DATEADD(YEAR,-100,GETDATE()))
ORDER BY Jizda.dtZacatek DESC
) IDNaves_IDAuto
ORDER BY dtZacatek DESC, lkmDo DESC
) Km
Run Code Online (Sandbox Code Playgroud)
现在没有任何排序:
然而,这样做的优化有点危险。现在Jizda
是嵌套循环连接的外表。考虑 in 中的一行,@Auto
其中 NULL [@Auto].dtServis
、 NULL [@Auto].dtStart
,并且没有与JizdaTachograf
byIDNaves
和匹配IDAuto
。SQL Server 将读取所有 180k 行Jizda
并执行 180k 索引查找JizdaTachograf
,最终不返回任何行。我不知道这种情况发生的可能性有多大,但它有可能发生。
根据问题中提供的信息,我的建议是尝试第一个查询,看看它是否足够快。如果没有,请使用过滤器来实现这两个查询。扫描 845 行表变量根本不需要时间,因此您可以通过对表的不同部分进行操作的两个单独的 UPDATE` 语句来充分利用这两个查询。当没有非 NULL 日期列时,第一个查询可能会更有效:
WHERE [@Auto].dtServis IS NULL AND [@Auto].dtStart IS NULL;
Run Code Online (Sandbox Code Playgroud)
当存在非 NULL 日期列时,第二个查询可能会更有效(我假设该列具有一定的选择性):
WHERE [@Auto].dtServis IS NOT NULL OR [@Auto].dtStart IS NOT NULL
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
3585 次 |
最近记录: |