webForumDet fria alternativet

Långsam Sql-fråga

2 svar · 638 visningar · startad av Zida

ZidaMedlem sedan juni 20108 inlägg
#1

Har stött på följande sql-fråga som är väldigt långsam, någon som har tid och lust att ta en titt på hur den skulle kunna snabbas upp?

SELECT * FROM (

SELECT tblCenter.centerId as cId, tblCenter.centerName, tblCenter.centerCity as cCity, tblCenter.centerRegistrationGoal FROM tblregistrations INNER JOIN tblCenter ON tblCourses.centerId = tblCenter.centerId LEFT JOIN tblCourses ON tblCourses.courseId = tblregistrations.regCourseId AND tblRegistrations.regTransactionStatus <> 'Failed' AND tblRegistrations.regTransactionStatus <> 'Started' WHERE tblCourses.courseExpire >= '" + currentDate + "' AND tblCenter.siteId = 1 AND tblCenter.centerDanceCenter = 1 GROUP BY tblCenter.centerCity ORDER BY tblCenter.centerCity) as t1 

LEFT JOIN (

SELECT COUNT(tblregistrations.regId) as regCount, tblCenter.centerId as cId FROM tblregistrations INNER JOIN tblCenter ON tblCourses.centerId = tblCenter.centerId LEFT JOIN tblCourses ON tblCourses.courseId = tblregistrations.regCourseId AND tblRegistrations.regTransactionStatus <> 'Failed' AND tblRegistrations.regTransactionStatus <> 'Started' WHERE tblCourses.courseExpire >= '" + currentDate + "' AND tblCenter.siteId = 1 AND tblCenter.centerDanceCenter = 1 AND tblRegistrations.regPaymentMetohod = 'Faktura' OR tblRegistrations.regPaymentMetohod = 'Payex' GROUP BY tblCenter.centerCity ORDER BY tblCenter.centerCity) as t2 ON t1.cId = t2.cId 

LEFT JOIN (

SELECT COUNT(tblregistrations.regId) as reserveCount, tblCenter.centerId as cId FROM tblregistrations INNER JOIN tblCenter ON tblCourses.centerId = tblCenter.centerId LEFT JOIN tblCourses ON tblCourses.courseId = tblregistrations.regCourseId AND tblRegistrations.regTransactionStatus <> 'Failed' AND tblRegistrations.regTransactionStatus <> 'Started' WHERE tblCourses.courseExpire >= '" + currentDate + "' AND tblCenter.siteId = 1 AND tblCenter.centerDanceCenter = 1 AND tblRegistrations.regPaymentMetohod = 'Reserv' GROUP BY tblCenter.centerCity ORDER BY tblCenter.centerCity) as t3 ON t1.cId = t3.cId GROUP BY t1.cCity
GildebrandMedlem sedan juni 2009918 inlägg
#2

Hur stora är tabellerna den jobbar mot?

aasahMedlem sedan mars 20033 451 inlägg
#3

Oj! Inte undra på att den tar tid... börjar med att indentera eländet så man har en chans att se vad som händer:

SELECT
        *
