RussianLDP Рейтинг@Mail.ru
WebMoney: 
WMZ Z294115950220 
WMR R409981405661 
WME E134003968233 
Visa 
4274 3200 2453 6495 

Small. Fast. Reliable.
Choose any three.
Профилирование SQL-запросов

1. Обзор

SQLite содержит встроенную поддержку профилирования SQL-запросов, но это не позволено по умолчанию. Чтобы позволить поддержку профилирования запроса, SQLite должен быть собран с опцией:

-DSQLITE_ENABLE_STMT_SCANSTATUS

Сборка SQLite с этим выбором позволяет sqlite3_stmt_scanstatus_v2() API, который обеспечивает доступ к различным профильным метрикам. Остаток этой страницы обсуждает профильные отчеты, произведенные оболочкой командной строки SQLite, используя эти метрики, а не API непосредственно.

Профильные отчеты, произведенные оболочкой, очень похожи на отчеты о плане запросов, произведенные EXPLAIN QUERY PLAN. Эта страница предполагает, что читатель знаком с этим форматом.

В оболочке командной строки, собранной с выбором выше, профилирование запроса позволено, используя команду ".scanstats on":

sqlite> .scanstats on

После того, как позволено, оболочка автоматически производит профиль запроса после того, как каждый SQL-запрос выполнен. Профилирование запроса может быть отключено, используя ".scanstats off":

