Общи правила за опростяване на SQL изрази

Търся някои "правила за извод" (подобни на правила за задаване на операции или логически правила), които мога да използвам, за да намаля сложността или размера на SQL заявка. Съществува ли нещо такова? Някакви документи, някакви инструменти? Някакви еквивалентности, които сте открили сами? По някакъв начин е подобно на оптимизирането на заявки, но не по отношение на производителността.

За да го заявите различно: Имайки (сложна) заявка с JOIN, SUBSELECT, UNION, възможно ли е (или не) да я намалите до по-прост, еквивалентен SQL оператор, който дава същия резултат, като използвате някои правила за трансформация?

И така, търся еквивалентни трансформации на SQL изрази като факта, че повечето SUBSELECT могат да бъдат пренаписани като JOIN.


person MicSim    schedule 01.07.2009    source източник
comment
Моят подход е да науча релационната теория като цяло и релационната алгебра в частност. След това се научете да забелязвате конструкциите, използвани в SQL за прилагане на оператори от релационната алгебра (напр. универсално количествено определяне, известно още като деление) и смятане (напр. екзистенциално количествено определяне). Лошото е, че SQL има функции, които не се срещат в релационния модел, напр. нули, които вероятно е най-добре да се преработят така или иначе. Препоръчителна литература: SQL и релационна теория: Как да пишем точно SQL код по C. J. дата.   -  person onedaywhen    schedule 13.03.2012


Отговори (8)


За да го заявите различно: Имайки (сложна) заявка с JOIN, SUBSELECT, UNION, възможно ли е (или не) да я намалите до по-прост, еквивалентен SQL оператор, който дава същия резултат, като използвате някои правила за трансформация?

Точно с това си изкарват прехраната оптимизаторите (не че казвам, че винаги се справят добре).

Тъй като SQL е базиран на набор език, обикновено има повече от един начин за преобразуване на една заявка в друга.

Като тази заявка:

SELECT  *
FROM    mytable
WHERE   col1 > @value1 OR col2 < @value2

може да се трансформира в това:

SELECT  *
FROM    mytable
WHERE   col1 > @value1
UNION
SELECT  *
FROM    mytable
WHERE   col2 < @value2

или това:

SELECT  mo.*
FROM    (
        SELECT  id
        FROM    mytable
        WHERE   col1 > @value1
        UNION
        SELECT  id
        FROM    mytable
        WHERE   col2 < @value2
        ) mi
JOIN    mytable mo
ON      mo.id = mi.id

, които изглеждат по-грозни, но могат да доведат до по-добри планове за изпълнение.

Едно от най-често срещаните неща е да замените тази заявка:

SELECT  *
FROM    mytable
WHERE   col IN
        (
        SELECT  othercol
        FROM    othertable
        )

с този:

SELECT  *
FROM    mytable mo
WHERE   EXISTS
        (
        SELECT  NULL
        FROM    othertable o
        WHERE   o.othercol = mo.col
        )

В някои RDBMS (като PostgreSQL), DISTINCT и GROUP BY използват различни планове за изпълнение, така че понякога е по-добре да замените единия с другия:

SELECT  mo.grouper,
        (
        SELECT  SUM(col)
        FROM    mytable mi
        WHERE   mi.grouper = mo.grouper
        )
FROM    (
        SELECT  DISTINCT grouper
        FROM    mytable
        ) mo

vs.

SELECT  mo.grouper, SUM(col)
FROM    mytable
GROUP BY
        mo.grouper

В PostgreSQL, DISTINCT сортира и GROUP BY хешове.

MySQL липсва FULL OUTER JOIN, така че може да се пренапише като следното:

SELECT  t1.col1, t2.col2
FROM    table1 t1
LEFT OUTER JOIN
        table2 t2
ON      t1.id = t2.id

vs.

SELECT  t1.col1, t2.col2
FROM    table1 t1
LEFT JOIN
        table2 t2
ON      t1.id = t2.id
UNION ALL
SELECT  NULL, t2.col2
FROM    table1 t1
RIGHT JOIN
        table2 t2