FROM
        (
                SELECT
                        tblCenter.centerId as cId,
                        tblCenter.centerName,
                        tblCenter.centerCity as cCity,
                        tblCenter.centerRegistrationGoal
                FROM
                        tblregistrations
                        INNER JOIN tblCenter ON
                                tblCourses.centerId = tblCenter.centerId
                        LEFT JOIN tblCourses ON
                                tblCourses.courseId = tblregistrations.regCourseId AND
                                tblRegistrations.regTransactionStatus <> 'Failed' AND
                                tblRegistrations.regTransactionStatus <> 'Started'
               WHERE
                        tblCourses.courseExpire >= '" + currentDate + "' AND
                        tblCenter.siteId = 1 AND
                        tblCenter.centerDanceCenter = 1
                GROUP BY
                        tblCenter.centerCity
                ORDER BY
                        tblCenter.centerCity
        ) as t1
        LEFT JOIN (
                SELECT
                        COUNT(tblregistrations.regId) as regCount,
                        tblCenter.centerId as cId
                FROM
                        tblregistrations
                        INNER JOIN tblCenter ON
                                tblCourses.centerId = tblCenter.centerId
                        LEFT JOIN tblCourses ON
                                tblCourses.courseId = tblregistrations.regCourseId AND
                                tblRegistrations.regTransactionStatus <> 'Failed' AND
                                tblRegistrations.regTransactionStatus <> 'Started'
                WHERE
                        tblCourses.courseExpire >= '" + currentDate + "' AND
                        tblCenter.siteId = 1 AND
                        tblCenter.centerDanceCenter = 1 AND
                        tblRegistrations.regPaymentMetohod = 'Faktura' OR
                        tblRegistrations.regPaymentMetohod = 'Payex'
                GROUP BY
                        tblCenter.centerCity
                ORDER BY
                        tblCenter.centerCity
        ) as t2 ON
                t1.cId = t2.cId
        LEFT JOIN (
                SELECT
                        COUNT(tblregistrations.regId) as reserveCount,
                        tblCenter.centerId as cId
                FROM
                        tblregistrations
                        INNER JOIN tblCenter ON
                                tblCourses.centerId = tblCenter.centerId
                        LEFT JOIN tblCourses ON
                                tblCourses.courseId = tblregistrations.regCourseId AND
                                tblRegistrations.regTransactionStatus <> 'Failed' AND
                                tblRegistrations.regTransactionStatus <> 'Started'
                WHERE
                        tblCourses.courseExpire >= '" + currentDate + "' AND
                        tblCenter.siteId = 1 AND
                        tblCenter.centerDanceCenter = 1 AND
                        tblRegistrations.regPaymentMetohod = 'Reserv'
                GROUP BY
                        tblCenter.centerCity
                ORDER BY
                        tblCenter.centerCity
        ) as t3 ON
                t1.cId = t3.cId
        GROUP BY t1.cCity

Här finns åtskilligt att notera och förändra!

Notera först något "livsfarligt"
Du har i mittfrågan:

WHERE
        tblCourses.courseExpire >= '" + currentDate + "' AND
        tblCenter.siteId = 1 AND
        tblCenter.centerDanceCenter = 1 AND
        tblRegistrations.regPaymentMetohod = 'Faktura' OR
        tblRegistrations.regPaymentMetohod = 'Payex'

Blanda aldrig AND och OR utan parenteser! Vad säger du här? Jo att antingen ska detta gälla:

        tblCourses.courseExpire >= '" + currentDate + "' AND
        tblCenter.siteId = 1 AND
        tblCenter.centerDanceCenter = 1 AND
        tblRegistrations.regPaymentMetohod = 'Faktura'

eller detta:

        tblRegistrations.regPaymentMetohod = 'Payex'

Men jag gissar att du menar:

WHERE
        tblCourses.courseExpire >= '" + currentDate + "' AND
        tblCenter.siteId = 1 AND
        tblCenter.centerDanceCenter = 1 AND 
        (
                tblRegistrations.regPaymentMetohod = 'Faktura' OR
                tblRegistrations.regPaymentMetohod = 'Payex'
        )

Du gör väsentligen exakt samma JOINer på exakt samma tre tabeller tre skilda gånger
i stället för att göra JOINen bara en gång och sedan återanvända resultatet genom att spara undan JOIN-resultatet i en temptabell.

Du vill alltså, väsentligen, spara undan resultatet av:

SELECT
        tblCenter.centerId,
        tblCenter.centerName,
        tblCenter.centerCity,
        tblCenter.centerRegistrationGoal,
        tblCourses.centerId,
        tblCourses.courseId,
        tblRegistrations.regCourseId,
        tblRegistrations.regTransactionStatus,
        tblRegistrations.regId,
        tblRegistrations.regPaymentMetohod