sqlite> .scanstats on
sqlite> SELECT a FROM t1, t2 WHERE a IN (1,2,3) AND a=d+e;
QUERY PLAN (cycles=255831538 [100%])
|--SEARCH t1 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=60048488 [23%] loops=1 rows=3)
`--SCAN t2                                           (cycles=133558052 [52%] loops=3 rows=150000)

2. Простые случаи: строки и циклы

Consider a database with the following schema:

CREATE VIRTUAL TABLE ft USING fts5(text);
CREATE TABLE t1(a, b);
CREATE TABLE t2(c INTEGER PRIMARY KEY, d);

Затем после первого выполнения ".scanstats on":

sqlite3> SELECT * FROM t1, t2 WHERE t2.c=t1.a;
<...query results...>
QUERY PLAN (cycles=1140768 [100%])
|--SCAN t1                                           (cycles=455974 [40%] loops=1 rows=500)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=619820 [54%] loops=500 rows=250)

Текст в примере ниже "<...query results...>" является отчетом о профиле для запроса соединения. Части отчета о профиле, которые подобны EXPLAIN QUERY PLAN, указывают, что запрос осуществляется, делая полное сканирование таблицы "t1", с поиском INTEGER PRIMARY KEY на таблице "t2" для каждой строки, которую посещают.

Формат "loops=1" в строке "SCAN t1" указывает, что этот цикл это полное сканирование таблицы "t1" выполнен точно однажды. "rows=500" указывает, что тот единственный просмотр посетил 500 строк.

Строка "SEARCH t2 USING ..." содержит аннотацию "loops=500", чтобы указать, что этот цикл (действительно поиск INTEGER PRIMARY KEY) отработал 500 раз. Смысл: он сработал по разу для каждой строки, которую посещает полное сканирование таблицы "t1". "rows=250" означает, что в целом те 500 циклов посетили 250 строк. Другими словами, только половина поисков INTEGER PRIMARY KEY на t2 были успешны, для другой половины поисков не было никакой строки, чтобы найти.

Счетчик циклов для SEARCH и SCAN является не обязательно тем же самым, как количество строк, произведенных внешним циклом. Например, если запрос выше был изменен следующим образом:

sqlite3> SELECT * FROM t1, t2 WHERE t1.b<=100 AND t2.c=t1.a;
<...query results...>
QUERY PLAN (cycles=561002 [100%])
|--SCAN t1                                           (cycles=345950 [62%] loops=1 rows=500)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=128690 [23%] loops=100 rows=50)

На этот раз, даже при том, что "SCAN t1" все еще посещает 500 строк, поиск "SEARCH t2" выполняется только 100 раз. Это вызвано тем, что SQLite смог отказаться от строк в t1, которые не соответствовали "t1.b<=100".

Измерения "cycles" основаны на CPU time-stamp counter, и являются хорошим полномочием в течение времени часов. Для запроса выше общее количество циклов было 561002. Для каждого из этих двух циклов ("SCAN t1..." и "SEARCH t2...") счетчик циклов представляет время, проведенное в операциях, которые могут быть непосредственно приписаны только этому циклу. Определенно, это время, проведенное, проводя и извлекая данные из b-деревьев базы данных для того цикла. Эти значения никогда не составляют в целом полные циклы для запроса, поскольку есть другие внутренние операции, выполненные SQLite, которые не относятся непосредственно ни к одному циклу.

Счетчик циклов для "SCAN t1" был 345950, это 62% общего количества для запроса. Эти 100 поисков, выполненных циклом "SEARCH t1", заняли 128690 циклов, 23% от общего количества.

Когда виртуальная таблица используется, у метрик "rows" и "loops" есть то же самое значение, что касается циклов на обычных таблицах SQLite. "cycles" являются полными циклами, потребляемыми в рамках методов виртуальных таблиц, связанных с циклом. Например:

sqlite3> SELECT * FROM ft('sqlite'), t2 WHERE t2.c=ft.rowid;
<...query results...>
QUERY PLAN (cycles=836434 [100%]
|--SCAN ft VIRTUAL TABLE INDEX 0:M1                  (cycles=739602 [88%] loops=1 rows=48)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=62866 [8%] loops=48 rows=25)

В этом случае единый запрос (loops=1) на таблице fts5 "ft" вернул 48 строк (rows=48) и занял 739602 циклов (cycles=739602), что составляло примерно 88% полного времени выполнения запроса.

3. Сложные случаи

Используя ту же самую схему, как в предыдущей секции, рассмотрите этот более сложный пример:

sqlite3> WITH cnt(i) AS (
  SELECT 1 UNION SELECT i+1 FROM cnt WHERE i<100)
SELECT *, (SELECT d FROM t2 WHERE c=ft.rowid) FROM
          (SELECT count(*), a FROM t1 GROUP BY a) AS v1 CROSS JOIN
          ft('sqlite'), cnt WHERE cnt.i=ft.rowid AND v1.a=ft.rowid;
<...query results...>
QUERY PLAN (cycles=177665334 [100%])
|--CO-ROUTINE v1                                        (cycles=4500444 [3%])
|  |--SCAN t1                                           (cycles=397052 [0%] loops=1 rows=500)
|  `--USE TEMP B-TREE FOR GROUP BY
|--MATERIALIZE cnt                                      (cycles=1275068 [1%])
|  |--SETUP
|  |  `--SCAN CONSTANT ROW
|  `--RECURSIVE STEP
|     `--SCAN cnt                                       (cycles=129166 [0%] loops=100 rows=100)
|--SCAN v1                                              (loops=1 rows=500)
|--SCAN ft VIRTUAL TABLE INDEX 0:M1=                    (cycles=161874340 [91%] loops=500 rows=271)
|--SCAN cnt                                             (cycles=7336350 [4%] loops=95 rows=9500)
`--CORRELATED SCALAR SUBQUERY 3                         (cycles=168538 [0%] loops=37)
   `--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=94724 [0%] loops=37 rows=21)

Самая сложная часть примера выше понимает план запросов это часть отчета, который был бы также произведен EXPLAIN QUERY PLAN. Другие интересные места:

  • Подзапрос "v1" осуществляется как созапрос. В этом случае относительно подзапроса сообщают отдельно количество циклов для всего подзапроса. Есть также строка "SCAN v1", это представляет подвызов созапроса от главного запроса. У этого входа нет циклов, связанных с ним, поскольку все значение подзапроса приписано созапросу. У этого действительно есть значения "loops" и "rows": подзапрос просматривается однажды и возвращает 500 строк.

  • Рекурсивный подзапрос "cnt" осуществлен (кэшированный в таблице temp) прежде, чем главным запросом будут управлять. Все значение материализации приписано "MATERIALIZE cnt". Есть также пункт "SCAN cnt", представляющий просмотры осуществленного подзапроса. Значение циклов, связанное с этим пунктом, представляет время, потраченное, чтобы просмотреть временную таблицу, содержащую осуществленный подзапрос, который является отдельным от циклов, используемых, чтобы наполнить его.

  • Есть циклы и измерения для скалярного подзапроса также. Они представляют полные циклы, потребляемые, выполняя подзапрос и число раз, которое это было выполнено, соответственно.

  • Где один пункт это родитель другого, как в "CORRELATED SCALAR SUBQUERY 3" и "SEARCH t2 USING...", значение циклов, связанное с родителем, включает те циклы, связанные со всеми дочерними элементами. Во всех случаях значения процента касаются полных циклов, используемых запросом, не циклов, используемых родителем.

Следующий запрос использует автоматический индекс и внешнюю сортировку:

sqlite> SELECT * FROM t2, (SELECT count(*) AS cnt, d FROM t2 GROUP BY d) AS
                 v2 WHERE v2.d=t2.d AND t2.d>100 ORDER BY v2.cnt;
<...query results...>
QUERY PLAN (cycles=6234376 [100%])
|--MATERIALIZE v2                                     (cycles=2351916 [38%])
|  |--SCAN t2                                         (cycles=188428 [3%] loops=1 rows=250)
|  `--USE TEMP B-TREE FOR GROUP BY
|--SCAN t2                                            (cycles=455736 [7%] loops=1 rows=250)
|--CREATE AUTOMATIC INDEX ON v2(d, cnt)               (cycles=1668380 [27%] loops=1 rows=250)
|--SEARCH v2 USING AUTOMATIC COVERING INDEX (d=?)     (cycles=932824 [15%] loops=200 rows=200)
`--USE TEMP B-TREE FOR ORDER BY                       (cycles=662456 [11%] loops=1 rows=200)

Здесь интересно следующее:

  • Этот запрос осуществляет подзапрос в таблице temp, затем создает автоматический (то есть, переходный) индекс на нем, затем использует тот индекс, чтобы оптимизировать соединение. Все три из этих шагов, "MATERIALIZE v2", "CREATE AUTOMATIC INDEX" и "SEARCH ... USING AUTOMATIC INDEX", имеют отдельное количество циклов. "rows", связанное со строкой "CREATE AUTOMATIC INDEX", представляет общее количество строк, включенных в индекс. "loops" и "rows" связанные со строкой "SEARCH ... USING AUTOMATIC INDEX", представляют количество поисков индекса, которое использовалось, и общее количество строк, найденных теми поисками.

  • Внешняя сортировка "USE TEMP B-TREE FOR ORDER BY" также составляется отдельно. Количество циклов представляет дополнительные циклы, потребляемые, сортируя возвращенные строки выше тех, которые использовались бы, если строки были возвращены в произвольном порядке. Количество строк представляет количество сортированных строк.

4. Оценки планировщика

Кроме ".scanstats on", чтобы включить профилирование и ".scanstats off", чтобы выключить, оболочка также принимает ".scanstats est":

sqlite> .scanstats est

Это позволяет специальный вид профилирования отчета, который включает два дополнительных значения, связанных с каждым элементом "SCAN..." и "SEARCH..." профиля запроса:

  • rpl (rows per loop): Это значение метрики "rows", разделенное на метрику "loops".
  • est (estimate): Это количество строк на цикл, которое планировщик запроса возвратил бы текущим элементом запроса. Если это значение радикально отличается от фактических строк на значение цикла, а производительность запросов неудовлетворительная, качество этой оценки может быть улучшено, управляя ANALYZE.
sqlite> SELECT a FROM t1, t2 WHERE a IN (1,2,3) AND a=d+e ORDER BY a;
<query results...>
QUERY PLAN (cycles=264725190 [100%]
|--SEARCH t1 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=60511568 [23%] loops=1 rows=3 rpl=3.0 est=3.0)
`--SCAN t2                                           (cycles=139461608 [53%] loops=3 rows=150000 rpl=50000.0 est=1048576.0)