ON      t1.id = t2.id
WHERE   t1.id IS NULL

, но вижте тази статия в моя блог за това как да направите това по-ефективно в MySQL:

Тази йерархична заявка в Oracle:

SELECT  DISTINCT(animal_id) AS animal_id
FROM    animal
START WITH
        animal_id = :id
CONNECT BY
        PRIOR animal_id IN (father, mother)
ORDER BY
        animal_id

може да се трансформира в това:

SELECT  DISTINCT(animal_id) AS animal_id
FROM    (
        SELECT  0 AS gender, animal_id, father AS parent
        FROM    animal
        UNION ALL
        SELECT  1, animal_id, mother
        FROM    animal
        )
START WITH
        animal_id = :id
CONNECT BY
        parent = PRIOR animal_id
ORDER BY
        animal_id

, като последният е по-ефективен.

Вижте тази статия в моя блог за подробности относно плана за изпълнение:

За да намерите всички диапазони, които се припокриват с дадения диапазон, можете да използвате следната заявка:

SELECT  *
FROM    ranges
WHERE   end_date >= @start
        AND start_date <= @end

, но в SQL Server тази по-сложна заявка дава същите резултати по-бързо:

SELECT  *
FROM    ranges
WHERE   (start_date > @start AND start_date <= @end)
        OR (@start BETWEEN start_date AND end_date)

, и вярвате или не, имам статия в блога си и за това:

SQL Server също няма ефективен начин за извършване на кумулативни агрегати, така че тази заявка:

SELECT  mi.id, SUM(mo.value) AS running_sum
FROM    mytable mi
JOIN    mytable mo
ON      mo.id <= mi.id
GROUP BY
        mi.id

могат да бъдат по-ефективно пренаписани с помощта на, Господи, помогни ми, курсори (правилно ме чухте: cursors, more efficiently и SQL Server в едно изречение).

Вижте тази статия в моя блог за това как да го направите:

Има определен вид заявка, често срещана във финансовите приложения, която търси ефективния курс за валута, като тази в Oracle:

SELECT  TO_CHAR(SUM(xac_amount * rte_rate), 'FM999G999G999G999G999G999D999999')
FROM    t_transaction x
JOIN    t_rate r
ON      (rte_currency, rte_date) IN
        (
        SELECT  xac_currency, MAX(rte_date)
        FROM    t_rate
        WHERE   rte_currency = xac_currency
                AND rte_date <= xac_date
        )

Тази заявка може да бъде силно пренаписана, за да използва условие за равенство, което позволява HASH JOIN вместо NESTED LOOPS:

WITH v_rate AS
        (
        SELECT  cur_id AS eff_currency, dte_date AS eff_date, rte_rate AS eff_rate
        FROM    (
                SELECT  cur_id, dte_date,
                        (
                        SELECT  MAX(rte_date)
                        FROM    t_rate ri
                        WHERE   rte_currency = cur_id
                                AND rte_date <= dte_date
                        ) AS rte_effdate
                FROM    (
                        SELECT  (
                                SELECT  MAX(rte_date)
                                FROM    t_rate
                                ) - level + 1 AS dte_date
                        FROM    dual
                        CONNECT BY
                                level <=
                                (
                                SELECT  MAX(rte_date) - MIN(rte_date)
                                FROM    t_rate
                                )
                        ) v_date,
                        (
                        SELECT  1 AS cur_id
                        FROM    dual
                        UNION ALL
                        SELECT  2 AS cur_id
                        FROM    dual
                        ) v_currency
                ) v_eff
        LEFT JOIN
                t_rate
        ON      rte_currency = cur_id
                AND rte_date = rte_effdate
        )
SELECT  TO_CHAR(SUM(xac_amount * eff_rate), 'FM999G999G999G999G999G999D999999')
FROM    (
        SELECT  xac_currency, TRUNC(xac_date) AS xac_date, SUM(xac_amount) AS xac_amount, COUNT(*) AS cnt
        FROM    t_transaction x
        GROUP BY
                xac_currency, TRUNC(xac_date)
        )