FROM
        tblregistrations
        INNER JOIN tblCenter ON
                tblCourses.centerId = tblCenter.centerId
        LEFT JOIN tblCourses ON
                tblCourses.courseId = tblregistrations.regCourseId AND
                tblRegistrations.regTransactionStatus <> 'Failed' AND
                tblRegistrations.regTransactionStatus <> 'Started'
WHERE -- Följande villkor finns i alla tre fälten
        tblCourses.courseExpire >= '" + currentDate + "' AND
        tblCenter.siteId = 1 AND
        tblCenter.centerDanceCenter = 1

vilket är precis de kolumner du behöver för samtliga tre subfrågor ovan i SELECT, ON och WHERE-delarna.

Men, då kommer vi till ett stort problem för den här koden. Så här bör inte en serie JOINer se ut! Se till att JOIN:a tabeller i den ordning du kan knyta ihop dem i varandra, så att du hela tiden kan matcha "nästa" tabell mot de som står högre upp! Då får du med så få rader som möjligt i JOINen och ju färre rader, ju fortare går det. Detta därför att DB:n direkt ser om det finns matcher till den här raden eller inte och behöver bara titta på (åtminstone) den första tabellen en gång.

Visserligen spelar det roll om tabellerna hämtas ut med INNER JOIN eller LEFT JOIN och generellt bör alla INNER JOINer stå före LEFT JOINerna, men då måste man också kunna binda ihop INNER JOINade tabeller innan man blandar in LEFT JOINade, och det gör inte du. Både tblRegistrations och tblCenter JOINas mot tblCourses som kommer in med LEFT JOIN, vilket gör resultatet tveksamt om du verkligen har en INNER JOIN där du tror? Samtidigt kräver du i WHERE att både tabellen tblCenter och tblCourses ska ha andra värden än NULL på vissa av sina kolumner, varför du ändå måste ha matchning hela vägen. En INNER JOIN är alltid mindre resurskrävande än en LEFT JOIN så vi ersätter med INNER JOIN.

Vad använder du för DBMS? Jag är inte säker på om syntaxen för temptabeller ser likadan ut överallt? Exemplet nedan använder Microsoft SQL Servers T-SQL, men det finns motsvarigheter i andra system.

En första del i lösningen blir därför:

SELECT
        R.regId,
        R.regTransactionStatus,
        R.regPaymentMetohod,  -- Borde det vara Method månne?
        Co.CourseId,
        Ctr.centerId,
        Ctr.centerName,
        Ctr.centerCity,
        Ctr.centerRegistrationGoal,
        Ctr.centerId
INTO #CCR
FROM
        tblregistrations R
        INNER JOIN tblCourses Co ON
                Co.courseId = R.regCourseId AND
                R.regTransactionStatus <> 'Failed' AND
                R.regTransactionStatus <> 'Started'
        INNER JOIN tblCenter Ctr ON
                Co.centerId = Ctr.centerId
WHERE  
        Co.courseExpire >= '" + currentDate + "' AND
        Ctr.siteId = 1 AND
        Ctr.centerDanceCenter = 1

Nu skapar du en temptabell #CCR (namnval efter CoursesCenterRegistrations) där varje kolumn heter samma som i ursprungsfrågan.

Eftersom vi nu bara har INNER JOINer och vi kräver att Co.courseId = R.regCourseId och Co.centerId = Ctr.centerId, så behöver vi inte ta ut alla fyra dessa kolumner, det räcker med en ur varje jämförelse. (Ju färre kolumner, ju snabbare frågor.)

I och med att du skapar en temptabell måste du också komma ihåg att ta bort den när du är färdig med den med:

DROP TABLE #CCR

Nu kan vi, till att börja med, förenkla din ursprungliga fråga till:

SELECT
        *