5. Низкоуровневые данные о профилировании

Также поддержана команда ".scanstats vm". Это позволяет профильный отчет низшего уровня, показывая сколько раз каждая инструкция VM была выполнена, и процент тактов, которые прошли, в то время как это выполнялось:

sqlite> .scanstats vm

Затем:

sqlite> SELECT count(*) FROM t2 WHERE (d % 5) = 0;
<query results...>
addr  cycles  nexec   opcode         p1    p2    p3    p4             p5  comment
----  ------  ------  -------------  ----  ----  ----  -------------  --  -------------
0     0.0%    1       Init           1     18    0                    0   Start at 18
1     0.0%    1       Null           0     1     1                    0   r[1..1]=NULL
2     0.0%    1       OpenRead       0     2     0     2              0   root=2 iDb=0; t2
3     0.0%    1       ColumnsUsed    0     0     0     2              0
4     0.0%    1       Explain        4     0     0     SCAN t2        0
5     0.0%    1       CursorHint     0     0     0     EQ(REM(c1,5),0) 0
6     0.0%    1       Rewind         0     14    0                    0
7     46.86%  150000  Column         0     1     3                    0   r[3]= cursor 0 column 1
8     18.94%  150000  Remainder      4     3     2                    0   r[2]=r[3]%r[4]
9     5.41%   150000  ReleaseReg     3     1     0                    0   release r[3] mask 0
10    12.09%  150000  Ne             5     13    2                    80  if r[2]!=r[5] goto 13
11    1.02%   30000   ReleaseReg     2     1     0                    0   release r[2] mask 0
12    2.95%   30000   AggStep1       0     0     1     count(0)       0   accum=r[1] step(r[0])
13    12.72%  150000  Next           0     7     0                    1
14    0.0%    1       AggFinal       1     0     0     count(0)       0   accum=r[1] N=0
15    0.0%    1       Copy           1     6     0                    0   r[6]=r[1]
16    0.0%    1       ResultRow      6     1     0                    0   output=r[6]
17    0.01%   1       Halt           0     0     0                    0
18    0.0%    1       Transaction    0     0     1     0              1   usesStmtJournal=0
19    0.0%    1       Integer        5     4     0                    0   r[4]=5
20    0.0%    1       Integer        0     5     0                    0   r[5]=0
21    0.0%    1       Goto           0     1     0                    0