JOIN    v_rate
ON      eff_currency = xac_currency
        AND eff_date = xac_date

Въпреки че е адски обемиста, последната заявка е 6 пъти по-бърза.

Основната идея тук е замяната на <= с =, което изисква изграждане на календарна таблица в паметта. до JOIN с.

person Quassnoi    schedule 01.07.2009
comment
Грешка в първия ви пример: UNION прави ИЛИ, а не И. - person Alex Martelli; 01.07.2009
comment
+1 Това са някои чудесни примери за трансформации на заявки. Също така показва, че някои от оптимизираните заявки всъщност не са просто изглеждащите, напр. първа заявка срещу трета, което е жалко, тъй като може да се предположи, че простата заявка ще бъде по-лесна за анализиране от оптимизатора. С други думи изглежда, че оптимизирането не е необходимо равно на опростяването - person kristof; 01.07.2009
comment
Патриот ;), не съм съгласен с това, защото UNION елиминира дубликати, тезите не са еквивалентни: Като тази заявка: SELECT * FROM mytable WHERE col1 › @value1 OR col2 ‹ @value2 може да се трансформира в това: SELECT * FROM mytable WHERE col1 › @value1 UNION SELECT * FROM mytable WHERE col2 ‹ @value2 - person A-K; 01.07.2009
comment
@Alex: докато таблицата има дефиниран ПЪРВИЧЕН КЛЮЧ, те са еквивалентни. Ред, който отговаря на двете условия с ИЛИ, ще бъде избран точно веднъж, било то с ИЛИ или с UNION. Ако таблицата има точни дубликати (което означава, че няма PRIMARY KEY), тогава да, те ще бъдат елиминирани с UNION, но не и с OR. - person Quassnoi; 01.07.2009
comment
@Quassnoi: Първата част: пренаписване на OR в UNION. След това получих Index Seeks вместо Clustered Index Scans (на SQL Server 2000) ... доста добре. - person MicSim; 01.10.2010
comment
Харесва ми, че посочихте, че в SQl грозният код често е най-добрият за производителност. Подлудява ме, когато хората искат да вземат добре работещ код и да го направят по-елегантен и да убият производителността. - person HLGEM; 13.07.2011

Ето няколко от работата с Oracle 8 и 9 (разбира се, понякога обратното може да направи заявката по-проста или по-бърза):

Скобите могат да бъдат премахнати, ако не се използват за отмяна на приоритета на оператора. Прост пример е, когато всички булеви оператори във вашата клауза where са еднакви: where ((a or b) or c) е еквивалентно на where a or b or c.

Подзаявката често (ако не винаги) може да бъде обединена с основната заявка, за да се опрости. Според моя опит това често значително подобрява производителността:

select foo.a,
       bar.a
  from foomatic  foo,
       bartastic bar
 where foo.id = bar.id and
       bar.id = (
         select ban.id
           from bantabulous ban
          where ban.bandana = 42
       )
;

е еквивалентно на

select foo.a,
       bar.a
  from foomatic    foo,
       bartastic   bar,
       bantabulous ban
 where foo.id = bar.id and
       bar.id = ban.id and
       ban.bandana = 42
;

Използването на ANSI обединения отделя много от логиката на „код маймуна“ от наистина интересните части на клаузата where: Предишната заявка е еквивалентна на

select foo.a,
       bar.a
  from foomatic    foo
  join bartastic   bar on bar.id = foo.id
  join bantabulous ban on ban.id = bar.id
 where ban.bandana = 42
;

Ако искате да проверите за съществуването на ред, не използвайте count(*), вместо това използвайте rownum = 1 или поставете заявката в клауза where exists, за да извлечете само един ред вместо всички.