FROM
        (
                SELECT
                        centerId as cId,
                        centerName,
                        centerCity as cCity,
                        centerRegistrationGoal
                FROM
                        #CCR
                GROUP BY
                        centerCity
                ORDER BY
                        centerCity
        ) as t1
        LEFT JOIN (
                SELECT
                        COUNT(regId) as regCount,
                        centerId as cId
                FROM
                        #CCR
                WHERE
                        regPaymentMetohod = 'Faktura' OR
                        regPaymentMetohod = 'Payex'
                GROUP BY
                        centerCity
                ORDER BY
                        centerCity
        ) as t2 ON
                t1.cId = t2.cId
        LEFT JOIN (
                SELECT
                        COUNT(regId) as reserveCount,
                        centerId as cId
                FROM
                        #CCR
                WHERE
                        regPaymentMetohod = 'Reserv'
                GROUP BY
                        centerCity
                ORDER BY
                        centerCity
        ) as t3 ON
                t1.cId = t3.cId
        GROUP BY t1.cCity

Det finns mer att göra, men detta var början.

Sedan ska vi titta lite närmare på alla dessa GROUP BY.
GROUP BY används när man vill räkna ut antal, min, max, summa eller medel av något och gruppera per en eller flera kolumner. Vill man inte det ska man inte använda GROUP BY som är en jobbig operation för SQL Servern. Är man bara ute efter att slippa dubbletter använder man DISTINCT.

I t1 ovan görs inga uträkningar alls, skippa GROUP BY.
I slutresultatet görs inga uträkningar alls, skippa GROUP BY.
I t1, t2 och t3 kvittar det hur raderna är ordnade det är bara i slutresultatet du bryr dig om detta. Ordna därför om resultatet i slutet en gång, inte tre gånger.

Det finns ingen anledning att ha t1 som ett subuttryck, däremot allt skäl att inte ha det. Ju färre subuttryck i FROM, ju fortare går det generellt sätt. (Det finns gånger då det är berättigat och snabbare upp, men man ska veta vad man gör.

Resultat:

SELECT
        R.regId,
        R.regTransactionStatus,
        R.regPaymentMetohod,  -- Borde det vara Method månne?
        Co.CourseId,
        Ctr.centerId,
        Ctr.centerName,
        Ctr.centerCity,
        Ctr.centerRegistrationGoal,
        Ctr.centerId
INTO #CCR
FROM
        tblregistrations R
        INNER JOIN tblCourses Co ON
                Co.courseId = R.regCourseId AND
                R.regTransactionStatus <> 'Failed' AND
                R.regTransactionStatus <> 'Started'
        INNER JOIN tblCenter Ctr ON
                Co.centerId = Ctr.centerId
WHERE
        Co.courseExpire >= '" + currentDate + "' AND
        Ctr.siteId = 1 AND
        Ctr.centerDanceCenter = 1

SELECT
        t1.centerId as cId,
        t1.centerName,
        t1.centerCity as cCity,
        t1.centerRegistrationGoal,
        ISNULL(t2.regCount, 0) as regCount,
        ISNULL(t3.reserveCount, 0) as reserveCount
FROM
        #CCR t1
        LEFT JOIN (
                SELECT
                        COUNT(regId) as regCount,
                        centerId as cId
                FROM
                        #CCR
                WHERE
                        regPaymentMetohod = 'Faktura' OR
                        regPaymentMetohod = 'Payex'
                GROUP BY
                        centerCity
        ) as t2 ON
                t1.centerId = t2.cId
        LEFT JOIN (
                SELECT
                        COUNT(regId) as reserveCount,
                        centerId as cId
                FROM
                        #CCR
                WHERE
                        regPaymentMetohod = 'Reserv'
                GROUP BY
                        centerCity
        ) as t3 ON
                t1.centerId = t3.cId
ORDER BY
        t1.centerCity

-- Städa        
DROP TABLE #CCR

Det här bör gå AVSEVÄRT snabbare.

Sedan finns det ju mer avancerade metoder att ta till: index, optimera join-ordningar mm men då måste man veta mer om hur stora tabellerna är och hur de används.

Genererad på 371 ms · cache AV · v20260730165559-full.f96bc7eb