person l0b0    schedule 01.07.2009
comment
Леле, хубаво предложение накрая. Никога не съм мислил да извадя логиката на присъединяване от клаузата where и да я поставя с дефинициите на таблицата и не съм я виждал да се използва често преди, но има много смисъл. - person Al Crowley; 01.07.2009

  • Предполагам, че очевидното е да търсите курсори, които могат да бъдат заменени с операция, базирана на SQL „Set“.
  • Следващото в списъка ми е да потърсите всякакви корелирани подзаявки, които могат да бъдат пренаписани като несвързана заявка
  • В дълги съхранени процедури разбийте отделни SQL изрази в техните собствени съхранени процедури. По този начин те ще получат собствен кеширан план за заявка.
  • Потърсете транзакции, чиито обхват може да бъде съкратен. Редовно намирам извлечения вътре в транзакция, които безопасно могат да бъдат извън нея.
  • Подизборите често могат да бъдат пренаписани като директни съединения (съвременните оптимизатори са добри в откриването на прости такива)

Както спомена @Quassnoi, оптимизаторът често върши добра работа. Един от начините да му помогнете е да гарантирате, че индексите и статистиките са актуални и че съществуват подходящи индекси за вашето работно натоварване на заявката.

person Mitch Wheat    schedule 01.07.2009
comment
относно разделянето на съхранени процедури на повече: не правете това, когато използвате временни таблици: тогава SqlServer (не знам за други) ще преизчисли плана на заявката при всяко изпълнение, като по този начин ще навреди на производителността! - person Hans Kesting; 01.07.2009
comment
@Hans Kesting: Не мисля, че това е вярно, ако всички изрази за създаване на DDL за всички ваши временни таблици са първите изрази във вашата съхранена процедура. - person Mitch Wheat; 02.07.2009

Харесва ми да замествам всякакъв вид подизбиране със заявка за присъединяване.

Това е очевидно:

SELECT  *
FROM    mytable mo
WHERE   EXISTS
        (
          SELECT  *
          FROM    othertable o
          WHERE   o.othercol = mo.col
        )

by

SELECT  mo.*
FROM    mytable mo inner join othertable o on o.othercol = mo.col

И този е под оценка:

SELECT  *
FROM    mytable mo
WHERE   NOT EXISTS
        (
          SELECT  *
          FROM    othertable o
          WHERE   o.othercol = mo.col
        )

by

SELECT  mo.*
FROM    mytable mo left outer join othertable o on o.othercol = mo.col
WHERE   o.othercol is null

Може да помогне на СУБД да избере добрия план за изпълнение в голяма заявка.

person Cyril Gandon    schedule 01.07.2009
comment
Те не винаги ще дават точно същите резултати: JOINing на таблица ще доведе до дублиране, ако има повече от едно съвпадение в дясната таблица за всяка конкретна стойност, която се обединява в лявата таблица. EXISTS и NOT EXISTS нямат този проблем. (Може да се разреши с помощта на DISTINCT, но това намалява ефективността.) - person Steve Chambers; 10.08.2013

Харесва ми всеки в екипа да следва набор от стандарти, за да направи кода четим, поддържаем, разбираем, измиваем и т.н. :)

  • всеки използва един и същи псевдоним
  • без курсори. без примки
  • защо дори да мислите за IN, когато можете да СЪЩЕСТВУВАТЕ
  • ОТСТЪК
  • Последователност в стила на кодиране

тук има още неща Кои са някои от най-полезните ви бази данни стандарти?

person Raj More    schedule 01.07.2009
comment
Съгласен. Наличието на стандарти в екип повишава четливостта, поддръжката и често също и производителността. Поне за четливост има няколко налични инструмента като напр. Форматиране/разкрасяване на SQLinForm - person Guido; 01.06.2013

Като се има предвид естеството на SQL, вие абсолютно трябва да сте наясно с последиците за производителността от всяко преработване. Рефакторинг на SQL приложения е добър ресурс за рефакторинг с силен акцент върху представянето (вижте глава 5).

person Jim Ferrans    schedule 01.07.2009

Въпреки че опростяването може да не е равносилно на оптимизация, опростяването може да бъде важно при писането на четим SQL код, който от своя страна е от решаващо значение за възможността да проверите вашия SQL код за концептуална коректност (не синтактична коректност, която вашата среда за разработка трябва да проверява вместо вас). Струва ми се, че в един идеален свят бихме написали най-простия, четим SQL код и след това оптимизаторът ще пренапише този SQL код, за да бъде във всякаква форма (може би по-подробна), която ще работи най-бързо.

Открих, че мисленето за SQL изрази като базирани на наборна логика е много полезно, особено ако трябва да комбинирам клаузи where или да разбера сложно отрицание на клауза where. В този случай използвам законите на булевата алгебра.

Най-важните за опростяване на клауза where вероятно са законите на DeMorgan (обърнете внимание, че "·" е "И", а "+" е "ИЛИ"):

  • НЕ (x · y) = НЕ x + НЕ y
  • НЕ (x + y) = НЕ x · НЕ y

Това се превежда в SQL на:

NOT (expr1 AND expr2) -> NOT expr1 OR NOT expr2
NOT (expr1 OR expr2) -> NOT expr1 AND NOT expr2

Тези закони могат да бъдат много полезни за опростяване на клаузи where с много вложени AND и OR части.

Също така е полезно да запомните, че изразът field1 IN (value1, value2, ...) е еквивалентен на field1 = value1 OR field1 = value2 OR .... Това ви позволява да отхвърлите IN () по един от двата начина:

NOT field1 IN (value1, value2)  -- for longer lists
NOT field1 = value1 AND NOT field1 = value2  -- for shorter lists

Подзаявката може да се разглежда и по този начин. Например това отрича клауза where:

NOT (table1.field1 = value1 AND EXISTS (SELECT * FROM table2 WHERE table1.field1 = table2.field2))

може да се пренапише като:

NOT table1.field1 = value1 OR NOT EXISTS (SELECT * FROM table2 WHERE table1.field1 = table2.field2))

Тези закони не ви казват как да трансформирате SQL заявка, използвайки подзаявка, в такава, използваща съединение, но булевата логика може да ви помогне да разберете типовете съединения и какво трябва да връща вашата заявка. Например, с таблици A и B, INNER JOIN е като A AND B, LEFT OUTER JOIN е като (A AND NOT B) OR (A AND B), което се опростява до A OR (A AND B), а FULL OUTER JOIN е A OR (A AND B) OR B, което се опростява до A OR B.

person Nick Seigal    schedule 13.03.2012
comment
Също така намирам, че често използвам правилото за пренаписване на импликации, т.е. ( P => Q ) <=> ( NOT ( P ) OR Q ) - person onedaywhen; 13.03.2012

Моят подход е да науча релационната теория като цяло и релационната алгебра в частност. След това се научете да забелязвате конструкциите, използвани в SQL за прилагане на оператори от релационната алгебра (напр. универсално количествено определяне, известно още като деление) и смятане (напр. екзистенциално количествено определяне). Лошото е, че SQL има функции, които не се срещат в релационния модел, напр. нули, които вероятно е най-добре да се преработят така или иначе. Препоръчителна литература: SQL и релационна теория: Как да напишем точен SQL Код от C. J. Дата.

В този смисъл не съм убеден, че "фактът, че повечето SUBSELECTs могат да бъдат пренаписани като JOIN" представлява опростяване.

Вземете например тази заявка:

SELECT c 
  FROM T1 
 WHERE c NOT IN ( SELECT c FROM T2 );

Пренапишете с помощта на JOIN

SELECT DISTINCT T1.c 
  FROM T1 NATURAL LEFT OUTER JOIN T2 
 WHERE T2.c IS NULL;

Присъединяването е по-подробно!

Като алтернатива разпознайте, че конструкцията прилага антисъединение върху проекцията на c, напр. псевдоалгрбра

T1 { c } antijoin T2 { c }

Опростяване с помощта на релационни оператори:

SELECT c FROM T1 EXCEPT SELECT c FROM T2;
person onedaywhen    schedule 13.03.2012